Skip to content

Commit

Permalink
Allow EcsTextFormatter to accept any custom EcsDocument implementation
Browse files Browse the repository at this point in the history
This way users can implement and use their own EcsDocument subclasses as
ITextFormatter

Fixes #167
  • Loading branch information
Mpdreamz committed Sep 6, 2022
1 parent e3eb781 commit 605b359
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 52 deletions.
8 changes: 4 additions & 4 deletions examples/aspnetcore-with-serilog/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ public static IWebHost BuildWebHost(string[] args) =>

// Ensure HttpContextAccessor is accessible
var httpAccessor = ctx.Configuration.Get<HttpContextAccessor>();

// 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);

Expand Down Expand Up @@ -61,4 +61,4 @@ public static void Main(string[] args)
}
}
}
}
}
21 changes: 15 additions & 6 deletions src/Elastic.CommonSchema.Serilog/EcsTextFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ namespace Elastic.CommonSchema.Serilog
/// <summary>
/// A serilog formatter that writes log events using the Elasticsearch Common Schema format
/// </summary>
public class EcsTextFormatter : ITextFormatter
public class EcsTextFormatter<TEcsDocument> : ITextFormatter
where TEcsDocument : EcsDocument, new()
{
private readonly EcsTextFormatterConfiguration _configuration;

public EcsTextFormatter() : this(new EcsTextFormatterConfiguration()) { }
protected EcsTextFormatterConfiguration<TEcsDocument> Configuration { get; }

public EcsTextFormatter(EcsTextFormatterConfiguration configuration) =>
_configuration = configuration ?? new EcsTextFormatterConfiguration();
public EcsTextFormatter() : this(new EcsTextFormatterConfiguration<TEcsDocument>()) { }

public EcsTextFormatter(EcsTextFormatterConfiguration<TEcsDocument> configuration) =>
Configuration = configuration ?? new EcsTextFormatterConfiguration<TEcsDocument>();

public virtual void Format(LogEvent logEvent, TextWriter output)
{
var ecsEvent = LogEventConverter.ConvertToEcs(logEvent, _configuration);
var ecsEvent = LogEventConverter.ConvertToEcs<TEcsDocument>(logEvent, Configuration);
output.WriteLine(ecsEvent.Serialize());
}
}

public class EcsTextFormatter : EcsTextFormatter<EcsDocument>
{
public EcsTextFormatter() : base() {}
public EcsTextFormatter(EcsTextFormatterConfiguration<EcsDocument> configuration) : base(configuration) {}
public EcsTextFormatter(EcsTextFormatterConfiguration configuration) : base(configuration) {}
}
}
45 changes: 15 additions & 30 deletions src/Elastic.CommonSchema.Serilog/EcsTextFormatterConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,29 @@ namespace Elastic.CommonSchema.Serilog
public interface IEcsTextFormatterConfiguration
{
bool MapCurrentThread { get; set; }
Func<EcsDocument, LogEvent, EcsDocument> MapCustom { get; set; }
bool MapExceptions { get; set; }
IHttpAdapter MapHttpAdapter { get; set; }
ISet<string> LogEventPropertiesToFilter { get;set; }
}

public class EcsTextFormatterConfiguration : IEcsTextFormatterConfiguration
public interface IEcsTextFormatterConfiguration<TEcsDocument> : IEcsTextFormatterConfiguration
where TEcsDocument : EcsDocument, new()
{
bool IEcsTextFormatterConfiguration.MapExceptions { get; set; } = true;
bool IEcsTextFormatterConfiguration.MapCurrentThread { get; set; } = true;

IHttpAdapter IEcsTextFormatterConfiguration.MapHttpAdapter { get; set; }
ISet<string> IEcsTextFormatterConfiguration.LogEventPropertiesToFilter { get; set; }

Func<EcsDocument, LogEvent, EcsDocument> 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<TEcsDocument, LogEvent, TEcsDocument> MapCustom { get; set; }
}

