From 4b40a57d34e35ffa5f2d03b3e49263343ce8d4d7 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 20 May 2025 21:32:46 +0200 Subject: [PATCH 1/2] Add support for ISetLoggingFailureListener (Serilog fallback logging) --- .../Elastic.Serilog.Sinks.Example.csproj | 2 +- .../Elastic.Serilog.Sinks.Example/Program.cs | 2 +- .../AspnetCoreExample.csproj | 2 +- .../Elastic.Apm.SerilogEnricher.csproj | 2 +- .../EcsTextFormatterConfiguration.cs | 5 +- .../Elastic.CommonSchema.Serilog.csproj | 2 +- .../LogEventConverter.cs | 14 +++++- .../Elastic.Serilog.Enrichers.Web.csproj | 2 +- .../ElasticsearchSink.cs | 48 +++++++++++++++---- .../ElasticsearchSinkExtensions.cs | 9 ++-- .../JsonConfigTestBase.cs | 4 +- 11 files changed, 66 insertions(+), 26 deletions(-) diff --git a/examples/Elastic.Serilog.Sinks.Example/Elastic.Serilog.Sinks.Example.csproj b/examples/Elastic.Serilog.Sinks.Example/Elastic.Serilog.Sinks.Example.csproj index d3cb304e..b15ee76a 100644 --- a/examples/Elastic.Serilog.Sinks.Example/Elastic.Serilog.Sinks.Example.csproj +++ b/examples/Elastic.Serilog.Sinks.Example/Elastic.Serilog.Sinks.Example.csproj @@ -9,7 +9,7 @@ - + diff --git a/examples/Elastic.Serilog.Sinks.Example/Program.cs b/examples/Elastic.Serilog.Sinks.Example/Program.cs index 6d9d755a..c4dd65ac 100644 --- a/examples/Elastic.Serilog.Sinks.Example/Program.cs +++ b/examples/Elastic.Serilog.Sinks.Example/Program.cs @@ -53,7 +53,7 @@ { BootstrapMethod = BootstrapMethod.Failure, DataStream = new DataStreamName("logs", "console-example"), - TextFormatting = new EcsTextFormatterConfiguration + TextFormatting = new EcsTextFormatterConfiguration { MapCustom = (e, _) => e }, diff --git a/examples/aspnetcore-with-serilog/AspnetCoreExample.csproj b/examples/aspnetcore-with-serilog/AspnetCoreExample.csproj index 19cd37b3..d4baf31d 100644 --- a/examples/aspnetcore-with-serilog/AspnetCoreExample.csproj +++ b/examples/aspnetcore-with-serilog/AspnetCoreExample.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Elastic.Apm.SerilogEnricher/Elastic.Apm.SerilogEnricher.csproj b/src/Elastic.Apm.SerilogEnricher/Elastic.Apm.SerilogEnricher.csproj index 774e6b10..7f45db7c 100644 --- a/src/Elastic.Apm.SerilogEnricher/Elastic.Apm.SerilogEnricher.csproj +++ b/src/Elastic.Apm.SerilogEnricher/Elastic.Apm.SerilogEnricher.csproj @@ -6,7 +6,7 @@ True - + diff --git a/src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs b/src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs index 3dc2adc5..1b17f3cc 100644 --- a/src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs +++ b/src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs @@ -81,8 +81,5 @@ public class EcsTextFormatterConfiguration : IEcsTextFormatterConf // ReSharper disable once ClassNeverInstantiated.Global /// - public class EcsTextFormatterConfiguration : EcsTextFormatterConfiguration - { - - } + public class EcsTextFormatterConfiguration : EcsTextFormatterConfiguration; } diff --git a/src/Elastic.CommonSchema.Serilog/Elastic.CommonSchema.Serilog.csproj b/src/Elastic.CommonSchema.Serilog/Elastic.CommonSchema.Serilog.csproj index c021f533..121f1edd 100644 --- a/src/Elastic.CommonSchema.Serilog/Elastic.CommonSchema.Serilog.csproj +++ b/src/Elastic.CommonSchema.Serilog/Elastic.CommonSchema.Serilog.csproj @@ -13,7 +13,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Elastic.CommonSchema.Serilog/LogEventConverter.cs b/src/Elastic.CommonSchema.Serilog/LogEventConverter.cs index db2ee4e9..e54b23cb 100644 --- a/src/Elastic.CommonSchema.Serilog/LogEventConverter.cs +++ b/src/Elastic.CommonSchema.Serilog/LogEventConverter.cs @@ -6,11 +6,21 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.Json.Serialization; using Serilog.Events; using static Elastic.CommonSchema.Serilog.SpecialProperties; namespace Elastic.CommonSchema.Serilog { + /// A specialized instance of that holds on to the original + /// This property won't be emitted to JSON but is used to report back to serilog failure pipelines + public class LogEventEcsDocument : EcsDocument + { + /// The original for bookkeeping, not send over to Elasticsearch + [JsonIgnore] + public LogEvent LogEvent { get; set; } = null!; + } + /// /// Elastic Common Schema converter for LogEvent /// @@ -178,7 +188,7 @@ private static bool PropertyAlreadyMapped(string property) } } - private static object PropertyValueToObject(LogEventPropertyValue propertyValue) + private static object? PropertyValueToObject(LogEventPropertyValue propertyValue) { switch (propertyValue) { @@ -187,7 +197,7 @@ private static object PropertyValueToObject(LogEventPropertyValue propertyValue) case ScalarValue sv: return sv.Value; case DictionaryValue dv: - return dv.Elements.ToDictionary(keySelector: kvp => kvp.Key.Value.ToString() ?? string.Empty, + return dv.Elements.ToDictionary(keySelector: kvp => kvp.Key?.Value?.ToString() ?? string.Empty, elementSelector: (kvp) => PropertyValueToObject(kvp.Value)); case StructureValue ov: { diff --git a/src/Elastic.Serilog.Enrichers.Web/Elastic.Serilog.Enrichers.Web.csproj b/src/Elastic.Serilog.Enrichers.Web/Elastic.Serilog.Enrichers.Web.csproj index d1bce2e0..ff3f245b 100644 --- a/src/Elastic.Serilog.Enrichers.Web/Elastic.Serilog.Enrichers.Web.csproj +++ b/src/Elastic.Serilog.Enrichers.Web/Elastic.Serilog.Enrichers.Web.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Elastic.Serilog.Sinks/ElasticsearchSink.cs b/src/Elastic.Serilog.Sinks/ElasticsearchSink.cs index a017b538..a6592405 100644 --- a/src/Elastic.Serilog.Sinks/ElasticsearchSink.cs +++ b/src/Elastic.Serilog.Sinks/ElasticsearchSink.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Elastic.Channels; using Elastic.Channels.Buffers; using Elastic.Channels.Diagnostics; using Elastic.CommonSchema; @@ -44,7 +45,7 @@ public interface IElasticsearchSinkOptions /// /// Provides configuration options to to control how and where data gets written /// - public class ElasticsearchSinkOptions : ElasticsearchSinkOptions + public class ElasticsearchSinkOptions : ElasticsearchSinkOptions { /// public ElasticsearchSinkOptions() { } @@ -56,7 +57,7 @@ public ElasticsearchSinkOptions(ITransport transport) : base(transport) { } /// public class ElasticsearchSinkOptions : IElasticsearchSinkOptions - where TEcsDocument : EcsDocument, new() + where TEcsDocument : LogEventEcsDocument, new() { /// public ElasticsearchSinkOptions() : this(new DistributedTransport(TransportHelper.Default())) { } @@ -110,18 +111,19 @@ public ElasticsearchSinkOptions() : this(new DistributedTransport(TransportHelpe /// /// This sink allows you to write serilog logs directly to Elasticsearch or Elastic Cloud /// - public class ElasticsearchSink : ElasticsearchSink + public class ElasticsearchSink : ElasticsearchSink { /// > public ElasticsearchSink(ElasticsearchSinkOptions options) : base(options) {} } /// > - public class ElasticsearchSink : ILogEventSink, IDisposable - where TEcsDocument : EcsDocument, new() + public class ElasticsearchSink : ILogEventSink, IDisposable, ISetLoggingFailureListener + where TEcsDocument : LogEventEcsDocument, new() { private readonly EcsTextFormatterConfiguration _formatterConfiguration; private readonly EcsDataStreamChannel _channel; + private ILoggingFailureListener _failureListener = SelfLog.FailureListener; /// public IElasticsearchSinkOptions Options { get; } @@ -133,7 +135,9 @@ public ElasticsearchSink(ElasticsearchSinkOptions options) _formatterConfiguration = options.TextFormatting; var channelOptions = new DataStreamChannelOptions(options.Transport) { - DataStream = options.DataStream + DataStream = options.DataStream, + ExportMaxRetriesCallback = EmitExportFailures + }; options.ConfigureChannel?.Invoke(channelOptions); _channel = new EcsDataStreamChannel(channelOptions, new [] { new SelfLogCallbackListener(options)}); @@ -142,18 +146,46 @@ public ElasticsearchSink(ElasticsearchSinkOptions options) _channel.BootstrapElasticsearch(options.BootstrapMethod, options.IlmPolicy); } + private void EmitExportFailures(IReadOnlyCollection documents) + { + var logs = documents + .Select(d => d.LogEvent) + .ToArray(); + _failureListener.OnLoggingFailed( + this, + LoggingFailureKind.Temporary, + "Failure to export events over to Elasticsearch.", + logs, + exception: null + ); + } + /// public void Emit(LogEvent logEvent) { var ecsDoc = LogEventConverter.ConvertToEcs(logEvent, _formatterConfiguration); - _channel.TryWrite(ecsDoc); + ecsDoc.LogEvent = logEvent; + if (!_channel.TryWrite(ecsDoc)) + { + _failureListener.OnLoggingFailed( + this, + LoggingFailureKind.Temporary, + "Failure to push event over the channel.", + [logEvent], + exception: null + ); + } } /// Disposes and flushed public void Dispose() => _channel.Dispose(); + + void ISetLoggingFailureListener.SetFailureListener(ILoggingFailureListener failureListener) => + _failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener)); } - internal class SelfLogCallbackListener : IChannelCallbacks where TEcsDocument : EcsDocument, new() + internal class SelfLogCallbackListener : IChannelCallbacks + where TEcsDocument : LogEventEcsDocument, new() { public Action? ExportExceptionCallback { get; } public Action? ExportResponseCallback { get; } diff --git a/src/Elastic.Serilog.Sinks/ElasticsearchSinkExtensions.cs b/src/Elastic.Serilog.Sinks/ElasticsearchSinkExtensions.cs index a1d4212e..f45bf894 100644 --- a/src/Elastic.Serilog.Sinks/ElasticsearchSinkExtensions.cs +++ b/src/Elastic.Serilog.Sinks/ElasticsearchSinkExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Elastic.CommonSchema; +using Elastic.CommonSchema.Serilog; using Elastic.Transport; using Serilog; using Serilog.Configuration; @@ -31,7 +32,7 @@ public static LoggerConfiguration Elasticsearch(this LoggerSinkConfiguration log /// This generic overload using allows you to use your own subclasses /// public static LoggerConfiguration Elasticsearch(this LoggerSinkConfiguration loggerConfiguration, ElasticsearchSinkOptions? options = null) - where TEcsDocument : EcsDocument, new() => + where TEcsDocument : LogEventEcsDocument, new() => loggerConfiguration.Sink( new ElasticsearchSink(options ?? new ElasticsearchSinkOptions()) , restrictedToMinimumLevel: options?.MinimumLevel ?? LevelAlias.Minimum @@ -76,7 +77,7 @@ public static LoggerConfiguration Elasticsearch( bool useSniffing = false, LoggingLevelSwitch? levelSwitch = null, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum - ) where TEcsDocument : EcsDocument, new() + ) where TEcsDocument : LogEventEcsDocument, new() { var transportConfig = useSniffing ? TransportHelper.Sniffing(nodes) : TransportHelper.Static(nodes); configureTransport?.Invoke(transportConfig); @@ -125,7 +126,7 @@ public static LoggerConfiguration ElasticCloud( Action? configureTransport = null, LoggingLevelSwitch? levelSwitch = null, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum - ) where TEcsDocument : EcsDocument, new() + ) where TEcsDocument : LogEventEcsDocument, new() { var transportConfig = TransportHelper.Cloud(cloudId, apiKey); configureTransport?.Invoke(transportConfig); @@ -176,7 +177,7 @@ public static LoggerConfiguration ElasticCloud( Action? configureTransport = null, LoggingLevelSwitch? levelSwitch = null, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum - ) where TEcsDocument : EcsDocument, new() + ) where TEcsDocument : LogEventEcsDocument, new() { var transportConfig = TransportHelper.Cloud(cloudId, username, password); configureTransport?.Invoke(transportConfig); diff --git a/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs b/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs index 754719a6..ed73d4f5 100644 --- a/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs +++ b/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs @@ -13,7 +13,7 @@ namespace Elastic.Serilog.Sinks.Tests; public class JsonConfigTestBase { protected static void GetBits(string json, - out ElasticsearchSink sink, + out ElasticsearchSink sink, out EcsTextFormatterConfiguration formatterConfig, out EcsDataStreamChannel channel, out ITransportConfiguration transportConfig) @@ -28,7 +28,7 @@ protected static void GetBits(string json, 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(); + sink = sinks?.FirstOrDefault() as ElasticsearchSink ?? throw new NullReferenceException(); formatterConfig = Reflect>(sink, "_formatterConfiguration"); channel = Reflect>(sink, "_channel"); From a7e2b2b2f57a6f9af537118ede012e2babab91f1 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 20 May 2025 21:40:12 +0200 Subject: [PATCH 2/2] update tests --- tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs b/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs index ed73d4f5..2614233a 100644 --- a/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs +++ b/tests/Elastic.Serilog.Sinks.Tests/JsonConfigTestBase.cs @@ -14,8 +14,8 @@ public class JsonConfigTestBase { protected static void GetBits(string json, out ElasticsearchSink sink, - out EcsTextFormatterConfiguration formatterConfig, - out EcsDataStreamChannel channel, + out EcsTextFormatterConfiguration formatterConfig, + out EcsDataStreamChannel channel, out ITransportConfiguration transportConfig) { var config = new ConfigurationBuilder() @@ -29,8 +29,8 @@ protected static void GetBits(string json, 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"); + formatterConfig = Reflect>(sink, "_formatterConfiguration"); + channel = Reflect>(sink, "_channel"); var transport = channel.Options.Transport as ITransport ?? throw new NullReferenceException(); transportConfig = transport.Configuration;