Skip to content

Commit

Permalink
Add better support for Serilog.Settings.Configuration (#441)
Browse files Browse the repository at this point in the history
#### 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": "<apiKey>",
           "username": "<username>",
           "password": "<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://<redacted>.es.us-central1.gcp.cloud.es.io",
           "cloudId": "<cloudId>",
           "apiKey": "<apiKey>",
           "username": "<username>",
           "password": "<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"
         }
       }
     ]
   }
 }
 ```
  • Loading branch information
Mpdreamz authored Sep 18, 2024
1 parent ef55ecb commit 2f9d18c
Show file tree
Hide file tree
Showing 9 changed files with 681 additions and 6 deletions.
7 changes: 7 additions & 0 deletions ecs-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
238 changes: 238 additions & 0 deletions src/Elastic.Serilog.Sinks/ConfigSinkExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Extension methods on <see cref="LoggerSinkConfiguration"/> to aid with serilog log configuration building
/// <para>These overloads exists entirely to make configuration through <c>Serilog.Settings.Configuration</c> easier</para>
/// </summary>
public static class ConfigSinkExtensions
{
/// <summary>
/// Write logs directly to Elasticsearch.
/// <para>This overload makes it easy to directly specify the endpoint <paramref name="nodes"/></para>
/// <para>Use <paramref name="loggerConfiguration"/> configure where and how data should be written</para>
/// </summary>
public static LoggerConfiguration Elasticsearch(
this LoggerSinkConfiguration loggerConfiguration,
BootstrapMethod bootstrapMethod,
ICollection<Uri> 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<string>? 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);
}

/// <summary>
/// Write logs directly to Elastic Cloud ( https://cloud.elastic.co/ ).
/// <para><paramref name="cloudId"/> describes your deployments endpoints (can be found in the Admin Console)</para>
/// <para><paramref name="apiKey"/> is used for authentication.</para>
/// <para>Use <paramref name="loggerConfiguration"/> configure where and how data should be written</para>
/// </summary>
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<string>? 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<string>? 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<string>(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));
}


/// <summary>
/// Write logs directly to Elastic Cloud ( https://cloud.elastic.co/ ).
/// <para><paramref name="cloudId"/> describes your deployments endpoints (can be found in the Admin Console)</para>
/// <para><paramref name="username"/> and <paramref name="password"/> are used for basic authentication.</para>
/// <para>Use <paramref name="loggerConfiguration"/> configure where and how data should be written</para>
/// </summary>
public static LoggerConfiguration ElasticCloud(
this LoggerSinkConfiguration loggerConfiguration,
string cloudId,
string username,
string password,
Action<ElasticsearchSinkOptions>? configureOptions = null,
Action<TransportConfiguration>? 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);
}
}
}
40 changes: 34 additions & 6 deletions src/Elastic.Serilog.Sinks/ElasticsearchSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@

namespace Elastic.Serilog.Sinks
{

/// <summary>
/// A read only view of the options provided to <see cref="ElasticsearchSink"/>
/// </summary>
public interface IElasticsearchSinkOptions
{
/// <inheritdoc cref="BootstrapMethod"/>
BootstrapMethod BootstrapMethod { get; }

/// <inheritdoc cref="IEcsTextFormatterConfiguration"/>
IEcsTextFormatterConfiguration EcsTextFormatterConfiguration { get; }

/// <inheritdoc cref="DataStreamName"/>
public DataStreamName DataStream { get; }

/// <summary>
/// The ILM Policy to apply, see the following for more details:
/// <para>https://www.elastic.co/guide/en/elasticsearch/reference/current/index-lifecycle-management.html</para>
/// Defaults to `logs` which is shipped by default with Elasticsearch
/// </summary>
public string? IlmPolicy { get; }

}

/// <summary>
/// Provides configuration options to <see cref="ElasticsearchSink"/> to control how and where data gets written
/// </summary>
Expand All @@ -30,7 +54,9 @@ public ElasticsearchSinkOptions(ITransport transport) : base(transport) { }
}

/// <inheritdoc cref="ElasticsearchSinkOptions{TEcsDocument}"/>
public class ElasticsearchSinkOptions<TEcsDocument> where TEcsDocument : EcsDocument, new()
public class ElasticsearchSinkOptions<TEcsDocument>
: IElasticsearchSinkOptions
where TEcsDocument : EcsDocument, new()
{
/// <inheritdoc cref="ElasticsearchSinkOptions"/>
public ElasticsearchSinkOptions() : this(new DistributedTransport(TransportHelper.Default())) { }
Expand All @@ -41,6 +67,8 @@ public ElasticsearchSinkOptions() : this(new DistributedTransport(TransportHelpe
/// <inheritdoc cref="ITransport{TConfiguration}"/>
internal ITransport Transport { get; }

IEcsTextFormatterConfiguration IElasticsearchSinkOptions.EcsTextFormatterConfiguration => TextFormatting;

/// <inheritdoc cref="EcsTextFormatterConfiguration{TEcsDocument}"/>
public EcsTextFormatterConfiguration<TEcsDocument> TextFormatting { get; set; } = new();

Expand All @@ -60,11 +88,7 @@ public ElasticsearchSinkOptions() : this(new DistributedTransport(TransportHelpe
/// <inheritdoc cref="BootstrapMethod"/>
public BootstrapMethod BootstrapMethod { get; set; }

/// <summary>
/// The ILM Policy to apply, see the following for more details:
/// <para>https://www.elastic.co/guide/en/elasticsearch/reference/current/index-lifecycle-management.html</para>
/// Defaults to `logs` which is shipped by default with Elasticsearch
/// </summary>
/// <inheritdoc cref="IElasticsearchSinkOptions.IlmPolicy"/>
public string? IlmPolicy { get; set; }

/// <summary>
Expand Down Expand Up @@ -99,9 +123,13 @@ public class ElasticsearchSink<TEcsDocument> : ILogEventSink, IDisposable
private readonly EcsTextFormatterConfiguration<TEcsDocument> _formatterConfiguration;
private readonly EcsDataStreamChannel<TEcsDocument> _channel;

/// <inheritdoc cref="IElasticsearchSinkOptions"/>
public IElasticsearchSinkOptions Options { get; }

/// <inheritdoc cref="ElasticsearchSink"/>>
public ElasticsearchSink(ElasticsearchSinkOptions<TEcsDocument> options)
{
Options = options;
_formatterConfiguration = options.TextFormatting;
var channelOptions = new DataStreamChannelOptions<TEcsDocument>(options.Transport)
{
Expand Down
Loading

0 comments on commit 2f9d18c

Please sign in to comment.