Commit 9533e37
[V9] New architecture + performance + features (#326)
* chore: scaffold V9 branch — archive V8, introduce CPM and slnx
- 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>
* chore: add F# library project scaffold for V9
- 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>
* feat: add PooledByteBufferWriter and Utf8TextWriter infrastructure
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>
* feat: implement complete V9 sink pipeline
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>
* chore: rename test/ to tests/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add UnitTests project with 47 tests across Labels, Grouping, LokiExceptionFormatter
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>
* chore: enforce strictest F# compiler settings solution-wide
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>
* feat: update docker-compose and add V9 sample projects
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>
* chore: rename sample/ to samples/ and update slnx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: wire format unit tests (74) + integration test scaffold
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>
* chore: Rider code formatting pass
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>
* feat: add StyleCop.Analyzers.Unstable to C# sample projects
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>
* chore: disable SA1518 and remove insert_final_newline convention
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>
* chore: remove StyleCop.ruleset, reference v8/ copy in editorconfig
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>
* chore: remove v8 folder references from code, tests and config
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>
* ci: add CI workflow for V9
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>
* Revert "ci: add CI workflow for V9"
This reverts commit c91665e.
* build: add FAKE 6.1.4 build script
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: update ci.yaml for V9; add Solution Items to slnx; format build.fsx
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>
* chore: add .gitattributes to enforce LF line endings
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>
* chore: add MIT copyright header to all F# source files; fix license expression
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>
* chore: remove codeql-analysis.yml
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>
* fix: three bugs found during LokiSinkOptions validation against real 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>
* refactor: replace two extension overloads with single flat-params method
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>
* test: add EnrichTraceId/SpanId unit tests and 2 new integration tests
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>
* fix: three Serilog.Settings.Configuration compatibility issues
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>
* fix: revert Nullable<TimeSpan> attempt — F# cannot match C# TimeSpan? 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>
* chore: complete MIT copyright header in all F# source files
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>
* chore: update dependencies via dotnet outdated
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>
* fix: wire RetryTimeLimit; drop TimeProvider; switch to Serilog native 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>
* chore: remove noise comments from source files
Five comments removed that explained WHAT rather than WHY:
LokiSink.fs: type annotation already on method signature.
LokiJsonTextFormatter.fs: obvious .NET IDisposable contract; caller reference.
Serialization.fs: Stream labels / Values array restate WritePropertyName calls; same Dispose contract.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add blank line after copyright header in all F# source files
Matches V8 style: empty line between the last header line and the
namespace/module declaration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: align V9 samples with V8 configuration
Console sample:
- Remove propertiesAsLabels: [ThreadId] — V8 did not have this; a simple
static label is sufficient to demonstrate basic sink usage
Web app appsettings.json:
- Restore static label app=web_app (was missing in V9)
- Restore propertiesAsLabels: [app] matching V8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: Rider warning fixes and formatting pass
Rider fixed warnings and reformatted code across src, tests, samples and build.
One introduced error reverted: Rider added namespace Serilog.Sinks.Grafana.Loki.Sample
to Person.cs and a matching using to Program.cs, but this creates CS0234 because
Serilog.Sinks.Grafana.Loki is already a namespace in the referenced F# library.
Person.cs reverted to global namespace (top-level statements stay in global scope).
77 unit tests pass unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: auto-format staged F# files on commit via Husky.Net + Fantomas
Tools (.config/dotnet-tools.json):
fantomas 7.0.5 — F# formatter (same engine Rider uses internally)
husky 0.9.1 — git hooks manager
Directory.Build.targets:
HuskyInstall target runs dotnet husky install after every restore.
Contributors get hooks automatically the first time they build —
no manual steps required. HUSKY=0 or CI=true skips installation.
ContinueOnError=true prevents failures on subsequent restores.
.husky/pre-commit (Option B — auto-format + re-stage):
1. Collects staged .fs/.fsx files under src/, tests/, and build.fsx
2. Runs fantomas on them (formats in-place)
3. Re-stages the formatted files — commit proceeds without interruption
Fantomas check on src/: all files already clean (exit 0).
Fantomas check on tests/: GroupingTests.fs and WireFormatTests.fs
needed formatting — formatted and verified (77 tests still pass).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: simplify HuskyInstall condition; set HUSKY=0 in ci.yaml
Removed the AND CI=='' check — CI env variable is not standardised across
providers. HUSKY=0 is the explicit, documented opt-out that Husky.Net itself
recommends. CI pipelines should set HUSKY=0 explicitly rather than relying on
an auto-detected CI variable.
Added HUSKY=0 to ci.yaml env section so hook installation is skipped on all
GitHub Actions runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: verify Fantomas hook auto-formats on commit
* Revert "test: verify Fantomas hook auto-formats on commit"
This reverts commit 1576d48.
* chore: improve HuskyInstall target following reference pattern
- BeforeTargets instead of AfterTargets: hooks installed before packages restore
- Exists(.git) condition: Docker/non-git contexts skipped automatically, no config needed
- IgnoreExitCode instead of ContinueOnError: truly silent on re-runs, no spurious warnings
- StandardErrorImportance=High: errors remain visible for debugging despite IgnoreExitCode
- Removed WorkingDirectory from tool restore (upward search finds .config/dotnet-tools.json)
- HUSKY=0 condition retained as explicit opt-out alongside the .git check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: harden sink correctness from PR review findings
- Labels.fs: use InvariantCulture for label values; apply sanitizeLabelKey to global label keys
- LokiExceptionFormatter.fs: add depth-20 guard against cyclic InnerException chains
- LokiJsonTextFormatter.fs: null-guard exceptionFormatter in primary constructor
- LokiSink.fs: null-coalesce Labels/PropertiesAsLabels arrays; log SelfLog on serialization failure; handle null response.Content; warn when X-Scope-OrgID header cannot be set
- LoggerConfigurationExtensions.fs: parse period/retryTimeLimit with InvariantCulture and emit descriptive error messages on invalid input
- WireFormatTests.fs: add new tests covering null scalar, non-finite double, sequence value serialization, URI validation, SpanId opt-out, custom formatter path, and HTTP error path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: three correctness issues found in second review pass
- Serialization.fs: restrict fast-path FormatToBuffer to exact LokiJsonTextFormatter
type so subclass Format overrides are honoured (isinst matched derived types)
- LokiSink.fs: dispose HttpResponseMessage via use! to avoid connection pool leak
- LokiSink.fs: guard X-Scope-OrgID header mutation behind ownsClient, consistent
with the existing Basic Auth guard (injected clients are pre-configured)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: normalise base URI trailing slash before push endpoint resolution
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>
* feat: add GrafanaLoki(LokiSinkOptions) overload per V9 spec
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>
* fix: preserve serialization exception stack trace; add real HTTP error 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>
* Revert "feat: add GrafanaLoki(LokiSinkOptions) overload per V9 spec"
This reverts commit ac0c81f.
* chore: simplify redundant namespace qualifier in ErrorHttpHandler
Threading.Tasks.Task → Task; namespace already open.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: simplify LokiSink, LoggerConfigurationExtensions and Labels
- 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>
* docs: replace stale V8 ILokiHttpClient section with V9 DelegatingHandler 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>
* Rewrite README for v9
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>
* Fix NuGet package icon filename in fsproj
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>
* Add appsettings.json binding test for Settings.Configuration
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>
* Use Nullable<TimeSpan> for period and retryTimeLimit
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>
* Add defaults drift-guard test; restore LokiCredentials object parameter
#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>
* feat: structured metadata and TraceId/SpanId routing (#255)
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>
* perf: reduce allocations and speed up the serialization path
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>
* test: add V8/V9/YetAnother benchmark suite and a FAKE target
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>
* chore: remove unused Hedgehog dependency and tidy imports/XML
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>
* style: pin F# formatting rules, adopt aligned brackets, drop BOMs
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: harden pipelines and add NuGet-baseline benchmark comparison
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>
* chore: update LICENSE copyright year range to 2020-2026
Matches the header every source file already carries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: add Copyright to package and assembly metadata
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>
* feat: ship embedded symbols and full SourceLink metadata in the package
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>
* chore: remove the v8/ reference copy
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>
* chore: remove V9_SPEC.md
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>
* docs: add CLAUDE.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: credit the OG Loki sink and YetAnother in README
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: validate tenant against Loki tenant ID rules at configuration time
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>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>1 parent dd4bf5d commit 9533e37
144 files changed
Lines changed: 6249 additions & 3874 deletions
File tree
- .config
- .github
- ISSUE_TEMPLATE
- workflows
- .husky
- benchmarks
- Serilog.Sinks.Grafana.Loki.Benchmarks.V8
- Serilog.Sinks.Grafana.Loki.Benchmarks.V9
- Serilog.Sinks.Grafana.Loki.Benchmarks.YetAnother
- build
- samples
- Serilog.Sinks.Grafana.Loki.SampleWebApp
- Controllers
- Properties
- Serilog.Sinks.Grafana.Loki.Sample
- sample
- Serilog.Sinks.Grafana.Loki.SampleWebApp
- Controllers
- Serilog.Sinks.Grafana.Loki.Sample
- src/Serilog.Sinks.Grafana.Loki
- HttpClients
- Infrastructure
- Models
- Utils
- tests
- Serilog.Sinks.Grafana.Loki.IntegrationTests
- Serilog.Sinks.Grafana.Loki.UnitTests
- test/Serilog.Sinks.Grafana.Loki.Tests
- HttpClientsTests
- InfrastructureTests
- IntegrationTests
- Approvals
- TestHelpers
- Backoff
- UtilsTests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
12 | 19 | | |
13 | 20 | | |
14 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
0 commit comments