![]() |
VOOZH | about |
dotnet add package Serilog.Sinks.Grafana.Loki --version 9.0.0
NuGet\Install-Package Serilog.Sinks.Grafana.Loki -Version 9.0.0
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Grafana.Loki" Version="9.0.0" />Directory.Packages.props
<PackageReference Include="Serilog.Sinks.Grafana.Loki" />Project file
paket add Serilog.Sinks.Grafana.Loki --version 9.0.0
#r "nuget: Serilog.Sinks.Grafana.Loki, 9.0.0"
#:package Serilog.Sinks.Grafana.Loki@9.0.0
#addin nuget:?package=Serilog.Sinks.Grafana.Loki&version=9.0.0Install as a Cake Addin
#tool nuget:?package=Serilog.Sinks.Grafana.Loki&version=9.0.0Install as a Cake Tool
π Made in Ukraine
π Build status
π NuGet version
π Latest release
π Documentation
By using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement ** to all the following statements:
Glory to Ukraine! πΊπ¦
The Serilog Grafana Loki sink project is a sink (basically a writer) for the Serilog logging framework. Structured log events are written to sinks and each sink is responsible for writing it to its own backend, database, store etc. This sink delivers the data to Grafana Loki, a horizontally-scalable, highly-available, multi-tenant log aggregation system. It allows you to use Grafana for visualizing your logs.
You can find more information about what Loki is over on Grafana's website here.
V9 is a ground-up rewrite of the sink in F#, keeping a public API that remains idiomatic to call from C#. The rewrite fixes a class of long-standing structural bugs and modernizes the delivery pipeline:
IBatchedLogEventSink, giving a bounded queue by default (50 000 events), async emission, and retry with
exponential backoff β no more dropped batches on failure and no dispose-time deadlocks.Utf8JsonWriter over pooled buffers β
no intermediate object graph and no intermediate strings.HttpClient / HttpMessageHandler. The old ILokiHttpClient/LokiGzipHttpClient hierarchy is
replaced by direct injection; gzip, retries, mTLS and bearer auth are now standard DelegatingHandler concerns.ILokiExceptionFormatter), TraceId/SpanId routing to the body or
Loki structured metadata, startup URI validation, and the log level exposed as a label using Grafana's vocabulary.See Migrating from V8 for the full list of breaking changes.
net8.0, net9.0, or net10.0 (earlier target frameworks are EOL and no longer supported).4.3.1 or later.FSharp.Core is pulled in automatically. No other sink packages are required.System.Text.Json serialization with pooled buffers (no intermediate object graph or strings)X-Scope-OrgID) supportHttpClient / HttpMessageHandler β gzip, retries, mTLS and bearer auth via DelegatingHandlerTraceId / SpanId from the ambient Activity (OpenTelemetry) β written to the log body or as structured metadataSerilog.Settings.Configuration (appsettings.json) supportThe Serilog.Sinks.Grafana.Loki
NuGet package can be found here. Install it via one of the
following commands:
NuGet command:
Install-Package Serilog.Sinks.Grafana.Loki
.NET CLI:
dotnet add package Serilog.Sinks.Grafana.Loki
In the following example, the sink will send log events to Loki available on http://localhost:3100:
using Serilog;
using Serilog.Sinks.Grafana.Loki;
Log.Logger = new LoggerConfiguration()
.WriteTo.GrafanaLoki(
"http://localhost:3100",
[new LokiLabel { Key = "app", Value = "web_app" }])
.CreateLogger();
Log.Information("The god of the day is {@God}", odin);
The sink posts to
<uri>/loki/api/v1/push. The push path is appended automatically β pass only the base address (a path prefix such ashttp://gateway/lokiis supported and preserved).
Used together with Serilog.Settings.Configuration, the same
sink can be configured from appsettings.json:
{
"Serilog": {
"Using": [
"Serilog.Sinks.Grafana.Loki"
],
"MinimumLevel": {
"Default": "Debug"
},
"WriteTo": [
{
"Name": "GrafanaLoki",
"Args": {
"uri": "http://localhost:3100",
"labels": [
{
"key": "app",
"value": "web_app"
}
],
"propertiesAsLabels": [
"app"
]
}
}
]
}
}
All options are passed as named arguments to WriteTo.GrafanaLoki(...). Only uri is required.
| Parameter | Type | Default | Description |
|---|---|---|---|
uri |
string |
β (required) | Loki base URI, e.g. http://localhost:3100. Validated at startup; must be an absolute http/https URI. |
labels |
LokiLabel[] |
[] |
Static labels attached to every stream. |
propertiesAsLabels |
string[] |
[] |
Log-event property names to promote to stream labels. |
propertiesAsStructuredMetadata |
string[] |
[] |
Property names to attach as per-line structured metadata (non-indexed; Loki 3.0+). |
handleLogLevelAsLabel |
bool |
true |
Add a level label using Grafana's level vocabulary. |
credentials |
LokiCredentials |
null |
Basic-auth credentials. From appsettings.json, an object with login/password. |
tenant |
string |
null |
Value for the X-Scope-OrgID multi-tenancy header; validated at startup. |
traceIdMode |
LokiFieldDestination |
None |
Where to write the event's TraceId: None, Body, or StructuredMetadata. |
spanIdMode |
LokiFieldDestination |
None |
Where to write the event's SpanId: None, Body, or StructuredMetadata. |
batchSizeLimit |
int |
1000 |
Maximum events per HTTP POST. |
queueLimit |
int |
50000 |
Maximum events buffered in memory before new events are dropped. |
period |
TimeSpan? |
1 s |
Flush interval. From appsettings.json, written as an "hh:mm:ss" string. |
eagerlyEmitFirstEvent |
bool |
true |
Flush immediately on the first event (surfaces misconfiguration early). |
retryTimeLimit |
TimeSpan? |
10 min |
Stop retrying a failed batch after this duration. From appsettings.json, an "hh:mm:ss" string. |
textFormatter |
ITextFormatter |
LokiJsonTextFormatter |
Per-event body formatter. |
exceptionFormatter |
ILokiExceptionFormatter |
LokiExceptionFormatter |
Exception serializer. |
httpClient |
HttpClient |
null |
Pre-built client (e.g. from IHttpClientFactory). The sink never disposes an injected client. |
httpMessageHandler |
HttpMessageHandler |
null |
Handler for the sink's own client (gzip, retries, β¦). Ignored when httpClient is set. |
restrictedToMinimumLevel |
LogEventLevel |
Verbose |
Minimum level handled by this sink. |
Note on
period/retryTimeLimit: in C# these areTimeSpan?(e.g.period: TimeSpan.FromSeconds(5)); leave them unset to use the defaults. Inappsettings.jsonthey are written as"hh:mm:ss"strings (e.g."00:00:05"), whichSerilog.Settings.Configurationconverts toTimeSpan.
A more complete C# example:
Log.Logger = new LoggerConfiguration()
.WriteTo.GrafanaLoki(
"http://localhost:3100",
labels: [new LokiLabel { Key = "app", Value = "my-service" }],
propertiesAsLabels: ["RequestPath"],
credentials: new LokiCredentials { Login = "user", Password = "pass" },
tenant: "my-tenant",
traceIdMode: LokiFieldDestination.StructuredMetadata,
queueLimit: 100_000,
period: TimeSpan.FromSeconds(2))
.CreateLogger();
Configuration details for appsettings.json are also documented
in the wiki.
Each log event is mapped to a Loki stream identified by its label set. Events that resolve to the same label set are grouped into one stream and ordered by timestamp.
Labels come from three sources, in descending priority:
labels option, attached to every stream.level label β added when handleLogLevelAsLabel is true (the default).propertiesAsLabels.When keys collide, a higher-priority source wins; a property is silently skipped if its key matches a global label or the
reserved level key. Unlike V8, properties promoted to labels are kept in the log body as well β promotion no longer
removes them from the event.
Other rules:
{0}) are prefixed with param, becoming param0.Verbose β trace, Debug β debug,
Information β info, Warning β warning, Error β error, Fatal β fatal (previously critical).Basic authentication is configured with a credentials object:
.WriteTo.GrafanaLoki(
"http://localhost:3100",
credentials: new LokiCredentials { Login = "user", Password = "pass" })
Basic auth is applied only to a client the sink creates. If you inject your own
httpClient, configure itsAuthorizationheader yourself β the sink never mutates an injected client.
Multi-tenancy is configured with tenant, which sets the X-Scope-OrgID header:
.WriteTo.GrafanaLoki("http://localhost:3100", tenant: "tenant-1")
The tenant ID is validated at configuration time against
Loki's tenant ID rules β alphanumerics plus
!-_.*'(), at most 150 bytes, and not . or .. β an invalid value throws ArgumentException instead of producing
batches Loki would reject.
Bearer tokens / OAuth2 are not a first-class option β add them through the injected client, either by setting a
default Authorization header on an HttpClient or via a DelegatingHandler (see below).
V9 accepts a standard HttpClient or HttpMessageHandler directly. Inject your own to add gzip compression, retries,
mTLS, bearer auth, or any other cross-cutting behaviour:
// GzipHandler.cs
using System.IO.Compression;
using System.Net.Http;
using System.Net.Http.Headers;
public class GzipHandler : DelegatingHandler
{
public GzipHandler() : base(new HttpClientHandler()) { }
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
if (request.Content is not null)
{
var bytes = await request.Content.ReadAsByteArrayAsync(ct);
using var ms = new MemoryStream();
using (var gz = new GZipStream(ms, CompressionLevel.Fastest, leaveOpen: true))
await gz.WriteAsync(bytes, ct);
request.Content = new ByteArrayContent(ms.ToArray());
request.Content.Headers.ContentType =
new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" };
request.Content.Headers.ContentEncoding.Add("gzip");
}
return await base.SendAsync(request, ct);
}
}
// Inject via httpMessageHandler β the sink creates and owns the HttpClient
// (and still applies basic auth / tenant headers).
Log.Logger = new LoggerConfiguration()
.WriteTo.GrafanaLoki(
"http://localhost:3100",
httpMessageHandler: new GzipHandler())
.CreateLogger();
// Or inject a pre-built HttpClient (e.g. from IHttpClientFactory β the sink never disposes it).
var httpClient = httpClientFactory.CreateClient("loki");
Log.Logger = new LoggerConfiguration()
.WriteTo.GrafanaLoki(
"http://localhost:3100",
httpClient: httpClient)
.CreateLogger();
traceIdMode and spanIdMode control where the event's TraceId / SpanId (populated by Serilog 4.x from the ambient
Activity) are written. Each takes a LokiFieldDestination:
| Value | Effect |
|---|---|
None (default) |
Not emitted. |
Body |
Written as a TraceId / SpanId field in the JSON log body. |
StructuredMetadata |
Attached as Loki structured metadata β recommended for trace IDs. |
.WriteTo.GrafanaLoki(
"http://localhost:3100",
traceIdMode: LokiFieldDestination.StructuredMetadata,
spanIdMode: LokiFieldDestination.StructuredMetadata)
From appsettings.json the mode binds from its name: "traceIdMode": "StructuredMetadata".
Trace context is typically populated for you by Serilog.AspNetCore / Serilog.Extensions.Logging; outside those, an
active Activity must be present for the IDs to be emitted.
Structured metadata attaches per-line
key/value pairs to a log entry without indexing them as labels. Unlike propertiesAsLabels it does not create new
streams (no cardinality cost), and unlike a body field it is queryable without a parser stage:
{app="web_app"} | RequestId="abc-123" # structured metadata β no parser needed
{app="web_app"} | json | RequestId="abc-123" # the equivalent for a body field
That makes it the right home for high-cardinality identifiers such as request, user, or trace IDs. Two sources feed it:
propertiesAsStructuredMetadata β property names to attach as metadata (the property is also kept in the body).traceIdMode / spanIdMode set to StructuredMetadata β routes TraceId / SpanId there instead of the body..WriteTo.GrafanaLoki(
"http://localhost:3100",
propertiesAsStructuredMetadata: ["RequestId", "UserId"],
traceIdMode: LokiFieldDestination.StructuredMetadata)
It is emitted as the optional third element of each push entry:
"values": [
[ "1700000000000000000", "{\"Message\":\"...\"}", { "RequestId": "abc-123", "TraceId": "..." } ]
]
Requires Loki 3.0+ (or 2.9 with
allow_structured_metadataenabled on a TSDB v13 schema). Against an older Loki, or one with structured metadata disabled, such a push is rejected β leave these options unset (the default) to stay compatible.
For the full picture β the three-tier model (labels vs structured metadata vs body), querying, and Loki configuration β see Structured metadata in the wiki.
By default the sink uses LokiJsonTextFormatter, which renders each log entry's body as a JSON object. This makes logs
easy to filter in Loki β see Grafana's write-up on
querying JSON logs.
The resulting push payload looks like:
{
"streams": [
{
"stream": { "app": "web_app", "level": "info" },
"values": [
[ "1700000000000000000", "{\"Message\":\"...\",\"MessageTemplate\":\"...\"}" ]
]
}
]
}
Each body object contains Message, MessageTemplate, an optional Exception, the TraceId / SpanId fields (when
their mode is Body), and every event property. Property names that collide with these reserved keys (Message,
MessageTemplate, Exception, TraceId, SpanId) are prefixed with _.
Custom text formatter. Implement Serilog.Formatting.ITextFormatter, or subclass LokiJsonTextFormatter and
override Format or SanitizePropertyName, then pass it via textFormatter:
{
"Serilog": {
"WriteTo": [
{
"Name": "GrafanaLoki",
"Args": {
"uri": "http://localhost:3100",
"textFormatter": "My.Awesome.Namespace.MyTextFormatter, MyCoolAssembly"
}
}
]
}
}
Custom exception formatter. Exception serialization is delegated to ILokiExceptionFormatter. The default
(LokiExceptionFormatter) recursively writes Type, Message, Source, StackTrace and inner exceptions. Replace it
to scrub PII, change the shape, or suppress stack traces:
using System.Text.Json;
using Serilog.Sinks.Grafana.Loki;
public class CompactExceptionFormatter : ILokiExceptionFormatter
{
public void Format(Utf8JsonWriter writer, Exception exception)
{
writer.WriteStartObject();
writer.WriteString("type", exception.GetType().Name);
writer.WriteString("message", exception.Message);
writer.WriteEndObject();
}
}
.WriteTo.GrafanaLoki(
"http://localhost:3100",
exceptionFormatter: new CompactExceptionFormatter())
Delivery is handled by Serilog 4.x's native batching infrastructure:
period (default 1 s) or once batchSizeLimit (default 1000) is reached.queueLimit (default 50 000). When the queue is full, new events are dropped
rather than growing memory without limit.retryTimeLimit (default 10 min), after which it
is dropped so the pipeline can make progress.Log.CloseAndFlush()) flushes cleanly without deadlocks.Delivery problems are reported through Serilog's SelfLog. Enable it during development to see HTTP errors and dropped
batches:
Serilog.Debugging.SelfLog.Enable(Console.Error);
V9 is a major release with breaking changes. The highlights:
| Area | V8 | V9 |
|---|---|---|
| Target frameworks | netstandard2.0, net5.0βnet8.0 |
net8.0, net9.0, net10.0 |
| Serilog | 2.x / 3.x | 4.3.1+ (native batching) |
| HTTP client | ILokiHttpClient / LokiGzipHttpClient subclasses |
Inject httpClient / httpMessageHandler; gzip via DelegatingHandler |
| Level label | always injected, collisions renamed | handleLogLevelAsLabel (default true); Fatal β fatal (was critical) |
| Property β label | removed the property from the body | property is kept in the body |
| Reserved-property renaming | IReservedPropertyRenamingStrategy |
removed β pipeline is immutable; reserved body keys are prefixed with _ |
leavePropertiesIntact |
flag | removed β no longer needed |
useInternalTimestamp |
flag | removed |
| Queue | unbounded by default | bounded at queueLimit (default 50 000) |
| Exception formatting | hardcoded | pluggable ILokiExceptionFormatter |
| Trace context | not supported | traceIdMode / spanIdMode (body or structured metadata) |
| URI validation | on first request | at logger configuration time |
FSharp.Core becomes a transitive dependency of all consumers.
The full, maintained list of breaking changes lives in the wiki.
Runnable examples live in the folder:
Serilog.Sinks.Grafana.Loki.Sample β a minimal console app.Serilog.Sinks.Grafana.Loki.SampleWebApp β an ASP.NET Core app configured from appsettings.json.| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 5 NuGet packages that depend on Serilog.Sinks.Grafana.Loki:
| Package | Downloads |
|---|---|
|
Convey.Logging
Convey.Logging |
|
|
Genocs.Logging
Logging abstractions and extensions for Genocs applications. |
|
|
Bones.Monitoring
Package Description |
|
|
MosFlightWidgetHelper
Package Description |
|
|
Only.CoffeeFace.Web
Only.CoffeeFace web api stuff |
Showing the top 14 popular GitHub repositories that depend on Serilog.Sinks.Grafana.Loki:
| Repository | Stars |
|---|---|
|
slskd/slskd
A modern client-server application for the Soulseek file sharing network.
|
|
|
GZTimeWalker/GZCTF
The GZ::CTF project, an open source CTF platform.
|
|
|
MUnique/OpenMU
This project aims to create an easy to use, extendable and customizable server for a MMORPG called "MU Online".
|
|
|
mehdihadeli/food-delivery-microservices
π A practical and cloud-native food delivery microservices, built with .Net Aspire, .Net 9, MassTransit, Domain-Driven Design, CQRS, Vertical Slice Architecture, Event-Driven Architecture, and the latest technologies.
|
|
|
snatch-dev/Convey
A simple recipe for .NET Core microservices.
|
|
|
mizrael/SuperSafeBank
Sample Event Sourcing implementation with .NET Core
|
|
|
TanvirArjel/CleanArchitecture
This repository contains the implementation of domain-driven design and clear architecture in ASP.NET Core.
|
|
|
compujuckel/AssettoServer
Custom Assetto Corsa server with focus on freeroam
|
|
|
philosowaffle/peloton-to-garmin
Convert workout data from Peloton into JSON/TCX/FIT files and automatically upload to Garmin Connect
|
|
|
mehdihadeli/vertical-slice-api-template
π° An asp.net core template based on .Net 9, Vertical Slice Architecture, CQRS, Minimal APIs, OpenTelemetry, API Versioning and OpenAPI.
|
|
|
Letterbook/Letterbook
Sustainable federated social media built for open correspondence
|
|
|
asynkron/realtimemap-dotnet
A showcase for Proto.Actor - an ultra-fast distributed actors solution for Go, C#, and Java/Kotlin.
|
|
|
marinasundstrom/YourBrand
Prototype enterprise system for e-commerce and consulting services
|
|
|
Dubzer/TgTranslator
Telegram bot that translates messages in groups
|
| Version | Downloads | Last Updated |
|---|---|---|
| 9.0.0 | 16,735 | 6/6/2026 |
| 8.3.2 | 1,203,611 | 12/30/2025 |
| 8.3.1 | 2,231,717 | 6/4/2025 |
| 8.3.0 | 7,589,592 | 1/30/2024 |
| 8.2.0 | 1,165,121 | 10/30/2023 |
| 8.2.0-beta.3 | 236 | 10/28/2023 |
| 8.2.0-beta.2 | 10,937 | 10/5/2023 |
| 8.2.0-beta.1 | 17,318 | 7/17/2023 |
| 8.2.0-beta.0 | 321 | 7/10/2023 |
| 8.1.0 | 3,572,129 | 11/26/2022 |
| 8.0.1 | 365,424 | 10/13/2022 |
| 8.0.0 | 622,071 | 7/19/2022 |
| 8.0.0-beta.0 | 8,993 | 4/1/2022 |
| 7.1.1 | 1,226,938 | 3/23/2022 |
| 7.1.0 | 1,149,199 | 9/21/2021 |
| 7.0.2 | 54,510 | 8/30/2021 |
| 7.0.1 | 18,035 | 8/17/2021 |
| 7.0.0 | 14,508 | 8/15/2021 |
| 6.0.1 | 142,438 | 6/7/2021 |
For release notes, please see the change log on GitHub.