Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add better support for Serilog.Settings.Configuration #441

Merged
merged 4 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading