Skip to content
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

[Instrumentation.Hangfire] Add option to record exceptions #719

Merged
merged 11 commits into from
Oct 21, 2022
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Instrumentation.Hangfire/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Add support to optionally record exceptions
([#719](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/719))

* Update OTel API version to `1.3.1`.
([#631](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/631))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// <copyright file="HangfireInstrumentationOptions.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

namespace OpenTelemetry.Trace;

/// <summary>
/// Options for hangfire jobs instrumentation.
/// </summary>
public class HangfireInstrumentationOptions
{
/// <summary>
/// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not.
/// </summary>
/// <remarks>
/// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md.
/// </remarks>
public bool RecordException { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ namespace OpenTelemetry.Instrumentation.Hangfire.Implementation;
using global::Hangfire.Common;
using global::Hangfire.Server;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Trace;

internal class HangfireInstrumentationJobFilterAttribute : JobFilterAttribute, IServerFilter, IClientFilter
{
private readonly HangfireInstrumentationOptions options;

public HangfireInstrumentationJobFilterAttribute(HangfireInstrumentationOptions options)
{
this.options = options;
}

public void OnPerforming(PerformingContext performingContext)
{
// Short-circuit if nobody is listening
Expand Down Expand Up @@ -72,7 +80,7 @@ public void OnPerformed(PerformedContext performedContext)
{
if (performedContext.Exception != null)
{
activity.SetStatus(ActivityStatusCode.Error, performedContext.Exception.Message);
this.SetStatusAndRecordException(activity, performedContext.Exception);
}

activity.Dispose();
Expand Down Expand Up @@ -111,4 +119,14 @@ private static IEnumerable<string> ExtractActivityProperties(Dictionary<string,
{
return telemetryData.ContainsKey(key) ? new[] { telemetryData[key] } : Enumerable.Empty<string>();
}

private void SetStatusAndRecordException(Activity activity, System.Exception exception)
{
activity.SetStatus(ActivityStatusCode.Error, exception.Message);

if (this.options.RecordException)
{
activity.RecordException(exception);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

namespace OpenTelemetry.Trace;

using System;
using OpenTelemetry.Instrumentation.Hangfire.Implementation;
using OpenTelemetry.Internal;

Expand All @@ -28,12 +29,18 @@ public static class TracerProviderBuilderExtensions
/// Adds Hangfire instrumentation to the tracer provider.
/// </summary>
/// <param name="builder"><see cref="TracerProviderBuilder"/> being configured.</param>
/// <param name="configureHangfireInstrumentationOptions">Callback action for configuring <see cref="HangfireInstrumentationOptions"/>.</param>
/// <returns>The instance of <see cref="TracerProviderBuilder"/> to chain the calls.</returns>
public static TracerProviderBuilder AddHangfireInstrumentation(this TracerProviderBuilder builder)
public static TracerProviderBuilder AddHangfireInstrumentation(
this TracerProviderBuilder builder,
Action<HangfireInstrumentationOptions> configureHangfireInstrumentationOptions = null)
{
Guard.ThrowIfNull(builder);

Hangfire.GlobalJobFilters.Filters.Add(new HangfireInstrumentationJobFilterAttribute());
var options = new HangfireInstrumentationOptions();
configureHangfireInstrumentationOptions?.Invoke(options);

Hangfire.GlobalJobFilters.Filters.Add(new HangfireInstrumentationJobFilterAttribute(options));

return builder.AddSource(HangfireInstrumentation.ActivitySourceName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,56 @@ public async Task Should_Create_Activity_With_Status_Error_When_Job_Failed()
Assert.Contains("JOB TestJob.ThrowException", activity.DisplayName);
Assert.Equal(ActivityKind.Internal, activity.Kind);
Assert.Equal(ActivityStatusCode.Error, activity.Status);
Assert.NotNull(activity.StatusDescription);
Assert.Contains("An exception occurred during performance of the job.", activity.StatusDescription);
Assert.Empty(activity.Events);
}

[Fact]
public async Task Should_Create_Activity_With_Exception_Event_When_Job_Failed_And_Record_Exception_Is_True()
{
// Arrange
var exportedItems = new List<Activity>();
using var tel = Sdk.CreateTracerProviderBuilder()
.AddHangfireInstrumentation(options => options.RecordException = true)
.AddInMemoryExporter(exportedItems)
.Build();

// Act
var jobId = BackgroundJob.Enqueue<TestJob>(x => x.ThrowException());
await this.WaitJobProcessedAsync(jobId, 5);

// Assert
Assert.Single(exportedItems, i => i.GetTagItem("job.id") as string == jobId);
var activity = exportedItems.Single(i => i.GetTagItem("job.id") as string == jobId);
Assert.Contains("JOB TestJob.ThrowException", activity.DisplayName);
Assert.Equal(ActivityKind.Internal, activity.Kind);
Assert.Equal(ActivityStatusCode.Error, activity.Status);
Assert.Contains("An exception occurred during performance of the job.", activity.StatusDescription);
Assert.Single(activity.Events, evt => evt.Name == "exception");
}

[Fact]
public async Task Should_Create_Activity_Without_Exception_Event_When_Job_Failed_And_Record_Exception_Is_False()
{
// Arrange
var exportedItems = new List<Activity>();
using var tel = Sdk.CreateTracerProviderBuilder()
.AddHangfireInstrumentation(options => options.RecordException = false)
.AddInMemoryExporter(exportedItems)
.Build();

// Act
var jobId = BackgroundJob.Enqueue<TestJob>(x => x.ThrowException());
await this.WaitJobProcessedAsync(jobId, 5);

// Assert
Assert.Single(exportedItems, i => i.GetTagItem("job.id") as string == jobId);
var activity = exportedItems.Single(i => i.GetTagItem("job.id") as string == jobId);
Assert.Contains("JOB TestJob.ThrowException", activity.DisplayName);
Assert.Equal(ActivityKind.Internal, activity.Kind);
Assert.Equal(ActivityStatusCode.Error, activity.Status);
Assert.Contains("An exception occurred during performance of the job.", activity.StatusDescription);
Assert.Empty(activity.Events);
}

private async Task WaitJobProcessedAsync(string jobId, int timeToWaitInSeconds)
Expand Down