From f9f80778c743ea2f1d8c7eb008f171a6ed59ad28 Mon Sep 17 00:00:00 2001 From: oscarmorasu Date: Mon, 8 Apr 2019 12:50:42 -0700 Subject: [PATCH] Added FormatStackTraceAsArray option to write StackTrace as an array (#230) * Github releases added to yaml * Create new release * fix title of changelog (#211) * Added FormatStackTraceAsArray option to write StackTrace as an array * Added unit tests, minor code-style changes --- CHANGES.md | 7 +- GitVersion.yaml | 3 - GitVersion.yml | 2 +- appveyor.yml | 7 ++ .../DefaultJsonFormatter.cs | 13 +++ .../ElasticsearchJsonFormatter.cs | 23 +++- .../ExceptionAsObjectJsonFormatter.cs | 10 +- .../ElasticSearch/ElasticsearchSinkOptions.cs | 6 ++ .../ElasticSearch/ElasticsearchSinkState.cs | 6 +- .../ElasticsearchJsonFormatterTests.cs | 102 +++++++++++++++++- 10 files changed, 165 insertions(+), 14 deletions(-) delete mode 100644 GitVersion.yaml diff --git a/CHANGES.md b/CHANGES.md index ef6b24dc..e0a9a990 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,14 @@ -== Changelog -7.0 +## Changelog + +7.1 * DurableElasticsearchSink is rewritten to use the same base code as the sink for Serilog.Sinks.Seq. Nuget Serilog.Sinks.File is now used instead of deprecated Serilog.Sinks.RollingFile. Lots of new fintuning options for file storage is added in ElasticsearchSinkOptions. Updated Serilog.Sinks.Elasticsearch.Sample.Main with SetupLoggerWithPersistantStorage with all available options for durable mode. * Changed datatype on singleEventSizePostingLimit from int to long? with default value null. to make it possible ro reuse code from Sinks.Seq . * IndexDecider didnt worked well in buffer mode because of LogEvent was null. Added BufferIndexDecider. * Added BufferCleanPayload and an example which makes it possible to cleanup your invalid logging document if rejected from elastic because of inconsistent datatype on a field. It'seasy to miss errors in the self log now its possible to se logrows which is bad for elasticsearch in the elastic log. * Added BufferRetainedInvalidPayloadsLimitBytes A soft limit for the number of bytes to use for storing failed requests. * Added BufferFileCountLimit The maximum number of log files that will be retained. + * Formatting has been moved to seperate package. + 6.4 * Render message by default (#160). * Expose interface-typed options via appsettings (#162) diff --git a/GitVersion.yaml b/GitVersion.yaml deleted file mode 100644 index 736da6b6..00000000 --- a/GitVersion.yaml +++ /dev/null @@ -1,3 +0,0 @@ -mode: ContinuousDelivery -next-version: 5.2.0 -branches: {} diff --git a/GitVersion.yml b/GitVersion.yml index 130b2d92..24871076 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,4 +1,4 @@ -next-version: 6.4.0 +next-version: 7.0.0 branches: {} ignore: sha: [] diff --git a/appveyor.yml b/appveyor.yml index 668b543e..5c56a76c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,6 +22,13 @@ deploy: secure: bd9z4P73oltOXudAjPehwp9iDKsPtC+HbgshOrSgoyQKr5xVK+bxJQngrDJkHdY8 on: branch: /^(master|dev)$/ +- provider: GitHub + auth_token: + secure: XSO0LDYd89yw5rAQ8HvAgdX7NBo1m4bEqHlj0NZxtA6zKunLwCSYoVHU+k3cvQIP + on: + branch: master + artifact: /Serilog.*\.nupkg/ + tag: v$(appveyor_build_version) install: - choco install gitversion.portable -y assembly_info: diff --git a/src/Serilog.Formatting.Elasticsearch/DefaultJsonFormatter.cs b/src/Serilog.Formatting.Elasticsearch/DefaultJsonFormatter.cs index d8cd8e3b..3eea1f9e 100644 --- a/src/Serilog.Formatting.Elasticsearch/DefaultJsonFormatter.cs +++ b/src/Serilog.Formatting.Elasticsearch/DefaultJsonFormatter.cs @@ -328,6 +328,19 @@ protected virtual void WriteJsonProperty(string name, object value, ref string p precedingDelimiter = ","; } + /// + /// Writes out a json property with an array as the value + /// + protected virtual void WriteJsonArrayProperty(string name, IEnumerable sequence, ref string precedingDelimiter, TextWriter output) + { + output.Write(precedingDelimiter); + output.Write("\""); + output.Write(name); + output.Write("\":"); + WriteSequence(sequence, output); + precedingDelimiter = ","; + } + /// /// Allows a subclass to write out objects that have no configured literal writer. /// diff --git a/src/Serilog.Formatting.Elasticsearch/ElasticsearchJsonFormatter.cs b/src/Serilog.Formatting.Elasticsearch/ElasticsearchJsonFormatter.cs index ae49beea..5a162968 100644 --- a/src/Serilog.Formatting.Elasticsearch/ElasticsearchJsonFormatter.cs +++ b/src/Serilog.Formatting.Elasticsearch/ElasticsearchJsonFormatter.cs @@ -32,6 +32,7 @@ public class ElasticsearchJsonFormatter : DefaultJsonFormatter { readonly IElasticsearchSerializer _serializer; readonly bool _inlineFields; + readonly bool _formatStackTraceAsArray; /// /// Render message property name @@ -69,6 +70,7 @@ public class ElasticsearchJsonFormatter : DefaultJsonFormatter /// Inject a serializer to force objects to be serialized over being ToString() /// When set to true values will be written at the root of the json document /// If true, the message template will be rendered and written to the output as a + /// If true, splits the StackTrace by new line and writes it as a an array of strings /// property named RenderedMessageTemplate. public ElasticsearchJsonFormatter( bool omitEnclosingObject = false, @@ -77,11 +79,13 @@ public ElasticsearchJsonFormatter( IFormatProvider formatProvider = null, IElasticsearchSerializer serializer = null, bool inlineFields = false, - bool renderMessageTemplate = true) + bool renderMessageTemplate = true, + bool formatStackTraceAsArray = false) : base(omitEnclosingObject, closingDelimiter, renderMessage, formatProvider, renderMessageTemplate) { _serializer = serializer; _inlineFields = inlineFields; + _formatStackTraceAsArray = formatStackTraceAsArray; } /// @@ -219,8 +223,16 @@ protected void WriteSingleException(Exception exception, ref string delim, TextW this.WriteJsonProperty("ClassName", className, ref delim, output); this.WriteJsonProperty("Message", exception.Message, ref delim, output); this.WriteJsonProperty("Source", source, ref delim, output); - this.WriteJsonProperty("StackTraceString", stackTrace, ref delim, output); - this.WriteJsonProperty("RemoteStackTraceString", remoteStackTrace, ref delim, output); + if (_formatStackTraceAsArray) + { + this.WriteMultilineString("StackTrace", stackTrace, ref delim, output); + this.WriteMultilineString("RemoteStackTrace", stackTrace, ref delim, output); + } + else + { + this.WriteJsonProperty("StackTraceString", stackTrace, ref delim, output); + this.WriteJsonProperty("RemoteStackTraceString", remoteStackTrace, ref delim, output); + } this.WriteJsonProperty("RemoteStackIndex", remoteStackIndex, ref delim, output); this.WriteStructuredExceptionMethod(exceptionMethod, ref delim, output); this.WriteJsonProperty("HResult", hresult, ref delim, output); @@ -230,7 +242,12 @@ protected void WriteSingleException(Exception exception, ref string delim, TextW //JsonNET assumes string, simplejson writes array of numerics. //Skip for now //this.WriteJsonProperty("WatsonBuckets", watsonBuckets, ref delim, output); + } + private void WriteMultilineString(string name, string value, ref string delimeter, TextWriter output) + { + string[] lines = value.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + WriteJsonArrayProperty(name, lines, ref delimeter, output); } private void WriteStructuredExceptionMethod(string exceptionMethodString, ref string delim, TextWriter output) diff --git a/src/Serilog.Formatting.Elasticsearch/ExceptionAsObjectJsonFormatter.cs b/src/Serilog.Formatting.Elasticsearch/ExceptionAsObjectJsonFormatter.cs index 6941a7b7..6430c7d5 100644 --- a/src/Serilog.Formatting.Elasticsearch/ExceptionAsObjectJsonFormatter.cs +++ b/src/Serilog.Formatting.Elasticsearch/ExceptionAsObjectJsonFormatter.cs @@ -43,7 +43,15 @@ public class ExceptionAsObjectJsonFormatter : ElasticsearchJsonFormatter /// Supplies culture-specific formatting information, or null. /// Inject a serializer to force objects to be serialized over being ToString() /// When set to true values will be written at the root of the json document - public ExceptionAsObjectJsonFormatter(bool omitEnclosingObject = false, string closingDelimiter = null, bool renderMessage = false, IFormatProvider formatProvider = null, IElasticsearchSerializer serializer = null, bool inlineFields = false) : base(omitEnclosingObject, closingDelimiter, renderMessage, formatProvider, serializer, inlineFields) + /// If true, splits the StackTrace by new line and writes it as a an array of strings + public ExceptionAsObjectJsonFormatter(bool omitEnclosingObject = false, + string closingDelimiter = null, + bool renderMessage = false, + IFormatProvider formatProvider = null, + IElasticsearchSerializer serializer = null, + bool inlineFields = false, + bool formatStackTraceAsArray = false) + : base(omitEnclosingObject, closingDelimiter, renderMessage, formatProvider, serializer, inlineFields, formatStackTraceAsArray) { } diff --git a/src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchSinkOptions.cs b/src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchSinkOptions.cs index 4724d146..8397d403 100644 --- a/src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchSinkOptions.cs +++ b/src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchSinkOptions.cs @@ -247,6 +247,11 @@ public int QueueSizeLimit /// public int? BufferFileCountLimit { get; set; } + /// + /// When set to true splits the StackTrace by new line and writes it as a an array of strings. + /// + public bool FormatStackTraceAsArray { get; set; } + /// /// Configures the elasticsearch sink defaults /// @@ -265,6 +270,7 @@ public ElasticsearchSinkOptions() this.QueueSizeLimit = 100000; this.BufferFileCountLimit = 31; this.BufferFileSizeLimitBytes = 100L * 1024 * 1024; + this.FormatStackTraceAsArray = false; } /// diff --git a/src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchSinkState.cs b/src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchSinkState.cs index ca4dd3d4..94759422 100644 --- a/src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchSinkState.cs +++ b/src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchSinkState.cs @@ -94,7 +94,8 @@ public static ITextFormatter CreateDefaultFormatter(ElasticsearchSinkOptions opt formatProvider: options.FormatProvider, closingDelimiter: string.Empty, serializer: options.Serializer, - inlineFields: options.InlineFields + inlineFields: options.InlineFields, + formatStackTraceAsArray: options.FormatStackTraceAsArray ); } @@ -104,7 +105,8 @@ public static ITextFormatter CreateDefaultDurableFormatter(ElasticsearchSinkOpti formatProvider: options.FormatProvider, closingDelimiter: Environment.NewLine, serializer: options.Serializer, - inlineFields: options.InlineFields + inlineFields: options.InlineFields, + formatStackTraceAsArray: options.FormatStackTraceAsArray ); } diff --git a/test/Serilog.Sinks.Elasticsearch.Tests/ElasticsearchJsonFormatterTests.cs b/test/Serilog.Sinks.Elasticsearch.Tests/ElasticsearchJsonFormatterTests.cs index d7f4d7bd..6a2e1c2d 100644 --- a/test/Serilog.Sinks.Elasticsearch.Tests/ElasticsearchJsonFormatterTests.cs +++ b/test/Serilog.Sinks.Elasticsearch.Tests/ElasticsearchJsonFormatterTests.cs @@ -18,7 +18,49 @@ static LogEvent CreateLogEvent() => ( DateTimeOffset.Now, LogEventLevel.Debug, - exception: null, + //exception: CreateThrownException(), + exception: null, + messageTemplate: new MessageTemplate(Enumerable.Empty()), + properties: Enumerable.Empty() + ); + + static Exception CreateThrownException() + { + Exception retEx = null; + try + { + ThrowException(); + } + catch (Exception ex) + { + retEx = ex; + } + return retEx; + } + + static void ThrowException() + { + try + { + ThrowInnerException(); + } + catch(Exception ex) + { + throw new Exception("Test exception message", ex); + } + } + + static void ThrowInnerException() + { + throw new Exception("Test inner exception message"); + } + + static LogEvent CreateLogEventWithException() => + new LogEvent + ( + DateTimeOffset.Now, + LogEventLevel.Debug, + exception: CreateThrownException(), messageTemplate: new MessageTemplate(Enumerable.Empty()), properties: Enumerable.Empty() ); @@ -57,7 +99,7 @@ static void DoesNotContainsProperty(string propertyToCheck, string result) => StringComparison.CurrentCultureIgnoreCase ); - static string FormatProperty(string property) => $"\"{property}\":"; + static string FormatProperty(string property) => $"\"{property}\":"; #endregion [Theory] @@ -134,5 +176,61 @@ public void When_provide_closing_delimiter_should_use_it() Assert.EndsWith("closingDelimiter", result); }); } + + [Fact] + public void DefaultJsonFormater_Should_Render_Exceptions() + { + CheckProperties( + CreateLogEventWithException, + new ElasticsearchJsonFormatter(), + result => + { + string exceptionsProperty = FormatProperty("exceptions"); + ContainsProperty(exceptionsProperty, result); + + + string exceptionsValue = result.Substring(result.IndexOf(exceptionsProperty) + exceptionsProperty.Length); + + // Check the exceptions property is a JSON array + Assert.StartsWith("[", exceptionsValue); + + string stackTraceProperty = FormatProperty("StackTraceString"); + ContainsProperty(stackTraceProperty, exceptionsValue); + DoesNotContainsProperty(FormatProperty("StackTrace"), exceptionsValue); + + string stackTraceValue = exceptionsValue.Substring(exceptionsValue.IndexOf(stackTraceProperty) + stackTraceProperty.Length); + + // Check the StackTraceString property is a JSON string + Assert.StartsWith("\"", stackTraceValue); + }); + } + + [Fact] + public void DefaultJsonFormater_Should_Render_Exceptions_With_StackTrace_As_Array() + { + CheckProperties( + CreateLogEventWithException, + new ElasticsearchJsonFormatter(formatStackTraceAsArray: true), + result => + { + string exceptionsProperty = FormatProperty("exceptions"); + ContainsProperty(exceptionsProperty, result); + + + string exceptionsValue = result.Substring(result.IndexOf(exceptionsProperty) + exceptionsProperty.Length); + + // Check the exceptions property is a JSON array + Assert.StartsWith("[", exceptionsValue); + + string stackTraceProperty = FormatProperty("StackTrace"); + ContainsProperty(stackTraceProperty, exceptionsValue); + DoesNotContainsProperty(FormatProperty("StackTraceString"), exceptionsValue); + + string stackTraceValue = exceptionsValue.Substring(exceptionsValue.IndexOf(stackTraceProperty) + stackTraceProperty.Length); + + // Check the StackTrace property is a JSON array + Assert.StartsWith("[", stackTraceValue); + }); + } } }