Skip to content

Instantly share code, notes, and snippets.

@llaughlin
Created January 13, 2020 22:07
Show Gist options
  • Save llaughlin/9a2d529bbbe988c26962683292b90c34 to your computer and use it in GitHub Desktop.
Save llaughlin/9a2d529bbbe988c26962683292b90c34 to your computer and use it in GitHub Desktop.
Serilog - Log SourceMethod and SourceFile
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;
}
}
}
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;
}
}
}
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