Skip to content

Put an ILogger seam over the Serilog pipeline - #629

Open
phmatray wants to merge 1 commit into
Fallout-build:developfrom
phmatray:feat/428-ilogger-seam
Open

Put an ILogger seam over the Serilog pipeline#629
phmatray wants to merge 1 commit into
Fallout-build:developfrom
phmatray:feat/428-ilogger-seam

Conversation

@phmatray

Copy link
Copy Markdown

Summary

Puts Microsoft.Extensions.Logging.ILogger in front of Serilog, so framework code stops referencing Serilog directly. Serilog stays the provider and the pipeline in Logging.Configure is untouched — no behaviour change and no public-API change.

Part of #428 — first of that issue's four additive PRs. Theme decoupling, the Spectre presenter, the [Obsolete]/[Experimental] markers, and the breaking removals are all out of scope here.

File Change
Directory.Packages.props + Serilog.Extensions.Logging, + Microsoft.Extensions.Logging.Abstractions
src/Fallout.Build/Fallout.Build.csproj PackageReferences for those two plus Microsoft.Extensions.DependencyInjection
Logging.DependencyInjection.cs (new) AddFalloutLogging — configures the Serilog pipeline, registers ILoggerFactory / ILogger<> / ILogger over it
Logging.cs static façade: Factory, Logger, UseLoggerFactory
Execution/BuildManager.cs per-run composition root replacing the bare Logging.Configure(build)
tests/…/LoggerBridgeSpecs.cs (new) 16 bridge specs

Decisions worth reviewing

Not services.AddLogging(...). That installs MEL's own filter pipeline, default minimum Information — a second level authority that would drop trace and debug records before Serilog saw them and displace Logging.LevelSwitch. Registering the Serilog factory directly leaves the level switch as the only gate. The_bridge_does_not_filter_below_information is the regression guard.

The factory is left unbound (SerilogLoggerFactory(logger: null, dispose: false)), because Log.Logger is not stable for the process lifetime — Configure installs it late and Host.WriteErrorsAndWarnings swaps it again for the end-of-build summary. Binding still happens once per logger rather than per write, since the category is attached as SourceContext at construction. Two consequences the code depends on, both documented at the call sites: AddFalloutLogging configures the pipeline before registering the factory, so a container-resolved logger can never bind a stale one; and Logging.Logger is deliberately uncached.

Internal, not public. This is framework foundation, not public surface yet — consistent with the "internal foundation" note in AGENTS.md. The root AssemblyInfo.cs already grants InternalsVisibleTo to Fallout.Cli and the spec assemblies, which covers PR 4's CLI wiring.

Façade over DI, per the issue's decision: the ~85 Log.* call sites and the static BuildManager.Execute<T> are unchanged.

Provider lifetime. The ServiceProvider is declared outside the try so it survives into finally (Finish() still writes the outcome summary), but constructed inside it so a configuration failure returns the same exit code as before.

Test plan

  • dotnet build fallout.slnx — 0 errors; the 42 warnings are pre-existing and none are in touched files
  • dotnet test fallout.slnx — 830 passed, 7 skipped, 0 failed
  • 16 new bridge specs: level mapping (Trace→VerboseCritical→Fatal), no sub-Information filtering, level-switch gating, message templates staying templates, exceptions reaching LogEvent.Exception, factory not pinned to one pipeline, façade fallback with no container, UseLoggerFactory restore
  • Dogfood ./build.ps1 Compile — exit 0, file sinks and rolling cleanup still writing .fallout/temp/build.log, OnBuildFinished extensions still firing after the dispose reordering, console theming unchanged
  • git diff reviewed — no public API member added or changed

The bridge specs write through the process-global Log.Logger, so every message carries a marker and collected events are filtered to it; without that, a concurrent spec class's warning lands in the sink and fails an assertion.

Note on labels

Labelled skip-changelog rather than a category: nothing here is consumer-facing. Happy to switch it to enhancement if you'd rather the #428 work show up in the notes as it lands.

Generated with Claude Code

@phmatray

Copy link
Copy Markdown
Author

