diff --git a/.gitignore b/.gitignore index 763353e..5e1d78a 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,6 @@ UpgradeLog*.htm # Microsoft Fakes FakesAssemblies/ .vs/ + +# Rider settings +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 5ab26d6..5b40117 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,16 @@ A Serilog sink that writes events to Raygun ## Usage +Add the [Serilog](https://www.nuget.org/packages/serilog/) and [Serilog.Sinks.Raygun](https://www.nuget.org/packages/Serilog.Sinks.Raygun) nuget packages. +Via the package manager, or via the command line: + +```powershell + dotnet add package Serilog + dotnet add package Serilog.Sinks.Raygun +``` + +Then setup the logger configuration; inside the `Program.Main()`, `Global.asax.Application_Start`, etc. + ```csharp Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() @@ -23,6 +33,34 @@ Log.Logger = new LoggerConfiguration() .CreateLogger(); ``` +When configuring using a JSON configuration file use the following example. + +```json +{ + "Serilog": { + "Using": [ + "Serilog.Sinks.Raygun" + ], + "WriteTo": [ + { + "Name": "Raygun", + "Args": { + "applicationKey": "RaygunAPIKey", + "userNameProperty": "CustomUserNameProperty", + "applicationVersionProperty": "CustomAppVersionProperty", + "restrictedToMinimumLevel": "Error", + "ignoredFormFieldNames": ["ignoreField1", "ignoreField2"], + "tags": ["globalTag1", "globalTag2"], + "groupKeyProperty": "CustomGroupKeyProperty", + "tagsProperty": "CustomTagsProperty", + "userInfoProperty": "CustomUserInfoProperty" + } + } + ] + } +} +``` + ### applicationKey `type: string` @@ -133,21 +171,60 @@ var userInfo = new RaygunIdentifierMessage("12345") Log.ForContext("CustomUserInfoProperty", userInfo, true).Error(new Exception("random error"), "other information"); ``` +## Enrich with HTTP request and response data -## Raygun4Net features configured via RaygunSettings +_Note: This is only valid for .NET Standard 2.0 and above projects. In full framework ASP.NET applications the HTTP request and response are available to Raygun4Net through the `HttpContext.Current` accessor. +In .NET Core this is not available so you'll need to add the Serilog enricher using the `WithHttpDataForRaygun` method to capture the HTTP request and response data._ -This sink wraps the [Raygun4Net](https://github.com/MindscapeHQ/raygun4net) provider to build a crash report from an Exception and send it to Raygun. This makes the following Raygun4Net features available to you. To use these features, you need to add RaygunSettings to your configuration as explained below which is separate to the Serilog configuration. +### Configuration -**.NET Core** - -Add a RaygunSettings block to your appsettings.config file where you can populate the settings that you want to use. +All parameters to `WithHttpDataForRaygun` are optional. +```csharp +Log.Logger = new LoggerConfiguration() + .WriteTo.Raygun("RaygunAPIKey") + .Enrich.WithHttpDataForRaygun( + new HttpContextAccessor(), + LogEventLevel.Error, + RaygunSettings) + .CreateLogger(); ``` -"RaygunSettings": { - "Setting": "Value" + +When configuring using a JSON configuration file use the following example. + +```json +{ + "Serilog": { + "Using": [ + "Serilog.Sinks.Raygun" + ], + "Enrich": [ + { + "Name": "WithHttpDataForRaygun", + "Args": { + "RaygunSettings": { + "IsRawDataIgnored": true, + "UseXmlRawDataFilter": true, + "IsRawDataIgnoredWhenFilteringFailed": true, + "UseKeyValuePairRawDataFilter": true, + "IgnoreCookieNames": ["CookieName"], + "IgnoreHeaderNames": ["HeaderName"], + "IgnoreFormFieldNames": ["FormFieldName"], + "IgnoreQueryParameterNames": ["QueryParameterName"], + "IgnoreSensitiveFieldNames": ["SensitiveFieldNames"], + "IgnoreServerVariableNames": ["ServerVariableName"] + } + } + } + ] + } } ``` +## Raygun4Net features configured via RaygunSettings + +This sink wraps the [Raygun4Net](https://github.com/MindscapeHQ/raygun4net) provider to build a crash report from an Exception and send it to Raygun. This makes the following Raygun4Net features available to you. To use these features, you need to add RaygunSettings to your configuration as explained below which is separate to the Serilog configuration. + **.NET Framework** Add the following section within the configSections element of your app.config or web.config file. diff --git a/src/Serilog.Sinks.Raygun/LoggerConfigurationRaygunExtensions.cs b/src/Serilog.Sinks.Raygun/LoggerConfigurationRaygunExtensions.cs index 39b91ac..3aa525a 100644 --- a/src/Serilog.Sinks.Raygun/LoggerConfigurationRaygunExtensions.cs +++ b/src/Serilog.Sinks.Raygun/LoggerConfigurationRaygunExtensions.cs @@ -17,6 +17,10 @@ using Serilog.Configuration; using Serilog.Events; using Serilog.Sinks.Raygun; +#if NETSTANDARD2_0 +using Microsoft.AspNetCore.Http; +using Mindscape.Raygun4Net.AspNetCore; +#endif namespace Serilog { @@ -39,7 +43,8 @@ public static class LoggerConfigurationRaygunExtensions /// Specifies the tags to include with every log message. The log level will always be included as a tag. /// Specifies the form field names which to ignore when including request form data. /// The property containing the custom group key for the Raygun message. - /// The property where additional tags are stored when emitting log events + /// The property where additional tags are stored when emitting log events. + /// The property containing the RaygunIdentifierMessage structure used to populate user details. /// Logger configuration, allowing configuration to continue. /// A required parameter is null. public static LoggerConfiguration Raygun( @@ -62,5 +67,28 @@ public static LoggerConfiguration Raygun( new RaygunSink(formatProvider, applicationKey, wrapperExceptions, userNameProperty, applicationVersionProperty, tags, ignoredFormFieldNames, groupKeyProperty, tagsProperty, userInfoProperty), restrictedToMinimumLevel); } + +#if NETSTANDARD2_0 + /// + /// Add the to the enrichment configuration. + /// + /// + /// Optional HttpContext accessor that provides access to the current requests HttpContext. + /// Optional to enrich log events. Defaults to LogEventLevel.Error. + /// Optional that is used to apply http data filtering. + /// + /// + public static LoggerConfiguration WithHttpDataForRaygun( + this LoggerEnrichmentConfiguration enrich, + IHttpContextAccessor httpContextAccessor = null, + LogEventLevel restrictedToMinimumLevel = LogEventLevel.Error, + RaygunSettings raygunSettings = null) + { + if (enrich == null) + throw new ArgumentNullException(nameof(enrich)); + + return enrich.With(new RaygunClientHttpEnricher(httpContextAccessor ?? new HttpContextAccessor(), restrictedToMinimumLevel, raygunSettings ?? new RaygunSettings())); + } +#endif } } diff --git a/src/Serilog.Sinks.Raygun/Serilog.Sinks.Raygun.csproj b/src/Serilog.Sinks.Raygun/Serilog.Sinks.Raygun.csproj index 7e9f368..7ec9df6 100644 --- a/src/Serilog.Sinks.Raygun/Serilog.Sinks.Raygun.csproj +++ b/src/Serilog.Sinks.Raygun/Serilog.Sinks.Raygun.csproj @@ -14,24 +14,25 @@ serilog sink raygun Copyright © Serilog Contributors 2017-2020 Serilog event sink that writes to the Raygun service. - 5.0.2 + 5.1.0 Serilog - + - + - + + - + diff --git a/src/Serilog.Sinks.Raygun/Sinks/Raygun/LogEventPropertyExtensions.cs b/src/Serilog.Sinks.Raygun/Sinks/Raygun/LogEventPropertyExtensions.cs new file mode 100644 index 0000000..177bcf0 --- /dev/null +++ b/src/Serilog.Sinks.Raygun/Sinks/Raygun/LogEventPropertyExtensions.cs @@ -0,0 +1,30 @@ +using System.Collections; +using System.Linq; +using Serilog.Events; + +namespace Serilog.Sinks.Raygun +{ + public static class LogEventPropertyExtensions + { + public static string AsString(this LogEventProperty property) + { + var scalar = property.Value as ScalarValue; + return scalar?.Value != null ? property.Value.ToString("l", null) : null; + } + + public static int AsInteger(this LogEventProperty property, int defaultIfNull = 0) + { + var scalar = property.Value as ScalarValue; + return scalar?.Value != null ? int.TryParse(property.Value.ToString(), out int result) ? result : defaultIfNull : defaultIfNull; + } + + public static IDictionary AsDictionary(this LogEventProperty property) + { + if (!(property.Value is DictionaryValue value)) return null; + + return value.Elements.ToDictionary( + kv => kv.Key.ToString("l", null), + kv => kv.Value is ScalarValue scalarValue ? scalarValue.Value : kv.Value); + } + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Raygun/Sinks/Raygun/RaygunClientHttpEnricher.cs b/src/Serilog.Sinks.Raygun/Sinks/Raygun/RaygunClientHttpEnricher.cs new file mode 100644 index 0000000..a21e093 --- /dev/null +++ b/src/Serilog.Sinks.Raygun/Sinks/Raygun/RaygunClientHttpEnricher.cs @@ -0,0 +1,70 @@ +#if NETSTANDARD2_0 +using System; +using Microsoft.AspNetCore.Http; +using Mindscape.Raygun4Net; +using Mindscape.Raygun4Net.AspNetCore; +using Mindscape.Raygun4Net.AspNetCore.Builders; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Sinks.Raygun +{ + public class RaygunClientHttpEnricher : ILogEventEnricher + { + public const string RaygunRequestMessagePropertyName = "RaygunSink_RequestMessage"; + public const string RaygunResponseMessagePropertyName = "RaygunSink_ResponseMessage"; + + readonly IHttpContextAccessor _httpContextAccessor; + private readonly LogEventLevel _restrictedToMinimumLevel; + private readonly RaygunSettings _raygunSettings; + + public RaygunClientHttpEnricher(IHttpContextAccessor httpContextAccessor, LogEventLevel restrictedToMinimumLevel, RaygunSettings raygunSettings) + { + _httpContextAccessor = httpContextAccessor; + _restrictedToMinimumLevel = restrictedToMinimumLevel; + _raygunSettings = raygunSettings; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (logEvent.Level < _restrictedToMinimumLevel) + { + return; + } + + if (_httpContextAccessor?.HttpContext == null) + { + return; + } + + var options = new RaygunRequestMessageOptions + { + IsRawDataIgnored = _raygunSettings.IsRawDataIgnored, + UseXmlRawDataFilter = _raygunSettings.UseXmlRawDataFilter, + IsRawDataIgnoredWhenFilteringFailed = _raygunSettings.IsRawDataIgnoredWhenFilteringFailed, + UseKeyValuePairRawDataFilter = _raygunSettings.UseKeyValuePairRawDataFilter + }; + + options.AddCookieNames(_raygunSettings.IgnoreCookieNames ?? Array.Empty()); + options.AddHeaderNames(_raygunSettings.IgnoreHeaderNames ?? Array.Empty()); + options.AddFormFieldNames(_raygunSettings.IgnoreFormFieldNames ?? Array.Empty()); + options.AddQueryParameterNames(_raygunSettings.IgnoreQueryParameterNames ?? Array.Empty()); + options.AddSensitiveFieldNames(_raygunSettings.IgnoreSensitiveFieldNames ?? Array.Empty()); + options.AddServerVariableNames(_raygunSettings.IgnoreServerVariableNames ?? Array.Empty()); + + RaygunRequestMessage httpRequestMessage = RaygunAspNetCoreRequestMessageBuilder + .Build(_httpContextAccessor.HttpContext, options) + .GetAwaiter() + .GetResult(); + + RaygunResponseMessage httpResponseMessage = RaygunAspNetCoreResponseMessageBuilder.Build(_httpContextAccessor.HttpContext); + + // The Raygun request/response messages are stored in the logEvent properties collection. + // When the error is sent to Raygun, these messages are extracted from the known properties + // and then removed so as to not duplicate data in the payload. + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(RaygunRequestMessagePropertyName, httpRequestMessage, true)); + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(RaygunResponseMessagePropertyName, httpResponseMessage, true)); + } + } +} +#endif \ No newline at end of file diff --git a/src/Serilog.Sinks.Raygun/Sinks/Raygun/RaygunSink.cs b/src/Serilog.Sinks.Raygun/Sinks/Raygun/RaygunSink.cs index 5fff58b..2b75ecb 100644 --- a/src/Serilog.Sinks.Raygun/Sinks/Raygun/RaygunSink.cs +++ b/src/Serilog.Sinks.Raygun/Sinks/Raygun/RaygunSink.cs @@ -91,7 +91,7 @@ public RaygunSink(IFormatProvider formatProvider, if (wrapperExceptions != null) _client.AddWrapperExceptions(wrapperExceptions.ToArray()); - if(ignoredFormFieldNames != null) + if (ignoredFormFieldNames != null) _client.IgnoreFormFieldNames(ignoredFormFieldNames.ToArray()); _client.CustomGroupingKey += OnCustomGroupingKey; @@ -217,30 +217,49 @@ occurredOnPropertyValue is ScalarValue occurredOnScalar && properties.Remove(_groupKeyProperty); } +#if NETSTANDARD2_0 + // Add Http request/response messages if present and not already set + if (details.Request == null && + properties.TryGetValue(RaygunClientHttpEnricher.RaygunRequestMessagePropertyName, out var requestMessageProperty) && + requestMessageProperty is StructureValue requestMessageValue) + { + details.Request = BuildRequestMessageFromStructureValue(requestMessageValue); + properties.Remove(RaygunClientHttpEnricher.RaygunRequestMessagePropertyName); + } + + if (details.Response == null && + properties.TryGetValue(RaygunClientHttpEnricher.RaygunResponseMessagePropertyName, out var responseMessageProperty) && + responseMessageProperty is StructureValue responseMessageValue) + { + details.Response = BuildResponseMessageFromStructureValue(responseMessageValue); + properties.Remove(RaygunClientHttpEnricher.RaygunResponseMessagePropertyName); + } +#endif + // Simplify the remaining properties to be used as user-custom-data details.UserCustomData = properties - .Select(pv => new { Name = pv.Key, Value = RaygunPropertyFormatter.Simplify(pv.Value) }) - .ToDictionary(a => a.Name, b => b.Value); + .Select(pv => new { Name = pv.Key, Value = RaygunPropertyFormatter.Simplify(pv.Value) }) + .ToDictionary(a => a.Name, b => b.Value); } } } private static StackTrace GetCurrentExecutionStackTrace() { - StackTrace stackTrace = new StackTrace(); - - for (int frameIndex = 0; frameIndex < stackTrace.FrameCount; frameIndex++) - { - MethodBase method = stackTrace.GetFrame(frameIndex).GetMethod(); - string className = method?.ReflectedType?.FullName ?? ""; + StackTrace stackTrace = new StackTrace(); - if (!className.StartsWith("Serilog.")) + for (int frameIndex = 0; frameIndex < stackTrace.FrameCount; frameIndex++) { - return new StackTrace(frameIndex); + MethodBase method = stackTrace.GetFrame(frameIndex).GetMethod(); + string className = method?.ReflectedType?.FullName ?? ""; + + if (!className.StartsWith("Serilog.")) + { + return new StackTrace(frameIndex); + } } - } - return stackTrace; + return stackTrace; } private static RaygunIdentifierMessage BuildUserInformationFromStructureValue(StructureValue userStructure) @@ -249,26 +268,25 @@ private static RaygunIdentifierMessage BuildUserInformationFromStructureValue(St foreach (var property in userStructure.Properties) { - ScalarValue scalar = property.Value as ScalarValue; switch (property.Name) { case nameof(RaygunIdentifierMessage.Identifier): - userIdentifier.Identifier = scalar?.Value != null ? property.Value.ToString("l", null) : null; + userIdentifier.Identifier = property.AsString(); break; case nameof(RaygunIdentifierMessage.IsAnonymous): userIdentifier.IsAnonymous = "True".Equals(property.Value.ToString()); break; case nameof(RaygunIdentifierMessage.Email): - userIdentifier.Email = scalar?.Value != null ? property.Value.ToString("l", null) : null; + userIdentifier.Email = property.AsString(); break; case nameof(RaygunIdentifierMessage.FullName): - userIdentifier.FullName = scalar?.Value != null ? property.Value.ToString("l", null) : null; + userIdentifier.FullName = property.AsString(); break; case nameof(RaygunIdentifierMessage.FirstName): - userIdentifier.FirstName = scalar?.Value != null ? property.Value.ToString("l", null) : null; + userIdentifier.FirstName = property.AsString(); break; case nameof(RaygunIdentifierMessage.UUID): - userIdentifier.UUID = scalar?.Value != null ? property.Value.ToString("l", null) : null; + userIdentifier.UUID = property.AsString(); break; } } @@ -324,5 +342,69 @@ private static RaygunIdentifierMessage ParseUserInformation(string userInfo) return userIdentifier; } + + private static RaygunResponseMessage BuildResponseMessageFromStructureValue(StructureValue responseMessageStructure) + { + var responseMessage = new RaygunResponseMessage(); + + foreach (var property in responseMessageStructure.Properties) + { + switch (property.Name) + { + case nameof(RaygunResponseMessage.Content): + responseMessage.Content = property.AsString(); + break; + case nameof(RaygunResponseMessage.StatusCode): + responseMessage.StatusCode = property.AsInteger(); + break; + case nameof(RaygunResponseMessage.StatusDescription): + responseMessage.StatusDescription = property.AsString(); + break; + } + } + + return responseMessage; + } + + private static RaygunRequestMessage BuildRequestMessageFromStructureValue(StructureValue requestMessageStructure) + { + var requestMessage = new RaygunRequestMessage(); + + foreach (var property in requestMessageStructure.Properties) + { + switch (property.Name) + { + case nameof(RaygunRequestMessage.Url): + requestMessage.Url = property.AsString(); + break; + case nameof(RaygunRequestMessage.HostName): + requestMessage.HostName = property.AsString(); + break; + case nameof(RaygunRequestMessage.HttpMethod): + requestMessage.HttpMethod = property.AsString(); + break; + case nameof(RaygunRequestMessage.IPAddress): + requestMessage.IPAddress = property.AsString(); + break; + case nameof(RaygunRequestMessage.RawData): + requestMessage.RawData = property.AsString(); + break; + case nameof(RaygunRequestMessage.Headers): + requestMessage.Headers = property.AsDictionary(); + break; + case nameof(RaygunRequestMessage.QueryString): + requestMessage.QueryString = property.AsDictionary(); + break; + case nameof(RaygunRequestMessage.Form): + requestMessage.Form = property.AsDictionary(); + break; + case nameof(RaygunRequestMessage.Data): + requestMessage.Data = property.AsDictionary(); + break; + } + } + + return requestMessage; + } } }