Skip to content

Commit

Permalink
[Instrumentation.Hangfire] Add option to record exceptions (#719)
Browse files Browse the repository at this point in the history
  • Loading branch information
vitor-pinto-maersk authored Oct 21, 2022
1 parent ee5c8d6 commit d21d3ca
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 4 deletions.
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

0 comments on commit d21d3ca

Please sign in to comment.