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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Serilog.Sinks.OpenSearch" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Serilog.Sinks.OpenSearch" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Serilog.Sinks.OpenSearch" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Serilog.Sinks.OpenSearch --version 2.0.0
                    
#r "nuget: Serilog.Sinks.OpenSearch, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Serilog.Sinks.OpenSearch@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Serilog.Sinks.OpenSearch&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Serilog.Sinks.OpenSearch&version=2.0.0
                    
Install as a Cake Tool

Serilog.Sinks.OpenSearch Continuous Integration NuGet Badge

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 TraceId and SpanId for 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 forces Timestamp to @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.