Couldn't apply labels from the fork (no write access on this repo). Per the PR-creation flow this needs target/vCurrent plus a changelog-category label — skip-changelog is my suggestion, see the note at the end of the description.

@phmatray
phmatray marked this pull request as ready for review August 10, 2026 12:28
@phmatray
phmatray requested a review from a team as a code owner August 10, 2026 12:28
@ChrisonSimtian ChrisonSimtian added target/vCurrent Targets the current version skip-changelog Dont add to the Github Release Notes labels Aug 11, 2026
@ChrisonSimtian

Copy link
Copy Markdown
Collaborator

thanks for raising this PR, I'll have a look today. I applied those labels for you and approved the workflow run.
Welcome to fallout :-) and might I say, I like your Blazor form library. Helped me out a few times at a previous job ;-)

@ChrisonSimtian
ChrisonSimtian self-requested a review August 11, 2026 00:13

@ChrisonSimtian ChrisonSimtian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good PR. The reasoning is sound, the commit message and description are better than most of what lands here, and the tests are pointed at the right things. Two shape issues and one repo-rule miss below — all small, and I'd like them settled here rather than in PR 2, because 2–4 build directly on this.

What I verified

  • The unbound-factory analysis is correct. SerilogLogger binds Log.Logger at construction when logger: null and a category is supplied, and Host.WriteErrorsAndWarnings (src/Fallout.Build/Host.cs:96) reassigns Log.Logger without restoring it — so leaving the factory unbound is genuinely necessary, not defensive.
  • Avoiding services.AddLogging(...) is the right call, and The_bridge_does_not_filter_below_information is exactly the guard that keeps someone from "tidying" it back.
  • DelegateDisposable.SetAndRestore(() => staticField, ...) matches the existing pattern in this same file (ExecutingTargetLogEventEnricher.SetTargetEventProperty) — idiomatic here.
  • InternalsVisibleTo claim checks out: Fallout.Build, Fallout.Build.Specs, and Fallout.Cli are all in the root AssemblyInfo.cs.
  • Dispose ordering in the finally is right — the scope is restored before the provider that owns the factory is disposed.
  • Tests correctly join ProcessGlobalStateCollection and filter by marker, consistent with InMemorySinkSpecs and the other process-global specs.

One thing that is not yours: Log.CloseAndFlush() ends up closing the errors-and-warnings pipeline rather than the one holding the file sinks, because WriteErrorsAndWarnings swaps Log.Logger during Finish(). Pre-existing — it's what #454 (FT-9) is about. Called out only so it doesn't get attributed to this change later.

Also

docs/dependencies.md needs rows for the new packages — that file asks reviewers to call it out, so consider this the call-out. Microsoft.Extensions.DependencyInjection deserves a sentence of its own: Fallout.Build is consumer-facing, so every consumer now pulls the full container transitively. Sanctioned by #428 ("add refs to Fallout.Build"), just needs to be written down — the doc already makes the same complaint about the Azure packages.

Labels

Applied for you, and skip-changelog was the right instinct — nothing here is consumer-facing. When PR 3 lands the Spectre presenter, that one should be enhancement.

Happy to approve once 1 and 2 are addressed. Neither needs a redesign.

Comment thread src/Fallout.Build/Logging.DependencyInjection.cs Outdated
Comment thread src/Fallout.Build/Logging.DependencyInjection.cs Outdated
Comment thread src/Fallout.Build/Fallout.Build.csproj
coderabbitai[bot]

This comment was marked as off-topic.

@Fallout-build Fallout-build deleted a comment from coderabbitai Bot Aug 20, 2026
@ChrisonSimtian

Copy link
Copy Markdown
Collaborator

@phmatray just checking, did you intentionally open this PR and are you genuinely interested in contributing? Or was that your AI? Just wanna know if you'll actually read the code review or if we take it from here :-)

@phmatray

Copy link
Copy Markdown
Author

@ChrisonSimtian Thanks for applying the labels and approving the workflow run, and glad FormCraft was useful to you. Happy it helped :-)

