From abfb1c69c034d04890dacac5e0bda91f424525d0 Mon Sep 17 00:00:00 2001 From: "Mattias Kindborg @FantasticFiasco" Date: Sun, 12 Mar 2017 23:27:51 +0100 Subject: [PATCH] feat(formatting): add 4 different JSON formatting types Add support for the formatting types: - FormattingType.NormalRendered - FormattingType.Normal - FormattingType.CompactRendered - FormattingType.Compact The formatting type can be configured via Options and DurableOptions. --- CHANGELOG.md | 4 + README.md | 94 ++++++- .../LoggerSinkConfigurationExtensions.cs | 3 +- .../Sinks/Http/FormattingType.cs | 53 ++++ src/Serilog.Sinks.Http/Sinks/Http/Options.cs | 6 + .../Formatters/CompactJsonFormatter.cs | 134 ++++++++++ .../Http/Private/Formatters/Converter.cs | 43 +++ .../NormalJsonFormatter.cs} | 28 +- .../Private/{ => Http}/HttpClientWrapper.cs | 2 +- .../Http/Private/{ => Http}/HttpLogShipper.cs | 5 +- .../Private/{ => Sinks}/DurableHttpSink.cs | 6 +- .../Http/Private/{ => Sinks}/HttpSink.cs | 5 +- .../ExponentialBackoffConnectionSchedule.cs | 2 +- .../Http/Private/{ => Time}/PortableTimer.cs | 2 +- .../Controllers/BatchesController.cs | 1 + .../Controllers/Dtos/CompactEventDto.cs | 26 ++ .../{ => Dtos}/EventBatchRequestDto.cs | 2 +- .../Controllers/{ => Dtos}/EventDto.cs | 8 +- .../Controllers/Dtos/RenderingDto.cs | 26 ++ .../Controllers/EventsController.cs | 1 + .../Controllers/PayloadConvert.cs | 6 +- .../Event.cs | 4 + .../project.json | 17 +- .../ApiModels/ApiModel.cs | 1 + .../HttpSinkTest.cs | 2 +- .../SinkFixture.cs | 4 +- .../Formatters/CompactJsonFormatterTest.cs | 233 +++++++++++++++++ .../Http/Private/Formatters/ConverterTest.cs | 21 ++ .../Formatters/NormalJsonFormatterTest.cs | 246 ++++++++++++++++++ .../Http/Private/HttpJsonFormatterTest.cs | 90 ------- test/Serilog.Sinks.Http.Tests/project.json | 3 +- 31 files changed, 957 insertions(+), 121 deletions(-) create mode 100644 src/Serilog.Sinks.Http/Sinks/Http/FormattingType.cs create mode 100644 src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/CompactJsonFormatter.cs create mode 100644 src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/Converter.cs rename src/Serilog.Sinks.Http/Sinks/Http/Private/{HttpJsonFormatter.cs => Formatters/NormalJsonFormatter.cs} (83%) rename src/Serilog.Sinks.Http/Sinks/Http/Private/{ => Http}/HttpClientWrapper.cs (96%) rename src/Serilog.Sinks.Http/Sinks/Http/Private/{ => Http}/HttpLogShipper.cs (99%) rename src/Serilog.Sinks.Http/Sinks/Http/Private/{ => Sinks}/DurableHttpSink.cs (90%) rename src/Serilog.Sinks.Http/Sinks/Http/Private/{ => Sinks}/HttpSink.cs (95%) rename src/Serilog.Sinks.Http/Sinks/Http/Private/{ => Time}/ExponentialBackoffConnectionSchedule.cs (98%) rename src/Serilog.Sinks.Http/Sinks/Http/Private/{ => Time}/PortableTimer.cs (98%) create mode 100644 test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/CompactEventDto.cs rename test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/{ => Dtos}/EventBatchRequestDto.cs (97%) rename test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/{ => Dtos}/EventDto.cs (75%) create mode 100644 test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/RenderingDto.cs create mode 100644 test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/CompactJsonFormatterTest.cs create mode 100644 test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/ConverterTest.cs create mode 100644 test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/NormalJsonFormatterTest.cs delete mode 100644 test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/HttpJsonFormatterTest.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index cbc20659..7ee3c49b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](http://semver.org/) and is followi ## Unreleased +### Added + +- Support for the formatting types: `FormattingType.NormalRendered`, `FormattingType.Normal`, `FormattingType.CompactRendered` and `FormattingType.Compact`. The formatting type can be configured via `Options` and `DurableOptions`. + ## 3.0.0 2017-03-04 ### Added diff --git a/README.md b/README.md index 95dd38a2..1ff8706f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The sink is batching multiple log events into a single request, and the followin "events": [ { "Timestamp": "2016-11-03T00:09:11.4899425+01:00", - "Level": "Debug", + "Level": "Information", "MessageTemplate": "Logging {@Heartbeat} from {Computer}", "RenderedMessage": "Logging { UserName: \"Mike\", UserDomainName: \"Home\" } from \"Workstation\"", "Properties": { @@ -38,7 +38,7 @@ The sink is batching multiple log events into a single request, and the followin }, { "Timestamp": "2016-11-03T00:09:12.4905685+01:00", - "Level": "Debug", + "Level": "Information", "MessageTemplate": "Logging {@Heartbeat} from {Computer}", "RenderedMessage": "Logging { UserName: \"Mike\", UserDomainName: \"Home\" } from \"Workstation\"", "Properties": { @@ -71,6 +71,96 @@ The log events can be sent directly to Elasticsearch using [Serilog.Sinks.Elasti If you would like to send the log events to Logstash for further processing instead of sending them directly to Elasticsearch, this sink in combination with the [Logstash HTTP input plugin](https://www.elastic.co/blog/introducing-logstash-input-http-plugin) is the perfect match for you. It is a much better solution than having to install [Filebeat](https://www.elastic.co/products/beats/filebeat) on all your instances, mainly because it involves fewer moving parts. +### Formatting types + +#### FormattingType.NormalRendered + +The log event is normally formatted and the message template is rendered into a message. This is the most verbose formatting type and its network load is higher than the other options. + +Example: +```json +{ + "Timestamp": "2016-11-03T00:09:11.4899425+01:00", + "Level": "Information", + "MessageTemplate": "Logging {@Heartbeat} from {Computer}", + "RenderedMessage": "Logging { UserName: \"Mike\", UserDomainName: \"Home\" } from \"Workstation\"", + "Properties": { + "Heartbeat": { + "UserName": "Mike", + "UserDomainName": "Home" + }, + "Computer": "Workstation" + } +} +``` + +#### FormattingType.Normal + + The log event is normally formatted and its data normalized. The lack of a rendered message means improved network load compared to `FormattingType.NormalRendered`. Often this formatting type is complemented with a log server that is capable of rendering the messages of the incoming log events. + +Example: +```json +{ + "Timestamp": "2016-11-03T00:09:11.4899425+01:00", + "Level": "Information", + "MessageTemplate": "Logging {@Heartbeat} from {Computer}", + "Properties": { + "Heartbeat": { + "UserName": "Mike", + "UserDomainName": "Home" + }, + "Computer": "Workstation" + } +} +``` + +#### FormattingType.CompactRendered + +The log event is formatted with minimizing size as a priority but still render the message template into a message. This formatting type greatly reduce the network load and should be used in situations where bandwidth is of importance. + +The compact formatter adheres to the following rules: + +- Built-in field names are short and prefixed with an `@` +- The `Properties` property is flattened +- The Information level is omitted since it is considered to be the default + +Example: +```json +{ + "@t": "2016-11-03T00:09:11.4899425+01:00", + "@mt": "Logging {@Heartbeat} from {Computer}", + "@m":"Logging { UserName: \"Mike\", UserDomainName: \"Home\" } from \"Workstation\"", + "Heartbeat": { + "UserName": "Mike", + "UserDomainName": "Home" + }, + "Computer": "Workstation" +} +``` + +#### FormattingType.Compact + +The log event is formatted with minimizing size as a priority and its data is normalized. The lack of a rendered message means even smaller network load compared to `FormattingType.CompactRendered` and should be used in situations where bandwidth is of importance. Often this formatting type is complemented with a log server that is capable of rendering the messages of the incoming log events. + +The compact formatter adheres to the following rules: + +- Built-in field names are short and prefixed with an `@` +- The `Properties` property is flattened +- The Information level is omitted since it is considered to be the default + +Example: +```json +{ + "@t": "2016-11-03T00:09:11.4899425+01:00", + "@mt": "Logging {@Heartbeat} from {Computer}", + "Heartbeat": { + "UserName": "Mike", + "UserDomainName": "Home" + }, + "Computer": "Workstation" +} +``` + ### Install via NuGet If you want to include the HTTP POST sink in your project, you can [install it directly from NuGet](https://www.nuget.org/packages/Serilog.Sinks.Http/). diff --git a/src/Serilog.Sinks.Http/LoggerSinkConfigurationExtensions.cs b/src/Serilog.Sinks.Http/LoggerSinkConfigurationExtensions.cs index b9b32a1e..eb2532c4 100644 --- a/src/Serilog.Sinks.Http/LoggerSinkConfigurationExtensions.cs +++ b/src/Serilog.Sinks.Http/LoggerSinkConfigurationExtensions.cs @@ -17,7 +17,8 @@ using Serilog.Configuration; using Serilog.Events; using Serilog.Sinks.Http; -using Serilog.Sinks.Http.Private; +using Serilog.Sinks.Http.Private.Http; +using Serilog.Sinks.Http.Private.Sinks; namespace Serilog { diff --git a/src/Serilog.Sinks.Http/Sinks/Http/FormattingType.cs b/src/Serilog.Sinks.Http/Sinks/Http/FormattingType.cs new file mode 100644 index 00000000..3e671a51 --- /dev/null +++ b/src/Serilog.Sinks.Http/Sinks/Http/FormattingType.cs @@ -0,0 +1,53 @@ +// Copyright 2015-2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Serilog.Sinks.Http +{ + /// + /// Enum defining how log events are formatted when sent over the network. + /// + public enum FormattingType + { + /// + /// The log event is normally formatted and the message template is rendered into a message. + /// This is the most verbose formatting type and its network load is higher than the other + /// options. + /// + NormalRendered, + + /// + /// The log event is normally formatted and its data normalized. The lack of a rendered message + /// means improved network load compared to . Often this formatting + /// type is complemented with a log server that is capable of rendering the messages of the + /// incoming log events. + /// + Normal, + + /// + /// The log event is formatted with minimizing size as a priority but still render the message + /// template into a message. This formatting type greatly reduce the network load and should be + /// used in situations where bandwidth is of importance. + /// + CompactRendered, + + /// + /// The log event is formatted with minimizing size as a priority and its data is normalized. The + /// lack of a rendered message means even smaller network load compared to + /// and should be used in situations where bandwidth is of + /// importance. Often this formatting type is complemented with a log server that is capable of + /// rendering the messages of the incoming log events. + /// + Compact + } +} diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Options.cs b/src/Serilog.Sinks.Http/Sinks/Http/Options.cs index cfb8cd05..b1726a10 100644 --- a/src/Serilog.Sinks.Http/Sinks/Http/Options.cs +++ b/src/Serilog.Sinks.Http/Sinks/Http/Options.cs @@ -39,5 +39,11 @@ public class Options /// Default value is 265 KB. /// public long? EventBodyLimitBytes { get; set; } = 256 * 1024; + + /// + /// Gets or sets the formatting type. Default value is + /// . + /// + public FormattingType FormattingType { get; set; } = FormattingType.NormalRendered; } } diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/CompactJsonFormatter.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/CompactJsonFormatter.cs new file mode 100644 index 00000000..8405e8b3 --- /dev/null +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/CompactJsonFormatter.cs @@ -0,0 +1,134 @@ +// Copyright 2015-2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.IO; +using System.Linq; +using Serilog.Debugging; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Formatting.Json; +using Serilog.Parsing; + +namespace Serilog.Sinks.Http.Private.Formatters +{ + internal class CompactJsonFormatter : ITextFormatter + { + private static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter(); + + private readonly bool isRenderingMessage; + + public CompactJsonFormatter(bool isRenderingMessage) + { + this.isRenderingMessage = isRenderingMessage; + } + + public void Format(LogEvent logEvent, TextWriter output) + { + try + { + var buffer = new StringWriter(); + FormatContent(logEvent, buffer); + + // If formatting was successful, write to output + output.WriteLine(buffer.ToString()); + } + catch (Exception e) + { + LogNonFormattableEvent(logEvent, e); + } + } + + private void FormatContent(LogEvent logEvent, TextWriter output) + { + if (logEvent == null) + throw new ArgumentNullException(nameof(logEvent)); + if (output == null) + throw new ArgumentNullException(nameof(output)); + + output.Write("{\"@t\":\""); + output.Write(logEvent.Timestamp.UtcDateTime.ToString("o")); + + output.Write("\",\"@mt\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output); + + if (isRenderingMessage) + { + output.Write(",\"@m\":"); + var message = logEvent.MessageTemplate.Render(logEvent.Properties); + JsonValueFormatter.WriteQuotedJsonString(message, output); + } + + var tokensWithFormat = logEvent.MessageTemplate.Tokens + .OfType() + .Where(pt => pt.Format != null); + + // Better not to allocate an array in the 99.9% of cases where this is false + // ReSharper disable once PossibleMultipleEnumeration + if (tokensWithFormat.Any()) + { + output.Write(",\"@r\":["); + var delim = ""; + foreach (var r in tokensWithFormat) + { + output.Write(delim); + delim = ","; + var space = new StringWriter(); + r.Render(logEvent.Properties, space); + JsonValueFormatter.WriteQuotedJsonString(space.ToString(), output); + } + output.Write(']'); + } + + if (logEvent.Level != LogEventLevel.Information) + { + output.Write(",\"@l\":\""); + output.Write(logEvent.Level); + output.Write('\"'); + } + + if (logEvent.Exception != null) + { + output.Write(",\"@x\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.Exception.ToString(), output); + } + + foreach (var property in logEvent.Properties) + { + var name = property.Key; + if (name.Length > 0 && name[0] == '@') + { + // Escape first '@' by doubling + name = '@' + name; + } + + output.Write(','); + JsonValueFormatter.WriteQuotedJsonString(name, output); + output.Write(':'); + ValueFormatter.Format(property.Value, output); + } + + output.Write('}'); + } + + private static void LogNonFormattableEvent(LogEvent logEvent, Exception e) + { + SelfLog.WriteLine( + "Event at {0} with message template {1} could not be formatted into JSON and will be dropped: {2}", + logEvent.Timestamp.ToString("o"), + logEvent.MessageTemplate.Text, + e); + } + } +} diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/Converter.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/Converter.cs new file mode 100644 index 00000000..b6e4f21a --- /dev/null +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/Converter.cs @@ -0,0 +1,43 @@ +// Copyright 2015-2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using Serilog.Formatting; + +namespace Serilog.Sinks.Http.Private.Formatters +{ + internal static class Converter + { + public static ITextFormatter ToFormatter(FormattingType formattingType) + { + switch (formattingType) + { + case FormattingType.NormalRendered: + return new NormalJsonFormatter(true); + + case FormattingType.Normal: + return new NormalJsonFormatter(false); + + case FormattingType.CompactRendered: + return new CompactJsonFormatter(true); + + case FormattingType.Compact: + return new CompactJsonFormatter(false); + + default: + throw new ArgumentException($"Formatting type {formattingType} is not supported"); + } + } + } +} diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/HttpJsonFormatter.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/NormalJsonFormatter.cs similarity index 83% rename from src/Serilog.Sinks.Http/Sinks/Http/Private/HttpJsonFormatter.cs rename to src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/NormalJsonFormatter.cs index 9ace5a01..363a3b2e 100644 --- a/src/Serilog.Sinks.Http/Sinks/Http/Private/HttpJsonFormatter.cs +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Formatters/NormalJsonFormatter.cs @@ -22,12 +22,20 @@ using Serilog.Formatting.Json; using Serilog.Parsing; -namespace Serilog.Sinks.Http.Private +namespace Serilog.Sinks.Http.Private.Formatters { - internal class HttpJsonFormatter : ITextFormatter + internal class NormalJsonFormatter : ITextFormatter { private static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter(); + + private readonly bool isRenderingMessage; + + public NormalJsonFormatter(bool isRenderingMessage) + { + this.isRenderingMessage = isRenderingMessage; + } + public void Format(LogEvent logEvent, TextWriter output) { try @@ -44,7 +52,7 @@ public void Format(LogEvent logEvent, TextWriter output) } } - private static void FormatContent(LogEvent logEvent, TextWriter output) + private void FormatContent(LogEvent logEvent, TextWriter output) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); @@ -60,6 +68,14 @@ private static void FormatContent(LogEvent logEvent, TextWriter output) output.Write("\",\"MessageTemplate\":"); JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output); + if (isRenderingMessage) + { + output.Write(",\"RenderedMessage\":"); + + var message = logEvent.MessageTemplate.Render(logEvent.Properties); + JsonValueFormatter.WriteQuotedJsonString(message, output); + } + if (logEvent.Exception != null) { output.Write(",\"Exception\":"); @@ -71,13 +87,15 @@ private static void FormatContent(LogEvent logEvent, TextWriter output) WriteProperties(logEvent.Properties, output); } + // Better not to allocate an array in the 99.9% of cases where this is false var tokensWithFormat = logEvent.MessageTemplate.Tokens .OfType() - .Where(pt => pt.Format != null) - .ToArray(); + .Where(pt => pt.Format != null); + // ReSharper disable once PossibleMultipleEnumeration if (tokensWithFormat.Any()) { + // ReSharper disable once PossibleMultipleEnumeration WriteRenderings(tokensWithFormat.GroupBy(pt => pt.PropertyName), logEvent.Properties, output); } diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/HttpClientWrapper.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Http/HttpClientWrapper.cs similarity index 96% rename from src/Serilog.Sinks.Http/Sinks/Http/Private/HttpClientWrapper.cs rename to src/Serilog.Sinks.Http/Sinks/Http/Private/Http/HttpClientWrapper.cs index 58928ec7..2408486f 100644 --- a/src/Serilog.Sinks.Http/Sinks/Http/Private/HttpClientWrapper.cs +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Http/HttpClientWrapper.cs @@ -15,7 +15,7 @@ using System.Net.Http; using System.Threading.Tasks; -namespace Serilog.Sinks.Http.Private +namespace Serilog.Sinks.Http.Private.Http { internal class HttpClientWrapper : IHttpClient { diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/HttpLogShipper.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Http/HttpLogShipper.cs similarity index 99% rename from src/Serilog.Sinks.Http/Sinks/Http/Private/HttpLogShipper.cs rename to src/Serilog.Sinks.Http/Sinks/Http/Private/Http/HttpLogShipper.cs index ef990734..a34a5013 100644 --- a/src/Serilog.Sinks.Http/Sinks/Http/Private/HttpLogShipper.cs +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Http/HttpLogShipper.cs @@ -17,14 +17,15 @@ using System.Linq; using System.Net.Http; using System.Text; +using System.Threading.Tasks; using Serilog.Debugging; +using Serilog.Sinks.Http.Private.Time; using IOFile = System.IO.File; -using System.Threading.Tasks; #if HRESULTS using System.Runtime.InteropServices; #endif -namespace Serilog.Sinks.Http.Private +namespace Serilog.Sinks.Http.Private.Http { internal class HttpLogShipper : IDisposable { diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/DurableHttpSink.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Sinks/DurableHttpSink.cs similarity index 90% rename from src/Serilog.Sinks.Http/Sinks/Http/Private/DurableHttpSink.cs rename to src/Serilog.Sinks.Http/Sinks/Http/Private/Sinks/DurableHttpSink.cs index bb3aa602..fe43e68a 100644 --- a/src/Serilog.Sinks.Http/Sinks/Http/Private/DurableHttpSink.cs +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Sinks/DurableHttpSink.cs @@ -16,9 +16,11 @@ using System.Text; using Serilog.Core; using Serilog.Events; +using Serilog.Sinks.Http.Private.Formatters; +using Serilog.Sinks.Http.Private.Http; using Serilog.Sinks.RollingFile; -namespace Serilog.Sinks.Http.Private +namespace Serilog.Sinks.Http.Private.Sinks { internal class DurableHttpSink : ILogEventSink, IDisposable { @@ -43,7 +45,7 @@ public DurableHttpSink( sink = new RollingFileSink( options.BufferBaseFilename + "-{Date}.json", - new HttpJsonFormatter(), + Converter.ToFormatter(options.FormattingType), options.BufferFileSizeLimitBytes, null, Encoding.UTF8); diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/HttpSink.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Sinks/HttpSink.cs similarity index 95% rename from src/Serilog.Sinks.Http/Sinks/Http/Private/HttpSink.cs rename to src/Serilog.Sinks.Http/Sinks/Http/Private/Sinks/HttpSink.cs index 651134d4..d09b7fa6 100644 --- a/src/Serilog.Sinks.Http/Sinks/Http/Private/HttpSink.cs +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Sinks/HttpSink.cs @@ -21,9 +21,10 @@ using Serilog.Debugging; using Serilog.Events; using Serilog.Formatting; +using Serilog.Sinks.Http.Private.Formatters; using Serilog.Sinks.PeriodicBatching; -namespace Serilog.Sinks.Http.Private +namespace Serilog.Sinks.Http.Private.Sinks { internal class HttpSink : PeriodicBatchingSink { @@ -52,7 +53,7 @@ public HttpSink( this.requestUri = requestUri; this.options = options; - formatter = new HttpJsonFormatter(); + formatter = Converter.ToFormatter(options.FormattingType); } protected override async Task EmitBatchAsync(IEnumerable events) diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/ExponentialBackoffConnectionSchedule.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Time/ExponentialBackoffConnectionSchedule.cs similarity index 98% rename from src/Serilog.Sinks.Http/Sinks/Http/Private/ExponentialBackoffConnectionSchedule.cs rename to src/Serilog.Sinks.Http/Sinks/Http/Private/Time/ExponentialBackoffConnectionSchedule.cs index 7083d898..fc0e5ba1 100644 --- a/src/Serilog.Sinks.Http/Sinks/Http/Private/ExponentialBackoffConnectionSchedule.cs +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Time/ExponentialBackoffConnectionSchedule.cs @@ -14,7 +14,7 @@ using System; -namespace Serilog.Sinks.Http.Private +namespace Serilog.Sinks.Http.Private.Time { internal class ExponentialBackoffConnectionSchedule { diff --git a/src/Serilog.Sinks.Http/Sinks/Http/Private/PortableTimer.cs b/src/Serilog.Sinks.Http/Sinks/Http/Private/Time/PortableTimer.cs similarity index 98% rename from src/Serilog.Sinks.Http/Sinks/Http/Private/PortableTimer.cs rename to src/Serilog.Sinks.Http/Sinks/Http/Private/Time/PortableTimer.cs index 98bfb320..70c9890b 100644 --- a/src/Serilog.Sinks.Http/Sinks/Http/Private/PortableTimer.cs +++ b/src/Serilog.Sinks.Http/Sinks/Http/Private/Time/PortableTimer.cs @@ -16,7 +16,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Serilog.Sinks.Http.Private +namespace Serilog.Sinks.Http.Private.Time { internal class PortableTimer : IDisposable { diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/BatchesController.cs b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/BatchesController.cs index c93c1be8..dcbf39fb 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/BatchesController.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/BatchesController.cs @@ -1,5 +1,6 @@ using System.Linq; using Microsoft.AspNetCore.Mvc; +using Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos; namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers { diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/CompactEventDto.cs b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/CompactEventDto.cs new file mode 100644 index 00000000..227f9d5d --- /dev/null +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/CompactEventDto.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; + +namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos +{ + public class CompactEventDto + { + [JsonProperty("@t")] + public DateTime Timestamp { get; set; } + + [JsonProperty("@l")] + public string Level { get; set; } + + [JsonProperty("@mt")] + public string MessageTemplate { get; set; } + + [JsonProperty("@m")] + public string RenderedMessage { get; set; } + + [JsonProperty("@x")] + public string Exception { get; set; } + + [JsonProperty("@r")] + public string[] Renderings { get; set; } + } +} diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventBatchRequestDto.cs b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/EventBatchRequestDto.cs similarity index 97% rename from test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventBatchRequestDto.cs rename to test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/EventBatchRequestDto.cs index 730432a5..b53e0511 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventBatchRequestDto.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/EventBatchRequestDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers +namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos { public class EventBatchRequestDto { diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventDto.cs b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/EventDto.cs similarity index 75% rename from test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventDto.cs rename to test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/EventDto.cs index 6cd0c2be..54bf354f 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventDto.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/EventDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers +namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos { public class EventDto { @@ -11,8 +11,12 @@ public class EventDto public string MessageTemplate { get; set; } - public Dictionary Properties { get; set; } + public string RenderedMessage { get; set; } public string Exception { get; set; } + + public Dictionary Properties { get; set; } + + public Dictionary Renderings { get; set; } } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/RenderingDto.cs b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/RenderingDto.cs new file mode 100644 index 00000000..2e34e3c1 --- /dev/null +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/Dtos/RenderingDto.cs @@ -0,0 +1,26 @@ +namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos +{ + public class RenderingDto + { + public string Format { get; set; } + + public string Rendering { get; set; } + + public override bool Equals(object obj) + { + var other = obj as RenderingDto; + + if (other == null) + return false; + + return + Format == other.Format && + Rendering == other.Rendering; + } + + public override int GetHashCode() + { + return 0; + } + } +} diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventsController.cs b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventsController.cs index ee8a412f..f9f3ac4f 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventsController.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/EventsController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; +using Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos; namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers { diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/PayloadConvert.cs b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/PayloadConvert.cs index 06ee7818..5d6431a9 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/PayloadConvert.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/Controllers/PayloadConvert.cs @@ -1,4 +1,6 @@ -namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers +using Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos; + +namespace Serilog.Sinks.Http.IntegrationTests.Server.Controllers { public static class PayloadConvert { @@ -9,6 +11,7 @@ public static Event FromDto(EventDto @event) @event.Level, @event.MessageTemplate, @event.Properties, + @event.RenderedMessage, @event.Exception); } @@ -20,6 +23,7 @@ public static EventDto ToDto(Event @event) Level = @event.Level, MessageTemplate = @event.MessageTemplate, Properties = @event.Properties, + RenderedMessage = @event.RenderedMessage, Exception = @event.Exception }; } diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/Event.cs b/test/Serilog.Sinks.Http.IntegrationTests.Server/Event.cs index d0aa81e3..e6b5cb75 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests.Server/Event.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/Event.cs @@ -10,12 +10,14 @@ public Event( string level, string messageTemplate, Dictionary properties, + string renderedMessage, string exception) { Timestamp = timestamp; Level = level; MessageTemplate = messageTemplate; Properties = properties; + RenderedMessage = renderedMessage; Exception = exception; } @@ -25,6 +27,8 @@ public Event( public string MessageTemplate { get; } + public string RenderedMessage { get; set; } + public Dictionary Properties { get; } public string Exception { get; set; } diff --git a/test/Serilog.Sinks.Http.IntegrationTests.Server/project.json b/test/Serilog.Sinks.Http.IntegrationTests.Server/project.json index e5eab286..9178f916 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests.Server/project.json +++ b/test/Serilog.Sinks.Http.IntegrationTests.Server/project.json @@ -1,12 +1,15 @@ { "version": "1.0.0-*", - "dependencies": { - "NETStandard.Library": "1.6.0", - "Microsoft.AspNetCore.Mvc": "1.0.1", - "Microsoft.AspNetCore.Routing": "1.0.1", - "Microsoft.Extensions.Configuration.Json": "1.0.0", - "Microsoft.Extensions.Logging.Debug": "1.0.0", - "Newtonsoft.Json": "9.0.1" + "dependencies": { + "NETStandard.Library": "1.6.0", + "Microsoft.AspNetCore.Mvc": "1.0.1", + "Microsoft.AspNetCore.Routing": "1.0.1", + "Microsoft.Extensions.Configuration.Json": "1.0.0", + "Microsoft.Extensions.Logging.Debug": "1.0.0", + "Newtonsoft.Json": "9.0.1" + }, + "buildOptions": { + "keyFile": "../../Serilog.snk" }, "frameworks": { "netstandard1.6": { diff --git a/test/Serilog.Sinks.Http.IntegrationTests/ApiModels/ApiModel.cs b/test/Serilog.Sinks.Http.IntegrationTests/ApiModels/ApiModel.cs index 530d2aeb..e5b87784 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests/ApiModels/ApiModel.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests/ApiModels/ApiModel.cs @@ -8,6 +8,7 @@ using Polly; using Serilog.Sinks.Http.IntegrationTests.Server; using Serilog.Sinks.Http.IntegrationTests.Server.Controllers; +using Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos; using Xunit; using Xunit.Sdk; diff --git a/test/Serilog.Sinks.Http.IntegrationTests/HttpSinkTest.cs b/test/Serilog.Sinks.Http.IntegrationTests/HttpSinkTest.cs index 8b4f2e17..b9b0e9fe 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests/HttpSinkTest.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests/HttpSinkTest.cs @@ -3,7 +3,7 @@ namespace Serilog { - public class HttpSinkTest : SinkFixture + public class HttpSinkTest : SinkFixture { public HttpSinkTest() { diff --git a/test/Serilog.Sinks.Http.IntegrationTests/SinkFixture.cs b/test/Serilog.Sinks.Http.IntegrationTests/SinkFixture.cs index 984a976d..aba624af 100644 --- a/test/Serilog.Sinks.Http.IntegrationTests/SinkFixture.cs +++ b/test/Serilog.Sinks.Http.IntegrationTests/SinkFixture.cs @@ -8,7 +8,7 @@ namespace Serilog { - public abstract class SinkFixture : TestServerFixture + public abstract class SinkFixture : TestServerFixture { protected SinkFixture() { @@ -69,6 +69,7 @@ public async Task Payload() Assert.Equal(expected.Level.ToString(), @event.Level); Assert.Equal(expected.MessageTemplate.Text, @event.MessageTemplate); Assert.Equal(expected.Properties["Name"].ToString().Trim('"'), @event.Properties["Name"]); + Assert.Equal("Hello, \"Alice\"!", @event.RenderedMessage); Assert.Null(@event.Exception); } @@ -87,6 +88,7 @@ public async Task Exception() Assert.Equal(expected.Timestamp, @event.Timestamp); Assert.Equal(expected.Level.ToString(), @event.Level); Assert.Equal(expected.MessageTemplate.Text, @event.MessageTemplate); + Assert.Equal("Some error message", @event.RenderedMessage); Assert.Equal(expected.Exception.ToString(), @event.Exception); } diff --git a/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/CompactJsonFormatterTest.cs b/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/CompactJsonFormatterTest.cs new file mode 100644 index 00000000..c015e858 --- /dev/null +++ b/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/CompactJsonFormatterTest.cs @@ -0,0 +1,233 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos; +using Serilog.Support; +using Xunit; + +namespace Serilog.Sinks.Http.Private.Formatters +{ + public class CompactJsonFormatterTest + { + private readonly StringWriter output; + + private ILogger logger; + + public CompactJsonFormatterTest() + { + output = new StringWriter(); + } + + [Theory] + [InlineData(LogEventLevel.Verbose)] + [InlineData(LogEventLevel.Debug)] + [InlineData(LogEventLevel.Information)] + [InlineData(LogEventLevel.Warning)] + [InlineData(LogEventLevel.Error)] + [InlineData(LogEventLevel.Fatal)] + public void LogEventLevels(LogEventLevel level) + { + // Arrange + logger = CreateLogger(new CompactJsonFormatter(true)); + + // Act + logger.Write(level, "No properties"); + + // Assert + var @event = GetEvent(); + + if (level == LogEventLevel.Information) + { + Assert.Null(@event.Level); + } + else + { + Assert.NotNull(@event.Level); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EmptyEvent(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new CompactJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("No properties"); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Null(@event.Level); + Assert.Equal("No properties", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "No properties" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MinimalEvent(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new CompactJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("One {Property}", 42); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("One {Property}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "One 42" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Equal("42", GetProperty("Property")); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MultipleProperties(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new CompactJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("Property {First} and {Second}", "One", "Two"); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Property {First} and {Second}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "Property \"One\" and \"Two\"" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Equal("One", GetProperty("First")); + Assert.Equal("Two", GetProperty("Second")); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Exceptions(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new CompactJsonFormatter(isRenderingMessage)); + + // Act + logger.Information(new DivideByZeroException(), "With exception"); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("With exception", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "With exception" : null, @event.RenderedMessage); + Assert.NotNull(@event.Exception); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ExceptionAndProperties(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new CompactJsonFormatter(isRenderingMessage)); + + // Act + logger.Information(new DivideByZeroException(), "With exception and {Property}", 42); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("With exception and {Property}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "With exception and 42" : null, @event.RenderedMessage); + Assert.NotNull(@event.Exception); + Assert.Equal("42", GetProperty("Property")); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Renderings(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new CompactJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("One {Rendering:x8}", 42); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("One {Rendering:x8}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "One 0000002a" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Equal("42", GetProperty("Rendering")); + Assert.Equal(new[] { "0000002a" }, @event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MultipleRenderings(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new CompactJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("Rendering {First:x8} and {Second:x8}", 1, 2); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Rendering {First:x8} and {Second:x8}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "Rendering 00000001 and 00000002" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Equal("1", GetProperty("First")); + Assert.Equal("2", GetProperty("Second")); + Assert.Equal(new[] { "00000001", "00000002" }, @event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NastyException(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information(new NastyException(), "With exception"); + + // Assert + Assert.Equal(string.Empty, output.ToString()); + } + + private ILogger CreateLogger(ITextFormatter formatter) + { + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Sink(new TextWriterSink(output, formatter)) + .CreateLogger(); + } + + private CompactEventDto GetEvent() + { + return JsonConvert.DeserializeObject(output.ToString()); + } + + private string GetProperty(string name) + { + dynamic @event = JsonConvert.DeserializeObject(output.ToString()); + return @event[name]; + } + } +} diff --git a/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/ConverterTest.cs b/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/ConverterTest.cs new file mode 100644 index 00000000..73f3aa7c --- /dev/null +++ b/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/ConverterTest.cs @@ -0,0 +1,21 @@ +using System; +using Xunit; + +namespace Serilog.Sinks.Http.Private.Formatters +{ + public class ConverterTest + { + [Fact] + public void FormattingTypes() + { + foreach (FormattingType type in Enum.GetValues(typeof(FormattingType))) + { + // Act + var formatter = Converter.ToFormatter(type); + + // Assert + Assert.NotNull(formatter); + } + } + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/NormalJsonFormatterTest.cs b/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/NormalJsonFormatterTest.cs new file mode 100644 index 00000000..b7232eaa --- /dev/null +++ b/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/Formatters/NormalJsonFormatterTest.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Sinks.Http.IntegrationTests.Server.Controllers.Dtos; +using Serilog.Support; +using Xunit; + +namespace Serilog.Sinks.Http.Private.Formatters +{ + public class NormalJsonFormatterTest + { + private readonly StringWriter output; + + private ILogger logger; + + public NormalJsonFormatterTest() + { + output = new StringWriter(); + } + + [Theory] + [InlineData(LogEventLevel.Verbose)] + [InlineData(LogEventLevel.Debug)] + [InlineData(LogEventLevel.Information)] + [InlineData(LogEventLevel.Warning)] + [InlineData(LogEventLevel.Error)] + [InlineData(LogEventLevel.Fatal)] + public void LogEventLevels(LogEventLevel level) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(true)); + + // Act + logger.Write(level, "No properties"); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Level); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EmptyEvent(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("No properties"); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Information", @event.Level); + Assert.Equal("No properties", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "No properties" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Null(@event.Properties); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MinimalEvent(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("One {Property}", 42); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Information", @event.Level); + Assert.Equal("One {Property}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "One 42" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Equal(new Dictionary { { "Property", "42" } }, @event.Properties); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MultipleProperties(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("Property {First} and {Second}", "One", "Two"); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Information", @event.Level); + Assert.Equal("Property {First} and {Second}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "Property \"One\" and \"Two\"" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Equal(new Dictionary { { "First", "One" }, { "Second", "Two" } }, @event.Properties); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Exceptions(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information(new DivideByZeroException(), "With exception"); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Information", @event.Level); + Assert.Equal("With exception", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "With exception" : null, @event.RenderedMessage); + Assert.NotNull(@event.Exception); + Assert.Null(@event.Properties); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ExceptionAndProperties(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information(new DivideByZeroException(), "With exception and {Property}", 42); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Information", @event.Level); + Assert.Equal("With exception and {Property}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "With exception and 42" : null, @event.RenderedMessage); + Assert.NotNull(@event.Exception); + Assert.Equal(new Dictionary { { "Property", "42" } }, @event.Properties); + Assert.Null(@event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Renderings(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("One {Rendering:x8}", 42); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Information", @event.Level); + Assert.Equal("One {Rendering:x8}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "One 0000002a" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Equal(new Dictionary { { "Rendering", "42" } }, @event.Properties); + Assert.Equal( + new Dictionary + { + { + "Rendering", + new[] { new RenderingDto { Format = "x8", Rendering = "0000002a" } } + } + }, + @event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MultipleRenderings(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information("Rendering {First:x8} and {Second:x8}", 1, 2); + + // Assert + var @event = GetEvent(); + Assert.NotNull(@event.Timestamp); + Assert.Equal("Information", @event.Level); + Assert.Equal("Rendering {First:x8} and {Second:x8}", @event.MessageTemplate); + Assert.Equal(isRenderingMessage ? "Rendering 00000001 and 00000002" : null, @event.RenderedMessage); + Assert.Null(@event.Exception); + Assert.Equal(new Dictionary { { "First", "1" }, { "Second", "2" } }, @event.Properties); + Assert.Equal( + new Dictionary + { + { + "First", + new[] { new RenderingDto { Format = "x8", Rendering = "00000001" } } + }, + { + "Second", + new[] { new RenderingDto { Format = "x8", Rendering = "00000002" } } + } + }, + @event.Renderings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NastyException(bool isRenderingMessage) + { + // Arrange + logger = CreateLogger(new NormalJsonFormatter(isRenderingMessage)); + + // Act + logger.Information(new NastyException(), "With exception"); + + // Assert + Assert.Equal(string.Empty, output.ToString()); + } + + private ILogger CreateLogger(ITextFormatter formatter) + { + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Sink(new TextWriterSink(output, formatter)) + .CreateLogger(); + } + + private EventDto GetEvent() + { + return JsonConvert.DeserializeObject(output.ToString()); + } + } +} diff --git a/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/HttpJsonFormatterTest.cs b/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/HttpJsonFormatterTest.cs deleted file mode 100644 index 1e3d03f5..00000000 --- a/test/Serilog.Sinks.Http.Tests/Sinks/Http/Private/HttpJsonFormatterTest.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json.Linq; -using Serilog.Support; -using Xunit; - -namespace Serilog.Sinks.Http.Private -{ - public class HttpJsonFormatterTest - { - private readonly ILogger logger; - private readonly StringWriter output; - - public HttpJsonFormatterTest() - { - output = new StringWriter(); - var formatter = new HttpJsonFormatter(); - logger = new LoggerConfiguration() - .WriteTo.Sink(new TextWriterSink(output, formatter)) - .CreateLogger(); - } - - [Fact] - public void EmptyEvent() - { - AssertValidJson(log => log.Information("No properties")); - } - - [Fact] - public void MinimalEvent() - { - AssertValidJson(log => log.Information("One {Property}", 42)); - } - - [Fact] - public void MultipleProperties() - { - AssertValidJson(log => log.Information("Property {First} and {Second}", "One", "Two")); - } - - [Fact] - public void Exceptions() - { - AssertValidJson(log => log.Information(new DivideByZeroException(), "With exception")); - } - - [Fact] - public void ExceptionAndProperties() - { - AssertValidJson(log => log.Information(new DivideByZeroException(), "With exception and {Property}", 42)); - } - - [Fact] - public void Renderings() - { - AssertValidJson(log => log.Information("One {Rendering:x8}", 42)); - } - - [Fact] - public void MultipleRenderings() - { - AssertValidJson(log => log.Information("Rendering {First:x8} and {Second:x8}", 1, 2)); - } - - [Fact] - public void NastyException() - { - AssertIsDropped(log => log.Information(new NastyException(), "With exception")); - } - - private void AssertValidJson(Action act) - { - // Act - act(logger); - - // Assert - Unfortunately this will not detect all JSON formatting issues; better than - // nothing however - JObject.Parse(output.ToString()); - } - - private void AssertIsDropped(Action act) - { - // Act - act(logger); - - // Assert - Assert.Equal(string.Empty, output.ToString()); - } - } -} diff --git a/test/Serilog.Sinks.Http.Tests/project.json b/test/Serilog.Sinks.Http.Tests/project.json index a822456e..88d17089 100644 --- a/test/Serilog.Sinks.Http.Tests/project.json +++ b/test/Serilog.Sinks.Http.Tests/project.json @@ -1,9 +1,10 @@ -{ +{ "testRunner": "xunit", "dependencies": { "dotnet-test-xunit": "2.2.0-preview2-build1029", "NETStandard.Library": "1.6.1", "Serilog.Sinks.Http": { "target": "project" }, + "Serilog.Sinks.Http.IntegrationTests.Server": "1.0.0-*", "xunit": "2.2.0" }, "buildOptions": {