diff --git a/examples/aspnetcore-with-serilog/Program.cs b/examples/aspnetcore-with-serilog/Program.cs index e1f8f89c..b4fe7075 100644 --- a/examples/aspnetcore-with-serilog/Program.cs +++ b/examples/aspnetcore-with-serilog/Program.cs @@ -26,11 +26,11 @@ public static IWebHost BuildWebHost(string[] args) => // Ensure HttpContextAccessor is accessible var httpAccessor = ctx.Configuration.Get(); - + // Create a formatter configuration to se this accessor var formatterConfig = new EcsTextFormatterConfiguration(); - formatterConfig.MapHttpContext(httpAccessor); - + formatterConfig.MapHttpAdapter = new HttpAdapter(httpAccessor); + // Write events to the console using this configration var formatter = new EcsTextFormatter(formatterConfig); @@ -61,4 +61,4 @@ public static void Main(string[] args) } } } -} \ No newline at end of file +} diff --git a/src/Elastic.CommonSchema.Serilog/EcsTextFormatter.cs b/src/Elastic.CommonSchema.Serilog/EcsTextFormatter.cs index ebc18a2a..137954a0 100644 --- a/src/Elastic.CommonSchema.Serilog/EcsTextFormatter.cs +++ b/src/Elastic.CommonSchema.Serilog/EcsTextFormatter.cs @@ -12,19 +12,28 @@ namespace Elastic.CommonSchema.Serilog /// /// A serilog formatter that writes log events using the Elasticsearch Common Schema format /// - public class EcsTextFormatter : ITextFormatter + public class EcsTextFormatter : ITextFormatter + where TEcsDocument : EcsDocument, new() { - private readonly EcsTextFormatterConfiguration _configuration; - public EcsTextFormatter() : this(new EcsTextFormatterConfiguration()) { } + protected EcsTextFormatterConfiguration Configuration { get; } - public EcsTextFormatter(EcsTextFormatterConfiguration configuration) => - _configuration = configuration ?? new EcsTextFormatterConfiguration(); + public EcsTextFormatter() : this(new EcsTextFormatterConfiguration()) { } + + public EcsTextFormatter(EcsTextFormatterConfiguration configuration) => + Configuration = configuration ?? new EcsTextFormatterConfiguration(); public virtual void Format(LogEvent logEvent, TextWriter output) { - var ecsEvent = LogEventConverter.ConvertToEcs(logEvent, _configuration); + var ecsEvent = LogEventConverter.ConvertToEcs(logEvent, Configuration); output.WriteLine(ecsEvent.Serialize()); } } + + public class EcsTextFormatter : EcsTextFormatter + { + public EcsTextFormatter() : base() {} + public EcsTextFormatter(EcsTextFormatterConfiguration configuration) : base(configuration) {} + public EcsTextFormatter(EcsTextFormatterConfiguration configuration) : base(configuration) {} + } } diff --git a/src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs b/src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs index 0f5e0f3f..f9c70cbd 100644 --- a/src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs +++ b/src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs @@ -17,44 +17,29 @@ namespace Elastic.CommonSchema.Serilog public interface IEcsTextFormatterConfiguration { bool MapCurrentThread { get; set; } - Func MapCustom { get; set; } bool MapExceptions { get; set; } IHttpAdapter MapHttpAdapter { get; set; } ISet LogEventPropertiesToFilter { get;set; } } - public class EcsTextFormatterConfiguration : IEcsTextFormatterConfiguration + public interface IEcsTextFormatterConfiguration : IEcsTextFormatterConfiguration + where TEcsDocument : EcsDocument, new() { - bool IEcsTextFormatterConfiguration.MapExceptions { get; set; } = true; - bool IEcsTextFormatterConfiguration.MapCurrentThread { get; set; } = true; - - IHttpAdapter IEcsTextFormatterConfiguration.MapHttpAdapter { get; set; } - ISet IEcsTextFormatterConfiguration.LogEventPropertiesToFilter { get; set; } - - Func IEcsTextFormatterConfiguration.MapCustom { get; set; } = (b, e) => b; - -#if NETSTANDARD - public EcsTextFormatterConfiguration MapHttpContext(IHttpContextAccessor contextAccessor) => Assign(this, contextAccessor, (o, v) => o.MapHttpAdapter - = new HttpAdapter(v)); -#else - public EcsTextFormatterConfiguration MapHttpContext(HttpContext httpContext) => - Assign(this, httpContext, (o, v) => o.MapHttpAdapter = new HttpAdapter(v)); -#endif - public EcsTextFormatterConfiguration MapExceptions(bool value) => Assign(this, value, (o, v) => o.MapExceptions = v); - - public EcsTextFormatterConfiguration MapCurrentThread(bool value) => Assign(this, value, (o, v) => o.MapCurrentThread = v); + Func MapCustom { get; set; } + } - public EcsTextFormatterConfiguration MapCustom(Func value) => Assign(this, value, (o, v) => o.MapCustom = v); + public class EcsTextFormatterConfiguration : IEcsTextFormatterConfiguration + where TEcsDocument : EcsDocument, new() + { + public bool MapCurrentThread { get; set; } = true; + public bool MapExceptions { get; set; } = true; + public IHttpAdapter MapHttpAdapter { get; set; } + public ISet LogEventPropertiesToFilter { get; set; } + public Func MapCustom { get; set; } + } - public EcsTextFormatterConfiguration LogEventPropertiesToFilter(ISet value) => Assign(this, value, (o, v) => o.LogEventPropertiesToFilter = v); + public class EcsTextFormatterConfiguration : EcsTextFormatterConfiguration + { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static EcsTextFormatterConfiguration Assign( - EcsTextFormatterConfiguration self, TValue value, Action assign - ) - { - assign(self, value); - return self; - } } } diff --git a/src/Elastic.CommonSchema.Serilog/LogEventConverter.cs b/src/Elastic.CommonSchema.Serilog/LogEventConverter.cs index 22733c63..b17deb26 100644 --- a/src/Elastic.CommonSchema.Serilog/LogEventConverter.cs +++ b/src/Elastic.CommonSchema.Serilog/LogEventConverter.cs @@ -49,7 +49,8 @@ private static class SpecialKeys public const string RequestId = nameof(RequestId); } - public static EcsDocument ConvertToEcs(LogEvent logEvent, IEcsTextFormatterConfiguration configuration) + public static TEcsDocument ConvertToEcs(LogEvent logEvent, IEcsTextFormatterConfiguration configuration) + where TEcsDocument : EcsDocument, new() { var exceptions = logEvent.Exception != null ? new List { logEvent.Exception } @@ -58,7 +59,7 @@ public static EcsDocument ConvertToEcs(LogEvent logEvent, IEcsTextFormatterConfi if (configuration.MapHttpAdapter != null) exceptions.AddRange(configuration.MapHttpAdapter.Exceptions); - var ecsEvent = new EcsDocument + var ecsEvent = new TEcsDocument { Timestamp = logEvent.Timestamp, Message = logEvent.RenderMessage(), @@ -90,6 +91,9 @@ public static EcsDocument ConvertToEcs(LogEvent logEvent, IEcsTextFormatterConfi return ecsEvent; } + public static EcsDocument ConvertToEcs(LogEvent logEvent, IEcsTextFormatterConfiguration configuration) => + ConvertToEcs(logEvent, configuration); + private static Service GetService(LogEvent logEvent) { if (!logEvent.TryGetScalarPropertyValue("ElasticApmServiceName", out var serviceName)) diff --git a/tests/Elastic.CommonSchema.Serilog.Tests/LogEventPropFilterTests.cs b/tests/Elastic.CommonSchema.Serilog.Tests/LogEventPropFilterTests.cs index 6129aee6..1bc13acf 100644 --- a/tests/Elastic.CommonSchema.Serilog.Tests/LogEventPropFilterTests.cs +++ b/tests/Elastic.CommonSchema.Serilog.Tests/LogEventPropFilterTests.cs @@ -28,8 +28,8 @@ public LogEventPropFilterTests(ITestOutputHelper output) : base(output) .Enrich.WithEnvironmentUserName() .Enrich.WithElasticApmCorrelationInfo(); - Formatter = new EcsTextFormatter(new EcsTextFormatterConfiguration() - .LogEventPropertiesToFilter(new HashSet(){{ "foo" }})); + var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = new HashSet() { { "foo" } } }; + Formatter = new EcsTextFormatter(config); } private LogEvent BuildLogEvent() @@ -74,8 +74,8 @@ public void FilterLogEventProperty() => TestLogger((logger, getLogEvents) => [Fact] public void NullFilterLogEventProperty() => TestLogger((logger, getLogEvents) => { - Formatter = new EcsTextFormatter(new EcsTextFormatterConfiguration() - .LogEventPropertiesToFilter(null)); + var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = null }; + Formatter = new EcsTextFormatter(config); var evnt = BuildLogEvent(); logger.Write(evnt); @@ -98,8 +98,8 @@ public void NullFilterLogEventProperty() => TestLogger((logger, getLogEvents) => [Fact] public void EmptyFilterLogEventProperty() => TestLogger((logger, getLogEvents) => { - Formatter = new EcsTextFormatter(new EcsTextFormatterConfiguration() - .LogEventPropertiesToFilter(new HashSet())); + var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = new HashSet() }; + Formatter = new EcsTextFormatter(config); var evnt = BuildLogEvent(); logger.Write(evnt); @@ -121,8 +121,8 @@ public void EmptyFilterLogEventProperty() => TestLogger((logger, getLogEvents) = [Fact] public void CaseInsensitiveFilterLogEventProperty() => TestLogger((logger, getLogEvents) => { - Formatter = new EcsTextFormatter(new EcsTextFormatterConfiguration() - .LogEventPropertiesToFilter(new HashSet(StringComparer.OrdinalIgnoreCase){{ "FOO" }})); + var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = new HashSet(StringComparer.OrdinalIgnoreCase){{ "FOO" }} }; + Formatter = new EcsTextFormatter(config); var evnt = BuildLogEvent(); logger.Write(evnt); @@ -144,8 +144,8 @@ public void CaseInsensitiveFilterLogEventProperty() => TestLogger((logger, getLo [Fact] public void CaseSensitiveFilterLogEventProperty() => TestLogger((logger, getLogEvents) => { - Formatter = new EcsTextFormatter(new EcsTextFormatterConfiguration() - .LogEventPropertiesToFilter(new HashSet(StringComparer.Ordinal){{ "FOO" }})); + var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = new HashSet(StringComparer.Ordinal){{ "FOO" }} }; + Formatter = new EcsTextFormatter(config); var evnt = BuildLogEvent(); logger.Write(evnt); diff --git a/tests/Elastic.CommonSchema.Serilog.Tests/Repro/GithubIssue167.cs b/tests/Elastic.CommonSchema.Serilog.Tests/Repro/GithubIssue167.cs new file mode 100644 index 00000000..7fa84261 --- /dev/null +++ b/tests/Elastic.CommonSchema.Serilog.Tests/Repro/GithubIssue167.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Elastic.CommonSchema.Serialization; +using FluentAssertions; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.TestCorrelator; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.CommonSchema.Serilog.Tests +{ + public class GithubIssue167 + { + public class ContosoDocument : EcsDocument + { + [JsonPropertyName("contoso"), DataMember(Name = "contoso")] + public Contoso Contoso { get; set; } + + protected override bool TryRead(string propertyName, out Type type) + { + type = propertyName switch + { + "contoso" => typeof(Contoso), + _ => null + }; + return type != null; + } + + protected override bool ReceiveProperty(string propertyName, object value) => + propertyName switch + { + "contoso" => null != (Contoso = value as Contoso), + _ => false + }; + + protected override void WriteAdditionalProperties(Action write) => write("contoso", Contoso); + } + + public class Contoso + { + [JsonPropertyName("company_name"), DataMember(Name = "company_name")] + public string CompanyName { get; set; } + } + + public class ContosoEcsTextFormatter : EcsTextFormatter + { + public override void Format(LogEvent logEvent, TextWriter output) + { + var ecsEvent = LogEventConverter.ConvertToEcs(logEvent, Configuration); + ecsEvent.Contoso = new Contoso { CompanyName = "Elastic", }; + output.WriteLine(ecsEvent.Serialize()); + } + } + + private LoggerConfiguration LoggerConfiguration { get; } + private ContosoEcsTextFormatter Formatter { get; } + + public GithubIssue167(ITestOutputHelper output) + { + Formatter = new ContosoEcsTextFormatter(); + LoggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Console(Formatter) + .WriteTo.TestOutput(output, formatter: Formatter, LogEventLevel.Verbose) + .WriteTo.TestCorrelator(); + } + + [Fact] + public void CanFormatBaseImplementationOfEcsDocument() + { + using var context = TestCorrelator.CreateContext(); + var logger = LoggerConfiguration.CreateLogger().ForContext(GetType()); + + logger.Information("My log message!"); + + var logEvents = TestCorrelator.GetLogEventsFromCurrentContext().ToList(); + + logEvents.Should().HaveCount(1); + + var ecsEvents = ToEcsEvents(logEvents); + + var (_, info) = ecsEvents.First(); + info.Timestamp.Should().NotBeNull(); + info.Contoso.Should().NotBeNull(); + info.Contoso.CompanyName.Should().Be("Elastic"); + } + + private List ToFormattedStrings(List logEvents) => + logEvents + .Select(l => + { + using var stringWriter = new StringWriter(); + Formatter.Format(l, stringWriter); + return stringWriter.ToString(); + }) + .ToList(); + + protected List<(string Json, ContosoDocument Base)> ToEcsEvents(List logEvents) => + ToFormattedStrings(logEvents) + .Select(s => (s, EcsSerializerFactory.Deserialize(s))) + .ToList(); + } +}