diff --git a/ecs-dotnet.sln b/ecs-dotnet.sln
index 52899458..8d046b3c 100644
--- a/ecs-dotnet.sln
+++ b/ecs-dotnet.sln
@@ -131,6 +131,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.NLog.Targets.Integr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "playground", "examples\playground\playground.csproj", "{86AEB76A-C210-4250-8541-B349C26C1683}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Serilog.Sinks.Tests", "tests\Elastic.Serilog.Sinks.Tests\Elastic.Serilog.Sinks.Tests.csproj", "{933FD923-A2DC-49E3-B21E-8BA888DB5924}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -281,6 +283,10 @@ Global
{86AEB76A-C210-4250-8541-B349C26C1683}.Debug|Any CPU.Build.0 = Debug|Any CPU
{86AEB76A-C210-4250-8541-B349C26C1683}.Release|Any CPU.ActiveCfg = Release|Any CPU
{86AEB76A-C210-4250-8541-B349C26C1683}.Release|Any CPU.Build.0 = Release|Any CPU
+ {933FD923-A2DC-49E3-B21E-8BA888DB5924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {933FD923-A2DC-49E3-B21E-8BA888DB5924}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {933FD923-A2DC-49E3-B21E-8BA888DB5924}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {933FD923-A2DC-49E3-B21E-8BA888DB5924}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -323,6 +329,7 @@ Global
{692F8035-F3F9-4714-8C9D-D54AF4CEB0E0} = {7610B796-BB3E-4CB2-8296-79BBFF6D23FC}
{D1C3CAFB-A59D-4E3F-ADD1-4CB281E5349D} = {947B298F-9139-4868-B337-729541932E4D}
{86AEB76A-C210-4250-8541-B349C26C1683} = {05075402-8669-45BD-913A-BD40A29BBEAB}
+ {933FD923-A2DC-49E3-B21E-8BA888DB5924} = {3582B07D-C2B0-49CC-B676-EAF806EB010E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7F60C4BB-6216-4E50-B1E4-9C38EB484843}
diff --git a/src/Elastic.Serilog.Sinks/ConfigSinkExtensions.cs b/src/Elastic.Serilog.Sinks/ConfigSinkExtensions.cs
new file mode 100644
index 00000000..94684159
--- /dev/null
+++ b/src/Elastic.Serilog.Sinks/ConfigSinkExtensions.cs
@@ -0,0 +1,238 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Channels;
+using Elastic.Channels;
+using Elastic.CommonSchema;
+using Elastic.Ingest.Elasticsearch;
+using Elastic.Ingest.Elasticsearch.DataStreams;
+using Elastic.Transport;
+using Serilog;
+using Serilog.Configuration;
+using Serilog.Core;
+using Serilog.Events;
+
+namespace Elastic.Serilog.Sinks
+{
+ ///
+ /// Extension methods on to aid with serilog log configuration building
+ /// These overloads exists entirely to make configuration through Serilog.Settings.Configuration easier
+ ///
+ public static class ConfigSinkExtensions
+ {
+ ///
+ /// Write logs directly to Elasticsearch.
+ /// This overload makes it easy to directly specify the endpoint
+ /// Use configure where and how data should be written
+ ///
+ public static LoggerConfiguration Elasticsearch(
+ this LoggerSinkConfiguration loggerConfiguration,
+ BootstrapMethod bootstrapMethod,
+ ICollection nodes,
+ bool useSniffing = true,
+ string? dataStream = null,
+ string? ilmPolicy = null,
+ string? apiKey = null,
+ string? username = null,
+ string? password = null,
+
+ bool? includeHost = null,
+ bool? includeActivity = null,
+ bool? includeProcess = null,
+ bool? includeUser = null,
+ ICollection? filterProperties = null,
+
+ int? maxRetries = null,
+ int? maxConcurrency = null,
+ int? maxInflight = null,
+ int? maxExportSize = null,
+ TimeSpan? maxLifeTime = null,
+ BoundedChannelFullMode? fullMode = null,
+
+ Uri? proxy = null,
+ string? proxyUsername = null,
+ string? proxyPassword = null,
+ string? fingerprint = null,
+ bool debugMode = false,
+
+ LoggingLevelSwitch? levelSwitch = null,
+ LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum
+ )
+ {
+ var transportConfig = !useSniffing ? TransportHelper.Static(nodes) : TransportHelper.Sniffing(nodes);
+ SetTransportConfig(transportConfig, apiKey, username, password, proxy, proxyUsername, proxyPassword, fingerprint, debugMode
+ );
+
+ var sinkOptions = CreateSinkOptions(transportConfig,
+ bootstrapMethod, dataStream, ilmPolicy, includeHost, includeActivity, includeProcess, includeUser, filterProperties
+ );
+
+ SetBufferOptions(sinkOptions, maxRetries, maxConcurrency, maxInflight, maxExportSize, maxLifeTime, fullMode);
+
+ return loggerConfiguration.Sink(new ElasticsearchSink(sinkOptions), restrictedToMinimumLevel, levelSwitch);
+ }
+
+ ///
+ /// Write logs directly to Elastic Cloud ( https://cloud.elastic.co/ ).
+ /// describes your deployments endpoints (can be found in the Admin Console)
+ /// is used for authentication.
+ /// Use configure where and how data should be written
+ ///
+ public static LoggerConfiguration ElasticCloud(
+ this LoggerSinkConfiguration loggerConfiguration,
+ BootstrapMethod bootstrapMethod,
+ Uri? endpoint = null,
+ string? cloudId = null,
+ string? apiKey = null,
+ string? username = null,
+ string? password = null,
+ string? dataStream = null,
+ string? ilmPolicy = null,
+
+ bool? includeHost = null,
+ bool? includeActivity = null,
+ bool? includeProcess = null,
+ bool? includeUser = null,
+ ICollection? filterProperties = null,
+
+ int? maxRetries = null,
+ int? maxConcurrency = null,
+ int? maxInflight = null,
+ int? maxExportSize = null,
+ TimeSpan? maxLifeTime = null,
+ BoundedChannelFullMode? fullMode = null,
+
+ Uri? proxy = null,
+ string? proxyUsername = null,
+ string? proxyPassword = null,
+ string? fingerprint = null,
+ bool debugMode = false,
+
+ LoggingLevelSwitch? levelSwitch = null,
+ LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum
+ )
+ {
+ var transportConfig = (endpoint, cloudId, apiKey, username, password) switch
+ {
+ ({ } s, null, _, _, _) => TransportHelper.Static(new[] { s }),
+ (null, { } id, { } k, _, _) => TransportHelper.Cloud(id, k),
+ (null, { } id, null, { } u, { } p) => TransportHelper.Cloud(id, u, p),
+ _ => throw new ArgumentException("Invalid cloud configuration")
+ };
+
+ SetTransportConfig(transportConfig, apiKey, username, password, proxy, proxyUsername, proxyPassword, fingerprint, debugMode);
+
+ var sinkOptions = CreateSinkOptions(transportConfig,
+ bootstrapMethod, dataStream, ilmPolicy, includeHost, includeActivity, includeProcess, includeUser, filterProperties
+ );
+
+ SetBufferOptions(sinkOptions, maxRetries, maxConcurrency, maxInflight, maxExportSize, maxLifeTime, fullMode);
+
+ return loggerConfiguration.Sink(new ElasticsearchSink(sinkOptions), restrictedToMinimumLevel, levelSwitch);
+ }
+
+ private static void SetBufferOptions(ElasticsearchSinkOptions sinkOptions, int? maxRetries, int? maxConcurrency, int? maxInflight, int? maxExportSize,
+ TimeSpan? maxLifeTime, BoundedChannelFullMode? fullMode
+ ) =>
+ sinkOptions.ConfigureChannel = channelOpts =>
+ {
+ var b = channelOpts.BufferOptions;
+ if (maxRetries.HasValue)
+ b.ExportMaxRetries = maxRetries.Value;
+ if (maxConcurrency.HasValue)
+ b.ExportMaxConcurrency = maxConcurrency.Value;
+ if (maxInflight.HasValue)
+ b.InboundBufferMaxSize = maxInflight.Value;
+ if (maxExportSize.HasValue)
+ b.OutboundBufferMaxSize = maxExportSize.Value;
+ if (maxLifeTime.HasValue)
+ b.OutboundBufferMaxLifetime = maxLifeTime.Value;
+ if (fullMode.HasValue)
+ b.BoundedChannelFullMode = fullMode.Value;
+ };
+
+ private static ElasticsearchSinkOptions CreateSinkOptions(
+ TransportConfiguration transportConfig,
+ BootstrapMethod bootstrapMethod, string? dataStream, string? ilmPolicy, bool? includeHost,
+ bool? includeActivity, bool? includeProcess, bool? includeUser, ICollection? filterProperties
+ )
+ {
+ var sinkOptions = new ElasticsearchSinkOptions(new DistributedTransport(transportConfig));
+ if (dataStream != null)
+ {
+ var tokens = dataStream.Split('-');
+ if (tokens.Length > 3)
+ throw new ArgumentOutOfRangeException(nameof(dataStream), $"Data stream name should be at most 3 tokens: {dataStream}");
+ if (tokens.Length == 3)
+ sinkOptions.DataStream = new DataStreamName(tokens[0], tokens[1], tokens[2]);
+ if (tokens.Length == 2)
+ sinkOptions.DataStream = new DataStreamName(tokens[0], tokens[1]);
+ if (tokens.Length == 1)
+ sinkOptions.DataStream = new DataStreamName(tokens[0]);
+ }
+ sinkOptions.BootstrapMethod = bootstrapMethod;
+
+ if (ilmPolicy != null)
+ sinkOptions.IlmPolicy = ilmPolicy;
+
+ if (includeHost.HasValue)
+ sinkOptions.TextFormatting.IncludeHost = includeHost.Value;
+ if (includeProcess.HasValue)
+ sinkOptions.TextFormatting.IncludeProcess = includeProcess.Value;
+ if (includeActivity.HasValue)
+ sinkOptions.TextFormatting.IncludeActivityData = includeActivity.Value;
+ if (includeUser.HasValue)
+ sinkOptions.TextFormatting.IncludeUser = includeUser.Value;
+ if (filterProperties != null)
+ sinkOptions.TextFormatting.LogEventPropertiesToFilter = new HashSet(filterProperties);
+ return sinkOptions;
+ }
+
+ private static void SetTransportConfig(TransportConfiguration transportConfig,
+ string? apiKey, string? username, string? password,
+ Uri? proxy, string? proxyUsername, string? proxyPassword, string? fingerprint, bool debugMode
+ )
+ {
+ if (proxy != null && proxyUsername != null && proxyPassword != null)
+ transportConfig.Proxy(proxy, proxyUsername, proxyPassword);
+ else if (proxy != null)
+ transportConfig.Proxy(proxy);
+
+ if (fingerprint != null)
+ transportConfig.CertificateFingerprint(fingerprint);
+
+ if (debugMode)
+ transportConfig.EnableDebugMode();
+
+ if (username != null && password != null)
+ transportConfig.Authentication(new BasicAuthentication(username, password));
+ if (apiKey != null)
+ transportConfig.Authentication(new ApiKey(apiKey));
+ }
+
+
+ ///
+ /// Write logs directly to Elastic Cloud ( https://cloud.elastic.co/ ).
+ /// describes your deployments endpoints (can be found in the Admin Console)
+ /// and are used for basic authentication.
+ /// Use configure where and how data should be written
+ ///
+ public static LoggerConfiguration ElasticCloud(
+ this LoggerSinkConfiguration loggerConfiguration,
+ string cloudId,
+ string username,
+ string password,
+ Action? configureOptions = null,
+ Action? configureTransport = null,
+ LoggingLevelSwitch? levelSwitch = null,
+ LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum
+ )
+ {
+ var transportConfig = TransportHelper.Cloud(cloudId, username, password);
+ configureTransport?.Invoke(transportConfig);
+ var sinkOptions = new ElasticsearchSinkOptions(new DistributedTransport(transportConfig));
+ configureOptions?.Invoke(sinkOptions);
+
+ return loggerConfiguration.Sink(new ElasticsearchSink(sinkOptions), restrictedToMinimumLevel, levelSwitch);
+ }
+ }
+}
diff --git a/src/Elastic.Serilog.Sinks/ElasticsearchSink.cs b/src/Elastic.Serilog.Sinks/ElasticsearchSink.cs
index a7386832..a017b538 100644
--- a/src/Elastic.Serilog.Sinks/ElasticsearchSink.cs
+++ b/src/Elastic.Serilog.Sinks/ElasticsearchSink.cs
@@ -17,6 +17,30 @@
namespace Elastic.Serilog.Sinks
{
+
+ ///
+ /// A read only view of the options provided to
+ ///
+ public interface IElasticsearchSinkOptions
+ {
+ ///
+ BootstrapMethod BootstrapMethod { get; }
+
+ ///
+ IEcsTextFormatterConfiguration EcsTextFormatterConfiguration { get; }
+
+ ///
+ public DataStreamName DataStream { get; }
+
+ ///
+ /// The ILM Policy to apply, see the following for more details:
+ /// https://www.elastic.co/guide/en/elasticsearch/reference/current/index-lifecycle-management.html
+ /// Defaults to `logs` which is shipped by default with Elasticsearch
+ ///
+ public string? IlmPolicy { get; }
+
+ }
+
///
/// Provides configuration options to to control how and where data gets written
///
@@ -30,7 +54,9 @@ public ElasticsearchSinkOptions(ITransport transport) : base(transport) { }
}
///
- public class ElasticsearchSinkOptions where TEcsDocument : EcsDocument, new()
+ public class ElasticsearchSinkOptions
+ : IElasticsearchSinkOptions
+ where TEcsDocument : EcsDocument, new()
{
///
public ElasticsearchSinkOptions() : this(new DistributedTransport(TransportHelper.Default())) { }
@@ -41,6 +67,8 @@ public ElasticsearchSinkOptions() : this(new DistributedTransport(TransportHelpe
///
internal ITransport Transport { get; }
+ IEcsTextFormatterConfiguration IElasticsearchSinkOptions.EcsTextFormatterConfiguration => TextFormatting;
+
///
public EcsTextFormatterConfiguration TextFormatting { get; set; } = new();
@@ -60,11 +88,7 @@ public ElasticsearchSinkOptions() : this(new DistributedTransport(TransportHelpe
///
public BootstrapMethod BootstrapMethod { get; set; }
- ///
- /// The ILM Policy to apply, see the following for more details:
- /// https://www.elastic.co/guide/en/elasticsearch/reference/current/index-lifecycle-management.html
- /// Defaults to `logs` which is shipped by default with Elasticsearch
- ///
+ ///
public string? IlmPolicy { get; set; }
///
@@ -99,9 +123,13 @@ public class ElasticsearchSink : ILogEventSink, IDisposable
private readonly EcsTextFormatterConfiguration _formatterConfiguration;
private readonly EcsDataStreamChannel _channel;
+ ///
+ public IElasticsearchSinkOptions Options { get; }
+
/// >
public ElasticsearchSink(ElasticsearchSinkOptions options)
{
+ Options = options;
_formatterConfiguration = options.TextFormatting;
var channelOptions = new DataStreamChannelOptions(options.Transport)
{
diff --git a/src/Elastic.Serilog.Sinks/README.md b/src/Elastic.Serilog.Sinks/README.md
index 5dc7be6b..c2d4e098 100644
--- a/src/Elastic.Serilog.Sinks/README.md
+++ b/src/Elastic.Serilog.Sinks/README.md
@@ -67,6 +67,107 @@ Log.Information("The time is {TraceId}", "my-trace-id");
Will override `trace.id` on the resulting ECS json document.
+### Application Settings Configuration
+
+This sink can be configured through `appsettings.json` when used in combination with [`Serilog.Settings.Configuration`](https://github.com/serilog/serilog-settings-configuration).
+
+#### Elasticsearch appsettings configuration
+
+When configuring through `appsettings` only the `bootstrapMethod` configuration is **required**
+
+```json5
+{
+ "Serilog": {
+ "Using": [ "Elastic.Serilog.Sinks" ],
+ "MinimumLevel": { "Default": "Information" },
+ "WriteTo": [
+ {
+ "Name": "Elasticsearch",
+ "Args": {
+ "bootstrapMethod": "Silent",
+ "nodes": [ "http://elastichost:9200" ],
+ "useSniffing": true,
+ "apiKey": "",
+ "username": "",
+ "password": "",
+
+ "ilmPolicy" : "my-policy",
+ "dataStream" : "logs-dotnet-default",
+ "includeHost" : true,
+ "includeUser" : true,
+ "includeProcess" : true,
+ "includeActivity" : true,
+ "filterProperties" : [ "prop1", "prop2" ],
+ "proxy": "http://localhost:8200",
+ "proxyUsername": "x",
+ "proxyPassword": "y",
+ "debugMode": false,
+
+ //EXPERT settings, do not set unless you need to
+ "maxRetries": 3,
+ "maxConcurrency": 20,
+ "maxInflight": 100000,
+ "maxExportSize": 1000,
+ "maxLifeTime": "00:00:05",
+ "fullMode": "Wait"
+ }
+ }
+ ]
+ }
+}
+```
+
+#### Elastic Cloud appsettings configuration
+
+When configuring through `appsettings` only the `bootstrapMethod` configuration is **required**
+
+You can specify either `endpoint` or `cloudId`, `cloudId` will take precedence.
+
+You'll need to specify either `apiKey` or `username` and `password`.
+
+```json5
+{
+ "Serilog": {
+ "Using": [ "Elastic.Serilog.Sinks" ],
+ "MinimumLevel": { "Default": "Information" },
+ "WriteTo": [
+ {
+ "Name": "ElasticCloud",
+ "Args": {
+ "bootstrapMethod": "Silent",
+ "endpoint": "https://.es.us-central1.gcp.cloud.es.io",
+ "cloudId": "",
+ "apiKey": "",
+ "username": "",
+ "password": "",
+
+ "ilmPolicy" : "my-policy",
+ "dataStream" : "logs-dotnet-default",
+ "includeHost" : true,
+ "includeUser" : true,
+ "includeProcess" : true,
+ "includeActivity" : true,
+ "filterProperties" : [ "prop1", "prop2" ],
+ "proxy": "http://localhost:8200",
+ "proxyUsername": "x",
+ "proxyPassword": "y",
+ "debugMode": false,
+
+ //EXPERT settings, do not set unless you need to
+ "maxRetries": 3,
+ "maxConcurrency": 20,
+ "maxInflight": 100000,
+ "maxExportSize": 1000,
+ "maxLifeTime": "00:00:05",
+ "fullMode": "Wait"
+ }
+ }
+ ]
+ }
+}
+```
+
+
### Comparison with [`Serilog.Sinks.Elasticsearch`](https://github.com/serilog-contrib/serilog-sinks-elasticsearch)
* `Serilog.Sinks.Elasticsearch` is an amazing community led sink that has a ton of options and works against older Elasticsearch versions `< 8.0`.
diff --git a/tests/Elastic.Serilog.Sinks.Tests/AppSettingsConfigTests.cs b/tests/Elastic.Serilog.Sinks.Tests/AppSettingsConfigTests.cs
new file mode 100644
index 00000000..3436789c
--- /dev/null
+++ b/tests/Elastic.Serilog.Sinks.Tests/AppSettingsConfigTests.cs
@@ -0,0 +1,163 @@
+using System.Threading.Channels;
+using Elastic.Ingest.Elasticsearch;
+using FluentAssertions;
+using Xunit;
+
+namespace Elastic.Serilog.Sinks.Tests;
+
+public class AppSettingsConfigTests : JsonConfigTestBase
+{
+ [Fact]
+ public void SimpleConfiguration()
+ {
+ var json =
+ CreateJson("Elasticsearch", // language=json
+ $$"""
+ {
+ "nodes": [ "http://elastichost:9200" ]
+ }
+ """);
+
+ GetBits(json, out var sink, out var formatterConfig, out var channel, out var transportConfig);
+
+ transportConfig.NodePool.Nodes.Should().NotBeNullOrEmpty()
+ .And.Contain(n => n.Uri.ToString() == "http://elastichost:9200/");
+
+ }
+
+ [Fact]
+ public void ComplexElasticsearchOptions()
+ {
+ var json =
+ CreateJson("Elasticsearch", // language=json
+ $$"""
+ {
+ "bootstrapMethod": "Silent",
+ "nodes": [ "http://elastichost:9200" ],
+ "useSniffing": false,
+ "ilmPolicy" : "my-policy",
+ "dataStream" : "logs-myapplication-default",
+ "includeHost" : false,
+ "includeUser" : false,
+ "includeProcess" : true,
+ "includeActivity" : false,
+ "filterProperties" : [ "prop1", "prop2" ],
+ "proxy": "http://localhost:8200",
+ "proxyUsername": "x",
+ "proxyPassword": "y",
+
+ "debugMode": true,
+
+ "apiKey": "api-key",
+
+ "maxRetries": 2,
+ "maxConcurrency": 20,
+ "maxInflight": 1000000,
+ "maxExportSize": 10000,
+ "maxLifeTime": "00:01:00",
+ "fullMode": "DropNewest"
+
+ }
+ """);
+
+ GetBits(json, out var sink, out var formatterConfig, out var channel, out var transportConfig);
+
+ sink.Options.BootstrapMethod.Should().Be(BootstrapMethod.Silent);
+ sink.Options.IlmPolicy.Should().Be("my-policy");
+ sink.Options.DataStream.ToString().Should().Be("logs-myapplication-default");
+ sink.Options.EcsTextFormatterConfiguration.LogEventPropertiesToFilter.Should().BeEquivalentTo("prop1", "prop2");
+ sink.Options.EcsTextFormatterConfiguration.IncludeHost.Should().Be(false);
+ sink.Options.EcsTextFormatterConfiguration.IncludeActivityData.Should().Be(false);
+ sink.Options.EcsTextFormatterConfiguration.IncludeProcess.Should().Be(true);
+ sink.Options.EcsTextFormatterConfiguration.IncludeUser.Should().Be(false);
+ formatterConfig.IncludeUser.Should().Be(sink.Options.EcsTextFormatterConfiguration.IncludeUser);
+ formatterConfig.IncludeProcess.Should().Be(sink.Options.EcsTextFormatterConfiguration.IncludeProcess);
+
+ channel.Options.DataStream.ToString().Should().Be("logs-myapplication-default");
+
+ channel.Options.BufferOptions.ExportMaxRetries.Should().Be(2);
+ channel.Options.BufferOptions.ExportMaxConcurrency.Should().Be(20);
+ channel.Options.BufferOptions.InboundBufferMaxSize.Should().Be(1_000_000);
+ channel.Options.BufferOptions.OutboundBufferMaxSize.Should().Be(10_000);
+ channel.Options.BufferOptions.OutboundBufferMaxLifetime.Should().Be(TimeSpan.FromMinutes(1));
+ channel.Options.BufferOptions.BoundedChannelFullMode.Should().Be(BoundedChannelFullMode.DropNewest);
+
+ transportConfig.NodePool.Nodes.Should().NotBeNullOrEmpty()
+ .And.Contain(n => n.Uri.ToString() == "http://elastichost:9200/");
+
+ transportConfig.ProxyAddress.Should().Be("http://localhost:8200/");
+ transportConfig.ProxyUsername.Should().Be("x");
+ transportConfig.ProxyPassword.Should().Be("y");
+ //because debugMode was set
+ transportConfig.DisableDirectStreaming.Should().Be(true);
+ transportConfig.Authentication.Should().NotBeNull();
+
+ }
+
+ [Fact]
+ public void ComplexCloudOptions()
+ {
+ var json =
+ CreateJson("ElasticCloud", // language=json
+ $$"""
+ {
+ "bootstrapMethod": "Silent",
+ "endpoint": "http://elastichost:9200",
+ "apiKey": "api-key",
+ "ilmPolicy" : "my-policy",
+ "dataStream" : "logs-myapplication-default",
+ "includeHost" : false,
+ "includeUser" : false,
+ "includeProcess" : true,
+ "includeActivity" : false,
+ "filterProperties" : [ "prop1", "prop2" ],
+ "proxy": "http://localhost:8200",
+ "proxyUsername": "x",
+ "proxyPassword": "y",
+
+ "debugMode": true,
+
+ "maxRetries": 2,
+ "maxConcurrency": 20,
+ "maxInflight": 1000000,
+ "maxExportSize": 10000,
+ "maxLifeTime": "00:01:00",
+ "fullMode": "DropNewest"
+
+ }
+ """);
+
+ GetBits(json, out var sink, out var formatterConfig, out var channel, out var transportConfig);
+
+ sink.Options.BootstrapMethod.Should().Be(BootstrapMethod.Silent);
+ sink.Options.IlmPolicy.Should().Be("my-policy");
+ sink.Options.DataStream.ToString().Should().Be("logs-myapplication-default");
+ sink.Options.EcsTextFormatterConfiguration.LogEventPropertiesToFilter.Should().BeEquivalentTo("prop1", "prop2");
+ sink.Options.EcsTextFormatterConfiguration.IncludeHost.Should().Be(false);
+ sink.Options.EcsTextFormatterConfiguration.IncludeActivityData.Should().Be(false);
+ sink.Options.EcsTextFormatterConfiguration.IncludeProcess.Should().Be(true);
+ sink.Options.EcsTextFormatterConfiguration.IncludeUser.Should().Be(false);
+ formatterConfig.IncludeUser.Should().Be(sink.Options.EcsTextFormatterConfiguration.IncludeUser);
+ formatterConfig.IncludeProcess.Should().Be(sink.Options.EcsTextFormatterConfiguration.IncludeProcess);
+
+ channel.Options.DataStream.ToString().Should().Be("logs-myapplication-default");
+
+ channel.Options.BufferOptions.ExportMaxRetries.Should().Be(2);
+ channel.Options.BufferOptions.ExportMaxConcurrency.Should().Be(20);
+ channel.Options.BufferOptions.InboundBufferMaxSize.Should().Be(1_000_000);
+ channel.Options.BufferOptions.OutboundBufferMaxSize.Should().Be(10_000);
+ channel.Options.BufferOptions.OutboundBufferMaxLifetime.Should().Be(TimeSpan.FromMinutes(1));
+ channel.Options.BufferOptions.BoundedChannelFullMode.Should().Be(BoundedChannelFullMode.DropNewest);
+
+ transportConfig.NodePool.Nodes.Should().NotBeNullOrEmpty()
+ .And.Contain(n => n.Uri.ToString() == "http://elastichost:9200/");
+
+ transportConfig.ProxyAddress.Should().Be("http://localhost:8200/");
+ transportConfig.ProxyUsername.Should().Be("x");
+ transportConfig.ProxyPassword.Should().Be("y");
+ //because debugMode was set
+ transportConfig.DisableDirectStreaming.Should().Be(true);
+ transportConfig.Authentication.Should().NotBeNull();
+
+ }
+}
diff --git a/tests/Elastic.Serilog.Sinks.Tests/ConfigurationBuilderExtensions.cs b/tests/Elastic.Serilog.Sinks.Tests/ConfigurationBuilderExtensions.cs
new file mode 100644
index 00000000..f6ba93f8
--- /dev/null
+++ b/tests/Elastic.Serilog.Sinks.Tests/ConfigurationBuilderExtensions.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.Configuration;
+
+namespace Elastic.Serilog.Sinks.Tests;
+
+internal static class ConfigurationBuilderExtensions
+{
+ public static IConfigurationBuilder AddJsonString(this IConfigurationBuilder builder, string json) =>
+ builder.Add(new JsonStringConfigSource(json));
+}
diff --git a/tests/Elastic.Serilog.Sinks.Tests/Elastic.Serilog.Sinks.Tests.csproj b/tests/Elastic.Serilog.Sinks.Tests/Elastic.Serilog.Sinks.Tests.csproj
new file mode 100644
index 00000000..1e76d668
--- /dev/null
+++ b/tests/Elastic.Serilog.Sinks.Tests/Elastic.Serilog.Sinks.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net6.0
+ enable
+ enable
+ preview
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs b/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs
new file mode 100644
index 00000000..fa6420a9
--- /dev/null
+++ b/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs
@@ -0,0 +1,63 @@
+using System.Reflection;
+using Elastic.CommonSchema;
+using Elastic.CommonSchema.Serilog;
+using Elastic.Ingest.Elasticsearch.CommonSchema;
+using Elastic.Transport;
+using FluentAssertions;
+using Microsoft.Extensions.Configuration;
+using Serilog;
+using Serilog.Core;
+
+namespace Elastic.Serilog.Sinks.Tests;
+
+public class JsonConfigTestBase
+{
+ protected static void GetBits(string json,
+ out ElasticsearchSink sink,
+ out EcsTextFormatterConfiguration formatterConfig,
+ out EcsDataStreamChannel channel,
+ out ITransportConfiguration transportConfig)
+ {
+ var config = new ConfigurationBuilder()
+ .AddJsonString(json)
+ .Build();
+
+ var loggerConfig = new LoggerConfiguration()
+ .ReadFrom.Configuration(config);
+
+ var field = loggerConfig.GetType().GetField("_logEventSinks", BindingFlags.Instance | BindingFlags.NonPublic);
+ var sinks = field?.GetValue(loggerConfig) as IList;
+ sinks.Should().HaveCount(1);
+ sink = sinks?.FirstOrDefault() as ElasticsearchSink ?? throw new NullReferenceException();
+ formatterConfig = Reflect>(sink, "_formatterConfiguration");
+ channel = Reflect>(sink, "_channel");
+ var transport = channel.Options.Transport;
+ transportConfig = transport.GetType().GetProperty("Configuration")?.GetValue(transport) as TransportConfiguration ?? throw new NullReferenceException();
+
+ sink.Should().NotBeNull();
+ formatterConfig.Should().NotBeNull();
+ channel.Should().NotBeNull();
+ transportConfig.Should().NotBeNull();
+
+
+ }
+ private static TReturn Reflect(object obj, string fieldName) where TReturn : class =>
+ obj.GetType().BaseType?.GetRuntimeFields().FirstOrDefault(f => f.Name == fieldName)?.GetValue(obj) as TReturn ?? throw new NullReferenceException(fieldName);
+
+ protected string CreateJson(string to, string argsBlock) =>
+ // language=json
+ $$"""
+ {
+ "Serilog": {
+ "Using": [ "Elastic.Serilog.Sinks" ],
+ "MinimumLevel": { "Default": "Information" },
+ "WriteTo": [
+ {
+ "Name": "{{to}}",
+ "Args": {{argsBlock}}
+ }
+ ]
+ }
+ }
+ """;
+}
diff --git a/tests/Elastic.Serilog.Sinks.Tests/JsonStringConfigSource.cs b/tests/Elastic.Serilog.Sinks.Tests/JsonStringConfigSource.cs
new file mode 100644
index 00000000..61e97742
--- /dev/null
+++ b/tests/Elastic.Serilog.Sinks.Tests/JsonStringConfigSource.cs
@@ -0,0 +1,46 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.Json;
+
+namespace Elastic.Serilog.Sinks.Tests;
+
+public class JsonStringConfigSource : IConfigurationSource
+{
+ private readonly string _json;
+
+ public JsonStringConfigSource(string json) => _json = json;
+
+ public IConfigurationProvider Build(IConfigurationBuilder builder) =>
+ new JsonStringConfigProvider(_json);
+
+ public static IConfigurationSection LoadSection(string json, string section) =>
+ new ConfigurationBuilder().Add(new JsonStringConfigSource(json)).Build().GetSection(section);
+
+ public static IDictionary LoadData(string json)
+ {
+ var provider = new JsonStringConfigProvider(json);
+ provider.Load();
+ return provider.Data;
+ }
+
+ private class JsonStringConfigProvider : JsonConfigurationProvider
+ {
+ private readonly string _json;
+
+ public JsonStringConfigProvider(string json) : base(new JsonConfigurationSource { Optional = true }) => _json = json;
+
+ public new IDictionary Data => base.Data;
+
+ public override void Load() => Load(StringToStream(_json));
+
+ private static Stream StringToStream(string str)
+ {
+ var memStream = new MemoryStream();
+ var textWriter = new StreamWriter(memStream);
+ textWriter.Write(str);
+ textWriter.Flush();
+ memStream.Seek(0, SeekOrigin.Begin);
+
+ return memStream;
+ }
+ }
+}