-
Notifications
You must be signed in to change notification settings - Fork 402
[Instrumentation.Hangfire] Add metrics support #3258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Kielek
merged 55 commits into
open-telemetry:main
from
gorbach:feature/2075-hangfire-telemetry
Nov 24, 2025
Merged
Changes from 3 commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
2e804df
[Instrumentation.Hangfire] Add metrics support
gorbach 08198ac
Align metrics with workflow semconv: use duration{state=pending}, mo…
gorbach d43ee2c
Configure metrics in HangfireMetricsInstrumentationOptions class, add…
gorbach bc135e8
Add workflow.status metric to track complete job lifecycle
gorbach 96d8aa8
Use job formatter from hangfire
gorbach 742b2a0
Fix tests
gorbach aa3ac32
Fix and speed up tests
gorbach 1f909bd
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach 42476f8
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach accd8bc
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach 302542a
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach 5af844c
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach eae641c
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach 38883b8
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach 2e7cdff
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach 3254ed2
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach 693d1be
build fix
gorbach 044e500
Rename metrics/attributes
gorbach b58920e
Remove `DisplayNameFunc` from PublicAPI.Unshipped.txt and README.md
gorbach cef13e5
Merge branch 'main' into feature/2075-hangfire-telemetry
gorbach 90022d0
remove wrong link
gorbach ded8e66
update CHANGELOG.md
gorbach 64514b8
Merge branch 'main' into feature/2075-hangfire-telemetry
gorbach 2c494a9
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach ecd3163
update CHANGELOG.md
gorbach 85aa0f3
Merge branch 'main' into feature/2075-hangfire-telemetry
gorbach d4186b4
CHANGELOG fix
Kielek 6ec1c12
Fix build
gorbach 98d3925
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 6683846
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 39eae7a
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 6b24590
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach ffc2a14
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach bf60f2b
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach e4dfc84
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach c5f113e
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 53a98e2
Update test/OpenTelemetry.Instrumentation.Hangfire.Tests/HangfireMetr…
gorbach a4c3dd3
Update test/OpenTelemetry.Instrumentation.Hangfire.Tests/HangfireMetr…
gorbach 762e83b
Fix lint warnings
gorbach c75d4e3
reformat code with dotnet format
gorbach d0dbb8b
reformat code with dotnet format
gorbach afd0b94
Make HangfireTests assembly not signed
gorbach 0694238
make lines in README.md shorter
gorbach 2872f35
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 9960ebd
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 3b61b6a
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 422862c
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 00f1924
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 2b19e59
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach aabc0a2
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach ac8dbfa
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 27f9dd8
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 99cae94
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach 4b95bb7
Merge branch 'main' into feature/2075-hangfire-telemetry
Kielek aa2d422
md lint
Kielek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
src/OpenTelemetry.Instrumentation.Hangfire/HangfireMetricsInstrumentationOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| using Hangfire; | ||
|
|
||
| namespace OpenTelemetry.Metrics; | ||
|
|
||
| /// <summary> | ||
| /// Options for Hangfire metrics instrumentation. | ||
| /// </summary> | ||
| public sealed class HangfireMetricsInstrumentationOptions | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets a value indicating whether to record the pending state duration in metrics. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// When enabled, records workflow.execution.duration with state="pending", representing | ||
| /// the time jobs spend waiting in the queue before execution starts. | ||
| /// This requires an additional database call per job execution to retrieve the enqueue timestamp. | ||
| /// Default is <see langword="false"/> to avoid performance impact in high-throughput scenarios. | ||
| /// When disabled, only execution duration (state="executing") is recorded. | ||
| /// </remarks> | ||
| public bool RecordQueueLatency { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a delegate used to format the job name for metrics. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Defaults to <c>backgroundJob.Job.ToString()</c>. | ||
| /// </remarks> | ||
| public Func<BackgroundJob, string> DisplayNameFunc { get; set; } = | ||
| backgroundJob => backgroundJob.Job?.ToString() ?? "unknown"; | ||
| } |
86 changes: 86 additions & 0 deletions
86
src/OpenTelemetry.Instrumentation.Hangfire/Implementation/HangfireMetrics.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| using System.Diagnostics.Metrics; | ||
| using System.Reflection; | ||
| using OpenTelemetry.Internal; | ||
|
|
||
| namespace OpenTelemetry.Instrumentation.Hangfire.Implementation; | ||
|
|
||
| /// <summary> | ||
| /// Centralized metrics definitions for Hangfire instrumentation. | ||
| /// </summary> | ||
| internal static class HangfireMetrics | ||
| { | ||
| // Metric name constants | ||
| internal const string ExecutionCountMetricName = "workflow.execution.count"; | ||
| internal const string ExecutionDurationMetricName = "workflow.execution.duration"; | ||
| internal const string ExecutionStatusMetricName = "workflow.execution.status"; | ||
| internal const string ExecutionErrorsMetricName = "workflow.execution.errors"; | ||
| internal const string WorkflowCountMetricName = "workflow.count"; | ||
|
|
||
| internal static readonly Assembly Assembly = typeof(HangfireMetrics).Assembly; | ||
| internal static readonly AssemblyName AssemblyName = Assembly.GetName(); | ||
| internal static readonly string MeterName = AssemblyName.Name!; | ||
|
|
||
| private static readonly string InstrumentationVersion = Assembly.GetPackageVersion(); | ||
|
|
||
| /// <summary> | ||
| /// The meter instance for all Hangfire metrics. | ||
| /// </summary> | ||
| public static readonly Meter Meter = new(MeterName, InstrumentationVersion); | ||
|
|
||
| /// <summary> | ||
| /// Counter for the number of task executions which have been initiated. | ||
| /// Follows OpenTelemetry workflow semantic conventions. | ||
| /// </summary> | ||
| public static readonly Counter<long> ExecutionCount = | ||
| Meter.CreateCounter<long>( | ||
| ExecutionCountMetricName, | ||
| unit: "{executions}", | ||
| description: "The number of task executions which have been initiated."); | ||
|
|
||
| /// <summary> | ||
| /// Histogram for duration of an execution grouped by task, type and result. | ||
| /// Follows OpenTelemetry workflow semantic conventions. | ||
| /// Records duration for different execution phases using workflow.execution.state attribute: | ||
| /// - state=pending: Time spent waiting in queue before execution. | ||
| /// - state=executing: Time spent in actual execution. | ||
| /// </summary> | ||
| public static readonly Histogram<double> ExecutionDuration = | ||
| Meter.CreateHistogram<double>( | ||
| ExecutionDurationMetricName, | ||
| unit: "s", | ||
| description: "Duration of an execution grouped by task, type and result."); | ||
|
|
||
| /// <summary> | ||
| /// UpDownCounter for the number of actively running tasks grouped by task and state. | ||
| /// Follows OpenTelemetry workflow semantic conventions. | ||
| /// </summary> | ||
| public static readonly UpDownCounter<long> ExecutionStatus = | ||
| Meter.CreateUpDownCounter<long>( | ||
| ExecutionStatusMetricName, | ||
| unit: "{executions}", | ||
| description: "The number of actively running tasks grouped by task, type and the current state."); | ||
|
|
||
| /// <summary> | ||
| /// Counter for the number of errors encountered in task runs (eg. compile, test failures). | ||
| /// Follows OpenTelemetry workflow semantic conventions. | ||
| /// </summary> | ||
| public static readonly Counter<long> ExecutionErrors = | ||
| Meter.CreateCounter<long>( | ||
| ExecutionErrorsMetricName, | ||
| unit: "{error}", | ||
| description: "The number of errors encountered in task runs (eg. compile, test failures)."); | ||
|
|
||
| /// <summary> | ||
| /// Counter for the number of workflow instances which have been initiated. | ||
| /// Follows OpenTelemetry workflow semantic conventions. | ||
| /// In Hangfire, this tracks individual job completions. For batch workflows, this would track batch completion. | ||
| /// </summary> | ||
| public static readonly Counter<long> WorkflowCount = | ||
| Meter.CreateCounter<long>( | ||
| WorkflowCountMetricName, | ||
| unit: "{workflows}", | ||
| description: "The number of workflow instances which have been initiated."); | ||
| } |
41 changes: 41 additions & 0 deletions
41
...nTelemetry.Instrumentation.Hangfire/Implementation/HangfireMetricsErrorFilterAttribute.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| using Hangfire.Common; | ||
| using Hangfire.Server; | ||
| using OpenTelemetry.Metrics; | ||
|
|
||
| namespace OpenTelemetry.Instrumentation.Hangfire.Implementation; | ||
|
|
||
| /// <summary> | ||
| /// Hangfire filter that records OpenTelemetry error metrics for job execution failures. | ||
| /// Follows OpenTelemetry workflow semantic conventions for workflow.execution.errors metric. | ||
| /// </summary> | ||
| internal sealed class HangfireMetricsErrorFilterAttribute : JobFilterAttribute, IServerFilter | ||
| { | ||
| private readonly HangfireMetricsInstrumentationOptions options; | ||
|
|
||
| #pragma warning disable CA1019 // Define accessors for attribute arguments | ||
| public HangfireMetricsErrorFilterAttribute(HangfireMetricsInstrumentationOptions options) | ||
| #pragma warning restore CA1019 // Define accessors for attribute arguments | ||
| { | ||
| this.options = options ?? throw new ArgumentNullException(nameof(options)); | ||
| } | ||
|
|
||
|
gorbach marked this conversation as resolved.
Outdated
|
||
| public void OnPerforming(PerformingContext performingContext) | ||
| { | ||
| } | ||
|
|
||
| public void OnPerformed(PerformedContext performedContext) | ||
| { | ||
| if (performedContext.Exception != null) | ||
| { | ||
| var errorTags = HangfireTagBuilder.BuildErrorTags( | ||
| performedContext.BackgroundJob, | ||
| this.options.DisplayNameFunc, | ||
| performedContext.Exception); | ||
|
|
||
| HangfireMetrics.ExecutionErrors.Add(1, errorTags); | ||
| } | ||
| } | ||
| } | ||
42 changes: 42 additions & 0 deletions
42
src/OpenTelemetry.Instrumentation.Hangfire/Implementation/HangfireMetricsInstrumentation.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| using Hangfire; | ||
| using OpenTelemetry.Metrics; | ||
|
|
||
| namespace OpenTelemetry.Instrumentation.Hangfire.Implementation; | ||
|
|
||
| /// <summary> | ||
| /// Hangfire metrics instrumentation following OpenTelemetry workflow semantic conventions. | ||
| /// </summary> | ||
| internal sealed class HangfireMetricsInstrumentation : IDisposable | ||
| { | ||
| private readonly List<object> filters = new(); | ||
|
|
||
| public HangfireMetricsInstrumentation(HangfireMetricsInstrumentationOptions options) | ||
| { | ||
| this.AddFilter(new HangfireMetricsJobFilterAttribute(options)); | ||
| this.AddFilter(new HangfireMetricsStateFilter(options)); | ||
| this.AddFilter(new HangfireMetricsErrorFilterAttribute(options)); | ||
|
gorbach marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Only register pending duration filter if enabled (requires DB call per job) | ||
| if (options.RecordQueueLatency) | ||
| { | ||
| this.AddFilter(new HangfirePendingDurationFilterAttribute(options)); | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| foreach (var filter in this.filters) | ||
| { | ||
| GlobalJobFilters.Filters.Remove(filter); | ||
| } | ||
| } | ||
|
|
||
| private void AddFilter(object filter) | ||
| { | ||
| this.filters.Add(filter); | ||
| GlobalJobFilters.Filters.Add(filter); | ||
| } | ||
| } | ||
81 changes: 81 additions & 0 deletions
81
...penTelemetry.Instrumentation.Hangfire/Implementation/HangfireMetricsJobFilterAttribute.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| using System.Diagnostics; | ||
| using Hangfire.Common; | ||
| using Hangfire.Server; | ||
| using OpenTelemetry.Metrics; | ||
|
|
||
| namespace OpenTelemetry.Instrumentation.Hangfire.Implementation; | ||
|
|
||
| /// <summary> | ||
| /// Hangfire filter that records OpenTelemetry metrics for job execution. | ||
| /// </summary> | ||
| internal sealed class HangfireMetricsJobFilterAttribute : JobFilterAttribute, IServerFilter | ||
| { | ||
| private const string StopwatchKey = "OpenTelemetry.Metrics.Stopwatch"; | ||
| private readonly HangfireMetricsInstrumentationOptions options; | ||
|
|
||
| #pragma warning disable CA1019 // Define accessors for attribute arguments | ||
| public HangfireMetricsJobFilterAttribute(HangfireMetricsInstrumentationOptions options) | ||
| #pragma warning restore CA1019 // Define accessors for attribute arguments | ||
| { | ||
| this.options = options ?? throw new ArgumentNullException(nameof(options)); | ||
| } | ||
|
|
||
| public void OnPerforming(PerformingContext performingContext) | ||
| { | ||
| performingContext.Items[StopwatchKey] = Stopwatch.StartNew(); | ||
| } | ||
|
|
||
| public void OnPerformed(PerformedContext performedContext) | ||
| { | ||
| // Get recurring job ID if this job was triggered by a recurring job | ||
| string? recurringJobId = null; | ||
| try | ||
| { | ||
| recurringJobId = performedContext.Connection.GetJobParameter( | ||
| performedContext.BackgroundJob.Id, | ||
| "RecurringJobId"); | ||
| } | ||
| catch | ||
| { | ||
| // If we can't get the recurring job ID, treat it as a non-recurring job | ||
| } | ||
|
|
||
| var backgroundJob = performedContext.BackgroundJob; | ||
| var displayNameFunc = this.options.DisplayNameFunc; | ||
|
|
||
| // Record execution count (without state attribute per semantic conventions) | ||
| var countTags = HangfireTagBuilder.BuildExecutionCountTags( | ||
| backgroundJob, | ||
| displayNameFunc, | ||
| performedContext.Exception); | ||
|
|
||
| HangfireMetrics.ExecutionCount.Add(1, countTags); | ||
|
|
||
| // Record execution duration (with state="executing" to differentiate from pending phase) | ||
| if (performedContext.Items.TryGetValue(StopwatchKey, out var stopwatchObj) && stopwatchObj is Stopwatch stopwatch) | ||
| { | ||
| stopwatch.Stop(); | ||
| var duration = stopwatch.Elapsed.TotalSeconds; | ||
|
|
||
| var durationTags = HangfireTagBuilder.BuildExecutionTags( | ||
| backgroundJob, | ||
| displayNameFunc, | ||
| performedContext.Exception, | ||
| workflowState: HangfireTagBuilder.StateExecuting); | ||
|
|
||
| HangfireMetrics.ExecutionDuration.Record(duration, durationTags); | ||
| } | ||
|
|
||
| // Record workflow-level metrics (includes trigger type) | ||
| var workflowTags = HangfireTagBuilder.BuildWorkflowTags( | ||
| backgroundJob, | ||
| displayNameFunc, | ||
| performedContext.Exception, | ||
| recurringJobId); | ||
|
|
||
| HangfireMetrics.WorkflowCount.Add(1, workflowTags); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.