Skip to content

Replace logging fallback with MEL-backed factory and AsyncLocal routing for acceptance tests#7758

Merged
danielmarbach merged 8 commits into
masterfrom
logging_fallback
May 19, 2026
Merged

Replace logging fallback with MEL-backed factory and AsyncLocal routing for acceptance tests#7758
danielmarbach merged 8 commits into
masterfrom
logging_fallback

Conversation

@danielmarbach
Copy link
Copy Markdown
Contributor

@danielmarbach danielmarbach commented May 17, 2026

Problem

When NServiceBus logs outside an endpoint slot scope (for example, from a static ILog field before startup or during shutdown), those logs currently fall through to System.Diagnostics.Trace. They are invisible to users and to the acceptance test framework.

There is also no way for downstream packages (such as Transactional Session) to attach endpoint context to logs when operating outside the message pipeline. The endpoint name and identifier are only available through the internal LogManager.BeginSlotScope mechanism.

The previous attempt to fix the first problem used a LogManager.UseFactory bridge in ScenarioRunner to route out-of-slot logs to the scenario's MEL factory. That approach mutates process-global static state, requires fragile teardown ordering, and cannot support parallel test execution.

Solution

Four changes address the production and test concerns independently, without global state mutation.

EndpointLoggingScope (new public type)

A DI-resolvable type that carries endpoint name and identifier as IReadOnlyList<KeyValuePair<string, object?>>, compatible with ILogger.BeginScope. Downstream packages can enrich their logs with endpoint context without depending on internal LogManager APIs:

var scope = provider.GetRequiredService<EndpointLoggingScope>();
using var _ = logger.BeginScope(scope);

Registered as a singleton per endpoint in EndpointCreator.Configure. LogScopeStates was refactored to share the scope entry construction logic.

MEL-backed fallback factory

FallbackLoggerFactory now builds an isolated ServiceCollection with RollingLoggerProvider and ColoredConsoleLoggerProvider. Out-of-slot logs go to a real rolling file and console output instead of System.Diagnostics.Trace. The factory is a lazy singleton with process-lifetime scope, never disposed.

AsyncLocal ambient default factory

LogManager.SetAmbientDefaultFactory stores a per-async-context MEL ILoggerFactory reference. The acceptance testing framework sets it after building the scenario's service provider, and clears it after disposal. Because AsyncLocal flows naturally through async chains, each scenario's out-of-slot logs reach ScenarioContext.Logs without touching global state. The slot-scoped hot path is unaffected; GetDefaultLogger is only reached on the cold out-of-slot path.

DI-managed slot cleanup

SlotUnregisterer is an IAsyncDisposable registered right after AddLogging in EndpointCreator.Configure. MS DI disposes services in reverse registration order, so the slot is unregistered before ILoggerFactory is disposed. This eliminates the window where a background thread could route logs through a disposed factory during container teardown. RunningEndpointInstance no longer calls LogManager.UnregisterSlot directly.

Trade-offs

  • FallbackLoggerFactory is never disposed. This is intentional: it is a safety net for the gap between process start and endpoint initialization, and between endpoint shutdown and process exit. The OS reclaims resources on exit.
  • ScenarioWithContext.Done spawns Task.Run that does not inherit the ambient factory. This is an accepted limitation because the Done polling loop only checks boolean flags and does not log.
  • AsyncLocal does not flow across ThreadPool.QueueUserWorkItem or fire-and-forget boundaries without ExecutionContext capture. Production out-of-slot code that uses these patterns still falls through to FallbackLoggerFactory, which is the correct behavior.

What stays unchanged

  • LogManager.UseFactory and LogManager.Use<T>: still work as deprecated APIs. The ambient factory only affects the out-of-slot routing path.
  • Slot registration and deferred log mechanics: unchanged.

@danielmarbach danielmarbach changed the title Replace trace fallback with AsyncLocal ambient factory and MEL-backed FallbackLoggerFactory Replace logging fallback with MEL-backed factory and AsyncLocal routing May 17, 2026
@danielmarbach danielmarbach changed the title Replace logging fallback with MEL-backed factory and AsyncLocal routing Replace logging fallback with MEL-backed factory and AsyncLocal routing for acceptance tests May 17, 2026
@danielmarbach danielmarbach marked this pull request as ready for review May 18, 2026 10:41
@danielmarbach
Copy link
Copy Markdown
Contributor Author

@andreasohlund please review the additions

@danielmarbach
Copy link
Copy Markdown
Contributor Author

Will test an alpha release locally with the transactional session later in the afternoon

… FallbackLoggerFactory

- Add AsyncLocal<MelILoggerFactory?> ambientDefaultFactory to LogManager for per-async-context default factory routing (replaces global UseFactory bridge in ScenarioRunner)
- Replace trace-writing FallbackLoggerFactory with real MEL-backed implementation using RollingLoggerProvider + ColoredConsoleLoggerProvider via isolated ServiceProvider
- Add SlotUnregisterer (IAsyncDisposable) registered after AddLogging in EndpointCreator.Configure so MS DI reverse-order disposal unregisters slot before ILoggerFactory disposal
- Modify SlotAwareLogger.GetDefaultLogger, WriteOrEnqueueScopedStartupLog, and UnregisterSlot drain to check ambient factory first
- Remove LogManager.UseFactory bridge and Use<DefaultFactory> teardown from ScenarioRunner; replace with SetAmbientDefaultFactory after BuildServiceProvider, clear after ServiceProvider disposal
- Add EndpointLoggingScope as DI-resolvable IReadOnlyList for ILogger.BeginScope
- Add LoggingBuilderExtensions with AddNServiceBusLoggingProviders
- Add acceptance tests for out-of-slot log routing (both WithServiceResolve and endpoint ServiceResolve) and EndpointLoggingScope
- Add unit tests for ambient factory routing, AsyncLocal isolation, SlotUnregisterer idempotency, and stale scope drain
@danielmarbach
Copy link
Copy Markdown
Contributor Author

Tested with the transactional session update branch and resolves the lost logs

@danielmarbach danielmarbach merged commit e06a582 into master May 19, 2026
4 checks passed
@danielmarbach danielmarbach deleted the logging_fallback branch May 19, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants