This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A Serilog sink that ships log events to Grafana Loki. V9 is a ground-up F# rewrite (src/) exposing a C#-idiomatic public API. Multi-targets net8.0/net9.0/net10.0, depends only on Serilog (4.3.1+) and FSharp.Core.
The FAKE build script (build.fsx, run via dotnet fsi) is the canonical entry point and works cross-platform:
dotnet fsi build.fsx # Default: Restore → Build → Test → Pack
dotnet fsi build.fsx -- --target Test # unit tests (after Build)
dotnet fsi build.fsx -- --target IntegrationTest # requires a Docker daemon (Testcontainers)
dotnet fsi build.fsx -- --target Pack # nupkg → ./artifacts
dotnet fsi build.fsx -- --target Benchmark # BenchmarkDotNet suites; BENCH_FILTER='*Sink*' to filterDirect dotnet equivalents (Release is what CI uses):
dotnet build Serilog.Sinks.Grafana.Loki.slnx -c Release
dotnet test tests/Serilog.Sinks.Grafana.Loki.UnitTests -c Release
dotnet test tests/Serilog.Sinks.Grafana.Loki.UnitTests --filter "FullyQualifiedName~WireFormat" # single test/class
dotnet test tests/Serilog.Sinks.Grafana.Loki.UnitTests -f net10.0 # single TFM (unit tests run on all 3)Formatting — Fantomas (local dotnet tool, config in .editorconfig):
dotnet tool restore
dotnet fantomas src tests benchmarks build.fsx # format
dotnet fantomas --check src tests benchmarks build.fsx # verifyA Husky pre-commit hook auto-formats staged .fs/.fsx under src/, tests/, benchmarks/, and build.fsx. Hooks self-install during restore via Directory.Build.targets; set HUSKY=0 to opt out (CI does).
Local Loki + Grafana for manual testing: docker compose up -d loki (Loki on :3100, Grafana on :3000).
src/Serilog.Sinks.Grafana.Loki/Serilog.Sinks.Grafana.Loki.fsproj lists <Compile> items dependencies-first; a file can only reference what is listed above it. New files must be inserted at the right layer:
- Infrastructure (no Serilog dependency):
PooledByteBufferWriter,Utf8TextWriter— pooled-buffer primitives. - Public contract types:
LokiLabel,LokiCredentials,LokiFieldDestination,ILokiExceptionFormatter,LokiExceptionFormatter,LokiJsonTextFormatter,LokiSinkOptions. - Internal functional pipeline (
[<AutoOpen>]modules):Labels(sanitisation, label-set building),Grouping(stream identity viaLabelEqualityComparer),Serialization(batch → Loki push JSON),LokiPushContent(HttpContent over the pooled buffer). - Sink wiring:
LokiSink. - Public API surface:
LoggerConfigurationExtensions— a single flat-parameterGrafanaLokiextension method (no options-object overload; signature is bound bySerilog.Settings.Configurationfrom appsettings.json).
GrafanaLoki(...) builds a LokiSinkOptions, validates the URI at startup, and registers LokiSink through Serilog 4.x native batching (IBatchedLogEventSink — batching, bounded queue, retry/backoff all live in Serilog core; there is no custom queue/timer). Per batch, EmitBatchAsync: groups events by label set → streams JSON in one forward pass with Utf8JsonWriter over reusable pooled buffers (SerializationBuffers, owned by the sink, used serially) → POSTs via LokiPushContent to loki/api/v1/push. No intermediate object graph or strings — keep it that way; allocation regressions are what the benchmark CI watches for.
- Immutable pipeline: never mutate a
LogEvent(the V8 mutable pipeline was the root cause of several long-standing bugs). Labels are derived from a read-only view; promoted properties stay in the body. - Functional-first internals, OOP surface: internals use modules/DUs/inline functions; everything public must be natural to call from C# (classes, interfaces,
[<Extension>]methods, null-tolerant — guard inputs withisNull, includingbox-coercion for records). - HttpClient ownership: auth headers and the tenant header are applied only to a client the sink created; an injected
HttpClientis never mutated (gzip/mTLS/retries are the caller'sDelegatingHandlerconcern). - Field routing:
LokiFieldDestinationsendsTraceId/SpanIdto the body, structured metadata, or nowhere. Structured metadata (the optional 3rd element of a push entry) is emitted only when non-empty, so default output stays byte-identical and pre-3.0-Loki-safe. - Errors surface via Serilog's
SelfLogand rethrow — no silent drops.
- Unit tests (
tests/...UnitTests, xUnit + Unquotetest <@ ... @>assertions, all 3 TFMs): wire-format tests assert on exact serialized JSON captured via fake handlers/loopback;AppSettingsBindingTestsbuilds a logger from JSON config to pin the appsettings contract;ExtensionDefaultsTestsguards default-value drift. - Integration tests (
tests/...IntegrationTests, net10.0 only): Testcontainers spins up a real Loki and queries pushed entries back. Fail fast without Docker — no skip logic. Not part of the FAKEDefaultchain. - Benchmarks (
benchmarks/): three BenchmarkDotNet executables —Current= this source (ProjectReference),NuGet= the published baseline (pin viaBaselineVersion/VersionOverride, default 9.0.0; CI moves it to the latest same-major release), andYetAnother(third-party yardstick) — sharing sources frombenchmarks/Shared/. Deliberately not in the .slnx (each pins a conflicting Serilog/sink closure with the same assembly name).Allocatedis the deterministic CI regression signal;Meanis noisy.compare-results.fsxdiffs two result dirs (used by.github/workflows/benchmark.ymlfor PR comparisons against the latest NuGet version).
TreatWarningsAsErrors+WarnLevel5 apply repo-wide (Directory.Build.props).- Central Package Management (
Directory.Packages.props): noVersion=onPackageReferences. The only sanctioned exception isVersionOverridein the benchmark baseline projects. - Versioning is MinVer from git tags (
vprefix, e.g.v9.0.0) — never hand-edit a<Version>. CI checkout needsfetch-depth: 0. - The library sets
DisableImplicitFSharpCoreReferenceand referencesFSharp.Coreas an explicit NuGet package — required so FSharp.Core flows transitively to C# consumers. Don't remove either side. - Packaging: strong-named, embedded PDB + SourceLink (
DebugType=embedded), deterministic in CI. - Formatting: 4-space indent, LF, BOM-less UTF-8,
insert_final_newline = false; F# uses Fantomas withfsharp_multiline_bracket_style = alignedandmax_line_length = 120. build.fsxdisables MSBuild's internal binlog (noBinLog) — FAKE 6.1.4 can't parse .NET 10's binlog v25; keep the workaround on new MSBuild-backed targets.- CI (
.github/workflows/ci.yaml) runs build/unit/integration on ubuntu-latest (Docker preinstalled); least-privilegepermissions:blocks are deliberate on all workflows.