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
18 changes: 17 additions & 1 deletion docs/guide/http/mediator.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,21 @@ With this mechanism, Wolverine is able to optimize the runtime function for Mini
and some internal dictionary lookups compared to the "classic mediator" approach at the top.

This approach is potentially valuable for cases where you want to process a command or event message both through messaging
or direct invocation and also want to execute the same message through an HTTP endpoint.
or direct invocation and also want to execute the same message through an HTTP endpoint.

## Full Tracing for InvokeAsync <Badge type="tip" text="5.25" />

When using Wolverine as a mediator behind HTTP endpoints, `InvokeAsync()` does not emit detailed log messages
by default. You can opt into full tracing to get the same structured logs as transport-received messages:

```csharp
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Emit the same structured log messages for InvokeAsync()
// as Wolverine does for transport-received messages
opts.InvokeTracing = InvokeTracingMode.Full;
}).StartAsync();
```

See [Instrumentation and Metrics](/guide/logging) for more details on Wolverine's logging capabilities.
27 changes: 27 additions & 0 deletions docs/guide/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,33 @@ public static void Configure(HandlerChain chain)

will be called by Wolverine to apply message type specific overrides to Wolverine's message handling.

## Full Tracing for InvokeAsync <Badge type="tip" text="5.25" />

By default, messages processed via `InvokeAsync()` (Wolverine's in-process mediator) use lightweight tracking
without emitting the same structured log messages that transport-received messages produce. If you need full
observability for inline invocations — for example, when using Wolverine purely as a mediator within an HTTP
application — you can opt into full tracing:

```csharp
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Emit the same structured log messages for InvokeAsync()
// as Wolverine does for transport-received messages
opts.InvokeTracing = InvokeTracingMode.Full;
}).StartAsync();
```

When `InvokeTracingMode.Full` is enabled, `InvokeAsync()` will emit:
- **Execution started** — logged at the configured `MessageExecutionLogLevel` (default `Debug`)
- **Message succeeded** — logged at the configured `MessageSuccessLogLevel` (default `Information`)
- **Message failed** — logged at `Error` level with the exception
- **Execution finished** — logged at the configured `MessageExecutionLogLevel`

These are the same log messages and event IDs that Wolverine already uses for messages received from
external transports like RabbitMQ, Kafka, or Azure Service Bus. This makes it easy to use a single
log query to observe all message processing regardless of how messages enter the system.

## Configuring Health Check Tracing

Wolverine's node agent controller performs health checks periodically (every 10 seconds by default) to maintain node assignments and cluster state. By default, these health checks emit Open Telemetry traces named `wolverine_node_assignments`, which can result in high trace volumes in observability platforms.
Expand Down
18 changes: 18 additions & 0 deletions docs/tutorials/mediator.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,21 @@ Using the `IMessageBus.Invoke<T>(message)` overload, the returned `ItemCreated`
of the message handler is returned from the `Invoke()` message. To be perfectly clear, this only
works if the message handler method returns a cascading message of the exact same type of the
designated `T` parameter.

## Full Tracing for InvokeAsync <Badge type="tip" text="5.25" />

By default, `InvokeAsync()` uses lightweight tracking without emitting detailed log messages. When using
Wolverine purely as an in-process mediator, you may want full observability for every message invocation.
You can opt into full tracing mode to emit the same structured log messages that transport-received messages produce:

```csharp
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Emit the same structured log messages for InvokeAsync()
// as Wolverine does for transport-received messages
opts.InvokeTracing = InvokeTracingMode.Full;
}).StartAsync();
```

See [Instrumentation and Metrics](/guide/logging) for more details on Wolverine's logging capabilities.
206 changes: 206 additions & 0 deletions src/Testing/CoreTests/Acceptance/invoke_tracing_mode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Wolverine;
using Wolverine.Tracking;
using Xunit;
using Shouldly;

namespace CoreTests.Acceptance;

public class invoke_tracing_mode
{
[Fact]
public void default_invoke_tracing_should_be_lightweight()
{
var options = new WolverineOptions();
options.InvokeTracing.ShouldBe(InvokeTracingMode.Lightweight);
}

[Fact]
public async Task invoke_with_lightweight_tracing_does_not_emit_execution_log_messages()
{
var logger = new CapturingLogger();

var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.InvokeTracing = InvokeTracingMode.Lightweight;
opts.Services.AddSingleton<ILoggerFactory>(new SingleLoggerFactory(logger));
})
.Build();

await host.StartAsync();

try
{
await host.InvokeMessageAndWaitAsync(new TracingTestMessage("hello"));

// Lightweight mode should NOT produce execution started/finished log messages
logger.Messages.ShouldNotContain(m =>
m.Contains("Started processing") && m.Contains("TracingTestMessage"));
logger.Messages.ShouldNotContain(m =>
m.Contains("Successfully processed") && m.Contains("TracingTestMessage"));
}
finally
{
await host.StopAsync();
host.Dispose();
}
}