To answer directly: yes, I opened this PR intentionally and I'm genuinely in. I found Fallout while digging through NUKE issues (I use NUKE on nearly all my repos) and I'm curious to see where this fork goes. The MCP integration idea in particular appeals to me a lot.

It's driven through my own Claude skill kit, but I'm the one steering it. I'll read the code review and finish the work. Fire away.

@phmatray

Copy link
Copy Markdown
Author

@ChrisonSimtian Thanks for the review. All three points are addressed in 9dfdf33, with replies in each thread. The branch is also synced with develop, which merged clean.

Point What changed
1. AddFalloutLogging mutates global state Split. It registers only. BuildManager.Execute calls Logging.Configure(build) explicitly before building the provider. The IFalloutBuild parameter is gone from the signature.
2. Singleton registrations pin the pipeline ILogger<T> and ILogger are transient. ILoggerFactory stays a singleton, since it is unbound and pins nothing. The residual constraint is documented: a component that outlives a pipeline swap must read Logging.Logger at the point of writing.
3. docs/dependencies.md Rows for all three packages, with the transitive-footprint note on the DI row.

Also took CodeRabbit's nitpick: StubLoggerFactory.CreateLogger returns NullLogger.Instance instead of throwing. AddProvider still throws, since the seam never calls it.

Verification

  • dotnet build fallout.slnx: 0 errors, no warnings in the touched files.
  • dotnet test fallout.slnx: 833 passed, 7 skipped, 0 failed.
  • ./build.ps1 Compile: exit 0. File sinks still write to .fallout/temp/build.log, and the OnBuildFinished extensions still fire.

Three specs were added, one per behaviour. I checked each one fails when its change is reverted, so they guard rather than just pass:

  • Registering_the_seam_leaves_the_pipeline_alone fails if Configure goes back inside the registration.
  • A_container_logger_binds_the_pipeline_current_at_resolution and The_container_hands_out_a_new_logger_per_resolution fail if the registrations go back to singleton.

One thing worth your call

On point 2, transient fixes the resolution, not the holding. A component that resolves a logger once and keeps it across the WriteErrorsAndWarnings swap still writes into the old pipeline. I documented the rule instead of building an indirection this PR was not asked to build, but IHostOutput in PR 3 is exactly the component that hits it. Say the word if you would rather it get something swap-aware, and I will design that in PR 2 or 3 rather than here.

Noted on the Log.CloseAndFlush ordering being #454 and not mine, and on enhancement for PR 3's label.

Framework code reaches logging through Serilog's static Log, which pins the
project to one logger implementation and leaks Serilog types outward. Introduce
Microsoft.Extensions.Logging.ILogger as the abstraction in front of it, keeping
Serilog as the provider and the pipeline itself untouched.

AddFalloutLogging registers the abstraction over the pipeline and nothing else,
so any container can call it any number of times. Configuring the pipeline stays
separate because Logging.Configure is not idempotent: it reassigns Serilog's
Log.Logger on every call, and with no build it installs a pipeline with no file
sinks, no host sink and no filter, wiping out whatever a previous caller had set
up. BuildManager.Execute runs Configure explicitly, just before it builds the
provider.

The registration deliberately avoids services.AddLogging, which would install
MEL's own filter pipeline with an Information default -- a second level authority
that would drop trace and debug records before Serilog saw them, displacing
Logging.LevelSwitch.

ILogger<T> and ILogger are transient, not singleton. Logger<T> binds its inner
logger in its constructor, so a singleton would pin every consumer to whichever
pipeline was current at the first resolution -- the same failure Logging.Logger
stays uncached to avoid. ILoggerFactory stays a singleton because it is left
unbound and pins nothing. Transient does not rescue a component that holds a
logger across a pipeline swap, so that remaining constraint is documented at the
registration and on CreateSerilogLoggerFactory.

BuildManager.Execute now owns a per-run composition root and feeds the resolved
factory to a static facade on Logging, so the ~85 Log.* call sites and the static
build engine are unchanged. The provider is declared outside the try so it
survives into Finish(), but built inside it so a configuration failure still
returns the same exit code as before.

The seam is internal: it is framework foundation, not public surface yet. Nothing
in the public API changes and no output changes.

