Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
2e804df
[Instrumentation.Hangfire] Add metrics support
gorbach Oct 21, 2025
08198ac
Align metrics with workflow semconv: use duration{state=pending}, mo…
gorbach Oct 22, 2025
d43ee2c
Configure metrics in HangfireMetricsInstrumentationOptions class, add…
gorbach Oct 22, 2025
bc135e8
Add workflow.status metric to track complete job lifecycle
gorbach Oct 24, 2025
96d8aa8
Use job formatter from hangfire
gorbach Oct 25, 2025
742b2a0
Fix tests
gorbach Oct 25, 2025
aa3ac32
Fix and speed up tests
gorbach Oct 25, 2025
1f909bd
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
42476f8
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
accd8bc
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
302542a
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
5af844c
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
eae641c
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
38883b8
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
2e7cdff
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
3254ed2
Update src/OpenTelemetry.Instrumentation.Hangfire/Implementation/Hang…
gorbach Oct 26, 2025
693d1be
build fix
gorbach Oct 26, 2025
044e500
Rename metrics/attributes
gorbach Oct 26, 2025
b58920e
Remove `DisplayNameFunc` from PublicAPI.Unshipped.txt and README.md
gorbach Oct 26, 2025
cef13e5
Merge branch 'main' into feature/2075-hangfire-telemetry
gorbach Oct 26, 2025
90022d0
remove wrong link
gorbach Oct 28, 2025
ded8e66
update CHANGELOG.md
gorbach Oct 28, 2025
64514b8
Merge branch 'main' into feature/2075-hangfire-telemetry
gorbach Oct 29, 2025
2c494a9
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 15, 2025
ecd3163
update CHANGELOG.md
gorbach Nov 15, 2025
85aa0f3
Merge branch 'main' into feature/2075-hangfire-telemetry
gorbach Nov 15, 2025
d4186b4
CHANGELOG fix
Kielek Nov 17, 2025
6ec1c12
Fix build
gorbach Nov 17, 2025
98d3925
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 18, 2025
6683846
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 18, 2025
39eae7a
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 18, 2025
6b24590
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 18, 2025
ffc2a14
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 18, 2025
bf60f2b
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 18, 2025
e4dfc84
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 18, 2025
c5f113e
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 18, 2025
53a98e2
Update test/OpenTelemetry.Instrumentation.Hangfire.Tests/HangfireMetr…
gorbach Nov 18, 2025
a4c3dd3
Update test/OpenTelemetry.Instrumentation.Hangfire.Tests/HangfireMetr…
gorbach Nov 18, 2025
762e83b
Fix lint warnings
gorbach Nov 18, 2025
c75d4e3
reformat code with dotnet format
gorbach Nov 18, 2025
d0dbb8b
reformat code with dotnet format
gorbach Nov 18, 2025
afd0b94
Make HangfireTests assembly not signed
gorbach Nov 19, 2025
0694238
make lines in README.md shorter
gorbach Nov 20, 2025
2872f35
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 20, 2025
9960ebd
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 20, 2025
3b61b6a
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 20, 2025
422862c
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 20, 2025
00f1924
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 20, 2025
2b19e59
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 20, 2025
aabc0a2
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 20, 2025
ac8dbfa
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 20, 2025
27f9dd8
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 21, 2025
99cae94
Update src/OpenTelemetry.Instrumentation.Hangfire/README.md
gorbach Nov 21, 2025
4b95bb7
Merge branch 'main' into feature/2075-hangfire-telemetry
Kielek Nov 21, 2025
aa2d422
md lint
Kielek Nov 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
OpenTelemetry.Metrics.MeterProviderBuilderExtensions
OpenTelemetry.Trace.HangfireInstrumentationOptions
OpenTelemetry.Trace.HangfireInstrumentationOptions.DisplayNameFunc.get -> System.Func<Hangfire.BackgroundJob!, string!>!
OpenTelemetry.Trace.HangfireInstrumentationOptions.DisplayNameFunc.set -> void
Expand All @@ -7,6 +8,13 @@ OpenTelemetry.Trace.HangfireInstrumentationOptions.HangfireInstrumentationOption
OpenTelemetry.Trace.HangfireInstrumentationOptions.RecordException.get -> bool
OpenTelemetry.Trace.HangfireInstrumentationOptions.RecordException.set -> void
OpenTelemetry.Trace.TracerProviderBuilderExtensions
static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHangfireInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder!
static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHangfireInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, string? name, System.Action<OpenTelemetry.Metrics.HangfireMetricsInstrumentationOptions!>? configure) -> OpenTelemetry.Metrics.MeterProviderBuilder!
static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHangfireInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, System.Action<OpenTelemetry.Metrics.HangfireMetricsInstrumentationOptions!>? configure) -> OpenTelemetry.Metrics.MeterProviderBuilder!
OpenTelemetry.Metrics.HangfireMetricsInstrumentationOptions
OpenTelemetry.Metrics.HangfireMetricsInstrumentationOptions.HangfireMetricsInstrumentationOptions() -> void
OpenTelemetry.Metrics.HangfireMetricsInstrumentationOptions.RecordQueueLatency.get -> bool
OpenTelemetry.Metrics.HangfireMetricsInstrumentationOptions.RecordQueueLatency.set -> void
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHangfireInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder!
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHangfireInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, string? name, System.Action<OpenTelemetry.Trace.HangfireInstrumentationOptions!>? configure) -> OpenTelemetry.Trace.TracerProviderBuilder!
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHangfireInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action<OpenTelemetry.Trace.HangfireInstrumentationOptions!>? configure) -> OpenTelemetry.Trace.TracerProviderBuilder!
4 changes: 4 additions & 0 deletions src/OpenTelemetry.Instrumentation.Hangfire/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

