Skip to content

Custom HttpClient

Mykhailo Shevchuk edited this page Jun 5, 2026 · 3 revisions

v9 removes the ILokiHttpClient hierarchy (BaseLokiHttpClient, LokiHttpClient, LokiGzipHttpClient). The sink now works with a standard System.Net.Http.HttpClient, so any cross-cutting behaviour — gzip, retries, mTLS, bearer auth — is added with the normal .NET building blocks.

Two injection points

httpMessageHandler — the sink owns the client

Pass an HttpMessageHandler and the sink builds and owns the HttpClient around it. The sink still applies basic auth (credentialsLogin / credentialsPassword) and the tenant header.

.WriteTo.GrafanaLoki(
    "http://localhost:3100",
    httpMessageHandler: new GzipHandler())

httpClient — you own the client

Pass a fully built HttpClient (for example from IHttpClientFactory). The sink never disposes an injected client, and does not add auth/tenant headers to it — configure those yourself.

var httpClient = httpClientFactory.CreateClient("loki");
.WriteTo.GrafanaLoki("http://localhost:3100", httpClient: httpClient)

If both are supplied, httpClient wins and httpMessageHandler is ignored.

Gzip compression (replaces LokiGzipHttpClient)

// 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);
    }
}

Retries

Delivery-level retry and backoff are already handled by Serilog's batching (retryTimeLimit). If you also want per-request resilience (for example with Polly), add it as a handler:

// Microsoft.Extensions.Http.Polly
var handler = new PolicyHttpMessageHandler(retryPolicy)
{
    InnerHandler = new HttpClientHandler()
};
.WriteTo.GrafanaLoki("http://localhost:3100", httpMessageHandler: handler)

Bearer token / OAuth2

There is no first-class bearer-token option. Either set a default header on an injected HttpClient, or add a DelegatingHandler that attaches (and refreshes) the token per request:

public class BearerHandler : DelegatingHandler
{
    private readonly ITokenProvider _tokens;
    public BearerHandler(ITokenProvider tokens) : base(new HttpClientHandler()) => _tokens = tokens;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken ct)
    {
        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", await _tokens.GetTokenAsync(ct));
        return await base.SendAsync(request, ct);
    }
}

mTLS / client certificates

var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCertificate);
.WriteTo.GrafanaLoki("http://localhost:3100", httpMessageHandler: handler)

A DelegatingHandler passed to the sink must have an inner handler (the examples above use new HttpClientHandler()); otherwise the request pipeline has no terminus.

Clone this wiki locally