public EcsTextFormatterConfiguration MapCustom(Func<EcsDocument, LogEvent, EcsDocument> value) => Assign(this, value, (o, v) => o.MapCustom = v);
public class EcsTextFormatterConfiguration<TEcsDocument> : IEcsTextFormatterConfiguration<TEcsDocument>
where TEcsDocument : EcsDocument, new()
{
public bool MapCurrentThread { get; set; } = true;
public bool MapExceptions { get; set; } = true;
public IHttpAdapter MapHttpAdapter { get; set; }
public ISet<string> LogEventPropertiesToFilter { get; set; }
public Func<TEcsDocument, LogEvent, TEcsDocument> MapCustom { get; set; }
}

public EcsTextFormatterConfiguration LogEventPropertiesToFilter(ISet<string> value) => Assign(this, value, (o, v) => o.LogEventPropertiesToFilter = v);
public class EcsTextFormatterConfiguration : EcsTextFormatterConfiguration<EcsDocument>
{

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static EcsTextFormatterConfiguration Assign<TValue>(
EcsTextFormatterConfiguration self, TValue value, Action<IEcsTextFormatterConfiguration, TValue> assign
)
{
assign(self, value);
return self;
}
}
}
8 changes: 6 additions & 2 deletions src/Elastic.CommonSchema.Serilog/LogEventConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TEcsDocument>(LogEvent logEvent, IEcsTextFormatterConfiguration<TEcsDocument> configuration)
where TEcsDocument : EcsDocument, new()
{
var exceptions = logEvent.Exception != null
? new List<Exception> { logEvent.Exception }
Expand All @@ -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(),
Expand Down Expand Up @@ -90,6 +91,9 @@ public static EcsDocument ConvertToEcs(LogEvent logEvent, IEcsTextFormatterConfi
return ecsEvent;
}

public static EcsDocument ConvertToEcs(LogEvent logEvent, IEcsTextFormatterConfiguration<EcsDocument> configuration) =>
ConvertToEcs<EcsDocument>(logEvent, configuration);

private static Service GetService(LogEvent logEvent)
{
if (!logEvent.TryGetScalarPropertyValue("ElasticApmServiceName", out var serviceName))
Expand Down
20 changes: 10 additions & 10 deletions tests/Elastic.CommonSchema.Serilog.Tests/LogEventPropFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public LogEventPropFilterTests(ITestOutputHelper output) : base(output)
.Enrich.WithEnvironmentUserName()
.Enrich.WithElasticApmCorrelationInfo();

Formatter = new EcsTextFormatter(new EcsTextFormatterConfiguration()
.LogEventPropertiesToFilter(new HashSet<string>(){{ "foo" }}));
var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = new HashSet<string>() { { "foo" } } };
Formatter = new EcsTextFormatter(config);
}

private LogEvent BuildLogEvent()
Expand Down Expand Up @@ -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);
Expand All @@ -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<string>()));
var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = new HashSet<string>() };
Formatter = new EcsTextFormatter(config);

var evnt = BuildLogEvent();
logger.Write(evnt);
Expand All @@ -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<string>(StringComparer.OrdinalIgnoreCase){{ "FOO" }}));
var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = new HashSet<string>(StringComparer.OrdinalIgnoreCase){{ "FOO" }} };
Formatter = new EcsTextFormatter(config);

var evnt = BuildLogEvent();
logger.Write(evnt);
Expand All @@ -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<string>(StringComparer.Ordinal){{ "FOO" }}));
var config = new EcsTextFormatterConfiguration { LogEventPropertiesToFilter = new HashSet<string>(StringComparer.Ordinal){{ "FOO" }} };
Formatter = new EcsTextFormatter(config);

var evnt = BuildLogEvent();
logger.Write(evnt);
Expand Down
108 changes: 108 additions & 0 deletions tests/Elastic.CommonSchema.Serilog.Tests/Repro/GithubIssue167.cs
Original file line number Diff line number Diff line change
@@ -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<string, object> write) => write("contoso", Contoso);
}

public class Contoso
{
[JsonPropertyName("company_name"), DataMember(Name = "company_name")]
public string CompanyName { get; set; }
}

public class ContosoEcsTextFormatter : EcsTextFormatter<ContosoDocument>
{
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<string> ToFormattedStrings(List<LogEvent> 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<LogEvent> logEvents) =>
ToFormattedStrings(logEvents)
.Select(s => (s, EcsSerializerFactory<ContosoDocument>.Deserialize(s)))
.ToList();
}
}

0 comments on commit 605b359

Please sign in to comment.