Skip to content

Commit 1d88759

Browse files
mishamyteclaude
andauthored
bench: cover the custom ITextFormatter path (#350)
SinkBenchmarks never passes textFormatter, so every benchmarked configuration takes the built-in LokiJsonTextFormatter fast path. FormatterBenchmarks measures the public Format surface, which the sink does not use in production. The custom-formatter route through the serializer -- Utf8TextWriter over the pooled body buffer -- is therefore not benchmarked at all, and an allocation regression on it is invisible to the CI signal. Add a CustomFormatterSinkBenchmarks group driving the same end-to-end pipeline through MessageTemplateTextFormatter with Serilog's stock output template (the shape reported in #347). Added as a separate group rather than a Formatter param on SinkBenchmarks so the existing rows keep their names and pair with published baselines unchanged. Setup shared by both groups is extracted into SinkSetup, in both the Current and NuGet projects -- Benchmarks.fs is per-project, and the names and params have to match for compare-results.fsx to pair the rows. Refs #349 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5a8418f commit 1d88759

2 files changed

Lines changed: 153 additions & 54 deletions

File tree

benchmarks/Serilog.Sinks.Grafana.Loki.Benchmarks.Current/Benchmarks.fs

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111
namespace Benchmarks
1212

1313
open System
14+
open System.Globalization
1415
open System.IO
1516
open System.Net
1617
open System.Net.Http
1718
open BenchmarkDotNet.Attributes
1819
open Serilog
1920
open Serilog.Core
2021
open Serilog.Events
22+
open Serilog.Formatting
23+
open Serilog.Formatting.Display
2124
open Serilog.Sinks.Grafana.Loki
2225
open Benchmarks.Shared
2326

@@ -72,41 +75,30 @@ type FormatterBenchmarks() =
7275
[<Benchmark>]
7376
member this.Format_Exception() = this.FormatAll(withException)
7477

75-
// ── Group 2: end-to-end sink push (real production serialization + batching) ──────
76-
// Drives the public WriteTo.GrafanaLoki pipeline. By default the fake 204 transport
78+
// Shared setup for the two end-to-end groups below. By default the fake 204 transport
7779
// keeps the measurement deterministic; set LOKI_BENCH_TARGET (e.g. http://localhost:3100)
7880
// to push to a real Loki started via docker-compose instead.
79-
80-
[<Config(typeof<Config.SinkConfig>)>]
81-
type SinkBenchmarks() =
82-
let target = Environment.GetEnvironmentVariable "LOKI_BENCH_TARGET"
83-
let useReal = not (String.IsNullOrWhiteSpace target)
84-
let label: LokiLabel = { Key = "app"; Value = "benchmarks" }
85-
86-
let mutable events: LogEvent[] = [||]
87-
let mutable logger: Logger = null
88-
89-
[<Params(1000, 10000)>]
90-
member val EventCount = 1000 with get, set
91-
92-
[<Params("Simple", "Exception")>]
93-
member val Payload = "Simple" with get, set
94-
95-
[<IterationSetup>]
96-
member this.IterationSetup() =
97-
events <-
98-
if this.Payload = "Exception" then
99-
EventGen.buildWithException this.EventCount
100-
else
101-
EventGen.buildSimple this.EventCount
102-
81+
module private SinkSetup =
82+
let private target = Environment.GetEnvironmentVariable "LOKI_BENCH_TARGET"
83+
let private useReal = not (String.IsNullOrWhiteSpace target)
84+
let private label: LokiLabel = { Key = "app"; Value = "benchmarks" }
85+
86+
let buildEvents (payload: string) (count: int) =
87+
if payload = "Exception" then
88+
EventGen.buildWithException count
89+
else
90+
EventGen.buildSimple count
91+
92+
/// A null textFormatter selects the sink's built-in LokiJsonTextFormatter.
93+
let buildLogger (textFormatter: ITextFormatter) =
10394
let cfg = LoggerConfiguration()
10495

10596
let configured =
10697
if useReal then
10798
cfg.WriteTo.GrafanaLoki(
10899
target,
109100
labels = [| label |],
101+
textFormatter = textFormatter,
110102
batchSizeLimit = 1000,
111103
queueLimit = 10_000_000,
112104
period = Nullable(TimeSpan.FromHours 1.0)
@@ -115,13 +107,34 @@ type SinkBenchmarks() =
115107
cfg.WriteTo.GrafanaLoki(
116108
"http://localhost:9999",
117109
labels = [| label |],
110+
textFormatter = textFormatter,
118111
httpMessageHandler = (new Fake204Handler() :> HttpMessageHandler),
119112
batchSizeLimit = 1000,
120113
queueLimit = 10_000_000,
121114
period = Nullable(TimeSpan.FromHours 1.0)
122115
)
123116

124-
logger <- configured.CreateLogger()
117+
configured.CreateLogger()
118+
119+
// ── Group 2: end-to-end sink push (real production serialization + batching) ──────
120+
// Drives the public WriteTo.GrafanaLoki pipeline with the built-in formatter, which the
121+
// sink serializes through its internal FormatToBuffer fast path.
122+
123+
[<Config(typeof<Config.SinkConfig>)>]
124+
type SinkBenchmarks() =
125+
let mutable events: LogEvent[] = [||]
126+
let mutable logger: Logger = null
127+
128+
[<Params(1000, 10000)>]
129+
member val EventCount = 1000 with get, set
130+
131+
[<Params("Simple", "Exception")>]
132+
member val Payload = "Simple" with get, set
133+
134+
[<IterationSetup>]
135+
member this.IterationSetup() =
136+
events <- SinkSetup.buildEvents this.Payload this.EventCount
137+
logger <- SinkSetup.buildLogger null
125138

126139
[<Benchmark>]
127140
member _.Push() =
@@ -130,4 +143,41 @@ type SinkBenchmarks() =
130143

131144
// Disposing the logger flushes the batching sink synchronously — this is where
132145
// the batch is serialized and POSTed, so it must be inside the measured region.
146+
logger.Dispose()
147+
148+
// ── Group 3: end-to-end sink push through a custom ITextFormatter ─────────────────
149+
// Group 2 only ever exercises the built-in formatter. A user-supplied ITextFormatter
150+
// takes a different route through Serialization — Utf8TextWriter over the pooled body
151+
// buffer rather than FormatToBuffer — so without this group nothing in CI watches that
152+
// path, and an allocation regression there is invisible.
153+
154+
[<Config(typeof<Config.SinkConfig>)>]
155+
type CustomFormatterSinkBenchmarks() =
156+
// Serilog's stock output template: by far the most common custom formatter, and the
157+
// shape reported in #347. Invariant culture keeps rendering agent-independent.
158+
let formatter =
159+
MessageTemplateTextFormatter(
160+
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
161+
CultureInfo.InvariantCulture
162+
)
163+
164+
let mutable events: LogEvent[] = [||]
165+
let mutable logger: Logger = null
166+
167+
[<Params(1000)>]
168+
member val EventCount = 1000 with get, set
169+
170+
[<Params("Simple", "Exception")>]
171+
member val Payload = "Simple" with get, set
172+
173+
[<IterationSetup>]
174+
member this.IterationSetup() =
175+
events <- SinkSetup.buildEvents this.Payload this.EventCount
176+
logger <- SinkSetup.buildLogger formatter
177+
178+
[<Benchmark>]
179+
member _.Push() =
180+
for e in events do
181+
logger.Write(e)
182+
133183
logger.Dispose()

benchmarks/Serilog.Sinks.Grafana.Loki.Benchmarks.NuGet/Benchmarks.fs

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111
namespace Benchmarks
1212

1313
open System
14+
open System.Globalization
1415
open System.IO
1516
open System.Net
1617
open System.Net.Http
1718
open BenchmarkDotNet.Attributes
1819
open Serilog
1920
open Serilog.Core
2021
open Serilog.Events
22+
open Serilog.Formatting
23+
open Serilog.Formatting.Display
2124
open Serilog.Sinks.Grafana.Loki
2225
open Benchmarks.Shared
2326

@@ -72,41 +75,30 @@ type FormatterBenchmarks() =
7275
[<Benchmark>]
7376
member this.Format_Exception() = this.FormatAll(withException)
7477

75-
// ── Group 2: end-to-end sink push (real production serialization + batching) ──────
76-
// Drives the public WriteTo.GrafanaLoki pipeline. By default the fake 204 transport
78+
// Shared setup for the two end-to-end groups below. By default the fake 204 transport
7779
// keeps the measurement deterministic; set LOKI_BENCH_TARGET (e.g. http://localhost:3100)
7880
// to push to a real Loki started via docker-compose instead.
79-
80-
[<Config(typeof<Config.SinkConfig>)>]
81-
type SinkBenchmarks() =
82-
let target = Environment.GetEnvironmentVariable "LOKI_BENCH_TARGET"
83-
let useReal = not (String.IsNullOrWhiteSpace target)
84-
let label: LokiLabel = { Key = "app"; Value = "benchmarks" }
85-
86-
let mutable events: LogEvent[] = [||]
87-
let mutable logger: Logger = null
88-
89-
[<Params(1000, 10000)>]
90-
member val EventCount = 1000 with get, set
91-
92-
[<Params("Simple", "Exception")>]
93-
member val Payload = "Simple" with get, set
94-
95-
[<IterationSetup>]
96-
member this.IterationSetup() =
97-
events <-
98-
if this.Payload = "Exception" then
99-
EventGen.buildWithException this.EventCount
100-
else
101-
EventGen.buildSimple this.EventCount
102-
81+
module private SinkSetup =
82+
let private target = Environment.GetEnvironmentVariable "LOKI_BENCH_TARGET"
83+
let private useReal = not (String.IsNullOrWhiteSpace target)
84+
let private label: LokiLabel = { Key = "app"; Value = "benchmarks" }
85+
86+
let buildEvents (payload: string) (count: int) =
87+
if payload = "Exception" then
88+
EventGen.buildWithException count
89+
else
90+
EventGen.buildSimple count
91+
92+
/// A null textFormatter selects the sink's built-in LokiJsonTextFormatter.
93+
let buildLogger (textFormatter: ITextFormatter) =
10394
let cfg = LoggerConfiguration()
10495

10596
let configured =
10697
if useReal then
10798
cfg.WriteTo.GrafanaLoki(
10899
target,
109100
labels = [| label |],
101+
textFormatter = textFormatter,
110102
batchSizeLimit = 1000,
111103
queueLimit = 10_000_000,
112104
period = Nullable(TimeSpan.FromHours 1.0)
@@ -115,13 +107,34 @@ type SinkBenchmarks() =
115107
cfg.WriteTo.GrafanaLoki(
116108
"http://localhost:9999",
117109
labels = [| label |],
110+
textFormatter = textFormatter,
118111
httpMessageHandler = (new Fake204Handler() :> HttpMessageHandler),
119112
batchSizeLimit = 1000,
120113
queueLimit = 10_000_000,
121114
period = Nullable(TimeSpan.FromHours 1.0)
122115
)
123116

124-
logger <- configured.CreateLogger()
117+
configured.CreateLogger()
118+
119+
// ── Group 2: end-to-end sink push (real production serialization + batching) ──────
120+
// Drives the public WriteTo.GrafanaLoki pipeline with the built-in formatter, which the
121+
// sink serializes through its internal FormatToBuffer fast path.
122+
123+
[<Config(typeof<Config.SinkConfig>)>]
124+
type SinkBenchmarks() =
125+
let mutable events: LogEvent[] = [||]
126+
let mutable logger: Logger = null
127+
128+
[<Params(1000, 10000)>]
129+
member val EventCount = 1000 with get, set
130+
131+
[<Params("Simple", "Exception")>]
132+
member val Payload = "Simple" with get, set
133+
134+
[<IterationSetup>]
135+
member this.IterationSetup() =
136+
events <- SinkSetup.buildEvents this.Payload this.EventCount
137+
logger <- SinkSetup.buildLogger null
125138

126139
[<Benchmark>]
127140
member _.Push() =
@@ -130,4 +143,40 @@ type SinkBenchmarks() =
130143

131144
// Disposing the logger flushes the batching sink synchronously — this is where
132145
// the batch is serialized and POSTed, so it must be inside the measured region.
146+
logger.Dispose()
147+
148+
// ── Group 3: end-to-end sink push through a custom ITextFormatter ─────────────────
149+
// Group 2 only ever exercises the built-in formatter. A user-supplied ITextFormatter
150+
// takes a different route through the serializer, so without this group nothing in CI
151+
// watches that path, and an allocation regression there is invisible.
152+
153+
[<Config(typeof<Config.SinkConfig>)>]
154+
type CustomFormatterSinkBenchmarks() =
155+
// Serilog's stock output template: by far the most common custom formatter, and the
156+
// shape reported in #347. Invariant culture keeps rendering agent-independent.
157+
let formatter =
158+
MessageTemplateTextFormatter(
159+
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
160+
CultureInfo.InvariantCulture
161+
)
162+
163+
let mutable events: LogEvent[] = [||]
164+
let mutable logger: Logger = null
165+
166+
[<Params(1000)>]
167+
member val EventCount = 1000 with get, set
168+
169+
[<Params("Simple", "Exception")>]
170+
member val Payload = "Simple" with get, set
171+
172+
[<IterationSetup>]
173+
member this.IterationSetup() =
174+
events <- SinkSetup.buildEvents this.Payload this.EventCount
175+
logger <- SinkSetup.buildLogger formatter
176+
177+
[<Benchmark>]
178+
member _.Push() =
179+
for e in events do
180+
logger.Write(e)
181+
133182
logger.Dispose()

0 commit comments

Comments
 (0)