docs/dependencies.md gains rows for Serilog.Extensions.Logging,
Microsoft.Extensions.Logging.Abstractions and
Microsoft.Extensions.DependencyInjection. The DI row notes that Fallout.Build is
consumer-facing, so every consumer now pulls the container transitively.

First of the additive PRs in Fallout-build#428.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ChrisonSimtian

Copy link
Copy Markdown
Collaborator

Happy with this. Approving — the three review points are properly addressed, not just answered.

I checked the specs guard rather than merely pass: reverting the transient registrations to singleton fails The_container_hands_out_a_new_logger_per_resolution and A_container_logger_binds_the_pipeline_current_at_resolution. That's the right kind of test.

I rebased your branch — old head was 9dfdf33

develop moved under you (the Rider Safe Cleanup sweep, 8061dc9), so this had conflicted. Both hunks were formatting-only, so rather than bounce it back I resolved them and pushed:

  • using block in BuildManager.cs reordered — Microsoft.* now sorts after Fallout.*
  • SupportsAnsiOutput takes develop's line-break form, with DefaultCategoryName / loggerFactory above it
  • Squashed to one commit. Your two were both this one logical change, and CONTRIBUTING.md asks us to curate before merge — landing "seam with Configure inside the registration" as a permanent bisect target when the next commit takes it out again isn't worth it. Your rationale from both messages is preserved in the body.
  • One edit of mine: the <see cref="LogLevel.Information"/> in AddFalloutLogging's remarks bound to Fallout's own Fallout.Common.LogLevel, not MEL's — enclosing-namespace lookup beats the using directive. Now fully qualified. You'd already spotted the collision in the specs (MsLogLevel); it just didn't carry into the doc.

Verified on the rebase: build clean with no warnings in any touched file, 833 passed / 7 skipped / 0 failed. git reflog on 9dfdf33 if you want the old shape back.

Your open question — swap-aware loggers

Your read is right, and documenting the constraint instead of building the indirection was the correct call for this PR. PR 3 owns it. IHostOutput is the first component that genuinely outlives a pipeline swap, so the swap-aware access belongs there, next to the WriteErrorsAndWarnings fix that causes the swap in the first place. Nothing more needed here, and nothing needed in PR 2 either.

PRs 2-4

We're going to start these so the foundation you've landed doesn't sit unused — but that's us keeping momentum, not us taking the chain off you. You're very welcome on any of them, and I'd rather you were:

  • PR 2 — decoupled IHostTheme + SerilogConsoleThemeAdapter
  • PR 3 — Spectre IHostOutput presenter, WriteErrorsAndWarnings fix, the swap-aware bit above
  • PR 4[Obsolete] / [Experimental("FALLOUT002")] markers, LogLevel attribute overloads, CLI container wiring

Say which you want and it's yours — before we start, or mid-flight, in which case we'll hand the branch over rather than race you. They'll go up as drafts with maintainer edits enabled, so you can push straight onto them. Reviews on ours are just as welcome; you've got the most context on this seam of anyone now.

skip-changelog stays right for this one. PR 3 gets enhancement, as you said.

And noted on #661 / #662 — I'll get to those separately.


Fair's fair, since I asked you the same thing back in August: the rebase, the conflict resolution and this review were done with Claude Code, steered by me. The verification numbers above are from actual local runs, not claims — but CI still has to confirm the dogfood, because the one thing I couldn't run locally was ./build.sh Compile (this machine has SDK 10.0.400, the repo pins 10.0.302, and 10.0.400's targets want NuGet.Frameworks 7.9.0 against our pinned 6.x). So treat the workflow run as the real gate, not my green ticks.

@ChrisonSimtian
ChrisonSimtian self-requested a review September 9, 2026 02:02

@ChrisonSimtian ChrisonSimtian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI reviewed and approved PR

@ChrisonSimtian ChrisonSimtian added the Can Merge These PRs are just waiting to be merged, nothing more to do and maintainer has approved label Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Can Merge These PRs are just waiting to be merged, nothing more to do and maintainer has approved skip-changelog Dont add to the Github Release Notes target/vCurrent Targets the current version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants