Created
January 13, 2020 22:07
-
-
Save llaughlin/9a2d529bbbe988c26962683292b90c34 to your computer and use it in GitHub Desktop.
Serilog - Log SourceMethod and SourceFile
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using Serilog.Core; | |
using Serilog.Events; | |
namespace Logging.Enrichers | |
{ | |
public class MethodEnricher : ILogEventEnricher | |
{ | |
public const string _PropertyName = "SourceMethod"; | |
/// <summary>Enrich the log event.</summary> | |
/// <param name="logEvent">The log event to enrich.</param> | |
/// <param name="propertyFactory">Factory for creating new properties to add to the event.</param> | |
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) | |
{ | |
if (logEvent.Properties.ContainsKey(StackTraceEnricher._PropertyName)) return; | |
if (!logEvent.Properties.TryGetValue(Constants.SourceContextPropertyName, out LogEventPropertyValue sourceContextProperty)) return; | |
string sourceContext = sourceContextProperty.ToString().Trim('"'); | |
StackTrace trace = new StackTrace(10, true); | |
var currentMethod = | |
(from frame in trace.EnumerateFrames() | |
let method = frame.GetMethod() | |
where method.DeclaringType?.FullName == sourceContext | |
select new | |
{ | |
method, | |
file = frame.GetFileName(), | |
line = frame.GetFileLineNumber(), | |
column = frame.GetFileColumnNumber() | |
}) | |
.FirstOrDefault(); | |
if (currentMethod is null) return; | |
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(_PropertyName, currentMethod.method.Name)); | |
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("SourceFile", | |
$"{currentMethod.file}@{currentMethod.line}:{currentMethod.column}")); | |
} | |
} | |
public static class StackTraceExtensions | |
{ | |
public static IEnumerable<StackFrame> EnumerateFrames(this StackTrace stackTrace) | |
{ | |
int i = 0; | |
while (stackTrace.GetFrame(i++) is StackFrame frame) yield return frame; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using log4net; | |
using log4net.Appender; | |
using log4net.Core; | |
using log4net.Repository; | |
using log4net.Repository.Hierarchy; | |
using log4net.Util; | |
using Microsoft.Win32; | |
using Serilog; | |
using Serilog.Debugging; | |
using Serilog.Events; | |
using Serilog.Formatting.Compact; | |
using ILogger = Serilog.ILogger; | |
namespace Logging | |
{ | |
public class SerilogAppender : AppenderSkeleton | |
{ | |
private static readonly Func<SystemStringFormat, string> _FormatGetter = | |
ReflectionHelper.GetFieldAccessor<SystemStringFormat, string>("m_format"); | |
private static readonly Func<SystemStringFormat, object[]> _ArgumentsGetter = | |
ReflectionHelper.GetFieldAccessor<SystemStringFormat, object[]>("m_args"); | |
private ILogger _Logger; | |
public string StaticLogPath { get; set; } = @"%ProgramData%\Logs.json"; | |
public bool Shared { get; set; } = true; | |
public RollingInterval RollingInterval { get; set; } = RollingInterval.Infinite; | |
public bool RollOnFileSizeLimit { get; set; } = true; | |
public Level Level { get; set; } = Level.Verbose; | |
public long FileSizeLimitBytes { get; set; } = 52428800; // 50 MiB | |
public int RetainedFileCountLimit { get; set; } = 5; | |
public string SeqUrl { get; set; } = "http://localhost:5341"; | |
public string SeqApiKey { get; set; } = ""; | |
public int SeqBatchWindow { get; set; } = 2000; | |
public bool UseCompactFormat { get; set; } = true; | |
public static void AddToRepository(ILoggerRepository repository) | |
{ | |
if (Disabled) | |
return; | |
Hierarchy hierarchy = (Hierarchy)repository; | |
if (repository.GetAppenders().OfType<SerilogAppender>().Any()) | |
return; | |
SerilogAppender serilogAppender = new SerilogAppender(); | |
hierarchy.Root.AddAppender(serilogAppender); | |
serilogAppender.ActivateOptions(); | |
} | |
public override void ActivateOptions() | |
{ | |
if (Disabled) | |
return; | |
base.ActivateOptions(); | |
LogManager.GetRepository().ShutdownEvent += OnShutdown; | |
LoggerConfiguration configuration = new LoggerConfiguration() | |
.MinimumLevel.Is(ConvertLevel(Level)) | |
.Enrich.WithThreadId() | |
.Enrich.WithThreadName() | |
.Enrich.WithProperty("ProcessName", Process.GetCurrentProcess().ProcessName) | |
.Enrich.WithProperty("ProcessId", Process.GetCurrentProcess().Id) | |
.Enrich.WithProperty("UserName", Environment.UserName) | |
.Enrich.WithProperty("Domain", Environment.UserDomainName) | |
.Enrich.WithProperty("MachineName", Environment.MachineName) | |
.Enrich.With(new StackTraceEnricher(10)) | |
#if DEBUG | |
.Enrich.With(new MethodEnricher()) | |
#endif | |
; | |
if (StaticLogPath.HasValue() | |
&& Environment.ExpandEnvironmentVariables(StaticLogPath) is string staticLogPath | |
&& staticLogPath.Length > 0) | |
configuration.WriteTo.File(new CompactJsonFormatter(), | |
staticLogPath, | |
shared: Shared, | |
rollingInterval: RollingInterval, | |
rollOnFileSizeLimit: RollOnFileSizeLimit, | |
fileSizeLimitBytes: FileSizeLimitBytes, | |
retainedFileCountLimit: RetainedFileCountLimit); | |
if (SeqUrl.HasValue()) | |
configuration.WriteTo.Seq(SeqUrl, | |
period: TimeSpan.FromMilliseconds(SeqBatchWindow), | |
apiKey: SeqApiKey, | |
compact: UseCompactFormat); | |
_Logger = Log.Logger = configuration.CreateLogger(); | |
} | |
public static bool Disabled => _Disabled.Value; | |
private static readonly Lazy<bool> _Disabled = new Lazy<bool>(() => | |
{ | |
bool domain = Environment.UserDomainName.Equals("domain", StringComparison.OrdinalIgnoreCase); | |
bool forceDisabled = GetRegistryValue("DisableSerilog") == 1; | |
bool forceEnabled = GetRegistryValue("ForceSerilog") == 1; | |
if (forceDisabled) | |
return true; | |
if (forceEnabled) | |
return false; | |
if (penlinkDomain) | |
return false; | |
return true; | |
}); | |
private static int GetRegistryValue(string key) => | |
Try.Get(() => (int)Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Serilog", key, 0)); | |
private void OnShutdown(object sender, EventArgs args) | |
{ | |
if (Disabled) | |
return; | |
try | |
{ | |
Log.CloseAndFlush(); | |
Task.Delay(500).Wait(); | |
} | |
catch (ObjectDisposedException e) | |
{ | |
Console.WriteLine($"File already closed. Ex: {e}"); | |
} | |
} | |
protected override void Append(LoggingEvent loggingEvent) | |
{ | |
if (Disabled) | |
return; | |
ILogger logger = _Logger; | |
string loggerName = loggingEvent.LoggerName; | |
LogEventLevel level = ConvertLevel(loggingEvent.Level); | |
object[] objArray = null; | |
string messageTemplate; | |
if (loggingEvent.MessageObject is SystemStringFormat messageObject) | |
{ | |
messageTemplate = _FormatGetter(messageObject); | |
objArray = _ArgumentsGetter(messageObject); | |
} | |
else | |
{ | |
messageTemplate = loggingEvent.MessageObject?.ToString(); | |
} | |
logger = logger.ForContext("SourceContext", loggerName); | |
if (loggingEvent.GetProperties() is PropertiesDictionary props) | |
foreach ((string key, object val) in props.GetKeys().Select(k => (k, props[k]))) | |
{ | |
// don't include automatic log4net properties | |
if (key.StartsWith("log4net")) continue; | |
// don't include operation statistics that don't have stats | |
if (key == "operation" && val is LogicalThreadContextStack stack && stack.Count <= 0) continue; | |
logger = logger.ForContext(key, val, true); | |
} | |
logger.Write(level, loggingEvent.ExceptionObject, messageTemplate, objArray); | |
} | |
private static LogEventLevel ConvertLevel(Level log4netLevel) | |
{ | |
if (log4netLevel == Level.Verbose || log4netLevel == Level.Trace) | |
return LogEventLevel.Verbose; | |
if (log4netLevel == Level.Debug) | |
return LogEventLevel.Debug; | |
if (log4netLevel == Level.Info) | |
return LogEventLevel.Information; | |
if (log4netLevel == Level.Warn) | |
return LogEventLevel.Warning; | |
if (log4netLevel == Level.Error) | |
return LogEventLevel.Error; | |
if (log4netLevel == Level.Fatal) | |
return LogEventLevel.Fatal; | |
SelfLog.WriteLine("Unexpected log4net logging level ({0}) logging as Information", | |
log4netLevel.DisplayName); | |
return LogEventLevel.Information; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.Linq; | |
using Serilog.Core; | |
using Serilog.Events; | |
namespace Logging.Enrichers | |
{ | |
public class StackTraceEnricher : ILogEventEnricher | |
{ | |
public const string _PropertyName = "StackTrace"; | |
private readonly int _Depth; | |
private readonly LogEventLevel _MinimumLevel; | |
private readonly string _RootPrefix; | |
public StackTraceEnricher(int depth = 5, string rootNamespace = null, LogEventLevel minimumLevel = LogEventLevel.Warning) | |
{ | |
_Depth = depth; | |
_MinimumLevel = minimumLevel; | |
if (rootNamespace != null) | |
_RootPrefix = rootNamespace; | |
} | |
/// <summary>Enrich the log event.</summary> | |
/// <param name="logEvent">The log event to enrich.</param> | |
/// <param name="propertyFactory">Factory for creating new properties to add to the event.</param> | |
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) | |
{ | |
if (logEvent.Level < _MinimumLevel) | |
return; | |
var filtered = from frame in new StackTrace(10, true).EnumerateFrames() | |
let method = frame.GetMethod() | |
where method != null | |
let methodType = method.DeclaringType | |
where methodType != typeof(StackTraceEnricher) | |
where methodType != typeof(SerilogAppender) | |
let ns = methodType.Namespace | |
where ns.HasValue() | |
where ns.StartsWith(_RootPrefix) | |
select new | |
{ | |
fullName = $"{methodType}.{method.Name}", file = frame.GetFileName(), line = frame.GetFileLineNumber(), | |
column = frame.GetFileColumnNumber() | |
}; | |
string joined = string.Join(Environment.NewLine, filtered.Take(_Depth).Select(x => $"{x.fullName} in {x.file}@{x.line}:{x.column}")); | |
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(_PropertyName, joined)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment