Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions docs/guide/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,25 @@ public const string TooManySenderFailures = "TooManySenderFailures";
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Wolverine/Runtime/WolverineTracing.cs#L27-L121' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_wolverine_open_telemetry_tracing_spans_and_activities' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Handler Type Tagging

Wolverine automatically tags Open Telemetry activity spans with the handler type name during message processing. This provides per-handler tracing visibility in observability backends like Jaeger, Zipkin, or Honeycomb without any additional configuration.

For both message handlers and Wolverine.HTTP endpoints, Wolverine emits the `handler.type` tag containing the full .NET type name of the handler class. For message handlers, the existing `message.handler` tag is also set with the same value for backward compatibility.

These tags are memoized as string literals in Wolverine's generated code, so there is no runtime cost for computing the handler type name on each request.

Example activity tags for a message handler:
```
handler.type = "MyApp.Handlers.OrderPlacedHandler"
message.handler = "MyApp.Handlers.OrderPlacedHandler"
```

Example activity tags for an HTTP endpoint:
```
handler.type = "MyApp.Endpoints.OrderEndpoint"
```

## Message Correlation

::: tip
Expand Down
38 changes: 38 additions & 0 deletions src/Http/Wolverine.Http/CodeGen/TagHttpHandlerPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Diagnostics;
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.Core.Reflection;
using Wolverine.Runtime;

namespace Wolverine.Http.CodeGen;

internal class TagHttpHandlerPolicy : IHttpPolicy
{
public void Apply(IReadOnlyList<HttpChain> chains, GenerationRules rules, IServiceContainer container)
{
foreach (var chain in chains)
{
chain.Middleware.Insert(0, new TagHttpHandlerFrame(chain));
}
}
}

internal class TagHttpHandlerFrame : SyncFrame
{
private readonly HttpChain _chain;

public TagHttpHandlerFrame(HttpChain chain)
{
_chain = chain;
}

public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
var handlerTypeName = _chain.Method.HandlerType.FullNameInCode();
writer.WriteLine(
$"{typeof(Activity).FullNameInCode()}.{nameof(Activity.Current)}?.{nameof(Activity.SetTag)}(\"{WolverineTracing.HandlerType}\", \"{handlerTypeName}\");");

Next?.GenerateCode(method, writer);
}
}
1 change: 1 addition & 0 deletions src/Http/Wolverine.Http/WolverineHttpOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public WolverineHttpOptions()
Policies.Add(new UserNamePolicy());
Policies.Add(new RequiredEntityPolicy());
Policies.Add(new HttpChainResponseCacheHeaderPolicy());
Policies.Add(new TagHttpHandlerPolicy());

Policies.Add(TenantIdDetection);
}
Expand Down
104 changes: 104 additions & 0 deletions src/Testing/CoreTests/Runtime/handler_type_activity_tagging.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Diagnostics;
using JasperFx.Core;
using Microsoft.Extensions.Hosting;
using Wolverine.Runtime;
using Wolverine.Tracking;
using Xunit;

namespace CoreTests.Runtime;

public class handler_type_activity_tagging : IAsyncLifetime
{
private IHost _host = null!;
private readonly List<Activity> _capturedActivities = new();
private ActivityListener _listener = null!;

public async Task InitializeAsync()
{
// Set up an ActivityListener to capture Wolverine activities
_listener = new ActivityListener
{
ShouldListenTo = source => source.Name == "Wolverine",
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStopped = activity => _capturedActivities.Add(activity)
};
ActivitySource.AddActivityListener(_listener);

_host = await Host.CreateDefaultBuilder()
.UseWolverine().StartAsync();
}

public async Task DisposeAsync()
{
_listener.Dispose();
await _host.StopAsync();
_host.Dispose();
}

[Fact]
public async Task should_tag_handler_type_on_activity_for_message_handler()
{
await _host.InvokeMessageAndWaitAsync(new TracingTestMessage("hello"));

// Give a moment for activities to be captured
await Task.Delay(100.Milliseconds());

var handlerActivities = _capturedActivities
.Where(a => a.GetTagItem(WolverineTracing.HandlerType) != null)
.ToList();

handlerActivities.ShouldNotBeEmpty();

var handlerTypeTag = handlerActivities.First()
.GetTagItem(WolverineTracing.HandlerType) as string;
handlerTypeTag.ShouldNotBeNull();
handlerTypeTag.ShouldContain(nameof(TracingTestMessageHandler));
}

[Fact]
public async Task should_tag_message_handler_on_activity_for_message_handler()
{
await _host.InvokeMessageAndWaitAsync(new TracingTestMessage("hello"));

await Task.Delay(100.Milliseconds());

var handlerActivities = _capturedActivities
.Where(a => a.GetTagItem(WolverineTracing.MessageHandler) != null)
.ToList();

handlerActivities.ShouldNotBeEmpty();

var messageHandlerTag = handlerActivities.First()
.GetTagItem(WolverineTracing.MessageHandler) as string;
messageHandlerTag.ShouldNotBeNull();
messageHandlerTag.ShouldContain(nameof(TracingTestMessageHandler));
}

[Fact]
public async Task handler_type_and_message_handler_tags_should_have_same_value()
{
await _host.InvokeMessageAndWaitAsync(new TracingTestMessage("hello"));

await Task.Delay(100.Milliseconds());

var activity = _capturedActivities
.FirstOrDefault(a => a.GetTagItem(WolverineTracing.HandlerType) != null);

activity.ShouldNotBeNull();

var handlerType = activity.GetTagItem(WolverineTracing.HandlerType) as string;
var messageHandler = activity.GetTagItem(WolverineTracing.MessageHandler) as string;

handlerType.ShouldBe(messageHandler);
}
}

public record TracingTestMessage(string Text);

public static class TracingTestMessageHandler
{
public static void Handle(TracingTestMessage message)
{
// no-op handler for tracing tests
}
}
7 changes: 5 additions & 2 deletions src/Wolverine/Logging/TagHandlerFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
if (_chain.HandlerCalls().Length == 1)
{
var handlerTypeName = _chain.HandlerCalls()[0].HandlerType.FullNameInCode();
writer.WriteLine(
$"{typeof(Activity).FullNameInCode()}.{nameof(Activity.Current)}?.{nameof(Activity.SetTag)}(\"{WolverineTracing.MessageHandler}\", \"{_chain.HandlerCalls()[0].HandlerType.FullNameInCode()}\");");
$"{typeof(Activity).FullNameInCode()}.{nameof(Activity.Current)}?.{nameof(Activity.SetTag)}(\"{WolverineTracing.MessageHandler}\", \"{handlerTypeName}\");");
writer.WriteLine(
$"{typeof(Activity).FullNameInCode()}.{nameof(Activity.Current)}?.{nameof(Activity.SetTag)}(\"{WolverineTracing.HandlerType}\", \"{handlerTypeName}\");");
}

Next?.GenerateCode(method, writer);
}
}
1 change: 1 addition & 0 deletions src/Wolverine/Runtime/WolverineTracing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal static class WolverineTracing
public const string MessagingDestination = "messaging.destination"; // Use the destination Uri

public const string MessageHandler = "message.handler";
public const string HandlerType = "handler.type";

public const string
MessagingDestinationKind =
Expand Down
Loading