Make Brighter logging instance-scoped (deprecate static ApplicationLogging)#4185
Make Brighter logging instance-scoped (deprecate static ApplicationLogging)#4185thomhurst wants to merge 5 commits into
Conversation
Replace the process-wide static ApplicationLogging.LoggerFactory with an ILoggerFactory threaded as an instance through the object graph Brighter constructs. The DI extensions no longer copy the container's ILoggerFactory into the static, which caused use-after-dispose when a container was disposed and cross-talk between two Brighter instances in the same process. - CommandProcessorBuilder/DispatchBuilder gain ConfigureLogging(ILoggerFactory) - Runtime types (CommandProcessor, OutboxProducerMediator, Dispatcher, MessagePump, Reactor/Proactor, pipelines, scheduler) take an optional trailing ILoggerFactory; relational stores take an optional ILogger - Transport/store factories thread the factory down to the leaf objects - ApplicationLogging is [Obsolete], defaults to NullLoggerFactory and tolerates a disposed factory; nothing in Brighter writes to it any more - s_logger static fields become instance _logger fields Adds InstanceScopedLoggingTests (crosstalk isolation, sibling-dispose safety, null-factory fallback) and a migration guide. Existing call sites remain source-compatible via trailing optional parameters.
Code Review: Instance-scoped logging (deprecate static
|
Review: Instance-scoped logging (deprecate static
|
Adopt the idiomatic MEDI logging shape (ILogger<T>) where a class logs only on its own behalf and is either resolved from the container or constructed directly: - Relational Outbox/Inbox stores: ILogger? -> ILogger<TStore>? - DI-resolved handlers (Defer/Reject/UseInbox/RequestLogging, sync + async, and ConfigurationCommandHandler): ILoggerFactory -> ILogger<THandler> - Hosted services (TimedOutboxSweeper, TimedOutboxArchiver): ILoggerFactory -> ILogger<TService> (also corrects TimedOutboxArchiver, which was logging under the TimedOutboxSweeper category) Builders, factories and aggregates that mint loggers for child components (CommandProcessor, OutboxProducerMediator, Dispatcher, MessagePump, ConsumerFactory, transport producer/consumer/channel factories, etc.) keep ILoggerFactory, since they create loggers for types other than themselves. All new parameters remain trailing and optional, so call sites stay source-compatible.
Review: Make Brighter logging instance-scoped (#4185)Thanks for tackling this — routing logging through a process-wide static was a genuine latent bug, and the writeup of the two failure modes (last-writer-wins cross-talk + use-after-dispose on container disposal) is spot on. The overall approach is sound and the execution is consistent across a large surface (175 files). A few notes below, mostly minor. What works well
Observations / questions
Verification
Nice, careful piece of work overall. The message-creator gap (#1) is the only thing I'd want resolved or explicitly documented before merge; the rest are polish. 🤖 Generated with Claude Code |
|
Alright, we will need to look over this one carefully, as it was previously a choice to avoid passing loggers throughout the application. It may not be a breaking change, but it may change behavior for V10 users who don't transition, potentially resulting in a silent failure. That would mean, IMO, it would be V11, and it would be preferable to force success rather than allow silent failure. In addition, it may change log messages. Not necessarily adverse, but didn't really want to trigger V11 work until SEP/OCT |
|
Agreed it's a breaking change for anyone manually constructing these classes. We can hold off until V11 if you were happy with this change and park for now if needed. For MEDI users I don't believe they'd have to tweak any code. But also happy to discuss the design further. The current behaviour is a problem though for any two or more brighter's running under the same process. We noticed it earlier in a test suite. What do you mean by force success btw? Remove the NullLogger fallback? |
…ings) Quality cleanup from a /simplify pass over the instance-scoped logging work: - Message creators (Kafka, RMQ sync+async, Redis): use NullLogger<T>.Instance instead of NullLoggerFactory.Instance.CreateLogger<T>() (no allocation, matches the idiom in ApplicationLogging). - AzureServiceBusChannelFactory: drop the unused ILoggerFactory parameter/field (it logged nothing and threaded nothing). - MsSql ChannelFactory: take ILogger<ChannelFactory> rather than ILoggerFactory (it only logs for itself, so it is a leaf). - Remove 27 now-dead `using Paramore.Brighter.Logging;` imports left behind after the ApplicationLogging usages were removed. - Clarify the ApplicationLogging [Obsolete] remarks wording.
Review: Make Brighter logging instance-scoped (#4185)Thanks for tackling this — the use-after-dispose and cross-talk caused by the process-wide static were real, nasty bugs (especially under parallel test suites), and the instance-scoped approach is the right fix. The refactor is large but overwhelmingly mechanical and consistent: A few things worth addressing or at least calling out: 1. Message creators silently lose logging entirely (behavioural regression, not in the PR description)
private static readonly ILogger s_logger = NullLogger<RmqMessageCreator>.Instance;Under DI, the old static was populated with the container's real factory, so these creators previously logged through it. Now they log to a no-op for everyone, with no way to ever supply a real logger. The messages being silenced are exactly the diagnostics an operator wants —
If silencing these is intentional, it should at least be noted in the PR's "Behaviour change" section; right now that section only mentions user-constructed transports/stores falling back to no-op, not these always-on-no-op leaf objects. 2. Interface addition is a breaking change for implementers (minor)
3. Per-message
|
…gap) The Rmq/Kafka/Redis message creators previously held a permanent no-op NullLogger, so their diagnostics (malformed/missing-header warnings and deserialize failures) went nowhere after the move off the static logger. Convert each to an instance class whose ILogger<T> is created once by the owning consumer (Kafka/Redis via their ILoggerFactory, RMQ via the gateway's LoggerFactory) and reused for every message, so those log lines reach the configured logger again. Message-building logic is unchanged; only the previously-silent diagnostics are restored.
There was a problem hiding this comment.
Code Health Improved
(3 files improve in Code Health)
Gates Failed
Prevent hotspot decline
(2 hotspots with Complex Method)
Enforce advisory code health rules
(4 files with Complex Method, Constructor Over-Injection, Code Duplication)
Our agent can fix these. Install it.
Gates Passed
2 Quality Gates Passed
Reason for failure
| Prevent hotspot decline | Violations | Code Health Impact | |
|---|---|---|---|
| OutboxProducerMediator.cs | 1 rule in this hotspot | 6.97 → 6.95 | Suppress |
| KafkaMessageConsumer.cs | 1 rule in this hotspot | 6.87 → 6.86 | Suppress |
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| PipelineBuilder.cs | 1 advisory rule | 7.79 → 7.33 | Suppress |
| MessagePump.cs | 1 advisory rule | 10.00 → 9.69 | Suppress |
| OutboxProducerMediator.cs | 1 advisory rule | 6.97 → 6.95 | Suppress |
| KafkaMessageConsumer.cs | 1 advisory rule | 6.87 → 6.86 | Suppress |
View Improvements
| File | Code Health Impact | Categories Improved |
|---|---|---|
| SqsInlineMessageCreator.cs | 8.96 → 9.54 | Overall Code Complexity |
| S3LuggageStore.cs | 8.60 → 8.88 | Primitive Obsession, String Heavy Function Arguments |
| S3LuggageStore.cs | 8.60 → 8.88 | Primitive Obsession, String Heavy Function Arguments |
Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
|
Review: Instance-scoped logging (deprecate static Thanks for tackling this — moving off the process-wide static is the right call, and issue #4184 is a real correctness bug (cross-talk + use-after-dispose between Brighter instances). The approach is clean: trailing optional A few observations, mostly minor, plus one I would like you to confirm is intentional. 1. Base
2.
3.
Test coverage The new Minor
Overall this is a solid, well-scoped refactor that fixes a genuine bug with good backward-compat discipline. The main thing I would want resolved before merge is confirming whether the base-handler relay logging going silent (point 1) is intended, and ideally a test that exercises a real handler pipeline. Automated review — verified against the merge of this branch into master; I was unable to run the build/tests in this environment, so the compile-clean claim is taken on trust. |
Yes, because I get that stops users having compilation issues, but it feels like it could silently fail on folks who where not expecting it. |
I think it may help with some other aspects like testing too. We currently have to use a test capture context and I am not sure that is the best route |
Closes #4184
Problem
Logging was routed through the process-wide static
ApplicationLogging.LoggerFactory, which the DI extensions populated from the container'sILoggerFactory. Because it is shared across the process, two or more Brighter instances (e.g. parallel test suites) would:ILoggerFactory(last initializer wins), andILoggerFactorythe static still pointed at, causingObjectDisposedExceptionin other running instances.Change
Logging is now instance-scoped: an
ILoggerFactoryflows as an instance through the object graph Brighter constructs, sourced from the container (or builder) that created the instance. Nothing writes to the static any more.CommandProcessorBuilder/DispatchBuildergainConfigureLogging(ILoggerFactory); the DI extensions resolve the container factory and pass it through (instead of assigning the static).CommandProcessor,OutboxProducerMediator,Dispatcher,MessagePump,Reactor/Proactor,ConsumerFactory, the pipeline builders and the in-memory scheduler — take an optional trailingILoggerFactory? loggerFactory = null; relational stores take an optionalILogger? logger = null.ILoggerFactorydown to the leaf objects they construct, so it can be supplied for user-constructed pipelines too.s_loggerstatic fields become instance_loggerfields.ApplicationLoggingis marked[Obsolete], defaults toNullLoggerFactory.Instance, andCreateLogger<T>()tolerates a disposed factory. It remains only as a crash-free, no-op fallback and will be removed in a future release.All public constructor/builder changes are trailing optional parameters / additive methods, so existing call sites remain source-compatible.
Behaviour change
The static is no longer populated by the DI extensions. User-constructed transports/stores that are not passed an
ILoggerFactorylog to a no-op logger rather than to the (previously shared) static. The migration is described in the[Obsolete]message onApplicationLogging; worth a note in the release notes.Tests
InstanceScopedLoggingTests: a command processor logs only through its own container's factory (no cross-talk), is unaffected when a sibling container is disposed (noObjectDisposedException), and falls back safely when no factory is registered.MessageDispatchtests that assert on pump log output to pass a Serilog-backed test factory.TreatWarningsAsErrorsconfirms no remaining references to the obsolete static);Paramore.Brighter.Core.Tests(752) andParamore.Brighter.Extensions.Tests(102) pass.