diff --git a/sample/Sample/Program.cs b/sample/Sample/Program.cs index 03b087c..d350a91 100644 --- a/sample/Sample/Program.cs +++ b/sample/Sample/Program.cs @@ -30,6 +30,8 @@ public static void Main(string[] args) Thread.Sleep(1000); Log.Debug("Loop iteration done"); } + + Log.CloseAndFlush(); } } } diff --git a/src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs index 498d165..8f18ca5 100644 --- a/src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs @@ -51,6 +51,9 @@ public static class SeqLoggerConfigurationExtensions /// A soft limit for the number of bytes to use for storing failed requests. /// The limit is soft in that it can be exceeded by any single error payload, but in that case only that single error /// payload will be retained. + /// Use the compact log event format defined by + /// Serilog.Formatting.Compact. Has no effect on + /// durable log shipping. Requires Seq 3.3+. /// Logger configuration, allowing configuration to continue. /// A required parameter is null. public static LoggerConfiguration Seq( @@ -65,7 +68,8 @@ public static LoggerConfiguration Seq( long? eventBodyLimitBytes = 256 * 1024, LoggingLevelSwitch controlLevelSwitch = null, HttpMessageHandler messageHandler = null, - long? retainedInvalidPayloadsLimitBytes = null) + long? retainedInvalidPayloadsLimitBytes = null, + bool useCompactFormat = false) { if (loggerSinkConfiguration == null) throw new ArgumentNullException(nameof(loggerSinkConfiguration)); if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl)); @@ -84,7 +88,8 @@ public static LoggerConfiguration Seq( defaultedPeriod, eventBodyLimitBytes, controlLevelSwitch, - messageHandler); + messageHandler, + useCompactFormat); } else { diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/HttpLogShipper.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/HttpLogShipper.cs index 1939b63..9870eea 100644 --- a/src/Serilog.Sinks.Seq/Sinks/Seq/HttpLogShipper.cs +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/HttpLogShipper.cs @@ -33,9 +33,6 @@ namespace Serilog.Sinks.Seq { class HttpLogShipper : IDisposable { - const string ApiKeyHeaderName = "X-Seq-ApiKey"; - const string BulkUploadResource = "api/events/raw"; - static readonly TimeSpan RequiredLevelCheckInterval = TimeSpan.FromMinutes(2); readonly string _apiKey; @@ -206,9 +203,9 @@ void OnTick() var content = new StringContent(payload, Encoding.UTF8, "application/json"); if (!string.IsNullOrWhiteSpace(_apiKey)) - content.Headers.Add(ApiKeyHeaderName, _apiKey); + content.Headers.Add(SeqApi.ApiKeyHeaderName, _apiKey); - var result = _httpClient.PostAsync(BulkUploadResource, content).Result; + var result = _httpClient.PostAsync(SeqApi.BulkUploadResource, content).Result; if (result.IsSuccessStatusCode) { _connectionSchedule.MarkSuccess(); diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/SeqApi.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/SeqApi.cs index 48ab5f7..7b18f76 100644 --- a/src/Serilog.Sinks.Seq/Sinks/Seq/SeqApi.cs +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/SeqApi.cs @@ -19,6 +19,11 @@ namespace Serilog.Sinks.Seq { class SeqApi { + public const string BulkUploadResource = "api/events/raw"; + public const string ApiKeyHeaderName = "X-Seq-ApiKey"; + public const string RawEventFormatMimeType = "application/json"; + public const string CompactLogEventFormatMimeType = "application/vnd.serilog.clef"; + // Why not use a JSON parser here? For a very small case, it's not // worth taking on the extra payload/dependency management issues that // a full-fledged parser will entail. If things get more sophisticated diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/SeqSink.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/SeqSink.cs index f4009ba..35eb85c 100644 --- a/src/Serilog.Sinks.Seq/Sinks/Seq/SeqSink.cs +++ b/src/Serilog.Sinks.Seq/Sinks/Seq/SeqSink.cs @@ -22,6 +22,8 @@ using Serilog.Core; using Serilog.Debugging; using Serilog.Events; +using Serilog.Formatting.Compact; +using Serilog.Formatting.Json; using Serilog.Sinks.PeriodicBatching; namespace Serilog.Sinks.Seq @@ -31,12 +33,13 @@ class SeqSink : PeriodicBatchingSink readonly string _apiKey; readonly long? _eventBodyLimitBytes; readonly HttpClient _httpClient; - const string BulkUploadResource = "api/events/raw"; - const string ApiKeyHeaderName = "X-Seq-ApiKey"; + + static readonly JsonValueFormatter JsonValueFormatter = new JsonValueFormatter(); // If non-null, then background level checks will be performed; set either through the constructor // or in response to a level specification from the server. Never set to null after being made non-null. LoggingLevelSwitch _levelControlSwitch; + readonly bool _useCompactFormat; static readonly TimeSpan RequiredLevelCheckInterval = TimeSpan.FromMinutes(2); DateTime _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval); @@ -50,13 +53,15 @@ public SeqSink( TimeSpan period, long? eventBodyLimitBytes, LoggingLevelSwitch levelControlSwitch, - HttpMessageHandler messageHandler) + HttpMessageHandler messageHandler, + bool useCompactFormat) : base(batchPostingLimit, period) { if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl)); _apiKey = apiKey; _eventBodyLimitBytes = eventBodyLimitBytes; _levelControlSwitch = levelControlSwitch; + _useCompactFormat = useCompactFormat; var baseUri = serverUrl; if (!baseUri.EndsWith("/")) @@ -89,13 +94,23 @@ protected override async Task EmitBatchAsync(IEnumerable events) { _nextRequiredLevelCheckUtc = DateTime.UtcNow.Add(RequiredLevelCheckInterval); - var payload = FormatPayload(events, _eventBodyLimitBytes); + string payload, payloadContentType; + if (_useCompactFormat) + { + payloadContentType = SeqApi.CompactLogEventFormatMimeType; + payload = FormatCompactPayload(events, _eventBodyLimitBytes); + } + else + { + payloadContentType = SeqApi.RawEventFormatMimeType; + payload = FormatRawPayload(events, _eventBodyLimitBytes); + } - var content = new StringContent(payload, Encoding.UTF8, "application/json"); + var content = new StringContent(payload, Encoding.UTF8, payloadContentType); if (!string.IsNullOrWhiteSpace(_apiKey)) - content.Headers.Add(ApiKeyHeaderName, _apiKey); + content.Headers.Add(SeqApi.ApiKeyHeaderName, _apiKey); - var result = await _httpClient.PostAsync(BulkUploadResource, content).ConfigureAwait(false); + var result = await _httpClient.PostAsync(SeqApi.BulkUploadResource, content).ConfigureAwait(false); if (!result.IsSuccessStatusCode) throw new LoggingFailedException($"Received failed result {result.StatusCode} when posting events to Seq"); @@ -115,39 +130,56 @@ protected override async Task EmitBatchAsync(IEnumerable events) } } - internal static string FormatPayload(IEnumerable events, long? eventBodyLimitBytes) + internal static string FormatCompactPayload(IEnumerable events, long? eventBodyLimitBytes) { var payload = new StringWriter(); - payload.Write("{\"Events\":["); - var delimStart = ""; foreach (var logEvent in events) { var buffer = new StringWriter(); try { - RawJsonFormatter.FormatContent(logEvent, buffer); + CompactJsonFormatter.FormatEvent(logEvent, buffer, JsonValueFormatter); } catch (Exception ex) { - SelfLog.WriteLine( - "Event at {0} with message template {1} could not be formatted into JSON for Seq and will be dropped: {2}", - logEvent.Timestamp.ToString("o"), logEvent.MessageTemplate.Text, ex); - + LogNonFormattableEvent(logEvent, ex); continue; } var json = buffer.ToString(); + if (CheckEventBodySize(json, eventBodyLimitBytes)) + { + payload.WriteLine(json); + } + } + + return payload.ToString(); + } - if (eventBodyLimitBytes.HasValue && - Encoding.UTF8.GetByteCount(json) > eventBodyLimitBytes.Value) + internal static string FormatRawPayload(IEnumerable events, long? eventBodyLimitBytes) + { + var payload = new StringWriter(); + payload.Write("{\"Events\":["); + + var delimStart = ""; + foreach (var logEvent in events) + { + var buffer = new StringWriter(); + + try { - SelfLog.WriteLine( - "Event JSON representation exceeds the byte size limit of {0} set for this Seq sink and will be dropped; data: {1}", - eventBodyLimitBytes, json); + RawJsonFormatter.FormatContent(logEvent, buffer); } - else + catch (Exception ex) + { + LogNonFormattableEvent(logEvent, ex); + continue; + } + + var json = buffer.ToString(); + if (CheckEventBodySize(json, eventBodyLimitBytes)) { payload.Write(delimStart); payload.Write(json); @@ -165,5 +197,26 @@ protected override bool CanInclude(LogEvent evt) return levelControlSwitch == null || (int)levelControlSwitch.MinimumLevel <= (int)evt.Level; } + + static bool CheckEventBodySize(string json, long? eventBodyLimitBytes) + { + if (eventBodyLimitBytes.HasValue && + Encoding.UTF8.GetByteCount(json) > eventBodyLimitBytes.Value) + { + SelfLog.WriteLine( + "Event JSON representation exceeds the byte size limit of {0} set for this Seq sink and will be dropped; data: {1}", + eventBodyLimitBytes, json); + return false; + } + + return true; + } + + static void LogNonFormattableEvent(LogEvent logEvent, Exception ex) + { + SelfLog.WriteLine( + "Event at {0} with message template {1} could not be formatted into JSON for Seq and will be dropped: {2}", + logEvent.Timestamp.ToString("o"), logEvent.MessageTemplate.Text, ex); + } } } diff --git a/src/Serilog.Sinks.Seq/project.json b/src/Serilog.Sinks.Seq/project.json index aadf644..db9b4d8 100644 --- a/src/Serilog.Sinks.Seq/project.json +++ b/src/Serilog.Sinks.Seq/project.json @@ -1,5 +1,5 @@ { - "version": "2.0.1-*", + "version": "3.0.0-*", "description": "Serilog sink that writes to the Seq event server over HTTP/S.", "authors": [ "Serilog Contributors" ], "packOptions": { @@ -10,7 +10,8 @@ }, "dependencies": { "Serilog": "2.0.0", - "Serilog.Sinks.PeriodicBatching": "2.0.0" + "Serilog.Sinks.PeriodicBatching": "2.0.0", + "Serilog.Formatting.Compact": "1.0.0" }, "buildOptions": { "keyFile": "../../assets/Serilog.snk" diff --git a/test/Serilog.Sinks.Seq.Tests/SeqSinkTests.cs b/test/Serilog.Sinks.Seq.Tests/SeqSinkTests.cs index 355ed8c..e663a89 100644 --- a/test/Serilog.Sinks.Seq.Tests/SeqSinkTests.cs +++ b/test/Serilog.Sinks.Seq.Tests/SeqSinkTests.cs @@ -9,7 +9,7 @@ public class SeqSinkTests public void EventsAreFormattedIntoJsonPayloads() { var evt = Some.LogEvent("Hello, {Name}!", "Alice"); - var json = SeqSink.FormatPayload(new[] {evt}, null); + var json = SeqSink.FormatRawPayload(new[] {evt}, null); Assert.Contains("Name\":\"Alice", json); } @@ -17,8 +17,24 @@ public void EventsAreFormattedIntoJsonPayloads() public void EventsAreDroppedWhenJsonRenderingFails() { var evt = Some.LogEvent(new NastyException(), "Hello, {Name}!", "Alice"); - var json = SeqSink.FormatPayload(new[] { evt }, null); + var json = SeqSink.FormatRawPayload(new[] { evt }, null); Assert.Contains("[]", json); } + + [Fact] + public void EventsAreFormattedIntoCompactJsonPayloads() + { + var evt = Some.LogEvent("Hello, {Name}!", "Alice"); + var json = SeqSink.FormatCompactPayload(new[] { evt }, null); + Assert.Contains("Name\":\"Alice", json); + } + + [Fact] + public void EventsAreDroppedWhenCompactJsonRenderingFails() + { + var evt = Some.LogEvent(new NastyException(), "Hello, {Name}!", "Alice"); + var json = SeqSink.FormatCompactPayload(new[] { evt }, null); + Assert.Empty(json); + } } }