A Microsoft.Extensions.Logging provider that sends log events to Splunk.
builder.Logging.AddSplunk(o =>
{
o.Endpoint = new Uri("https://splunk.example.com:8088");
o.Token = "<HEC token>";
o.Index = "main";
o.Source = "my-app";
});Events are queued, batched and sent to the HTTP Event Collector (/services/collector/event) on a background task. Pending events are flushed when the provider is disposed.
By default the provider creates its own HttpClient with a RetryDelegatingHandler that retries failed requests (exceptions and non-success status codes) up to MaxRetries times (default 3), starting with RetryDelay (default 1 second) and doubling it on each retry. Batches that still fail are dropped and reported through OnError.
Use the overload that receives the IServiceProvider to supply your own HttpClient, for example one created by IHttpClientFactory. The Authorization: Splunk <token> header is added from Token unless the client already has one. The provider never disposes the client, and MaxRetries/RetryDelay are not applied to it.
builder.Services.AddHttpClient("splunk");
builder.Logging.AddSplunk((sp, o) =>
{
o.Endpoint = new Uri("https://splunk.example.com:8088");
o.Token = "<HEC token>";
o.HttpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient("splunk");
});To use Polly instead of the built-in retries, add a resilience handler to the named client (package Microsoft.Extensions.Http.Resilience, or Microsoft.Extensions.Http.Polly for the legacy API):
builder.Services.AddHttpClient("splunk")
.AddResilienceHandler("splunk", pipeline => pipeline
.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 5,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
})
.AddTimeout(TimeSpan.FromSeconds(30)));AddStandardResilienceHandler() is a ready-made alternative (retry, circuit breaker and timeouts). Then pass the client through o.HttpClient as shown above.
Events follow the layout produced by the splunk-otel-collector HEC exporter: the formatted message is the event, and all other data is flattened into fields.
{
"time": 1760135438.834,
"host": "my-host",
"source": "my-app",
"index": "main",
"event": "Hello world",
"fields": {
"otel.log.severity.text": "Warning",
"otel.log.severity.number": 13,
"scope.name": "MyApp.Category",
"logrecord.event.id": 5,
"otel.log.name": "Evt",
"exception.type": "InvalidOperationException",
"exception.message": "boom",
"exception.stacktrace": "...",
"trace_id": "...",
"span_id": "...",
"Name": "world"
}
}| Source (MEL) | HEC location |
|---|---|
| Formatted message | event |
| Time of logging (UTC) | time (epoch seconds) |
SplunkLoggerOptions.Host / Source / SourceType / Index |
host / source / sourcetype / index |
LogLevel |
fields["otel.log.severity.text"]: Trace=Trace, Debug=Debug, Information=Information, Warning=Warning, Error=Error, Critical=Critical |
LogLevel |
fields["otel.log.severity.number"]: 1, 5, 9, 13, 17, 21 respectively |
| Category name | fields["scope.name"] |
EventId.Id (when non-zero) |
fields["logrecord.event.id"] |
EventId.Name |
fields["otel.log.name"] |
Exception |
fields["exception.type"], ["exception.message"], ["exception.stacktrace"] |
Activity.Current |
fields["trace_id"], fields["span_id"] |
| Structured message properties | fields[<name>] (including {OriginalFormat}) |
| Resource attributes | fields["service.name"] (ServiceName option, default unknown_service:<process>), fields["telemetry.sdk.name"]/language/version |
Scopes (key/value pairs, when IncludeScopes is on) |
fields[<name>]; non-structured scopes are ignored |
Strings, booleans and finite numbers keep their JSON types. Everything else (including NaN/Infinity, Guid, dates, collections and custom types) is written as its invariant ToString() text by default.
Set SerializeComplexValues = true to serialize every property and scope value (primitives, enums, Guid, collections, dictionaries, custom types, ...) as JSON with SplunkLoggerOptions.JsonSerializerOptions. By default these options allow named floating point literals ("NaN", "Infinity"). The options' Encoder (default UnsafeRelaxedJsonEscaping) and writer settings (WriteIndented, IndentCharacter, IndentSize, NewLine, MaxDepth) are also used for the event itself. If serializing an event fails, the event is dropped and OnError is called. Reflection-based serialization is not trim/AOT-safe; supply a source-generated TypeInfoResolver in the options for such apps.
tests/Splunk.Extensions.Logging.IntegrationTests sends the same log calls through this provider (directly to HEC) and through the OpenTelemetry .NET logger (OTLP -> splunk-otel-collector -> HEC), then queries Splunk and compares the indexed events. They are skipped when Splunk is not running.
docker compose -f tests/integration/docker-compose.yml up -d # Splunk takes ~1-2 minutes to become healthy
dotnet test tests/Splunk.Extensions.Logging.IntegrationTests
docker compose -f tests/integration/docker-compose.yml downEvent text, severity, exception, properties, scopes and otel.log.name are identical. Intentional differences: this provider also emits scope.name and logrecord.event.id, sets host to the machine name, and reports its own telemetry.sdk.version.
| Project | Path | Purpose |
|---|---|---|
Splunk.Extensions.Logging |
src/Splunk.Extensions.Logging/ |
Main library |
Splunk.Extensions.Logging.Tests |
tests/Splunk.Extensions.Logging.Tests/ |
Unit tests |
# Build the solution
dotnet build
# Run all tests
dotnet testMIT, see LICENSE.