Conversation
- Move all V8 source, tests, samples, build tooling and solution into v8/ for reference during V9 development - Add v8/Directory.Packages.props to opt V8 projects out of CPM - Reset Directory.Build.props for V9 (drop StyleCop, LangVersion; keep MinVer, SourceLink, strong-name signing) - Add Directory.Packages.props with Central Package Management enabled - Add Serilog.Sinks.Grafana.Loki.slnx (new solution format) - Add empty src/ and test/ placeholders for V9 projects - Add V9_SPEC.md design specification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Serilog.Sinks.Grafana.Loki.fsproj targeting net8.0;net9.0;net10.0 - No conditional PropertyGroups needed — all three TFMs have identical API surfaces; complexity of Serilog own csproj conditionals comes entirely from legacy TFM support we have dropped - CPM resolves Serilog 4.3.1 and Serilog.Sinks.PeriodicBatching 5.0.0 - Compile items commented-in order as files are created - Project added to Serilog.Sinks.Grafana.Loki.slnx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PooledByteBufferWriter — IBufferWriter<byte> over ArrayPool<byte>.Shared. Reused across ticks via Clear(); returned to the pool on Dispose(). Grows by doubling when capacity is exceeded. Utf8TextWriter — TextWriter subclass that encodes directly into the pooled buffer as UTF-8, eliminating the StringWriter + intermediate string that the V8 pipeline produced per log event. Write(char) has an ASCII fast path (single byte, no allocation); Write(string) and Write(ReadOnlySpan<char>) use Encoding.UTF8.GetBytes directly into the buffer span. Builds clean across net8.0, net9.0, net10.0 with 0 warnings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Public contract (OOP surface, C#-compatible): - LokiLabel, LokiCredentials — [<CLIMutable>] records - ILokiExceptionFormatter — [<AllowNullLiteral>] interface; replaces hardcoded SerializeException - LokiExceptionFormatter — default impl; pre-encoded JsonEncodedText property names - LokiJsonTextFormatter — public non-sealed class; virtual Format + SanitizePropertyName; internal FormatToBuffer fast path writes directly to PooledByteBufferWriter bypassing the ITextFormatter TextWriter round-trip - LokiSinkOptions — [<CLIMutable>] record + LokiSinkOptions.Defaults extension Internal functional pipeline ([<AutoOpen>] modules, inline hot-path helpers): - Labels — inline sanitizeLabelKey, logLevelToLabel, renderLabelValue, toUnixNanoseconds, buildLabelSet; label set is an immutable Map<string,string> - Grouping — groupIntoStreams via Seq.groupBy on structural Map equality - Serialization — single Utf8JsonWriter forward pass; LokiJsonTextFormatter fast path avoids double-encoding; external ITextFormatter falls back to Utf8TextWriter path Infrastructure: - LokiPushContent — HttpContent subclass; both SerializeToStreamAsync overloads required for net10 compatibility; TryComputeLength avoids chunked encoding - LokiSink — IBatchedLogEventSink; reusable per-tick PooledByteBufferWriter pair; label pipeline precomputed at construction; box coercion for record null-checks Public API: - LoggerConfigurationExtensions — two [<Extension>] overloads with C#-compatible [<Optional;DefaultParameterValue>] on restrictedToMinimumLevel; URI validated at construction time Builds 0 warnings / 0 errors across net8.0, net9.0, net10.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…kiExceptionFormatter Test stack: xUnit + Unquote 7.0.1 (quotation-based assertions) + Hedgehog 2.0.3 (property tests — plumbed in, first properties to follow). Key decisions: - inline leaf helpers (sanitizeLabelKey, logLevelToLabel, renderLabelValue, toUnixNanoseconds) stay inline; F# prohibits cross-assembly inlining of internal inline functions even with InternalsVisibleTo - buildLabelSet and groupIntoStreams: removed inline — too complex for meaningful compile-time inline, now testable directly from external assembly - Inline helpers tested indirectly through buildLabelSet (behaviour contracts) - toUnixNanoseconds: thin non-inline shim (timestampToNs) added to Labels.fs solely for precision tests — comment explains why it exists - Unquote + use-bound IDisposable: extract values before quotation (FS3155) - AggregateException in F#: explicit :> exn upcast required for array literals Coverage: label pipeline (27 tests), stream grouping (11 tests), exception formatter recursive serialization (14 tests — simple, inner, aggregate, deep). Builds 0 warnings / 0 errors across net8.0, net9.0, net10.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Directory.Build.props: - LangVersion=latest (F# 10 on all TFMs via .NET 10 SDK) - WarnLevel=5 (maximum warning category coverage) - TreatWarningsAsErrors=true (all FS warnings become errors) Library (src): already clean at these settings — 0 warnings confirmed. Test project: - MSBuildWarningsAsMessages=MSB3277: Hedgehog targets netstandard2.1 and transitively pulls FSharp.Core 8.0.403; SDK always provides the correct version at runtime on net8+, warning is a false positive - Remove redundant :> exn upcasts in AggregateException array literals (FS0066 → now an error under TreatWarningsAsErrors) Result: dotnet test passes 47/47 with 0 warnings, 0 errors on net8.0, net9.0, net10.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker-compose.yaml: - Remove deprecated top-level `version:` field (dropped in Compose spec) - Pin grafana/loki:3.7.2 and grafana/grafana:13.0.2 (was :latest) sample/Serilog.Sinks.Grafana.Loki.Sample (console, net10.0): - Top-level statements, collection expressions, primary constructors - Uses new LokiSinkOptions API with Labels + PropertiesAsLabels - Person model as a record, Serilog.Enrichers.Thread + Sinks.Console via CPM sample/Serilog.Sinks.Grafana.Loki.SampleWebApp (ASP.NET Core, net10.0): - ReadFrom.Configuration via Serilog.AspNetCore + Settings.Configuration - appsettings.json uses uri string overload (safe, validated path) - SerilogController updated to primary constructor injection - Builds 0 warnings / 0 errors Directory.Packages.props: add Samples group (Serilog.AspNetCore 10.0.0, Serilog.Enrichers.Thread 4.0.0, Serilog.Settings.Configuration 10.0.0, Serilog.Sinks.Console 6.0.0) Serilog.Sinks.Grafana.Loki.slnx: add /sample/ solution folder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UnitTests — WireFormatTests.fs (27 new tests, 74 total): - FakeHttpHandler captures body bytes, auth header, tenant header, URI - makeSink: injects HttpClient (sink does NOT own it, for structure/label tests) - makeSinkWithHandler: injects HttpMessageHandler (sink owns client, applies auth) - 21 test cases: JSON structure, level labels (theory × 6), label behaviour, body content, HTTP auth/tenant headers Library change: add HttpMessageHandler to LokiSinkOptions - Sink creates its own HttpClient(handler) when HttpMessageHandler is set - Closes real usability gap: users can inject retry/compression handlers without managing the full HttpClient lifecycle IntegrationTests (new project, Testcontainers 4.12.0): - LokiContainerFixture: IAsyncLifetime, ContainerBuilder(grafana/loki:3.7.2), waits on /ready endpoint before tests run - LokiQueryClient: query_range API, retry-with-backoff (16 × 300 ms ~5 s) - LokiIntegrationTests: 8 tests (IClassFixture, unique runId per test) basic e2e, label selector, body roundtrip, exception, two batches, Fatal=fatal label value, 200-event batch, gzip via DelegatingHandler - GzipRequestHandler: minimal DelegatingHandler demo; base.SendAsync extracted to private method to work around F# closure restriction AssemblyInfo.fs: add InternalsVisibleTo for IntegrationTests Serilog.Sinks.Grafana.Loki.slnx: add integration test project Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Normalizes whitespace, indentation and blank lines across all F# source, test, sample and config files. No logic changes — all 74 unit tests and 8 integration tests pass unchanged after formatting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
StyleCop.Analyzers.Unstable 1.2.0.556 (latest, supersedes 1.2.0-beta.556 used in V8). Applied only to C# projects via Directory.Build.props Condition=$(Language)=='C#'; F# projects are unaffected — Roslyn analyzers do not run against F# source. Configuration: - .editorconfig at repo root — the primary config mechanism for .Unstable 1.2.x (StyleCop.ruleset kept as human-readable reference but not used for enforcement) - Rule decisions mirror V8 ruleset exactly: all documentation and file-header rules disabled, spacing/ordering/naming/layout enforced as errors StyleCop.ruleset: - Kept at repo root as a human-readable record of rule decisions - ToolsVersion bumped 16.0 → 17.0 Sample fixes triggered by StyleCop: - Person record extracted to its own Person.cs (SA1402 — one type per file); no namespace to match Program.cs top-level statements scope - SA1518 (file must end with newline): added trailing newlines to Program.cs and SerilogController.cs in both sample projects 74 unit tests + 8 integration tests pass unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Preference is no trailing newlines at end of files. - .editorconfig: insert_final_newline = false, SA1518 severity = none - Reverted trailing newlines added to sample files in previous commit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The .ruleset file was not used for enforcement (StyleCop.Analyzers.Unstable 1.2.x only reads .editorconfig). Removing it to avoid confusion. Rule decisions are preserved at v8/StyleCop.ruleset and referenced in .editorconfig. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clean up all references to the v8 folder path that will be deleted: - .editorconfig: drop path reference to StyleCop.ruleset in v8 - README.md: remove version tag from formatter description - WireFormatTests.fs: two test names reworded - LabelsTests.fs: inline data comment updated - LokiIntegrationTests.fs: GzipRequestHandler comment updated - bug_report.yml: placeholder updated to v9.0.0 V9_SPEC.md keeps historical comparisons as design rationale. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two jobs: - build: restore, build (Release), unit tests (net8/9/10), pack, upload .nupkg artifact - integration: runs after build, requires Docker (ubuntu-latest has it); net10 only Triggers on push/PR to main, master, v9. fetch-depth: 0 for MinVer to compute the correct version from git tags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This reverts commit c91665e.
build.fsx replaces the V8 Bullseye C# console app with an F#-native
build script using FAKE 6.1.4 inline NuGet refs — self-bootstrapping,
no dotnet-tools.json or shell wrappers required.
Targets:
Restore → Build → Test → Pack → Default
↘ IntegrationTest
Pack → Push (requires NUGET_API_KEY env var)
Build compiles the full solution so Test and Pack can use --no-build.
Known limitation: FAKE 6.1.4 uses an MSBuild binary log parser that
only supports up to format v16, while the .NET 10 SDK emits v25.
Fixed by DisableInternalBinLog = true on all MSBuildParams.
Usage: dotnet fsi build.fsx [-- --target <name>]
ci.yaml to be updated separately after review.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ci.yaml: - .NET 6.0.x/7.0.x/8.0.x -> 8.x/9.x/10.x - ./build.sh / ./build.ps1 -> dotnet fsi build.fsx -- --target <name> - Remove dotnet --info noise - Add integration test job (ubuntu, Docker, runs after build matrix) - Add publish job (ubuntu, release-only, separated from build matrix) - Artifact upload per OS in build job Serilog.Sinks.Grafana.Loki.slnx: - Add Solution Items virtual folder mirroring V8 convention: .gitignore, .editorconfig, LICENSE, README.md, SECURITY.md, Directory.Build.props/targets, Directory.Packages.props, build.fsx, docker-compose.yaml, grafana-datasources.yaml build.fsx: Rider formatting pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds * text=auto eol=lf to normalise all text files to LF in the repo and on checkout, eliminating the per-commit CRLF warning on Windows. Binary assets (.snk, .png, .dll, .exe) marked explicitly. git add --renormalize applied to fix 29 existing CRLF files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…xpression Copyright 2020-2026 Mykhailo Shevchuk & Contributors — added to all 15 .fs files in src/Serilog.Sinks.Grafana.Loki/ following the same pattern used in V8 C# source (sink source only, not tests/samples/build). PackageLicenseExpression corrected from Apache-2.0 to MIT — the LICENSE file has always been MIT; the wrong expression was an inherited bug from a template, never caught because StyleCop SA1633/SA1634 were disabled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CodeQL does not support F#. The library is entirely F# — the workflow only analysed the two trivial C# sample files (Program.cs, Person.cs, SerilogController.cs) while skipping 100% of the actual library code, giving a false sense of security coverage. Can be re-added if GitHub CodeQL adds F# support in the future. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Loki FSharp.Core not copy-local for C# consumers: SDK marks FSharp.Core as SDK-only, so the DLL is not in shared runtime and C# apps fail at runtime with assembly-not-found. Fixed via DisableImplicitFSharpCoreReference=true plus explicit PackageReference for FSharp.Core 10.1.300 in CPM, making it a proper transitive dep. LokiSinkOptions.Defaults not visible from C#: Optional F# type extensions (in AutoOpen module) compile as methods on the module class, not on the original type. C# could not call LokiSinkOptions.Defaults. Fixed by moving Defaults, DefaultBatchSizeLimit and DefaultQueueLimit into the type body as intrinsic members. new LokiSinkOptions from C# zero-initialises all fields: CLIMutable F# records set unset fields to 0/false/null when constructed with C# new. BatchSizeLimit=0 caused PeriodicBatchingSink to throw. Console sample updated to use LokiSinkOptions.Defaults as starting point; doc comment on the type warns against the new pattern and shows correct usage. GrafanaLoki(options) overload default-merging is tracked as follow-up. All 74 unit tests pass. 13/13 LokiSinkOptions scenarios validated against real Loki 3.7.2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Following the same pattern as V8 and Serilog.Sinks.Seq: one method,
uri is the only required parameter, everything else is optional with
explicit defaults. This makes invalid configuration impossible — all
defaults are declared in the method signature, no zero-initialisation
trap from new LokiSinkOptions { ... }.
Changes:
- GrafanaLoki(options) overload removed
- GrafanaLoki(uri) convenience overload replaced
- New: GrafanaLoki(uri, labels, propertiesAsLabels, handleLogLevelAsLabel,
credentialsLogin, credentialsPassword, tenant, enrichTraceId,
enrichSpanId, batchSizeLimit, queueLimit, period,
eagerlyEmitFirstEvent, retryTimeLimit, textFormatter,
exceptionFormatter, httpClient, httpMessageHandler, timeProvider,
restrictedToMinimumLevel)
- LokiCredentials no longer a parameter — split into credentialsLogin +
credentialsPassword strings (same ergonomics as Seq apiKey)
- Null-default reference params use typed null: T annotation required by F#
- LokiSinkOptions stays public (useful for F# power users and tests)
Console sample updated to use named-parameter flat API.
74 unit tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unit tests (+3, total 77): - TraceId written when EnrichTraceId=true: uses ActivityTraceId.CreateRandom() to create a LogEvent with a known trace ID, verifies it appears in body - SpanId written when EnrichSpanId=true: same pattern for span ID - TraceId absent when EnrichTraceId=false (default): confirms opt-in behavior Uses mkEventWithTrace helper that bypasses Activity.Current requirement Integration tests (+2, total 10): - exceptionFormatter: custom shape survives Loki roundtrip: NoTypeFormatter writes only Message, test verifies Type is absent after Loki roundtrip - batching: 3x4 events across separate EmitBatchAsync calls all arrive: simulates batchSizeLimit splitting; verifies no events lost across posts Decisions from manual S10/eagerlyEmitFirstEvent validation: not added as automated test — timing-sensitive, flaky under load, already validated manually. S11/S12 auth enforcement needs Loki auth_enabled=true — covered by wire format tests for header correctness. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Extension class must be AbstractClass+Sealed for Settings.Configuration Serilog.Settings.Configuration v10 filters types with IsSealed&&IsAbstract (the C# static class pattern). F# [<Extension>] type with private () does not match. Fix: [<AbstractClass; Sealed; Extension>] produces IsAbstract=True IsSealed=True in IL, making the class discoverable. 2. period and retryTimeLimit changed from TimeSpan to string [<Optional>] TimeSpan compiles with default=System.Reflection.Missing which Serilog.Settings.Configuration cannot use when building the reflection call for optional params with no supplied value. String with null default works correctly; appsettings.json users pass 00:00:01 format (standard TimeSpan string, which Serilog.Settings.Configuration converts automatically). Method body parses the string; null/empty = use framework default. appsettings.json validation results (all via config, zero code changes): - uri, labels, propertiesAsLabels, handleLogLevelAsLabel ✓ - credentialsLogin + credentialsPassword (Basic auth) ✓ - tenant (X-Scope-OrgID) ✓ - enrichTraceId + enrichSpanId (ASP.NET Core Activity present) ✓ - batchSizeLimit, queueLimit, period, eagerlyEmitFirstEvent ✓ - ThreadId promoted to stream label (propertiesAsLabels) ✓ - TraceId and SpanId in log body from HTTP request Activity ✓ - Exception field present on error events ✓ 77 unit tests pass unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… semantics Investigation result: - C# TimeSpan?=null emits DefaultParameterValue=null (valid for Settings.Configuration) - F# [<Optional>] Nullable<TimeSpan> emits DefaultParameterValue=Missing (same broken behavior as plain TimeSpan) - F# has no way to emit DefaultParameterValue=null for value types at compile time The string workaround remains the correct approach for period/retryTimeLimit. Code callers pass 00:00:02 format; Settings.Configuration users pass the same. Comment updated to explain the root cause. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V9 headers were missing the last 4 disclaimer lines that V8 had. Applied to all 15 .fs files in src/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unused packages audit: all 17 packages in CPM are actively referenced. FAKE packages (Fake.Core.Target, Fake.DotNet.Cli, Fake.IO.FileSystem): already at latest 6.1.4. Updated via `dotnet outdated Serilog.Sinks.Grafana.Loki.slnx -u`: Microsoft.SourceLink.GitHub 10.0.203 -> 10.0.300 (patch) Microsoft.NET.Test.Sdk 17.13.0 -> 18.6.0 (major, VS versioning scheme) xunit.runner.visualstudio 3.0.2 -> 3.1.5 (minor) Serilog.Sinks.Console 6.0.0 -> 6.1.1 (patch) 77 unit tests pass unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… batching RetryTimeLimit was stored in LokiSinkOptions but never consumed — PeriodicBatchingSinkOptions has no RetryTimeLimit property. Fixed by switching from PeriodicBatchingSink to Serilog 4.x native batching: sinkConfig.Sink(IBatchedLogEventSink, BatchingOptions) where BatchingOptions has BatchSizeLimit, BufferingTimeLimit, EagerlyEmitFirstEvent, QueueLimit, RetryTimeLimit. TimeProvider was stored but never read anywhere in the sink. Dropped — no current use case (useInternalTimestamp was removed; event.Timestamp is always used directly from Serilog). Side effects of the batching switch: - Serilog.Core.IBatchedLogEventSink (IReadOnlyCollection<LogEvent>) replaces Serilog.Sinks.PeriodicBatching.IBatchedLogEventSink (IEnumerable<LogEvent>) - Serilog.Sinks.PeriodicBatching removed as library dependency (no longer needed) - Tests updated: open Serilog.Core instead of Serilog.Sinks.PeriodicBatching; Array.ofList used to convert test event lists to IReadOnlyCollection - LokiSinkOptions.TimeProvider field removed - timeProvider parameter removed from GrafanaLoki extension method 77 unit tests pass unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RFC 3986 §5.2.2 strips everything right of the last '/' in the base path,
so Uri("http://proxy/gateway", "loki/api/v1/push") resolves to the wrong
endpoint. Appending '/' when absent makes path-prefixed proxy URIs work.
Adds regression test for the path-prefix case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the options-object extension method documented in V9_SPEC.md,
enabling C# callers to use the full options object pattern:
.WriteTo.GrafanaLoki(new LokiSinkOptions { Uri = "...", ... })
and F# callers to pass a record directly:
.WriteTo.GrafanaLoki({ LokiSinkOptions.Defaults with Uri = "..." })
Routes to the same `wire` helper as the flat-parameter overload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r test
- LokiSink.fs: replace `raise ex` with ExceptionDispatchInfo.Capture().Throw()
so the original stack trace is preserved when propagating serialization failures
(`reraise()` is not legal inside task {} CEs)
- WireFormatTests.fs: replace misleading happy-path test with a genuine error-path
test using ErrorHttpHandler returning 500, and rename to match actual behaviour
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This reverts commit ac0c81f.
Threading.Tasks.Task → Task; namespace already open. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LoggerConfigurationExtensions.fs: extract parseTimeSpan helper to remove duplicated period/retryTimeLimit parsing; use Defaults fields as fallbacks - LokiSink.fs: invert ownsClient branch so injected-client path is the simple case; collapse nested credentials null-check; open System.Threading.Tasks - Labels.fs: replace manual Map.fold key-extraction with Map.keys |> Set.ofSeq Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ler pattern The V8 ILokiHttpClient / BaseLokiHttpClient / LokiGzipHttpClient hierarchy was entirely removed in V9. Update the 'Custom HTTP Client' README section to show the V9 replacement: inject a DelegatingHandler via httpMessageHandler, or a pre-built HttpClient via httpClient (e.g. IHttpClientFactory). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update for the F# V9 rewrite: flat GrafanaLoki parameters, configuration reference, labels/auth/trace/batching sections, DelegatingHandler-based custom HttpClient, and a migrating-from-v8 table. Repoint links to serilog-contrib and drop the comparison section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The project referenced assets/icon.png, but the committed icon is assets/serilog-sink-nuget.png, so dotnet pack failed with NU5019. Point PackageIcon and the packaged None item at the real filename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds a logger entirely from JSON and captures the Loki push request on a loopback socket, proving Serilog.Settings.Configuration discovers the F# GrafanaLoki extension method and binds the flat args (uri, labels[]/propertiesAsLabels[] arrays, handleLogLevelAsLabel, batchSizeLimit, string period). Adds Serilog.Settings.Configuration + Microsoft.Extensions.Configuration.Json to the test project. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the string workaround. A struct-default DefaultParameterValue (Nullable<TimeSpan>()) emits the same [opt]+HasDefault(nullref) metadata as C#'s TimeSpan? = null, so Serilog.Settings.Configuration binds it: omitted -> null -> sink default; a JSON 'hh:mm:ss' string is converted to TimeSpan. Drops the parseTimeSpan helper. Validated end-to-end with the ASP.NET Core sample (captured the Loki POST with period/retryTimeLimit omitted from appsettings). Updates README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#2: ExtensionDefaultsTests asserts the GrafanaLoki literal defaults (batchSizeLimit, queueLimit, bools) equal LokiSinkOptions.Defaults so the two can't drift. #3: credentials is a single LokiCredentials parameter again (V8 shape), not flat credentialsLogin/credentialsPassword. F# records are not null-admissible (FS0043 on DefaultParameterValue(null)), so LokiCredentials becomes an [<AllowNullLiteral>] class to serve as a nullable optional that Settings.Configuration binds from a JSON object. AppSettingsBindingTests now also asserts the credentials object binds and yields a Basic-auth header. Verified Console and AspNetCore samples end-to-end against a real Loki (docker-compose). Updates README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add LokiFieldDestination (None/Body/StructuredMetadata) with TraceIdMode, SpanIdMode and PropertiesAsStructuredMetadata. Per-line structured metadata is emitted as the optional third element of each push entry, written only when non-empty so default output stays byte-identical and pre-3.0 Loki safe. Covered by unit + integration tests (98 unit tests green on net8/9/10) and documented in the README. Also registers the BenchmarkDotNet package version and bumps Microsoft.Extensions.Configuration.Json to 10.0.8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allocation/throughput pass on the V9 sink, applied one change at a time and validated against the full test suite. End-to-end at 10k events: Simple 19.5 -> 1.36 MB, 27 -> 18 ms; Exception 134 -> 66 MB, 188 -> 121 ms. - group events via IEqualityComparer<LogEvent> instead of a per-event F# Map (drops the per-event Map build plus structural-hash boxing) - read ex.StackTrace/ex.Source once (StackTrace rebuilds on every access) - render Message via RenderMessage(TextWriter); timestamp via Utf8Formatter - Events list -> array with a boxing-free sort; reuse the body Utf8JsonWriter - write structured metadata directly to the writer (no per-event Map) DateTime/DateTimeOffset body values now render as ISO 8601 / invariant (previously culture-dependent ToString()). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BenchmarkDotNet suites comparing V9 against V8 (8.3.2) and the third-party Serilog.Sinks.Loki.YetAnother (4.0.5), each driven through its public API over an in-process fake transport with a shared workload, config and entry point. Three executables because V8 and V9 share an assembly name and cannot coexist in one process. - build.fsx: add a cross-platform `Benchmark` target (dotnet fsi), kept out of the Default chain and the solution so CI stays lean - .husky/pre-commit: also Fantomas-format benchmarks/ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hedgehog/Hedgehog.Xunit were added with the initial test project but never used (no property-based test was ever written): drop both package versions and references, plus the MSB3277 suppression that existed only to mask Hedgehog's transitive FSharp.Core 8.x. All three TFMs build clean without it. Also drop two unused `open`s in LokiSink.fs left over from the perf pass, and normalize self-closing tag spacing in Directory.Build.targets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an explicit Fantomas section to .editorconfig instead of relying on implicit defaults (read by the CLI, the Husky hook, and Rider alike): - fsharp_multiline_bracket_style = aligned (dotnet/fsharp compiler style) - fsharp_keep_max_number_of_blank_lines = 1; max_line_length = 120 pinned - [*] gains charset = utf-8, indent_style/size, trim_trailing_whitespace, with overrides for YAML (2-space indent) and Markdown (keep hard breaks) Reformat the 9 files affected by the bracket style and strip the UTF-8 BOMs from src/ so the repo is uniformly BOM-less (v8/ left as is - slated for removal). Formatting is idempotent under the new rules; 108/108 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci.yaml: least-privilege token (contents: read), concurrency that cancels superseded PR runs only, job timeouts, workflow_dispatch, and NuGet caching. lock.yml gets the explicit permissions it needs. New benchmark.yml compares this source against the latest published package: on PRs touching src/, benchmarks/ or Directory.Packages.props it runs the end-to-end sink group for both sides and posts a delta table to the run summary and a sticky PR comment (Allocated is exact even on shared runners and serves as the regression signal; Mean is indicative). Manual dispatch adds a custom --filter and the YetAnother yardstick. The baseline version is resolved from NuGet at run time and guarded by API major; the V8 benchmark project gains an overridable BaselineVersion property and the shared config a brief JSON exporter feeding the new compare-results.fsx (dotnet fsi, in keeping with the repo's F#-first tooling). New dependency-review.yml flags vulnerable dependency changes on PRs, and dependabot now ignores the two deliberately pinned benchmark packages. Also corrects the integration-test fsproj comment: without Docker the tests fail, they are not skipped; CI runs them on ubuntu-latest, which has Docker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Matches the header every source file already carries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neither V8 nor V9 ever shipped a copyright field in the nuspec; the property also flows into AssemblyCopyright. Matches the source headers and LICENSE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restores what V8 shipped and V9 had lost: the package now carries symbols and a working SourceLink story. - DebugType=embedded folds the portable PDB (with its SourceLink map) into the assembly: line numbers in consumer stack traces and step-into that fetches the exact source from GitHub, with no symbol server involved - EmbedUntrackedSources covers build-generated files - PublishRepositoryUrl writes the repository url/branch/commit into the nuspec (previously commit-only) - ContinuousIntegrationBuild makes CI builds deterministic - PackageReleaseNotes points at the GitHub change log, as V8 did Verified on the packed output: PE debug directory shows EmbeddedPortablePdb + Reproducible, no loose .pdb, nuspec gains repository url and releaseNotes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The V8 source remains fully available on the master branch and the v8.x release tags; this copy matched it byte-for-byte modulo line-ending normalization. Pre-deletion audit confirmed everything was migrated or deliberately dropped per V9_SPEC.md (renaming strategy, HTTP client hierarchy, hand-rolled batching infra superseded by Serilog 4.x native batching), test coverage mapped to the V9 suites, samples ported, package metadata (symbols, SourceLink, copyright) restored. CodeQL stays dropped: it has no F# support. Also removes the pre-rewrite F# prototype and the CPM opt-out shim that existed only for this folder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The design-time spec served its purpose; the README and the project wiki are the living documentation now, and the spec had drifted (still described the EnrichTraceId/EnrichSpanId bools superseded by TraceIdMode/SpanIdMode and listed #255 as deferred though implemented). History preserves it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An invalid tenant previously surfaced only as a SelfLog warning with the X-Scope-OrgID header silently dropped - logs would then be 401-rejected or land under the wrong tenant. Now tenant gets the same fail-fast treatment as uri: alphanumerics plus !-_.*'(), at most 150 bytes, and not '.' or '..' (https://grafana.com/docs/loki/latest/operations/multi-tenancy/), throwing ArgumentException at CreateLogger time. Unlike v8's regex, '..' is only rejected as the exact value, and the documented 150-byte cap is enforced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Benchmark comparisonBaseline:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
V9 is a ground-up rewrite of the sink in F#, keeping a public API that stays idiomatic from C#. The custom delivery stack is replaced with Serilog 4.x native batching and a streaming serialization pipeline.
Highlights
IBatchedLogEventSink): bounded queue (50 000 by default), retry with exponential backoff (retryTimeLimit), fully async emission — no more dropped-on-failure batches (Exception while emitting periodic batch from Serilog.Sinks.Grafana.Loki.LokiSink #305) or dispose-time deadlocksUtf8JsonWriterover pooled buffers, no intermediate object graph or strings: on the Simple · 10 000 workload ~55× less allocation and ~4.5× faster than V8 (Benchmarks)HttpClient/HttpMessageHandler— replaces theILokiHttpClienthierarchy; gzip, retries, mTLS, bearer auth are standardDelegatingHandlerconcernspropertiesAsStructuredMetadataplustraceIdMode/spanIdModerouting (body or per-line metadata); opt-in, default output stays byte-identical and pre-3.0-Loki safeILokiExceptionFormatter)uriandtenant(validated against Loki's tenant ID rules) throwArgumentExceptionatCreateLoggertimenet8.0/net9.0/net10.0, requires Serilog 4.3.1+;FSharp.Corearrives as a transitive dependencyBreaking changes
Full list: Breaking changes · practical checklist: Upgrading from v8 to v9 · also in the README migration section.
Verification
After merge
The
v9.0.0tag will be cut manually onmaster(MinVer derives the package version from it).🤖 Generated with Claude Code