diff --git a/docs/guide/http/mediator.md b/docs/guide/http/mediator.md
index 6bc891d76..d46bd838f 100644
--- a/docs/guide/http/mediator.md
+++ b/docs/guide/http/mediator.md
@@ -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
+
+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.
diff --git a/docs/guide/logging.md b/docs/guide/logging.md
index 2d6939da5..6e99b4fb4 100644
--- a/docs/guide/logging.md
+++ b/docs/guide/logging.md
@@ -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
+
+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.
diff --git a/docs/tutorials/mediator.md b/docs/tutorials/mediator.md
index 75df0194a..34d624cdf 100644
--- a/docs/tutorials/mediator.md
+++ b/docs/tutorials/mediator.md
@@ -258,3 +258,21 @@ Using the `IMessageBus.Invoke(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
+
+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.
diff --git a/src/Testing/CoreTests/Acceptance/invoke_tracing_mode.cs b/src/Testing/CoreTests/Acceptance/invoke_tracing_mode.cs
new file mode 100644
index 000000000..ad192d4d7
--- /dev/null
+++ b/src/Testing/CoreTests/Acceptance/invoke_tracing_mode.cs
@@ -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(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(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(new SingleLoggerFactory(logger));
+ })
+ .Build();
+
+ await host.StartAsync();
+
+ try
+ {
+ await Should.ThrowAsync(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(new SingleLoggerFactory(logger));
+ })
+ .Build();
+
+ await host.StartAsync();
+
+ try
+ {
+ await Should.ThrowAsync(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");
+ }
+}
+
+///
+/// Simple logger that captures formatted messages for test assertions
+///
+internal class CapturingLogger : ILogger
+{
+ public List Messages { get; } = new();
+ public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new();
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
+ Func 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 state) where TState : notnull => null;
+}
+
+///
+/// Logger factory that returns the same logger instance for all categories
+///
+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() { }
+}
diff --git a/src/Wolverine/Runtime/Handlers/Executor.cs b/src/Wolverine/Runtime/Handlers/Executor.cs
index 11f7fb698..cb6c8eeac 100644
--- a/src/Wolverine/Runtime/Handlers/Executor.cs
+++ b/src/Wolverine/Runtime/Handlers/Executor.cs
@@ -263,7 +263,7 @@ public static IExecutor Build(IWolverineRuntime runtime, ObjectPool
+/// An alternative Executor implementation that adds full structured logging to
+/// InvokeAsync() / InvokeInlineAsync() calls — the same log messages that
+/// transport-received messages already emit. Activated by setting
+/// on .
+///
+internal class TracingExecutor : IExecutor
+{
+ public const int MessageSucceededEventId = 104;
+ public const int MessageFailedEventId = 105;
+ public const int ExecutionStartedEventId = 102;
+ public const int ExecutionFinishedEventId = 103;
+
+ private readonly ObjectPool _contextPool;
+ private readonly ILogger _logger;
+ private readonly IMessageTracker _tracker;
+ private readonly FailureRuleCollection _rules;
+ private readonly TimeSpan _timeout;
+ private readonly string _messageTypeName;
+
+ private readonly Action _executionStarted;
+ private readonly Action _executionFinished;
+ private readonly Action _messageSucceeded;
+ private readonly Action _messageFailed;
+
+ public TracingExecutor(ObjectPool contextPool, IWolverineRuntime runtime,
+ IMessageHandler handler, FailureRuleCollection rules, TimeSpan timeout)
+ : this(contextPool, runtime.LoggerFactory.CreateLogger(handler.MessageType), handler,
+ runtime.MessageTracking, rules, timeout)
+ {
+ }
+
+ public TracingExecutor(ObjectPool contextPool, ILogger logger,
+ IMessageHandler handler, IMessageTracker tracker, FailureRuleCollection rules, TimeSpan timeout)
+ {
+ _contextPool = contextPool;
+ Handler = handler;
+ _tracker = tracker;
+ _rules = rules;
+ _timeout = timeout;
+ _logger = logger;
+
+ _messageTypeName = handler.MessageType.ToMessageTypeName();
+
+ _messageSucceeded =
+ LoggerMessage.Define(handler.SuccessLogLevel, MessageSucceededEventId,
+ "Successfully processed message {Name}#{envelope} from {ReplyUri}");
+
+ _messageFailed = LoggerMessage.Define(LogLevel.Error, MessageFailedEventId,
+ "Failed to process message {Name}#{envelope} from {ReplyUri}");
+
+ _executionStarted = LoggerMessage.Define(handler.ProcessingLogLevel,
+ ExecutionStartedEventId,
+ "{CorrelationId}: Started processing {Name}#{Id}");
+
+ _executionFinished = LoggerMessage.Define(handler.ProcessingLogLevel,
+ ExecutionFinishedEventId,
+ "{CorrelationId}: Finished processing {Name}#{Id}");
+ }
+
+ public IMessageHandler Handler { get; }
+
+ public async Task InvokeInlineAsync(Envelope envelope, CancellationToken cancellation)
+ {
+ using var activity = Handler.TelemetryEnabled ? WolverineTracing.StartExecuting(envelope) : null;
+
+ _tracker.ExecutionStarted(envelope);
+ _executionStarted(_logger, envelope.CorrelationId!, _messageTypeName, envelope.Id, null);
+
+ var context = _contextPool.Get();
+ context.ReadEnvelope(envelope, InvocationCallback.Instance);
+
+ envelope.Attempts = 1;
+
+ try
+ {
+ while (await InvokeAsync(context, cancellation) == InvokeResult.TryAgain)
+ {
+ envelope.Attempts++;
+ }
+
+ await context.FlushOutgoingMessagesAsync();
+ activity?.SetStatus(ActivityStatusCode.Ok);
+ _tracker.ExecutionFinished(envelope);
+ _messageSucceeded(_logger, _messageTypeName, envelope.Id,
+ envelope.Destination?.ToString() ?? "local", null);
+ }
+ catch (Exception e)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, e.GetType().Name);
+ _tracker.ExecutionFinished(envelope, e);
+ _messageFailed(_logger, _messageTypeName, envelope.Id,
+ envelope.Destination?.ToString() ?? "local", e);
+ throw;
+ }
+ finally
+ {
+ _contextPool.Return(context);
+ _executionFinished(_logger, envelope.CorrelationId!, _messageTypeName, envelope.Id, null);
+ activity?.Stop();
+ }
+ }
+
+ public async Task InvokeAsync(object message, MessageBus bus, CancellationToken cancellation = default,
+ TimeSpan? timeout = null, DeliveryOptions? options = null)
+ {
+ var envelope = new Envelope(message)
+ {
+ ReplyUri = TransportConstants.RepliesUri,
+ ReplyRequested = typeof(T).ToMessageTypeName(),
+ ResponseType = typeof(T),
+ TenantId = options?.TenantId ?? bus.TenantId,
+ DoNotCascadeResponse = true
+ };
+
+ options?.Override(envelope);
+
+ bus.TrackEnvelopeCorrelation(envelope, Activity.Current);
+
+ await InvokeInlineAsync(envelope, cancellation);
+
+ if (envelope.Response == null)
+ {
+ return default!;
+ }
+
+ return (T)envelope.Response;
+ }
+
+ public Task InvokeAsync(object message, MessageBus bus, CancellationToken cancellation = default,
+ TimeSpan? timeout = null, DeliveryOptions? options = null)
+ {
+ var envelope = new Envelope(message)
+ {
+ TenantId = options?.TenantId ?? bus.TenantId
+ };
+
+ options?.Override(envelope);
+
+ bus.TrackEnvelopeCorrelation(envelope, Activity.Current);
+ return InvokeInlineAsync(envelope, cancellation);
+ }
+
+ public async Task ExecuteAsync(MessageContext context, CancellationToken cancellation)
+ {
+ var envelope = context.Envelope;
+ _tracker.ExecutionStarted(envelope!);
+ _executionStarted(_logger, envelope!.CorrelationId!, _messageTypeName, envelope.Id, null);
+
+ envelope.Attempts++;
+
+ using var timeout = new CancellationTokenSource(_timeout);
+ using var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellation);
+
+ try
+ {
+ await Handler.HandleAsync(context, combined.Token);
+ if (context.Envelope!.ReplyRequested.IsNotEmpty())
+ {
+ await context.AssertAnyRequiredResponseWasGenerated();
+ }
+
+ Activity.Current?.SetStatus(ActivityStatusCode.Ok);
+
+ _messageSucceeded(_logger, _messageTypeName, envelope.Id,
+ envelope.Destination!.ToString(), null);
+
+ _tracker.ExecutionFinished(envelope);
+
+ return MessageSucceededContinuation.Instance;
+ }
+ catch (Exception e)
+ {
+ _messageFailed(_logger, _messageTypeName, envelope.Id, envelope.Destination!.ToString(), e);
+
+ _tracker
+ .ExecutionFinished(envelope, e);
+
+ await context.ClearAllAsync();
+
+ Activity.Current?.SetStatus(ActivityStatusCode.Error, e.GetType().Name);
+ return _rules.DetermineExecutionContinuation(e, envelope);
+ }
+ finally
+ {
+ _executionFinished(_logger, envelope.CorrelationId!, _messageTypeName, envelope.Id, null);
+ }
+ }
+
+ public async Task InvokeAsync(MessageContext context, CancellationToken cancellation)
+ {
+ if (context.Envelope == null)
+ {
+ throw new ArgumentOutOfRangeException(nameof(context.Envelope));
+ }
+
+ try
+ {
+ await Handler.HandleAsync(context, cancellation);
+ if (context.Envelope.ReplyRequested.IsNotEmpty())
+ {
+ await context.AssertAnyRequiredResponseWasGenerated();
+ }
+
+ return InvokeResult.Success;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Invocation of {Message} failed!", context.Envelope.Message);
+
+ var retry = _rules.TryFindInlineContinuation(e, context.Envelope);
+ if (retry == null)
+ {
+ throw;
+ }
+
+ return await retry
+ .ExecuteInlineAsync(context, context.Runtime, DateTimeOffset.UtcNow, Activity.Current, cancellation)
+ .ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/Wolverine/WolverineOptions.cs b/src/Wolverine/WolverineOptions.cs
index 4ea212327..ac59a567c 100644
--- a/src/Wolverine/WolverineOptions.cs
+++ b/src/Wolverine/WolverineOptions.cs
@@ -77,6 +77,27 @@ public enum UnknownMessageBehavior
DeadLetterQueue
}
+///
+/// Controls how much tracing and logging is applied to InvokeAsync() calls.
+/// By default, InvokeAsync uses lightweight tracking without detailed log messages.
+///
+public enum InvokeTracingMode
+{
+ ///
+ /// Default behavior. InvokeAsync() uses lightweight tracking without
+ /// emitting log messages for execution start/finish/success/failure.
+ ///
+ Lightweight,
+
+ ///
+ /// InvokeAsync() will emit the same structured log messages as transport-received
+ /// messages, including execution started, execution finished, message succeeded,
+ /// and message failed log entries. Use this mode when you need full observability
+ /// for messages invoked as an in-process mediator.
+ ///
+ Full
+}
+
public class MetricsOptions
{
///
@@ -172,6 +193,15 @@ public WolverineOptions(string? assemblyName)
///
public bool EnableRelayOfUserName { get; set; }
+ ///
+ /// Controls how much tracing and logging is applied to InvokeAsync() calls.
+ /// Default is which uses minimal tracking.
+ /// Set to to emit the same structured log messages
+ /// as transport-received messages, useful for full observability when using Wolverine
+ /// as an in-process mediator.
+ ///
+ public InvokeTracingMode InvokeTracing { get; set; } = InvokeTracingMode.Lightweight;
+
///
/// What is the policy within this application for whether or not it is valid to allow Service Location within
/// the generated code for message handlers or HTTP endpoints. Default is AllowedByWarn. Just keep in mind that