Skip to content

Commit 375be12

Browse files
committed
Use relaxed JSON escaping for log bodies
The body formatter and batch serializer build Utf8JsonWriters with no options, so they use the default HTML-safe JavaScriptEncoder, which unicode-escapes every quote, ', <, >, & and all non-ASCII characters. Because Serilog quotes string property values by default, this makes most stored Loki lines unreadable (a regression from v8.x, which formatted bodies with Serilog's JsonValueFormatter and standard escaping). Construct the writers with JsonWriterOptions(Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping). Output stays valid JSON and UTF-8, so consumers (including the json parser) are unaffected. Add a unit test covering markup and non-ASCII characters.
1 parent d3687c9 commit 375be12

3 files changed

Lines changed: 39 additions & 3 deletions

File tree

src/Serilog.Sinks.Grafana.Loki/LokiJsonTextFormatter.fs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace Serilog.Sinks.Grafana.Loki
1212

1313
open System
1414
open System.Buffers
15+
open System.Text.Encodings.Web
1516
open System.Text.Json
1617
open Serilog.Events
1718
open Serilog.Formatting
@@ -30,6 +31,12 @@ type LokiJsonTextFormatter(exceptionFormatter: ILokiExceptionFormatter, enrichTr
3031
static let pTraceId = JsonEncodedText.Encode "TraceId"
3132
static let pSpanId = JsonEncodedText.Encode "SpanId"
3233

34+
// Relaxed escaping keeps the body readable: only JSON-mandatory escapes, non-ASCII verbatim.
35+
// The default Utf8JsonWriter encoder is HTML-safe and would \uXXXX-escape every '"', '<', '>',
36+
// '&', '\'' and all non-ASCII. Output stays valid JSON, so consumers are unaffected.
37+
static let relaxedWriterOptions =
38+
JsonWriterOptions(Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping)
39+
3340
// Names that collide with top-level JSON keys; prefixed with '_' when seen as properties.
3441
static let reserved =
3542
Collections.Generic.HashSet<string>(
@@ -173,7 +180,9 @@ type LokiJsonTextFormatter(exceptionFormatter: ILokiExceptionFormatter, enrichTr
173180
use buffer = new PooledByteBufferWriter(256)
174181
// Throwaway writer + scratch for the message render. The internal sink path passes reused
175182
// instances instead; the public path stays allocation-light but fully thread-safe.
176-
use jsonWriter = new Utf8JsonWriter(buffer :> IBufferWriter<byte>)
183+
use jsonWriter =
184+
new Utf8JsonWriter(buffer :> IBufferWriter<byte>, relaxedWriterOptions)
185+
177186
use messageBuffer = new PooledByteBufferWriter(128)
178187
use messageWriter = new Utf8TextWriter(messageBuffer)
179188
self.FormatToBuffer(logEvent, jsonWriter, messageWriter)

src/Serilog.Sinks.Grafana.Loki/Serialization.fs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,22 @@ open System
1616
open System.Buffers
1717
open System.Buffers.Text
1818
open System.Collections.Generic
19+
open System.Text.Encodings.Web
1920
open System.Text.Json
2021
open Microsoft.FSharp.NativeInterop
2122
open Serilog.Events
2223
open Serilog.Formatting
2324
open Serilog.Sinks.Grafana.Loki.Infrastructure
2425

26+
/// Relaxed JSON escaping for the bytes the sink emits. The default Utf8JsonWriter encoder is
27+
/// HTML-safe and renders every '"', '<', '>', '&', '\'' and all non-ASCII as \uXXXX, which makes
28+
/// the stored log line unreadable; the relaxed encoder escapes only what JSON mandates and emits
29+
/// non-ASCII verbatim. Output stays valid JSON (and valid UTF-8), so consumers are unaffected.
30+
[<AutoOpen>]
31+
module private JsonWriterDefaults =
32+
let relaxedWriterOptions =
33+
JsonWriterOptions(Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping)
34+
2535
/// Reusable per-sink serialization scratch: the buffers and writers reused across every batch
2636
/// (cleared/reset between uses), bundled so the serializer takes one value instead of several
2737
/// same-typed positional args (easy to mis-order), and disposed as a unit by the sink.
@@ -30,7 +40,10 @@ type internal SerializationBuffers() =
3040
let main = new PooledByteBufferWriter(4096)
3141
let body = new PooledByteBufferWriter(256)
3242
let message = new PooledByteBufferWriter(256)
33-
let bodyWriter = new Utf8JsonWriter(body :> IBufferWriter<byte>)
43+
44+
let bodyWriter =
45+
new Utf8JsonWriter(body :> IBufferWriter<byte>, relaxedWriterOptions)
46+
3447
let messageWriter = new Utf8TextWriter(message)
3548

3649
/// Envelope buffer holding the full push payload; read by LokiPushContent after serialize.
@@ -87,7 +100,8 @@ module internal Serialization =
87100
| :? LokiJsonTextFormatter as fmt when fmt.GetType() = typeof<LokiJsonTextFormatter> -> fmt
88101
| _ -> Unchecked.defaultof<LokiJsonTextFormatter>
89102

90-
use jsonWriter = new Utf8JsonWriter(buffers.Main :> IBufferWriter<byte>)
103+
use jsonWriter =
104+
new Utf8JsonWriter(buffers.Main :> IBufferWriter<byte>, relaxedWriterOptions)
91105

92106
// Reused stack scratch for the per-event Unix-nanosecond timestamp. Hoisted out of
93107
// the event loop so the localloc happens once per batch, not once per event. An

tests/Serilog.Sinks.Grafana.Loki.UnitTests/WireFormatTests.fs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,19 @@ let ``body: contains Message and MessageTemplate`` () : Task =
252252
test <@ hasMsg && hasTpl @>
253253
}
254254

255+
[<Fact>]
256+
let ``body: quotes, markup and non-ASCII are not unicode-escaped`` () : Task =
257+
task {
258+
let handler, sink = makeSink id
259+
use _ = sink
260+
do! flush sink [ mkInfo [ "Note", box "a>b <c> & \"q\" Привет" ] ]
261+
let raw = handler.LastBodyText
262+
// Relaxed escaping emits '<' '>' '&' and non-ASCII verbatim; the default HTML-safe encoder
263+
// would render them as \uXXXX, so these literal substrings could not appear in the payload.
264+
test <@ raw.Contains("a>b <c> &") @>
265+
test <@ raw.Contains("Привет") @>
266+
}
267+
255268
[<Fact>]
256269
let ``body: reserved property "Message" sanitized to "_Message"`` () : Task =
257270
task {

0 commit comments

Comments
 (0)