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; + } + } +}