From 7aef559303d934e11532444aabbbbeae9bc68925 Mon Sep 17 00:00:00 2001 From: Rafal Zabrowarny Date: Sun, 12 Nov 2017 16:16:02 +0100 Subject: [PATCH 1/3] Logging facility --- .gitattributes | 63 +++++++++++++++ ...erviceStack.Serilog.RequestLogsFeature.sln | 25 ++++++ .../Logging/LatestLogEntriesCollector.cs | 72 +++++++++++++++++ .../Logging/LogEventFactory.cs | 64 +++++++++++++++ .../Logging/RequestLogger.cs | 60 +++++++++++++++ .../Logging/RequestLoggerOptions.cs | 28 +++++++ .../Plugin/Feature.cs | 68 ++++++++++++++++ .../Plugin/FeatureService.cs | 25 ++++++ .../Plugin/FeatureValidator.cs | 14 ++++ .../Properties/AssemblyInfo.cs | 36 +++++++++ ...iceStack.Serilog.RequestLogsFeature.csproj | 77 +++++++++++++++++++ .../packages.config | 10 +++ 12 files changed, 542 insertions(+) create mode 100644 .gitattributes create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature.sln create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Logging/LatestLogEntriesCollector.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLoggerOptions.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureService.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureValidator.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Properties/AssemblyInfo.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/packages.config diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/src/ServiceStack.Serilog.RequestLogsFeature.sln b/src/ServiceStack.Serilog.RequestLogsFeature.sln new file mode 100644 index 0000000..8780991 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2005 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceStack.Serilog.RequestLogsFeature", "ServiceStack.Serilog.RequestLogsFeature\ServiceStack.Serilog.RequestLogsFeature.csproj", "{1ABEF0BF-672E-4559-AFC8-B28138CCB19B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1ABEF0BF-672E-4559-AFC8-B28138CCB19B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ABEF0BF-672E-4559-AFC8-B28138CCB19B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ABEF0BF-672E-4559-AFC8-B28138CCB19B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ABEF0BF-672E-4559-AFC8-B28138CCB19B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8A43D06A-F29A-4F9A-9ECA-2D13FAAA8190} + EndGlobalSection +EndGlobal diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LatestLogEntriesCollector.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LatestLogEntriesCollector.cs new file mode 100644 index 0000000..6cc1f2b --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LatestLogEntriesCollector.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ServiceStack.Auth; +using ServiceStack.Host; +using ServiceStack.Support; +using ServiceStack.Web; + +namespace ServiceStack.Serilog.RequestLogsFeature.Logging +{ + internal class LatestLogEntriesCollector + { + private ConcurrentQueue LatestLogs { get; } = new ConcurrentQueue(); + private const int MAX_LATEST_LOG_ENTRIES = 1000; + + + internal void TryAdd(IRequest request, object response, TimeSpan elapsed, RequestLoggerOptions opt = null) + { + if (LatestLogs.Count < LatestLogEntriesCollector.MAX_LATEST_LOG_ENTRIES || LatestLogs.TryDequeue(out RequestLogEntry entry)) + LatestLogs.Enqueue(CreateEntry(request, response, elapsed, opt)); + } + + internal List GetLatestLogs(int? take) + { + return LatestLogs + .Take(take ?? int.MaxValue) + .ToList(); + + } + + private RequestLogEntry CreateEntry(IRequest request, object response, TimeSpan elapsed, RequestLoggerOptions opt = null) + { + var entry = new RequestLogEntry(); + + entry.Id = request.GetId().ToString().ToInt64(); + entry.DateTime = DateTime.Now; + entry.StatusCode = request.Response.StatusCode; + entry.StatusDescription = request.Response.StatusDescription; + entry.HttpMethod = request.Verb.ToUpper(); + entry.AbsoluteUri = request.AbsoluteUri; + entry.PathInfo = request.PathInfo; + entry.RequestBody = opt != null && opt.EnableRequestBodyTracking ? request.GetRawBody() : String.Empty; + entry.RequestDto = request.Dto; + entry.UserAuthId = request.GetSession()?.UserAuthId; + entry.SessionId = request.GetSessionId(); + entry.IpAddress = request.UserHostAddress; + entry.ForwardedFor = request.Headers[HttpHeaders.XForwardedFor]; + entry.Referer = request.Headers[HttpHeaders.Referer]; + entry.Headers = request.Headers.ToDictionary(); + entry.FormData = request.FormData.ToDictionary(); + entry.Items = request.Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString()); + entry.Session = opt != null && opt.EnableSessionTracking ? request.GetSession() : null; + entry.ResponseDto = request.GetResponseDto(); + + entry.ErrorResponse = InMemoryRollingRequestLogger.ToSerializableErrorResponse(request.Response); + if(response is Exception exception) + { + exception = exception.InnerException ?? exception; + entry.ExceptionSource = exception.Source; + entry.ExceptionData = exception.Data; + + } + + entry.RequestDuration = elapsed; + + return entry; + } + } +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs new file mode 100644 index 0000000..7036797 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Serilog.Core; +using Serilog.Events; +using Serilog.Parsing; +using ServiceStack.Web; + +namespace ServiceStack.Serilog.RequestLogsFeature.Logging +{ + internal class LogEventFactory + { + private static readonly string Property_HttpMethod_Key = "Method"; + private static readonly string Property_Url_Key = "Url"; + private static readonly string Property_StatusCode_Key = "Status"; + private static readonly string Property_Headers_Key = "Headers"; + private static readonly string Property_Body_Key = "Body"; + + private static readonly MessageTemplate LogEventMessageTemplate = + new MessageTemplateParser().Parse($@"HTTP {{{Property_HttpMethod_Key}}} {{{Property_Url_Key}}} responded {{{Property_StatusCode_Key}}}"); + + internal LogEvent Create(IRequest request, RequestLoggerOptions opt) => + new LogEvent( + timestamp: DateTimeOffset.Now, + exception: null, + level: LogEventLevel.Information, + messageTemplate: LogEventFactory.LogEventMessageTemplate, + properties: GetProperties(request, opt) + ); + + private static IEnumerable GetProperties(IRequest request, RequestLoggerOptions opt) + { + yield return WithHttpMethod(request); + yield return WithUrl(request); + yield return WithStatusCode(request); + yield return WithHeaders(request); + if(opt.EnableRequestBodyTracking)yield return WithRequestBody(request); + yield break; + } + + private static LogEventProperty WithHttpMethod(IRequest request) + => new LogEventProperty($"{Property_HttpMethod_Key}", new ScalarValue(request.Verb.ToUpper())); + + private static LogEventProperty WithUrl(IRequest request) + => new LogEventProperty($"{Property_Url_Key}", new ScalarValue(request.PathInfo)); + + private static LogEventProperty WithStatusCode(IRequest request) + => new LogEventProperty($"{Property_StatusCode_Key}", new ScalarValue(request.Response.StatusCode)); + + private static LogEventProperty WithHeaders(IRequest request) + { + var headersAsLogEventProps = request + .Headers + .ToDictionary() + .Select(dictItem => new LogEventProperty(dictItem.Key, new ScalarValue(dictItem.Value))) + ; + + return new LogEventProperty($"{Property_Headers_Key}", new StructureValue(headersAsLogEventProps)); + } + + private static LogEventProperty WithRequestBody(IRequest request) + => new LogEventProperty($"{Property_Body_Key}", new ScalarValue(request?.GetRawBody() ?? String.Empty)); + } +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs new file mode 100644 index 0000000..8d74442 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Serilog; +using ServiceStack.Host; +using ServiceStack.Web; + +namespace ServiceStack.Serilog.RequestLogsFeature.Logging +{ + public class RequestLogger : IRequestLogger + { + private LogEventFactory LogEventFactory{ get; } = new LogEventFactory(); + private InMemoryRollingRequestLogger LatestLogEntriesCollector { get; } = new InMemoryRollingRequestLogger(capacity:1000); + private RequestLoggerOptions Options { get; set; } = new RequestLoggerOptions(); + + public bool EnableSessionTracking { get => Options.EnableSessionTracking; + set => LatestLogEntriesCollector.EnableSessionTracking = Options.EnableSessionTracking = value; } + public bool EnableRequestBodyTracking { get => Options.EnableRequestBodyTracking; + set => LatestLogEntriesCollector.EnableRequestBodyTracking = Options.EnableRequestBodyTracking = value; } + public bool EnableResponseTracking { get => Options.EnableResponseTracking; + set => LatestLogEntriesCollector.EnableResponseTracking = Options.EnableResponseTracking = value; } + public bool EnableErrorTracking { get => Options.EnableErrorTracking; + set => LatestLogEntriesCollector.EnableErrorTracking = Options.EnableErrorTracking = value; } + public bool LimitToServiceRequests { get => Options.LimitToServiceRequests; + set => LatestLogEntriesCollector.LimitToServiceRequests = Options.LimitToServiceRequests = value; } + public string[] RequiredRoles { get => Options.RequiredRoles; + set => LatestLogEntriesCollector.RequiredRoles = Options.RequiredRoles = value; } + public Func SkipLogging { get => Options.SkipLogging; + set => LatestLogEntriesCollector.SkipLogging = Options.SkipLogging = value; } + public Type[] ExcludeRequestDtoTypes { get => Options.ExcludeRequestDtoTypes; + set => LatestLogEntriesCollector.ExcludeRequestDtoTypes = Options.ExcludeRequestDtoTypes = value; } + public Type[] HideRequestBodyForRequestDtoTypes { get => Options.HideRequestBodyForRequestDtoTypes; set => Options.HideRequestBodyForRequestDtoTypes = value; } + + public List GetLatestLogs(int? take) + { + return LatestLogEntriesCollector.GetLatestLogs(take); + } + + public void Log(IRequest request, object requestDto, object response, TimeSpan elapsed) + { + RequestLoggerOptions loggingOptions = Options.Clone() as RequestLoggerOptions; + + if (request.Items.ContainsKey(Plugin.Feature.SerilogRequestLogsLoggerKey)) + { + var logger = request.Items[Plugin.Feature.SerilogRequestLogsLoggerKey] as ILogger; + + var logEvent = LogEventFactory + .Create(request, loggingOptions); + + logger + .ForContext() + .Write(logEvent); + + LatestLogEntriesCollector + .Log(request, requestDto, response, elapsed); + } + } + } +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLoggerOptions.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLoggerOptions.cs new file mode 100644 index 0000000..85db4c1 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLoggerOptions.cs @@ -0,0 +1,28 @@ +using System; +using ServiceStack.Web; + +namespace ServiceStack.Serilog.RequestLogsFeature.Logging +{ + internal class RequestLoggerOptions : ICloneable + { + public bool EnableSessionTracking { get; set; } + public bool EnableRequestBodyTracking { get; set; } + public bool EnableResponseTracking { get; set; } + public bool EnableErrorTracking { get; set; } + public bool LimitToServiceRequests { get; set; } + public string[] RequiredRoles { get; set; } + public Func SkipLogging { get; set; } + public Type[] ExcludeRequestDtoTypes { get; set; } + public Type[] HideRequestBodyForRequestDtoTypes { get; set; } + + internal RequestLoggerOptions GetCopy() + { + return (RequestLoggerOptions)MemberwiseClone(); + } + + public object Clone() + { + return GetCopy(); + } + } +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs new file mode 100644 index 0000000..8b4c209 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using Serilog; +using ServiceStack.FluentValidation; +using ServiceStack.Logging.Serilog; +using ServiceStack.Serilog.RequestLogsFeature.Logging; +using ServiceStack.Web; + +namespace ServiceStack.Serilog.RequestLogsFeature.Plugin +{ + public class Feature : IPlugin + { + private readonly FeatureValidator Validator = new FeatureValidator(); + + public const string SerilogRequestLogsLoggerKey = "SerilogRequestLogs.Logger"; + + /// + /// SerilogRequestLogs service Route, default is /serilogrequestlogs + /// + public string AtRestPath { get; set; } + + /// + /// Serilog Logger instance + /// + public IRequestLogger RequestLogger { get; set; } + + public Feature() + { + AtRestPath = "/serilogrequestlogs"; + } + + public void Register(IAppHost appHost) + { + var requestLogger = new RequestLogger(); + + //Validator.ValidateAndThrow(this); + + appHost.GlobalRequestFiltersAsync.Add(RequestFilter); + appHost.GlobalResponseFilters.Add(ResponseFilter); + + appHost.Register(requestLogger); + + appHost.RegisterService(AtRestPath); + + appHost.GetPlugin() + .AddDebugLink(AtRestPath, "Serilog Request Logs"); + } + + private Task RequestFilter(IRequest request, IResponse response, object dto) + { + if (request.Items.ContainsKey(SerilogRequestLogsLoggerKey)) + request.Items.Remove(SerilogRequestLogsLoggerKey); + + request.Items.Add(SerilogRequestLogsLoggerKey, CreateSerilogLogger()); + + return Task.CompletedTask; + } + + private void ResponseFilter(IRequest request, IResponse response, object dto) + { + + } + + private ILogger CreateSerilogLogger() + { + return Log.Logger; + } + } +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureService.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureService.cs new file mode 100644 index 0000000..bcab317 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace ServiceStack.Serilog.RequestLogsFeature.Plugin +{ + public class SerilogRequestLogs + { + } + + public class SerilogRequestLogsResponse + { + public SerilogRequestLogsResponse() + { + this.Results = new List(); + } + + public List Results { get; set; } + public ResponseStatus ResponseStatus { get; set; } + } + + [DefaultRequest(typeof(SerilogRequestLogs))] + [Restrict(VisibilityTo = RequestAttributes.None)] + public class FeatureService : Service + { + } +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureValidator.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureValidator.cs new file mode 100644 index 0000000..04bcf92 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureValidator.cs @@ -0,0 +1,14 @@ +using ServiceStack.FluentValidation; + +namespace ServiceStack.Serilog.RequestLogsFeature.Plugin +{ + public class FeatureValidator : AbstractValidator + { + public FeatureValidator() + { + RuleFor(feat => feat.RequestLogger) + .NotNull() + ; + } + } +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Properties/AssemblyInfo.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7c0bf16 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ServiceStack.Serilog.RequestLogsFeature")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ServiceStack.Serilog.RequestLogsFeature")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1abef0bf-672e-4559-afc8-b28138ccb19b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj b/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj new file mode 100644 index 0000000..aa858f1 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj @@ -0,0 +1,77 @@ + + + + + Debug + AnyCPU + {1ABEF0BF-672E-4559-AFC8-B28138CCB19B} + Library + Properties + ServiceStack.Serilog.RequestLogsFeature + ServiceStack.Serilog.RequestLogsFeature + v4.6.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Serilog.2.5.0\lib\net46\Serilog.dll + + + ..\packages\ServiceStack.4.5.14\lib\net45\ServiceStack.dll + + + ..\packages\ServiceStack.Client.4.5.14\lib\net45\ServiceStack.Client.dll + + + ..\packages\ServiceStack.Common.4.5.14\lib\net45\ServiceStack.Common.dll + + + ..\packages\ServiceStack.Interfaces.4.5.14\lib\portable-wp80+sl5+net45+win8+wpa81+monotouch+monoandroid+xamarin.ios10\ServiceStack.Interfaces.dll + + + ..\packages\ServiceStack.Logging.Serilog.4.5.14\lib\net45\ServiceStack.Logging.Serilog.dll + + + ..\packages\ServiceStack.Text.4.5.14\lib\net45\ServiceStack.Text.dll + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/packages.config b/src/ServiceStack.Serilog.RequestLogsFeature/packages.config new file mode 100644 index 0000000..c826f9a --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file From 9aca4fc51c270d3ef0d1e9fc376265291c7b70d9 Mon Sep 17 00:00:00 2001 From: Rafal Zabrowarny Date: Sun, 12 Nov 2017 23:34:44 +0100 Subject: [PATCH 2/3] Configuration of ServiceStack plugin (either by appsettings or by service calls) --- .../Logging/LogEventFactory.cs | 58 ++++++++++++--- .../Logging/RequestLogger.cs | 16 +++-- .../Plugin/Feature.cs | 29 +++++--- .../Plugin/FeatureConfig.cs | 29 ++++++++ .../Plugin/FeatureService.cs | 70 ++++++++++++++++++- .../Plugin/FeatureValidator.cs | 2 +- ...iceStack.Serilog.RequestLogsFeature.csproj | 2 + 7 files changed, 177 insertions(+), 29 deletions(-) create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureConfig.cs diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs index 7036797..dcd7dd9 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs @@ -15,26 +15,55 @@ internal class LogEventFactory private static readonly string Property_StatusCode_Key = "Status"; private static readonly string Property_Headers_Key = "Headers"; private static readonly string Property_Body_Key = "Body"; + private static readonly string Property_Form_Key = "Form"; private static readonly MessageTemplate LogEventMessageTemplate = new MessageTemplateParser().Parse($@"HTTP {{{Property_HttpMethod_Key}}} {{{Property_Url_Key}}} responded {{{Property_StatusCode_Key}}}"); - internal LogEvent Create(IRequest request, RequestLoggerOptions opt) => + internal LogEvent Create(IRequest request, object dto, RequestLoggerOptions opt) => new LogEvent( timestamp: DateTimeOffset.Now, exception: null, level: LogEventLevel.Information, messageTemplate: LogEventFactory.LogEventMessageTemplate, - properties: GetProperties(request, opt) + properties: GetProperties(request, dto, opt) ); - private static IEnumerable GetProperties(IRequest request, RequestLoggerOptions opt) + private static IEnumerable GetProperties(IRequest request, object dto, RequestLoggerOptions opt) { - yield return WithHttpMethod(request); - yield return WithUrl(request); - yield return WithStatusCode(request); - yield return WithHeaders(request); - if(opt.EnableRequestBodyTracking)yield return WithRequestBody(request); + if(request != null) + { + yield return WithHttpMethod(request); + yield return WithUrl(request); + yield return WithStatusCode(request); + yield return WithHeaders(request); + } + + Type requestDtoType = dto.GetType(); + if(request != null + && + ( + opt.HideRequestBodyForRequestDtoTypes == null + || + opt.HideRequestBodyForRequestDtoTypes.All(@type => @type != requestDtoType) + ) + && + opt.EnableRequestBodyTracking + ) + { + yield return WithRequestBody(request); + yield return WithFormData(request); + } + + if(request != null + && + request.IsErrorResponse() + && + opt.EnableErrorTracking + ) + { + } + yield break; } @@ -60,5 +89,18 @@ private static LogEventProperty WithHeaders(IRequest request) private static LogEventProperty WithRequestBody(IRequest request) => new LogEventProperty($"{Property_Body_Key}", new ScalarValue(request?.GetRawBody() ?? String.Empty)); + + private static LogEventProperty WithFormData(IRequest request) + { + var formDataAsLogEventProps = request + .FormData + .ToDictionary() + .Select(dictItem => new LogEventProperty(dictItem.Key, new ScalarValue(dictItem.Value))) + ; + + return new LogEventProperty($"{Property_Form_Key}", new StructureValue(formDataAsLogEventProps)); + } + + } } diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs index 8d74442..9ac788c 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs @@ -41,16 +41,20 @@ public void Log(IRequest request, object requestDto, object response, TimeSpan e { RequestLoggerOptions loggingOptions = Options.Clone() as RequestLoggerOptions; - if (request.Items.ContainsKey(Plugin.Feature.SerilogRequestLogsLoggerKey)) + if (request.Items.ContainsKey(Plugin.SerilogRequestLogsFeature.SerilogRequestLogsLoggerKey)) { - var logger = request.Items[Plugin.Feature.SerilogRequestLogsLoggerKey] as ILogger; var logEvent = LogEventFactory - .Create(request, loggingOptions); + .Create(request, requestDto, loggingOptions); - logger - .ForContext() - .Write(logEvent); + if(logEvent != null) + { + var logger = request.Items[Plugin.SerilogRequestLogsFeature.SerilogRequestLogsLoggerKey] as ILogger; + + logger + .ForContext() + .Write(logEvent); + } LatestLogEntriesCollector .Log(request, requestDto, response, elapsed); diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs index 8b4c209..9a5a068 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs @@ -1,13 +1,12 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Serilog; -using ServiceStack.FluentValidation; -using ServiceStack.Logging.Serilog; using ServiceStack.Serilog.RequestLogsFeature.Logging; using ServiceStack.Web; namespace ServiceStack.Serilog.RequestLogsFeature.Plugin { - public class Feature : IPlugin + public class SerilogRequestLogsFeature : IPlugin { private readonly FeatureValidator Validator = new FeatureValidator(); @@ -19,24 +18,29 @@ public class Feature : IPlugin public string AtRestPath { get; set; } /// - /// Serilog Logger instance + /// Request Logger instance /// public IRequestLogger RequestLogger { get; set; } - public Feature() + /// + /// Delegate used to construct custom serilog logger instance + /// + public Func SerilogLoggerFactory { get; set; } + + public SerilogRequestLogsFeature() { AtRestPath = "/serilogrequestlogs"; } public void Register(IAppHost appHost) { - var requestLogger = new RequestLogger(); - //Validator.ValidateAndThrow(this); appHost.GlobalRequestFiltersAsync.Add(RequestFilter); appHost.GlobalResponseFilters.Add(ResponseFilter); - + + var requestLogger = new RequestLogger(); + requestLogger = new FeatureConfig().ApplyAppSettings(requestLogger, appHost); appHost.Register(requestLogger); appHost.RegisterService(AtRestPath); @@ -57,12 +61,15 @@ private Task RequestFilter(IRequest request, IResponse response, object dto) private void ResponseFilter(IRequest request, IResponse response, object dto) { - + request.Items.Remove(SerilogRequestLogsLoggerKey); } private ILogger CreateSerilogLogger() { - return Log.Logger; + return SerilogLoggerFactory != null + ? SerilogLoggerFactory.Invoke() + : Log.Logger + ; } } } diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureConfig.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureConfig.cs new file mode 100644 index 0000000..fd88125 --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureConfig.cs @@ -0,0 +1,29 @@ +using ServiceStack.Configuration; +using ServiceStack.Serilog.RequestLogsFeature.Logging; + +namespace ServiceStack.Serilog.RequestLogsFeature.Plugin +{ + public class FeatureConfig + { + public const string EnableSessionTrackingKey = "serilogrequestlogs:EnableSessionTracking"; + public const string EnableRequestBodyTrackingKey = "serilogrequestlogs:EnableRequestBodyTracking"; + public const string EnableResponseTrackingKey = "serilogrequestlogs:EnableResponseTracking"; + public const string EnableErrorTrackingKey = "serilogrequestlogs:EnableErrorTracking"; + public const string LimitToServiceRequestsKey = "serilogrequestlogs:LimitToServiceRequests"; + + public RequestLogger ApplyAppSettings(RequestLogger logger, IAppHost appHost) + { + if (logger == null) + return logger; + + var appSettings = new AppSettings(); + logger.EnableRequestBodyTracking = appSettings.Get(EnableSessionTrackingKey); + logger.EnableRequestBodyTracking = appSettings.Get(EnableRequestBodyTrackingKey); + logger.EnableResponseTracking = appSettings.Get(EnableResponseTrackingKey); + logger.EnableErrorTracking = appSettings.Get(EnableErrorTrackingKey); + logger.LimitToServiceRequests = appSettings.Get(LimitToServiceRequestsKey); + + return logger; + } + } +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureService.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureService.cs index bcab317..3caddba 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureService.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureService.cs @@ -1,11 +1,27 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using ServiceStack.DataAnnotations; +using ServiceStack.Web; namespace ServiceStack.Serilog.RequestLogsFeature.Plugin { + [Exclude(ServiceStack.Feature.Soap)] + [DataContract] public class SerilogRequestLogs { + [DataMember(Order = 1)] public bool? EnableSessionTracking { get; set; } + [DataMember(Order = 2)] public bool? EnableRequestBodyTracking { get; set; } + [DataMember(Order = 3)] public bool? EnableResponseTracking { get; set; } + [DataMember(Order = 4)] public bool? EnableErrorTracking { get; set; } + [DataMember(Order = 5)] public bool? LimitToServiceRequests { get; set; } + [DataMember(Order = 6)] public int Skip { get; set; } + [DataMember(Order = 7)] public int? Take { get; set; } } + [Exclude(ServiceStack.Feature.Soap)] + [DataContract] public class SerilogRequestLogsResponse { public SerilogRequestLogsResponse() @@ -13,13 +29,61 @@ public SerilogRequestLogsResponse() this.Results = new List(); } - public List Results { get; set; } - public ResponseStatus ResponseStatus { get; set; } + [DataMember(Order = 1)] public List Results { get; set; } + [DataMember(Order = 2)] public Dictionary Usage { get; set; } + [DataMember(Order = 3)] public ResponseStatus ResponseStatus { get; set; } } [DefaultRequest(typeof(SerilogRequestLogs))] [Restrict(VisibilityTo = RequestAttributes.None)] public class FeatureService : Service { + private static readonly Dictionary Usage = new Dictionary { + ["bool EnableSessionTracking"] = "Turn On/Off Tracking of Session", + ["bool EnableRequestBodyTracking"] = "Turn On/Off Tracking of Request Body", + ["bool EnableResponseTracking"] = "Turn On/Off Tracking of Responses", + ["bool EnableErrorTracking"] = "Turn On/Off Tracking of Errors", + ["bool LimitToServiceRequests"] = "Turn On/Off Limiting of Service Requests", + ["int Skip"] = "Skip past N results", + ["int Take"] = "Only look at last N results" + }; + + public IRequestLogger RequestLogger { get; set; } + + public SerilogRequestLogsResponse Any(SerilogRequestLogs request) + { + if (RequestLogger == null) + throw new Exception("No IRequestLogger is registered"); + + if (!HostContext.DebugMode) + RequiredRoleAttribute.AssertRequiredRoles(Request, RequestLogger.RequiredRoles); + + if (request.EnableSessionTracking.HasValue) + RequestLogger.EnableSessionTracking = request.EnableSessionTracking.Value; + + if (request.EnableRequestBodyTracking.HasValue) + RequestLogger.EnableRequestBodyTracking = request.EnableRequestBodyTracking.Value; + + if (request.LimitToServiceRequests.HasValue) + RequestLogger.LimitToServiceRequests = request.LimitToServiceRequests.Value; + + if (request.EnableResponseTracking.HasValue) + RequestLogger.EnableResponseTracking = request.EnableResponseTracking.Value; + + if (request.EnableErrorTracking.HasValue) + RequestLogger.EnableErrorTracking = request.EnableErrorTracking.Value; + + var logs = RequestLogger + .GetLatestLogs(request.Take) + .Skip(request.Skip) + .OrderByDescending(x => x.Id) + .ToList(); + + return new SerilogRequestLogsResponse + { + Results = logs, + Usage = Usage + }; + } } } diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureValidator.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureValidator.cs index 04bcf92..da610ec 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureValidator.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureValidator.cs @@ -2,7 +2,7 @@ namespace ServiceStack.Serilog.RequestLogsFeature.Plugin { - public class FeatureValidator : AbstractValidator + public class FeatureValidator : AbstractValidator { public FeatureValidator() { diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj b/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj index aa858f1..27eb7de 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj +++ b/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj @@ -53,6 +53,7 @@ + @@ -64,6 +65,7 @@ + From 3453d8503fbe654da66ca6a3686ef98bddeaee12 Mon Sep 17 00:00:00 2001 From: zabrowarnyrafal Date: Tue, 14 Nov 2017 14:25:17 +0100 Subject: [PATCH 3/3] assert request should be logged; log error response; log elapsed time; log session object; log response dto --- .../Logging/LatestLogEntriesCollector.cs | 72 --------- .../Logging/LogEntryPropertiesGenerator.cs | 8 + .../Logging/LogEventFactory.cs | 140 +++++++++++++----- .../Logging/RequestLogger.cs | 35 ++++- .../Logging/RequestLoggerOptions.cs | 7 +- .../Plugin/Feature.cs | 53 ++++++- .../Plugin/FeatureConfig.cs | 2 +- ...iceStack.Serilog.RequestLogsFeature.csproj | 2 +- 8 files changed, 200 insertions(+), 119 deletions(-) delete mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Logging/LatestLogEntriesCollector.cs create mode 100644 src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEntryPropertiesGenerator.cs diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LatestLogEntriesCollector.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LatestLogEntriesCollector.cs deleted file mode 100644 index 6cc1f2b..0000000 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LatestLogEntriesCollector.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using ServiceStack.Auth; -using ServiceStack.Host; -using ServiceStack.Support; -using ServiceStack.Web; - -namespace ServiceStack.Serilog.RequestLogsFeature.Logging -{ - internal class LatestLogEntriesCollector - { - private ConcurrentQueue LatestLogs { get; } = new ConcurrentQueue(); - private const int MAX_LATEST_LOG_ENTRIES = 1000; - - - internal void TryAdd(IRequest request, object response, TimeSpan elapsed, RequestLoggerOptions opt = null) - { - if (LatestLogs.Count < LatestLogEntriesCollector.MAX_LATEST_LOG_ENTRIES || LatestLogs.TryDequeue(out RequestLogEntry entry)) - LatestLogs.Enqueue(CreateEntry(request, response, elapsed, opt)); - } - - internal List GetLatestLogs(int? take) - { - return LatestLogs - .Take(take ?? int.MaxValue) - .ToList(); - - } - - private RequestLogEntry CreateEntry(IRequest request, object response, TimeSpan elapsed, RequestLoggerOptions opt = null) - { - var entry = new RequestLogEntry(); - - entry.Id = request.GetId().ToString().ToInt64(); - entry.DateTime = DateTime.Now; - entry.StatusCode = request.Response.StatusCode; - entry.StatusDescription = request.Response.StatusDescription; - entry.HttpMethod = request.Verb.ToUpper(); - entry.AbsoluteUri = request.AbsoluteUri; - entry.PathInfo = request.PathInfo; - entry.RequestBody = opt != null && opt.EnableRequestBodyTracking ? request.GetRawBody() : String.Empty; - entry.RequestDto = request.Dto; - entry.UserAuthId = request.GetSession()?.UserAuthId; - entry.SessionId = request.GetSessionId(); - entry.IpAddress = request.UserHostAddress; - entry.ForwardedFor = request.Headers[HttpHeaders.XForwardedFor]; - entry.Referer = request.Headers[HttpHeaders.Referer]; - entry.Headers = request.Headers.ToDictionary(); - entry.FormData = request.FormData.ToDictionary(); - entry.Items = request.Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString()); - entry.Session = opt != null && opt.EnableSessionTracking ? request.GetSession() : null; - entry.ResponseDto = request.GetResponseDto(); - - entry.ErrorResponse = InMemoryRollingRequestLogger.ToSerializableErrorResponse(request.Response); - if(response is Exception exception) - { - exception = exception.InnerException ?? exception; - entry.ExceptionSource = exception.Source; - entry.ExceptionData = exception.Data; - - } - - entry.RequestDuration = elapsed; - - return entry; - } - } -} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEntryPropertiesGenerator.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEntryPropertiesGenerator.cs new file mode 100644 index 0000000..fbab0fb --- /dev/null +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEntryPropertiesGenerator.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using Serilog.Events; +using ServiceStack.Web; + +namespace ServiceStack.Serilog.RequestLogsFeature.Logging +{ + public delegate IEnumerable LogEntryPropertiesGenerator(IRequest request, object requestDto, object responseDto); +} diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs index dcd7dd9..6695d71 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Serilog.Core; +using Serilog; using Serilog.Events; using Serilog.Parsing; using ServiceStack.Web; @@ -12,69 +12,79 @@ internal class LogEventFactory { private static readonly string Property_HttpMethod_Key = "Method"; private static readonly string Property_Url_Key = "Url"; - private static readonly string Property_StatusCode_Key = "Status"; + private static readonly string Property_StatusCode_Key = "StatusCode"; + private static readonly string Property_StatusDesc_Key = "StatusDescription"; + private static readonly string Property_Elapsed_Key = "Elasped"; private static readonly string Property_Headers_Key = "Headers"; private static readonly string Property_Body_Key = "Body"; private static readonly string Property_Form_Key = "Form"; + private static readonly string Property_ReqDto_Key = "RequestDto"; + private static readonly string Property_Session_Key = "Session"; + private static readonly string Property_Response_Key = "Response"; + private static readonly string Property_Error_Key = "Error"; private static readonly MessageTemplate LogEventMessageTemplate = - new MessageTemplateParser().Parse($@"HTTP {{{Property_HttpMethod_Key}}} {{{Property_Url_Key}}} responded {{{Property_StatusCode_Key}}}"); + new MessageTemplateParser().Parse($@"HTTP {{{Property_HttpMethod_Key}}} {{{Property_Url_Key}}} responded {{{Property_StatusCode_Key}}} in {{{Property_Elapsed_Key}}} ms"); - internal LogEvent Create(IRequest request, object dto, RequestLoggerOptions opt) => - new LogEvent( + internal LogEvent Create(IRequest request, object requestDto, object responseDto, TimeSpan elapsed, RequestLoggerOptions opt) + => new LogEvent( timestamp: DateTimeOffset.Now, exception: null, level: LogEventLevel.Information, messageTemplate: LogEventFactory.LogEventMessageTemplate, - properties: GetProperties(request, dto, opt) + properties: GetProperties(request, requestDto, responseDto, elapsed, opt) ); - private static IEnumerable GetProperties(IRequest request, object dto, RequestLoggerOptions opt) + private static IEnumerable GetProperties(IRequest request, object requestDto, object responseDto, TimeSpan elapsed, RequestLoggerOptions opt) { - if(request != null) - { - yield return WithHttpMethod(request); - yield return WithUrl(request); - yield return WithStatusCode(request); - yield return WithHeaders(request); - } + yield return WithHttpMethod(request); + yield return WithUrl(request); + yield return WithHeaders(request); + yield return WithStatusCode(request); + yield return WithStatusDescription(request); + yield return WithElapsedTime(elapsed); + - Type requestDtoType = dto.GetType(); - if(request != null - && - ( - opt.HideRequestBodyForRequestDtoTypes == null - || - opt.HideRequestBodyForRequestDtoTypes.All(@type => @type != requestDtoType) + bool IsRequestDtoExcludedFromLogging(Type dtoType) => opt.HideRequestBodyForRequestDtoTypes != null && opt.HideRequestBodyForRequestDtoTypes.Any(@type => @type != dtoType); + Type requestDtoType = (requestDto ?? request.Dto)?.GetType(); + if (requestDtoType != null + && !IsRequestDtoExcludedFromLogging(requestDtoType) + && opt.EnableRequestBodyTracking ) - && - opt.EnableRequestBodyTracking - ) { yield return WithRequestBody(request); yield return WithFormData(request); + yield return WithRequestDto(request, requestDto); } - - if(request != null - && - request.IsErrorResponse() - && - opt.EnableErrorTracking - ) + + + if (opt.EnableSessionTracking) yield return WithSession(request); + if (opt.EnableResponseTracking) yield return WithResponse(request, responseDto); + + if (request.IsErrorResponse() && opt.EnableErrorTracking) { + var prop = WithErrorResponse(request, responseDto); + if (prop != null) yield return prop; } - + + foreach (var prop in WithPropertiesFromDelegate(request, requestDto, responseDto, opt.LogEntryPropertiesGenerator)) + yield return prop; + + yield break; } private static LogEventProperty WithHttpMethod(IRequest request) - => new LogEventProperty($"{Property_HttpMethod_Key}", new ScalarValue(request.Verb.ToUpper())); + => new LogEventProperty(Property_HttpMethod_Key, new ScalarValue(request.Verb.ToUpper())); private static LogEventProperty WithUrl(IRequest request) - => new LogEventProperty($"{Property_Url_Key}", new ScalarValue(request.PathInfo)); + => new LogEventProperty(Property_Url_Key, new ScalarValue(request.PathInfo)); private static LogEventProperty WithStatusCode(IRequest request) - => new LogEventProperty($"{Property_StatusCode_Key}", new ScalarValue(request.Response.StatusCode)); + => new LogEventProperty(Property_StatusCode_Key, new ScalarValue(request.Response.StatusCode)); + + private static LogEventProperty WithStatusDescription(IRequest request) + => new LogEventProperty(Property_StatusDesc_Key, new ScalarValue(request.Response.StatusDescription)); private static LogEventProperty WithHeaders(IRequest request) { @@ -84,23 +94,73 @@ private static LogEventProperty WithHeaders(IRequest request) .Select(dictItem => new LogEventProperty(dictItem.Key, new ScalarValue(dictItem.Value))) ; - return new LogEventProperty($"{Property_Headers_Key}", new StructureValue(headersAsLogEventProps)); + return new LogEventProperty(Property_Headers_Key, new StructureValue(headersAsLogEventProps)); } private static LogEventProperty WithRequestBody(IRequest request) - => new LogEventProperty($"{Property_Body_Key}", new ScalarValue(request?.GetRawBody() ?? String.Empty)); + => new LogEventProperty(Property_Body_Key, new ScalarValue(request?.GetRawBody() ?? String.Empty)); - private static LogEventProperty WithFormData(IRequest request) - { + private static LogEventProperty WithFormData(IRequest request){ var formDataAsLogEventProps = request .FormData .ToDictionary() .Select(dictItem => new LogEventProperty(dictItem.Key, new ScalarValue(dictItem.Value))) ; - return new LogEventProperty($"{Property_Form_Key}", new StructureValue(formDataAsLogEventProps)); + return new LogEventProperty(Property_Form_Key, new StructureValue(formDataAsLogEventProps)); + } + + private static LogEventProperty WithRequestDto(IRequest request, object requestDto){ + var dto = requestDto ?? request.Dto; + var logger = request.Items[Plugin.SerilogRequestLogsFeature.SerilogRequestLogsLoggerKey] as ILogger; + logger.BindProperty(Property_ReqDto_Key, dto, true, out LogEventProperty logEventProperty); + + return logEventProperty; + } + + private static LogEventProperty WithSession(IRequest request) + { + var session = request.GetSession(); + var logger = request.Items[Plugin.SerilogRequestLogsFeature.SerilogRequestLogsLoggerKey] as ILogger; + logger.BindProperty(Property_Session_Key, session, true, out LogEventProperty logEventProperty); + + return logEventProperty; + } + + private static LogEventProperty WithResponse(IRequest request, object responseDto) + { + var logger = request.Items[Plugin.SerilogRequestLogsFeature.SerilogRequestLogsLoggerKey] as ILogger; + logger.BindProperty(Property_Response_Key, responseDto, true, out LogEventProperty logEventProperty); + + return logEventProperty; + } + + private static LogEventProperty WithElapsedTime(TimeSpan elapsed) + => new LogEventProperty(Property_Elapsed_Key, new ScalarValue(Math.Ceiling(elapsed.TotalMilliseconds))); + + private static LogEventProperty WithErrorResponse(IRequest request, object responseDto) + { + var logger = request.Items[Plugin.SerilogRequestLogsFeature.SerilogRequestLogsLoggerKey] as ILogger; + + + if (responseDto is IHttpResult errorResult) + { + logger.BindProperty(Property_Error_Key, errorResult.Response, true, out LogEventProperty logEventProperty); + return logEventProperty; + } + + if(responseDto is Exception exception) + { + var responseStatus = (exception.InnerException ?? exception).ToResponseStatus(); + logger.BindProperty(Property_Error_Key, responseStatus, true, out LogEventProperty logEventProperty); + return logEventProperty; + } + + return null; } + private static IEnumerable WithPropertiesFromDelegate(IRequest request, object requestDto, object responseDto, LogEntryPropertiesGenerator propertiesGenerator) + => propertiesGenerator != null ? propertiesGenerator.Invoke(request, requestDto, responseDto) : Enumerable.Empty(); } } diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs index 9ac788c..a0f73c3 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLogger.cs @@ -30,7 +30,13 @@ public class RequestLogger : IRequestLogger set => LatestLogEntriesCollector.SkipLogging = Options.SkipLogging = value; } public Type[] ExcludeRequestDtoTypes { get => Options.ExcludeRequestDtoTypes; set => LatestLogEntriesCollector.ExcludeRequestDtoTypes = Options.ExcludeRequestDtoTypes = value; } - public Type[] HideRequestBodyForRequestDtoTypes { get => Options.HideRequestBodyForRequestDtoTypes; set => Options.HideRequestBodyForRequestDtoTypes = value; } + public Type[] HideRequestBodyForRequestDtoTypes { get => Options.HideRequestBodyForRequestDtoTypes; + set => Options.HideRequestBodyForRequestDtoTypes = value; } + + + public LogEntryPropertiesGenerator LogEntryPropertiesGenerator { get => Options.LogEntryPropertiesGenerator; + set => Options.LogEntryPropertiesGenerator = value; } + public List GetLatestLogs(int? take) { @@ -39,13 +45,16 @@ public List GetLatestLogs(int? take) public void Log(IRequest request, object requestDto, object response, TimeSpan elapsed) { + if(!AssertCanLog(request, requestDto)) + return; + RequestLoggerOptions loggingOptions = Options.Clone() as RequestLoggerOptions; if (request.Items.ContainsKey(Plugin.SerilogRequestLogsFeature.SerilogRequestLogsLoggerKey)) { var logEvent = LogEventFactory - .Create(request, requestDto, loggingOptions); + .Create(request, requestDto, response, elapsed, loggingOptions); if(logEvent != null) { @@ -60,5 +69,27 @@ public void Log(IRequest request, object requestDto, object response, TimeSpan e .Log(request, requestDto, response, elapsed); } } + + private bool AssertCanLog(IRequest request, object requestDto) => ShouldNotLog(request, requestDto).All(r => r == false); + + private IEnumerable ShouldNotLog(IRequest request, object requestDto) + { + yield return request == null; + + if (SkipLogging != null) + yield return SkipLogging.Invoke(request); + + if (LimitToServiceRequests) + yield return (requestDto ?? request?.Dto) == null; + + if (RequiredRoles != null && RequiredRoles.Any()) + yield return RequiredRoles.Except(request?.GetSession()?.Roles).Any() == false; + + if (LimitToServiceRequests && ExcludeRequestDtoTypes != null && ExcludeRequestDtoTypes.Any()) + yield return (requestDto ?? request.Dto) == null || ExcludeRequestDtoTypes.Contains((requestDto ?? request.Dto).GetType()); + + yield break; + + } } } diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLoggerOptions.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLoggerOptions.cs index 85db4c1..204083c 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLoggerOptions.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Logging/RequestLoggerOptions.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Generic; +using Serilog; +using Serilog.Events; using ServiceStack.Web; namespace ServiceStack.Serilog.RequestLogsFeature.Logging { + internal class RequestLoggerOptions : ICloneable { public bool EnableSessionTracking { get; set; } @@ -14,7 +18,8 @@ internal class RequestLoggerOptions : ICloneable public Func SkipLogging { get; set; } public Type[] ExcludeRequestDtoTypes { get; set; } public Type[] HideRequestBodyForRequestDtoTypes { get; set; } - + public LogEntryPropertiesGenerator LogEntryPropertiesGenerator { get; set; } + internal RequestLoggerOptions GetCopy() { return (RequestLoggerOptions)MemberwiseClone(); diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs index 9a5a068..e242de2 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/Feature.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Serilog; +using Serilog.Core; using ServiceStack.Serilog.RequestLogsFeature.Logging; using ServiceStack.Web; @@ -27,6 +28,37 @@ public class SerilogRequestLogsFeature : IPlugin /// public Func SerilogLoggerFactory { get; set; } + /// + /// Delegate used to configure local instance of Serilog logger. + /// It should not be used with delegate. + /// + public Func SerilogLoggerBuilder { get; set; } + + /// + /// Delegate used to provide collection of properties included in log entry. + /// + public LogEntryPropertiesGenerator LogEntryPropertiesGenerator{ get; set; } + + /// + /// Collection of roles that user should have for logging. + /// + public string[] RequiredRoles { get; set; } + + /// + /// Delegate used to check if log entry should be created. + /// + public Func SkipLogging { get; set; } + + /// + /// Collection of request dtos' types that shouldn't be logged. + /// + public Type[] ExcludeRequestDtoTypes { get; set; } + + /// + /// Collection of request dtos' types that should not be included in log entry. + /// + public Type[] HideRequestBodyForRequestDtoTypes { get; set; } + public SerilogRequestLogsFeature() { AtRestPath = "/serilogrequestlogs"; @@ -41,6 +73,11 @@ public void Register(IAppHost appHost) var requestLogger = new RequestLogger(); requestLogger = new FeatureConfig().ApplyAppSettings(requestLogger, appHost); + requestLogger.LogEntryPropertiesGenerator = LogEntryPropertiesGenerator; + requestLogger.RequiredRoles = RequiredRoles; + requestLogger.SkipLogging = SkipLogging; + requestLogger.ExcludeRequestDtoTypes = ExcludeRequestDtoTypes; + requestLogger.HideRequestBodyForRequestDtoTypes = HideRequestBodyForRequestDtoTypes; appHost.Register(requestLogger); appHost.RegisterService(AtRestPath); @@ -61,14 +98,26 @@ private Task RequestFilter(IRequest request, IResponse response, object dto) private void ResponseFilter(IRequest request, IResponse response, object dto) { - request.Items.Remove(SerilogRequestLogsLoggerKey); + if (request.Items.ContainsKey(SerilogRequestLogsLoggerKey)) + { + var logger = request.Items[SerilogRequestLogsLoggerKey] as IDisposable; + logger?.Dispose(); + + request.Items.Remove(SerilogRequestLogsLoggerKey); + } } private ILogger CreateSerilogLogger() { + var loggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Information(); + + if (SerilogLoggerBuilder != null) + loggerConfiguration = SerilogLoggerBuilder(loggerConfiguration); + return SerilogLoggerFactory != null ? SerilogLoggerFactory.Invoke() - : Log.Logger + : loggerConfiguration.CreateLogger() ; } } diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureConfig.cs b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureConfig.cs index fd88125..4704201 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureConfig.cs +++ b/src/ServiceStack.Serilog.RequestLogsFeature/Plugin/FeatureConfig.cs @@ -17,7 +17,7 @@ public RequestLogger ApplyAppSettings(RequestLogger logger, IAppHost appHost) return logger; var appSettings = new AppSettings(); - logger.EnableRequestBodyTracking = appSettings.Get(EnableSessionTrackingKey); + logger.EnableSessionTracking = appSettings.Get(EnableSessionTrackingKey); logger.EnableRequestBodyTracking = appSettings.Get(EnableRequestBodyTrackingKey); logger.EnableResponseTracking = appSettings.Get(EnableResponseTrackingKey); logger.EnableErrorTracking = appSettings.Get(EnableErrorTrackingKey); diff --git a/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj b/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj index 27eb7de..5bae9b7 100644 --- a/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj +++ b/src/ServiceStack.Serilog.RequestLogsFeature/ServiceStack.Serilog.RequestLogsFeature.csproj @@ -62,7 +62,7 @@ - +