* Add metrics instrumentation following a POC/draft definition of workflow metrics
defined as part of [semantic-conventions/#1688](https://github.com/open-telemetry/semantic-conventions/issues/1688).
([#3258](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3258))

## 1.14.0-beta.1

Released 2025-Nov-13
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics.Metrics;
using OpenTelemetry.Internal;

namespace OpenTelemetry.Instrumentation.Hangfire.Implementation;

/// <summary>
/// Centralized metrics definitions for Hangfire instrumentation.
/// </summary>
internal static class HangfireMetrics
{
/// <summary>
/// Counter for the number of task executions which have been initiated.
/// Follows OpenTelemetry workflow semantic conventions.
/// </summary>
public static readonly Counter<long> ExecutionOutcome =
Meter!.CreateCounter<long>(
WorkflowMetricNames.ExecutionOutcome,
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>(
WorkflowMetricNames.ExecutionDuration,
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>(
WorkflowMetricNames.ExecutionStatus,
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>(
WorkflowMetricNames.ExecutionErrors,
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> WorkflowOutcome =
Meter!.CreateCounter<long>(
WorkflowMetricNames.WorkflowOutcome,
unit: "{workflows}",
description: "The number of workflow instances which have been initiated.");

/// <summary>
/// UpDownCounter for the number of actively running workflows grouped by definition and state.
/// Follows OpenTelemetry workflow semantic conventions.
/// In Hangfire, this tracks workflows that haven't entered the execution pipeline yet (e.g., scheduled jobs).
/// </summary>
public static readonly UpDownCounter<long> WorkflowStatus =
Meter!.CreateUpDownCounter<long>(
WorkflowMetricNames.WorkflowStatus,
unit: "{workflows}",
description: "The number of actively running workflows grouped by definition and the current state.");

/// <summary>
/// Returns Hangfire metric name.
/// </summary>
internal static readonly string MeterName = Meter.Name;

private static Meter? meter;

/// <summary>
/// Gets the meter instance for all Hangfire metrics.
/// </summary>
private static Meter Meter => meter ??= new(typeof(HangfireMetrics).Assembly.GetName().Name, typeof(HangfireMetrics).Assembly.GetPackageVersion());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using Hangfire.Common;
using Hangfire.Server;

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
{
public void OnPerforming(PerformingContext performingContext)
{
}

public void OnPerformed(PerformedContext performedContext)
{
if (performedContext.Exception != null)
{
var errorTags = HangfireTagBuilder.BuildErrorTags(performedContext);

HangfireMetrics.ExecutionErrors.Add(1, errorTags);
}
}
}
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());
this.AddFilter(new HangfireMetricsStateFilter());
this.AddFilter(new HangfireMetricsErrorFilterAttribute());

// Only register pending duration filter if enabled (requires DB call per job)
if (options.RecordQueueLatency)
{
this.AddFilter(new HangfirePendingDurationFilterAttribute());
}
}

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
using Hangfire.Common;
using Hangfire.Server;

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";

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;

// Record execution outcome (without state attribute per semantic conventions)
var countTags = HangfireTagBuilder.BuildExecutionCountTags(
backgroundJob,
performedContext.Exception);

HangfireMetrics.ExecutionOutcome.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,
performedContext.Exception,
workflowState: WorkflowAttributes.WorkflowStateValues.Executing);

HangfireMetrics.ExecutionDuration.Record(duration, durationTags);
}

// Record workflow-level metrics (includes trigger type)
var workflowTags = HangfireTagBuilder.BuildWorkflowTags(
backgroundJob,
performedContext.Exception,
recurringJobId);

HangfireMetrics.WorkflowOutcome.Add(1, workflowTags);
}
}
Loading