Serilog.Sinks.OpenSearch
2.0.0
dotnet add package Serilog.Sinks.OpenSearch --version 2.0.0
NuGet\Install-Package Serilog.Sinks.OpenSearch -Version 2.0.0
<PackageReference Include="Serilog.Sinks.OpenSearch" Version="2.0.0" />
<PackageVersion Include="Serilog.Sinks.OpenSearch" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.OpenSearch" />
paket add Serilog.Sinks.OpenSearch --version 2.0.0
#r "nuget: Serilog.Sinks.OpenSearch, 2.0.0"
#:package Serilog.Sinks.OpenSearch@2.0.0
#addin nuget:?package=Serilog.Sinks.OpenSearch&version=2.0.0
#tool nuget:?package=Serilog.Sinks.OpenSearch&version=2.0.0
Serilog.Sinks.OpenSearch

This repository contains two nuget packages: Serilog.Sinks.OpenSearch and Serilog.Formatting.OpenSearch.
Table of contents
What is this sink
The Serilog OpenSearch 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 OpenSearch, a NoSQL search engine. It does this in a similar structure as Logstash and makes it easy to use Kibana for visualizing your logs.
Features
- Simple configuration to get log events published to OpenSearch. Only server address is needed.
- All properties are stored inside fields in OpenSearch. This allows you to query on all the relevant data but also run analytics over this data.
- Be able to customize the store; specify the index name being used, the serializer or the connections to the server (load balanced).
- Durable mode; store the logevents first on disk before delivering them to OS making sure you never miss events if you have trouble connecting to your OS cluster.
- Automatically create the right mappings for the best usage of the log events in OS or automatically upload your own custom mapping.
- Versions 1 and 2 of OpenSearch supported. Version 1.0.0 of the sink targets netstandard2.0 and therefore can be run on any .NET Framework that supports it (both .NET Core and .NET Framework). The unit suite runs on .NET 8, .NET 9, and .NET 10.
- Non-durable delivery uses Serilog 4's native batching, retry scheduling, failure listeners, and fallback chains.
- The built-in formatter preserves Serilog's first-class
TraceIdandSpanIdfor distributed-trace correlation.
Quick start
OpenSearch sinks
Install-Package serilog.sinks.opensearch
Simplest way to register this sink is to use default configuration:
var loggerConfig = new LoggerConfiguration()
.WriteTo.OpenSearch(new OpenSearchSinkOptions(new Uri("http://localhost:9200")));
Or, if using .NET Core and Serilog.Settings.Configuration Nuget package and appsettings.json, default configuration would look like this:
{
"Serilog": {
"Using": [ "Serilog.Sinks.OpenSearch" ],
"MinimumLevel": "Warning",
"WriteTo": [
{
"Name": "OpenSearch",
"Args": {
"nodeUris": "http://localhost:9200"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName" ],
"Properties": {
"Application": "ImmoValuation.Swv - Web"
}
}
}
More elaborate configuration, using additional Nuget packages (e.g. Serilog.Enrichers.Environment) would look like:
{
"Serilog": {
"Using": [ "Serilog.Sinks.OpenSearch" ],
"MinimumLevel": "Warning",
"WriteTo": [
{
"Name": "OpenSearch",
"Args": {
"nodeUris": "http://localhost:9200"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName" ],
"Properties": {
"Application": "My app"
}
}
}
This way the sink will detect version of OpenSearch server (DetectOpenSearchVersion is set to true by default) and it will successfully handle AWS OpenSearch when it is running in Elasticsearch compatibility mode.
Disable detection of OpenSearch server version
Alternatively, DetectOpenSearchVersion can be set to false and certain option can be configured manually. In that case, the sink will assume version 1 of OpenSearch, but options will be ignored due to a potential version incompatibility.
For example, you can configure the sink to force registration of v1 index template. Be aware that the AutoRegisterTemplate option will not overwrite an existing template.
var loggerConfig = new LoggerConfiguration()
.WriteTo.OpenSearch(new OpenSearchSinkOptions(new Uri("http://localhost:9200") ){
DetectOpenSearchVersion = false,
AutoRegisterTemplate = true,
AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.OSv1
});
Configurable properties
Besides a registration of the sink in the code, it is possible to register it using appSettings reader (from v2.0.42+) reader (from v2.0.42+) as shown below.
This example shows the options that are currently available when using the appSettings reader.
<appSettings>
<add key="serilog:using" value="Serilog.Sinks.OpenSearch"/>
<add key="serilog:write-to:OpenSearch.nodeUris" value="http://localhost:9200;http://remotehost:9200"/>
<add key="serilog:write-to:OpenSearch.indexFormat" value="custom-index-{0:yyyy.MM}"/>
<add key="serilog:write-to:OpenSearch.templateName" value="myCustomTemplate"/>
<add key="serilog:write-to:OpenSearch.typeName" value="myCustomLogEventType"/>
<add key="serilog:write-to:OpenSearch.pipelineName" value="myCustomPipelineName"/>
<add key="serilog:write-to:OpenSearch.batchPostingLimit" value="50"/>
<add key="serilog:write-to:OpenSearch.batchAction" value="Create"/>
<add key="serilog:write-to:OpenSearch.period" value="2"/>
<add key="serilog:write-to:OpenSearch.retryTimeLimit" value="00:10:00"/>
<add key="serilog:write-to:OpenSearch.inlineFields" value="true"/>
<add key="serilog:write-to:OpenSearch.renderTraceId" value="true"/>
<add key="serilog:write-to:OpenSearch.renderSpanId" value="true"/>
<add key="serilog:write-to:OpenSearch.restrictedToMinimumLevel" value="Warning"/>
<add key="serilog:write-to:OpenSearch.bufferBaseFilename" value="C:\Temp\SerilogOpenSearchBuffer"/>
<add key="serilog:write-to:OpenSearch.bufferFileSizeLimitBytes" value="5242880"/>
<add key="serilog:write-to:OpenSearch.bufferLogShippingInterval" value="5000"/>
<add key="serilog:write-to:OpenSearch.bufferRetainedInvalidPayloadsLimitBytes" value="5000"/>
<add key="serilog:write-to:OpenSearch.bufferFileCountLimit " value="31"/>
<add key="serilog:write-to:OpenSearch.connectionGlobalHeaders" value="Authorization=Bearer SOME-TOKEN;OtherHeader=OTHER-HEADER-VALUE" />
<add key="serilog:write-to:OpenSearch.connectionTimeout" value="5" />
<add key="serilog:write-to:OpenSearch.emitEventFailure" value="WriteToSelfLog" />
<add key="serilog:write-to:OpenSearch.queueSizeLimit" value="100000" />
<add key="serilog:write-to:OpenSearch.autoRegisterTemplate" value="true" />
<add key="serilog:write-to:OpenSearch.autoRegisterTemplateVersion" value="OSv1" />
<add key="serilog:write-to:OpenSearch.detectOpenSearchVersion" value="false" />
<add key="serilog:write-to:OpenSearch.overwriteTemplate" value="false" />
<add key="serilog:write-to:OpenSearch.registerTemplateFailure" value="IndexAnyway" />
<add key="serilog:write-to:OpenSearch.deadLetterIndexName" value="deadletter-{0:yyyy.MM}" />
<add key="serilog:write-to:OpenSearch.numberOfShards" value="20" />
<add key="serilog:write-to:OpenSearch.numberOfReplicas" value="10" />
<add key="serilog:write-to:OpenSearch.formatProvider" value="My.Namespace.MyFormatProvider, My.Assembly.Name" />
<add key="serilog:write-to:OpenSearch.connection" value="My.Namespace.MyConnection, My.Assembly.Name" />
<add key="serilog:write-to:OpenSearch.serializer" value="My.Namespace.MySerializer, My.Assembly.Name" />
<add key="serilog:write-to:OpenSearch.connectionPool" value="My.Namespace.MyConnectionPool, My.Assembly.Name" />
<add key="serilog:write-to:OpenSearch.customFormatter" value="My.Namespace.MyCustomFormatter, My.Assembly.Name" />
<add key="serilog:write-to:OpenSearch.customDurableFormatter" value="My.Namespace.MyCustomDurableFormatter, My.Assembly.Name" />
<add key="serilog:write-to:OpenSearch.failureSink" value="My.Namespace.MyFailureSink, My.Assembly.Name" />
</appSettings>
With the appSettings configuration the nodeUris property is required. Multiple nodes can be specified using , or ; to separate them. All other properties are optional. Also required is the <add key="serilog:using" value="Serilog.Sinks.OpenSearch"/> setting to include this sink. All other properties are optional. If you do not explicitly specify an indexFormat-setting, a generic index such as 'logstash-[current_date]' will be used automatically.
And start writing your events using Serilog.
OpenSearch formatters
Install-Package serilog.formatting.opensearch
The Serilog.Formatting.OpenSearch nuget package consists of a several formatters:
OpenSearchJsonFormatter- custom json formatter that respects the configured property name handling and forcesTimestampto @timestamp.ExceptionAsObjectJsonFormatter- a json formatter which serializes any exception into an exception object.
Override default formatter if it's possible with selected sink
var loggerConfig = new LoggerConfiguration()
.WriteTo.Console(new OpenSearchJsonFormatter());
Development and tests
The unit project uses TUnit with Microsoft Testing Platform and awaited TUnit Assertions. The repository global.json selects the .NET 10 SDK; the .NET 8, .NET 9, and .NET 10 runtimes are needed to run every target.
Restore, build, and run the complete unit suite from the repository root:
dotnet restore test/Serilog.Sinks.OpenSearch.Tests/Serilog.Sinks.Opensearch.Tests.csproj
dotnet build test/Serilog.Sinks.OpenSearch.Tests/Serilog.Sinks.Opensearch.Tests.csproj -c Release --no-restore
dotnet test --project test/Serilog.Sinks.OpenSearch.Tests/Serilog.Sinks.Opensearch.Tests.csproj -c Release --no-build
Use -f net8.0, -f net9.0, or -f net10.0 to run one target. Use a TUnit tree-node filter for a focused run:
dotnet test --project test/Serilog.Sinks.OpenSearch.Tests/Serilog.Sinks.Opensearch.Tests.csproj -c Release -f net8.0 --no-build --treenode-filter "/*/*/OpenSearchJsonFormatterTests/*"
For an IDE-free debug session, replace dotnet test with dotnet run --project, put runner arguments after --, and attach a debugger to the launched test executable if required:
dotnet run --project test/Serilog.Sinks.OpenSearch.Tests/Serilog.Sinks.Opensearch.Tests.csproj -c Debug -f net8.0 -- --treenode-filter "/*/*/OpenSearchJsonFormatterTests/*"
TRX and Cobertura outputs must be attributable to one target, so give each target distinct paths:
dotnet test --project test/Serilog.Sinks.OpenSearch.Tests/Serilog.Sinks.Opensearch.Tests.csproj -c Release -f net8.0 --no-build --report-trx --results-directory artifacts/test-results/unit/net8.0 --coverage --coverage-output-format cobertura --coverage-output "$PWD/artifacts/coverage/unit/net8.0.cobertura.xml"
CI executes the same unit project once per target and uploads its TRX and Cobertura files.
The TUnit integration project uses Testcontainers to run the sink against OpenSearch 1.3.20 and 2.19.6. Its six scenarios cover explicit and detected template versions, indexing and final flush behavior, minimum-level filtering, and indexed TraceId/SpanId behavior. Docker must be running with Linux-container support:
dotnet restore test/Serilog.Sinks.OpenSearch.IntegrationTests/Serilog.Sinks.OpenSearch.IntegrationTests.csproj
dotnet build test/Serilog.Sinks.OpenSearch.IntegrationTests/Serilog.Sinks.OpenSearch.IntegrationTests.csproj -c Release --no-restore
dotnet test --project test/Serilog.Sinks.OpenSearch.IntegrationTests/Serilog.Sinks.OpenSearch.IntegrationTests.csproj -c Release --no-build
The first integration run may take several minutes while Docker downloads the OpenSearch images. See the integration-test README for the current matrix and direct executable command.
More information
Distributed trace context
The built-in OpenSearch formatter writes Serilog's first-class LogEvent.TraceId and LogEvent.SpanId at the document root by default. TraceId is a 32-character lowercase hexadecimal value and SpanId is a 16-character lowercase hexadecimal value. A field is omitted when that identifier was not captured; the formatter uses the event's captured context and does not consult Activity.Current while formatting.
Set OpenSearchSinkOptions.RenderTraceId or RenderSpanId to false to disable either field independently. The long WriteTo.OpenSearch() overload exposes the matching renderTraceId and renderSpanId named arguments. When an emitted first-class identifier collides with a user property named TraceId or SpanId (case-insensitively), the first-class identifier wins; otherwise the user property is preserved. Review index mappings before rollout if similarly named fields already exist with another type.
These controls apply only to the built-in OpenSearchJsonFormatter. A configured custom, durable-custom, or compact formatter owns its output completely and is not post-processed by the sink.
- Basic information on how to configure and use this sink.
- Configuration options which you can use.
- How to use the durability mode.
- Get the