Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions src/ServiceStack.Serilog.RequestLogsFeature.sln
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using Serilog.Events;
using ServiceStack.Web;

namespace ServiceStack.Serilog.RequestLogsFeature.Logging
{
public delegate IEnumerable<LogEventProperty> LogEntryPropertiesGenerator(IRequest request, object requestDto, object responseDto);
}
166 changes: 166 additions & 0 deletions src/ServiceStack.Serilog.RequestLogsFeature/Logging/LogEventFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Serilog;
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 = "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}}} in {{{Property_Elapsed_Key}}} ms");

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, requestDto, responseDto, elapsed, opt)
);

private static IEnumerable<LogEventProperty> GetProperties(IRequest request, object requestDto, object responseDto, TimeSpan elapsed, RequestLoggerOptions opt)
{
yield return WithHttpMethod(request);
yield return WithUrl(request);
yield return WithHeaders(request);
yield return WithStatusCode(request);
yield return WithStatusDescription(request);
yield return WithElapsedTime(elapsed);


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
)
{
yield return WithRequestBody(request);
yield return WithFormData(request);
yield return WithRequestDto(request, requestDto);
}


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()));

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 WithStatusDescription(IRequest request)
=> new LogEventProperty(Property_StatusDesc_Key, new ScalarValue(request.Response.StatusDescription));

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));

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));
}

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<LogEventProperty> WithPropertiesFromDelegate(IRequest request, object requestDto, object responseDto, LogEntryPropertiesGenerator propertiesGenerator)
=> propertiesGenerator != null ? propertiesGenerator.Invoke(request, requestDto, responseDto) : Enumerable.Empty<LogEventProperty>();

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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<IRequest, bool> 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 LogEntryPropertiesGenerator LogEntryPropertiesGenerator { get => Options.LogEntryPropertiesGenerator;
set => Options.LogEntryPropertiesGenerator = value; }


public List<RequestLogEntry> GetLatestLogs(int? take)
{
return LatestLogEntriesCollector.GetLatestLogs(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, response, elapsed, loggingOptions);

if(logEvent != null)
{
var logger = request.Items[Plugin.SerilogRequestLogsFeature.SerilogRequestLogsLoggerKey] as ILogger;

logger
.ForContext<IRequestLogger>()
.Write(logEvent);
}

LatestLogEntriesCollector
.Log(request, requestDto, response, elapsed);
}
}

private bool AssertCanLog(IRequest request, object requestDto) => ShouldNotLog(request, requestDto).All(r => r == false);

private IEnumerable<bool> 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;

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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; }
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<IRequest, bool> SkipLogging { get; set; }
public Type[] ExcludeRequestDtoTypes { get; set; }
public Type[] HideRequestBodyForRequestDtoTypes { get; set; }
public LogEntryPropertiesGenerator LogEntryPropertiesGenerator { get; set; }

internal RequestLoggerOptions GetCopy()
{
return (RequestLoggerOptions)MemberwiseClone();
}

public object Clone()
{
return GetCopy();
}
}
}
Loading