[Fact]
public async Task invoke_with_full_tracing_emits_execution_log_messages()
{
var logger = new CapturingLogger();

var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.InvokeTracing = InvokeTracingMode.Full;
opts.Services.AddSingleton<ILoggerFactory>(new SingleLoggerFactory(logger));
})
.Build();

await host.StartAsync();

try
{
await host.InvokeMessageAndWaitAsync(new TracingTestMessage("hello"));

// Full mode SHOULD produce execution started and succeeded log messages
logger.Messages.ShouldContain(m =>
m.Contains("Started processing") && m.Contains("TracingTestMessage"));
logger.Messages.ShouldContain(m =>
m.Contains("Successfully processed") && m.Contains("TracingTestMessage"));
}
finally
{
await host.StopAsync();
host.Dispose();
}
}

[Fact]
public async Task invoke_with_full_tracing_emits_failure_log_on_exception()
{
var logger = new CapturingLogger();

var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.InvokeTracing = InvokeTracingMode.Full;
opts.Services.AddSingleton<ILoggerFactory>(new SingleLoggerFactory(logger));
})
.Build();

await host.StartAsync();

try
{
await Should.ThrowAsync<InvalidOperationException>(async () =>
await host.InvokeMessageAndWaitAsync(new TracingTestFailingMessage()));

// Full mode SHOULD produce failed log message
logger.Messages.ShouldContain(m =>
m.Contains("Failed to process") && m.Contains("TracingTestFailingMessage"));
}
finally
{
await host.StopAsync();
host.Dispose();
}
}

[Fact]
public async Task invoke_with_full_tracing_emits_finished_log_on_exception()
{
var logger = new CapturingLogger();

var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.InvokeTracing = InvokeTracingMode.Full;
opts.Services.AddSingleton<ILoggerFactory>(new SingleLoggerFactory(logger));
})
.Build();

await host.StartAsync();

try
{
await Should.ThrowAsync<InvalidOperationException>(async () =>
await host.InvokeMessageAndWaitAsync(new TracingTestFailingMessage()));

// Should still log "Finished processing" even on failure
logger.Messages.ShouldContain(m =>
m.Contains("Finished processing") && m.Contains("TracingTestFailingMessage"));
}
finally
{
await host.StopAsync();
host.Dispose();
}
}
}

public record TracingTestMessage(string Text);

public static class TracingTestMessageHandler
{
public static void Handle(TracingTestMessage message)
{
// Simple handler — just completes successfully
}
}

public record TracingTestFailingMessage;

public static class TracingTestFailingMessageHandler
{
public static void Handle(TracingTestFailingMessage message)
{
throw new InvalidOperationException("Deliberate test failure");
}
}

/// <summary>
/// Simple logger that captures formatted messages for test assertions
/// </summary>
internal class CapturingLogger : ILogger
{
public List<string> Messages { get; } = new();
public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new();

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
var message = formatter(state, exception);
lock (Messages)
{
Messages.Add(message);
Entries.Add((logLevel, message, exception));
}
}

public bool IsEnabled(LogLevel logLevel) => true;

public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
}

/// <summary>
/// Logger factory that returns the same logger instance for all categories
/// </summary>
internal class SingleLoggerFactory : ILoggerFactory
{
private readonly ILogger _logger;

public SingleLoggerFactory(ILogger logger)
{
_logger = logger;
}

public ILogger CreateLogger(string categoryName) => _logger;
public void AddProvider(ILoggerProvider provider) { }
public void Dispose() { }
}
14 changes: 12 additions & 2 deletions src/Wolverine/Runtime/Handlers/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public static IExecutor Build(IWolverineRuntime runtime, ObjectPool<MessageConte
handler = batching.BuildHandler((WolverineRuntime)runtime);
}
}

if (handler == null)
{
return new NoHandlerExecutor(messageType, (WolverineRuntime)runtime);
Expand All @@ -273,6 +273,11 @@ public static IExecutor Build(IWolverineRuntime runtime, ObjectPool<MessageConte
var timeoutSpan = chain?.DetermineMessageTimeout(runtime.Options) ?? 5.Seconds();
var rules = chain?.Failures.CombineRules(handlerGraph.Failures) ?? handlerGraph.Failures;

if (runtime.Options.InvokeTracing == InvokeTracingMode.Full)
{
return new TracingExecutor(contextPool, runtime, handler, rules, timeoutSpan);
}

return new Executor(contextPool, runtime, handler, rules, timeoutSpan);
}

Expand All @@ -284,7 +289,12 @@ public static IExecutor Build(IWolverineRuntime runtime, ObjectPool<MessageConte
var rules = chain?.Failures.CombineRules(handlerGraph.Failures) ?? handlerGraph.Failures;

var logger = runtime.LoggerFactory.CreateLogger(handler.MessageType);


if (runtime.Options.InvokeTracing == InvokeTracingMode.Full)
{
return new TracingExecutor(contextPool, logger, handler, tracker, rules, timeoutSpan);
}

return new Executor(contextPool, logger, handler, tracker, rules, timeoutSpan);
}
}
Loading
Loading