Skip to content

Commit

Permalink
feat(diagnostics): add activity (open telemetry) support
Browse files Browse the repository at this point in the history
Addresses tracing aspects of HangfireIO#2408 for integration with Aspire, as well as all other OpenTelemetery based diagnostics,  and addresses HangfireIO#2017.

Add a default filter to start producer activities (spans) when jobs created, and consumer
activities when jobs performed. Pass the creation context through as TraceParent and TraceState job parameters, so that distributed tracing works across job scheduling.

Note that activity supports is only from netstandard2.0 onwards, and only creates activities if there is a configured listener.
  • Loading branch information
sgryphon committed Nov 17, 2024
1 parent c4bc42f commit 8c816d5
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 41 deletions.
3 changes: 2 additions & 1 deletion samples/NetCoreSample/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,8 @@
"MoreLinq.Source.MoreEnumerable.Pairwise": "[1.0.1, )",
"Newtonsoft.Json": "[11.0.1, )",
"StackTraceFormatter.Source": "[1.1.0, )",
"StackTraceParser.Source": "[1.3.0, )"
"StackTraceParser.Source": "[1.3.0, )",
"System.Diagnostics.DiagnosticSource": "[5.0.0, )"
}
},
"hangfire.netcore": {
Expand Down
41 changes: 36 additions & 5 deletions src/Hangfire.AspNetCore/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@
"resolved": "1.3.0",
"contentHash": "tOdf1XpHE2YQJpBERplZA2kMTJQtJnKzSc7GMO/yeNNGuXNueVqySfIr/gfU1vx9k2FO8u0zD4mtnZGwKQM+Zg=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA=="
},
"hangfire.core": {
"type": "Project",
"dependencies": {
Expand All @@ -120,7 +125,8 @@
"MoreLinq.Source.MoreEnumerable.Pairwise": "[1.0.1, )",
"Newtonsoft.Json": "[11.0.1, )",
"StackTraceFormatter.Source": "[1.1.0, )",
"StackTraceParser.Source": "[1.3.0, )"
"StackTraceParser.Source": "[1.3.0, )",
"System.Diagnostics.DiagnosticSource": "[5.0.0, )"
}
},
"hangfire.netcore": {
Expand Down Expand Up @@ -2318,14 +2324,38 @@
"contentHash": "tOdf1XpHE2YQJpBERplZA2kMTJQtJnKzSc7GMO/yeNNGuXNueVqySfIr/gfU1vx9k2FO8u0zD4mtnZGwKQM+Zg=="
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==",
"dependencies": {
"System.Memory": "4.5.4",
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
}
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.4.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "AwarXzzoDwX6BgrhjoJsk6tUezZEozOT5Y9QKF94Gl4JK91I4PIIBkBco9068Y9/Dra8Dkbie99kXB8+1BaYKw=="
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "9dLLuBxr5GNmOfl2jSMcsHuteEg32BEfUotmmUkmZjpR3RpVHE8YQwt0ow3p6prwA1ME8WqDVZqrr8z6H8G+Kw=="
"resolved": "5.0.0",
"contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
},
"System.Security.AccessControl": {
"type": "Transitive",
Expand Down Expand Up @@ -2361,7 +2391,8 @@
"MoreLinq.Source.MoreEnumerable.Pairwise": "[1.0.1, )",
"Newtonsoft.Json": "[11.0.1, )",
"StackTraceFormatter.Source": "[1.1.0, )",
"StackTraceParser.Source": "[1.3.0, )"
"StackTraceParser.Source": "[1.3.0, )",
"System.Diagnostics.DiagnosticSource": "[5.0.0, )"
}
},
"hangfire.netcore": {
Expand Down
161 changes: 161 additions & 0 deletions src/Hangfire.Core/DiagnosticsActivityFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#if NETSTANDARD2_0
#nullable enable

using System;
using System.Diagnostics;
using Hangfire.Client;
using Hangfire.Server;
using Hangfire.States;
using Hangfire.Storage;

namespace Hangfire
{
public sealed class DiagnosticsActivityFilter : IClientFilter, IServerFilter, IDisposable
{
public const string DefaultListenerName = "Hangfire";

private static readonly ActivitySource DefaultActivitySource = new ActivitySource(DefaultListenerName);

private const string ActivityItemsKeyName = "Diagnostics.Activity";

private const string ExceptionEventName = "exception";
private const string ExceptionMessageTag = "exception.message";
private const string ExceptionStackTraceTag = "exception.stacktrace";
private const string ExceptionTypeTag = "exception.type";

private const string MessagingDestinationNameTag = "messaging.destination.name";
private const string MessagingMessageId = "messaging.message.id";
private const string MessagingOperationName = "messaging.operation.name";
private const string MessagingOperationType = "messaging.operation.type";

private const string TraceParentParameterName = "traceparent";
private const string TraceStateParameterName = "tracestate";

private readonly ActivitySource _activitySource;

public DiagnosticsActivityFilter()
{
_activitySource = DefaultActivitySource;
}

public DiagnosticsActivityFilter(string? activitySourceName)
{
var name = activitySourceName ?? throw new ArgumentNullException(nameof(activitySourceName));
_activitySource = new ActivitySource(name);
}

public void OnCreating(CreatingContext context)
{
var activity = _activitySource.StartActivity(
$"create_job {context.Job.Type.Name}.{context.Job.Method.Name}",
ActivityKind.Producer);

if (activity != null)
{
activity.SetTag(MessagingOperationName, "create_job");
activity.SetTag(MessagingDestinationNameTag, $"{context.Job.Type.Name}.{context.Job.Method.Name}");
activity.SetTag(MessagingOperationType, "create");

activity.SetTag("job.type", context.Job.Type.FullName);
activity.SetTag("job.method", context.Job.Method.Name);
activity.SetTag("job.state", context.InitialState?.Name);
activity.SetTag("job.storage", context.Storage.ToString());

context.SetJobParameter(TraceParentParameterName, activity.Id);
context.SetJobParameter(TraceStateParameterName, activity.TraceStateString);

context.Items[ActivityItemsKeyName] = activity;
}
}

public void OnCreated(CreatedContext context)
{
if (context.Items.TryGetValue(ActivityItemsKeyName, out var item) &&
item is Activity activity)
{
if (context.Exception == null)
{
// NOTE: Need library 6.0 for SetStatus(ActivityStatusCode.Ok) (use tags instead)
activity.SetTag("otel.status_code", "OK");

activity.SetTag(MessagingMessageId, context.BackgroundJob.Id);
activity.SetTag("job.id", context.BackgroundJob.Id);
}
else
{
// NOTE: Library 9.0 has AddException (manually add event instead)
var exceptionTags = new ActivityTagsCollection
{
{ ExceptionMessageTag, context.Exception.Message },
{ ExceptionTypeTag, context.Exception.GetType().ToString() },
{ ExceptionStackTraceTag, context.Exception.ToString() }
};
activity.AddEvent(new ActivityEvent(ExceptionEventName, tags: exceptionTags));

// NOTE: Need library 6.0 for SetStatus(ActivityStatusCode.Error, "Exception") (use tags instead)
activity.SetTag("otel.status_code", "ERROR");
activity.SetTag("otel.status_description", "Exception");
}

activity.Dispose();
}
}

public void OnPerforming(PerformingContext context)
{
var parentId = context.GetJobParameter<string>(TraceParentParameterName);
var parentState = context.GetJobParameter<string>(TraceStateParameterName);
ActivityContext.TryParse(parentId, parentState, out var parentCtx);

var activity = _activitySource.StartActivity(
$"perform_job {context.BackgroundJob.Job.Type.Name}.{context.BackgroundJob.Job.Method.Name}",
ActivityKind.Consumer,
parentCtx);

if (activity != null)
{
activity.SetTag(MessagingOperationName, "perform_job");
activity.SetTag(MessagingDestinationNameTag, $"{context.BackgroundJob.Job.Type.Name}.{context.BackgroundJob.Job.Method.Name}");
activity.SetTag(MessagingOperationType, "process");
activity.SetTag(MessagingMessageId, context.BackgroundJob.Id);

context.Items[ActivityItemsKeyName] = activity;
}
}

public void OnPerformed(PerformedContext context)
{
if (context.Items.TryGetValue(ActivityItemsKeyName, out var item) && item is Activity activity)
{
if (context.Exception == null)
{
// NOTE: Need library 6.0 for SetStatus(ActivityStatusCode.Ok) (use tags instead)
activity.SetTag("otel.status_code", "OK");
}
else
{
// NOTE: Library 9.0 has AddException (manually add event instead)
var exceptionTags = new ActivityTagsCollection
{
{ ExceptionMessageTag, context.Exception.Message },
{ ExceptionTypeTag, context.Exception.GetType().ToString() },
{ ExceptionStackTraceTag, context.Exception.ToString() }
};
activity.AddEvent(new ActivityEvent(ExceptionEventName, tags: exceptionTags));

// NOTE: Need library 6.0 for SetStatus(ActivityStatusCode.Error, "Exception") (use tags instead)
activity.SetTag("otel.status_code", "ERROR");
activity.SetTag("otel.status_description", "Exception");
}

activity.Dispose();
}
}

public void Dispose()
{
_activitySource?.Dispose();
}
}
}
#endif
3 changes: 3 additions & 0 deletions src/Hangfire.Core/GlobalJobFilters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ static GlobalJobFilters()
Filters.Add(new AutomaticRetryAttribute());
Filters.Add(new StatisticsHistoryAttribute());
Filters.Add(new ContinuationsSupportAttribute());
#if NETSTANDARD2_0
Filters.Add(new DiagnosticsActivityFilter());
#endif
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Hangfire.Core/Hangfire.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<PackageReference Include="Microsoft.CSharp" Version="4.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.1" NoWarn="NU1903" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="5.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netstandard1.3' or '$(TargetFramework)'=='netstandard2.0'">
Expand Down
35 changes: 35 additions & 0 deletions src/Hangfire.Core/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,16 @@
"resolved": "1.3.0",
"contentHash": "tOdf1XpHE2YQJpBERplZA2kMTJQtJnKzSc7GMO/yeNNGuXNueVqySfIr/gfU1vx9k2FO8u0zD4mtnZGwKQM+Zg=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Direct",
"requested": "[5.0.0, )",
"resolved": "5.0.0",
"contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==",
"dependencies": {
"System.Memory": "4.5.4",
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
Expand All @@ -1199,6 +1209,31 @@
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.4.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
}
}
}
Expand Down
Loading

0 comments on commit 8c816d5

Please sign in to comment.