From f4f9b92a842cc05b0232ddeb6492edbc1bb6c635 Mon Sep 17 00:00:00 2001 From: Ramin Hoseinzad Date: Wed, 13 Sep 2023 11:06:45 +0330 Subject: [PATCH 1/2] Add excludedPrefixes option and improve performance Add a new excludedPrefixes option to the logger configuration, which allows users to specify prefixes that should not be logged. This can be useful in some cases where excluding prefixes is easier than including them. Change the method of getting the calling assembly from GetCallingAssembly to also use GetEntryAssembly, which fixes the problem of logging the wrong assembly name when the logger setup assembly is separated from the actual app assembly. Use a HashSet instead of a List to store and check the allowed assemblies, which improves the performance of the logger by reducing the number of comparisons. --- .../CallerInfoTests.cs | 24 ++++++++++- Serilog.Enrichers.CallerInfo/Enricher.cs | 34 ++++++++++++--- .../EnricherConfiguration.cs | 43 ++++++++++--------- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/Serilog.Enrichers.CallerInfo.Tests/CallerInfoTests.cs b/Serilog.Enrichers.CallerInfo.Tests/CallerInfoTests.cs index c292e9d..fa2f4b3 100644 --- a/Serilog.Enrichers.CallerInfo.Tests/CallerInfoTests.cs +++ b/Serilog.Enrichers.CallerInfo.Tests/CallerInfoTests.cs @@ -25,7 +25,7 @@ public void EnrichmentTest() public void LocalFunctionsAreNotIncluded() { Log.Logger = new LoggerConfiguration() - .Enrich.WithCallerInfo(includeFileInfo: true, "Serilog.Enrichers.CallerInfo.Tests", string.Empty) + .Enrich.WithCallerInfo(includeFileInfo: true, "Serilog.Enrichers.CallerInfo", string.Empty) .WriteTo.InMemory() .CreateLogger(); @@ -42,5 +42,27 @@ static void LocalFunction(string arg) .WithProperty("Method").WithValue("LocalFunctionsAreNotIncluded") .And.WithProperty("Namespace").WithValue("Serilog.Enrichers.CallerInfo.Tests.CallerInfoTests"); } + [Fact] + public void PrivateFunctionShouldBeAvailable() + { + Log.Logger = new LoggerConfiguration() + .Enrich.WithCallerInfo(includeFileInfo: true, "Serilog.Enrichers.CallerInfo", string.Empty) + .WriteTo.InMemory() + .CreateLogger(); + + + + PrivateLocalFunction("i like turtles"); + + InMemorySink.Instance.Should() + .HaveMessage("i like turtles") + .Appearing().Once() + .WithProperty("Method").WithValue(nameof(PrivateLocalFunction)) + .And.WithProperty("Namespace").WithValue("Serilog.Enrichers.CallerInfo.Tests.CallerInfoTests"); + } + static void PrivateLocalFunction(string arg) + { + Log.Information(arg); + } } } \ No newline at end of file diff --git a/Serilog.Enrichers.CallerInfo/Enricher.cs b/Serilog.Enrichers.CallerInfo/Enricher.cs index 522329f..9a8cc02 100644 --- a/Serilog.Enrichers.CallerInfo/Enricher.cs +++ b/Serilog.Enrichers.CallerInfo/Enricher.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Serilog.Core; @@ -10,13 +11,13 @@ namespace Serilog.Enrichers.CallerInfo public class Enricher : ILogEventEnricher { private readonly bool _includeFileInfo; - private readonly IEnumerable _allowedAssemblies; + private readonly ImmutableHashSet _allowedAssemblies; private readonly string _prefix; public Enricher(bool includeFileInfo, IEnumerable allowedAssemblies, string prefix = "") { _includeFileInfo = includeFileInfo; - _allowedAssemblies = allowedAssemblies ?? new List(); + _allowedAssemblies = allowedAssemblies.ToImmutableHashSet(equalityComparer: StringComparer.OrdinalIgnoreCase) ?? ImmutableHashSet.Empty; _prefix = prefix ?? string.Empty; } @@ -45,7 +46,8 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) if (_includeFileInfo) { - var fileName = frame.GetFileName(); + var fullFileName = frame.GetFileName(); + var fileName = GetCleanFileName(fullFileName); if (fileName != null) { logEvent.AddPropertyIfAbsent(new LogEventProperty($"{_prefix}SourceFile", new ScalarValue(fileName))); @@ -55,6 +57,26 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) } } } + + /// + /// Gets at most 3 levels of the full file name to make it easier to read and avoid leaking sensitive information. + /// + /// + /// + private static string GetCleanFileName(string fullFileName) + { + if (string.IsNullOrWhiteSpace(fullFileName)) + { + return null; + } + var split = fullFileName.Split('\\'); + if (split.Length < 3) + { + return fullFileName; + } + return $"{split[split.Length - 3]}\\{split[split.Length - 2]}\\{split[split.Length - 1]}"; + + } } internal static class Extensions @@ -63,15 +85,15 @@ internal static class Extensions /// Determines whether the resolved method originates in one of the allowed assemblies. /// /// The method to look up. - /// A list of fully qualified assembly names to check against. + /// A HashSet of fully qualified assembly names to check against. /// True if the method originates from one of the allowed assemblies, false otherwise. - internal static bool IsInAllowedAssembly(this ResolvedMethod method, IEnumerable allowedAssemblies) + internal static bool IsInAllowedAssembly(this ResolvedMethod method, ImmutableHashSet allowedAssemblies) { var type = method.DeclaringType; if (type != null) { var assemblyName = type.Assembly.GetName().Name; - return allowedAssemblies.Contains(assemblyName, StringComparer.OrdinalIgnoreCase); + return allowedAssemblies.Contains(assemblyName); } return false; diff --git a/Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs b/Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs index c9a4adb..7f37452 100644 --- a/Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs +++ b/Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs @@ -33,16 +33,19 @@ public static LoggerConfiguration WithCallerInfo( /// The prefix of assemblies to allow when finding the calling method in the stack trace. /// An optional prefix to prepend to all property values. /// The optional name of the assembly from which to discover other related ones with the given prefix. If not provided, the calling assembly of this method is used as the starting point. + /// Which assembly prefixes to exclude when finding the calling method in the stack trace. /// The modified logger configuration. public static LoggerConfiguration WithCallerInfo( this LoggerEnrichmentConfiguration enrichmentConfiguration, bool includeFileInfo, string assemblyPrefix, string prefix = "", - string startingAssembly = "") + string startingAssembly = "", + IEnumerable excludedPrefixes = null) { var startAssembly = string.IsNullOrWhiteSpace(startingAssembly) ? Assembly.GetCallingAssembly() : Assembly.Load(startingAssembly); - var referencedAssemblies = GetAssemblies(startAssembly, asm => asm.Name?.StartsWith(assemblyPrefix, StringComparison.OrdinalIgnoreCase) ?? false); + var referencedAssemblies = GetAssemblies(startAssembly, asm => asm.Name?.StartsWith(assemblyPrefix, StringComparison.OrdinalIgnoreCase) ?? false, asm => + excludedPrefixes?.Any(excluded => asm?.Name?.StartsWith(excluded, StringComparison.OrdinalIgnoreCase) ?? false) ?? false); return enrichmentConfiguration.WithCallerInfo(includeFileInfo, referencedAssemblies, prefix); } @@ -50,36 +53,34 @@ public static LoggerConfiguration WithCallerInfo( /// Find the assemblies that a starting Assembly references, filtering with some predicate.
/// Adapted from /// - /// The starting assembly. + /// The starting assembly. /// A filtering predicate based on the AssemblyName + /// An exclusion predicate based on the AssemblyName /// The list of referenced Assembly names - private static IEnumerable GetAssemblies(Assembly start, Func filter) + private static IEnumerable GetAssemblies(Assembly startingAssembly, Func filter, Func exclude=null) { - var asmNames = new List(); + + var asmNames = new HashSet(comparer:StringComparer.OrdinalIgnoreCase); var stack = new Stack(); - stack.Push(start); - + stack.Push(startingAssembly); + var entryAssembly = Assembly.GetEntryAssembly(); + if (!startingAssembly.FullName.Equals(entryAssembly?.FullName,StringComparison.OrdinalIgnoreCase)) + { + stack.Push(entryAssembly); + } do { var asm = stack.Pop(); - if (!filter(asm.GetName())) + var assemblyExistsInList = asmNames.Contains(asm.GetName().Name); + var assemblyIsNotExcluded = exclude == null || !exclude(asm.GetName()); + var assemblyIsIncluded = filter(asm.GetName()); + if (!assemblyExistsInList && assemblyIsIncluded &&assemblyIsNotExcluded ) { - continue; + asmNames.Add(asm.GetName().Name); } - - asmNames.Add(asm.GetName().Name); foreach (var reference in asm.GetReferencedAssemblies()) { - if (!filter(reference)) - { - continue; - } - - if (!asmNames.Contains(reference.Name)) - { - stack.Push(Assembly.Load(reference)); - asmNames.Add(reference.Name); - } + stack.Push(Assembly.Load(reference)); } } while (stack.Count > 0); From 186716c9dd9f8c784783ccbb61ff61633be79a70 Mon Sep 17 00:00:00 2001 From: raminhz90 Date: Wed, 13 Sep 2023 19:07:12 +0330 Subject: [PATCH 2/2] Add platform agnostic and customizable file cleaner-improve performance FilePath can now be customized to desired depth and will show full path by default and is platform agnostic returned to previous performance by not loading referenced assemblies of assemblies that are not included --- Serilog.Enrichers.CallerInfo/Enricher.cs | 43 +++++++++++++----- .../EnricherConfiguration.cs | 44 ++++++++++++++----- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/Serilog.Enrichers.CallerInfo/Enricher.cs b/Serilog.Enrichers.CallerInfo/Enricher.cs index 9a8cc02..8a616d2 100644 --- a/Serilog.Enrichers.CallerInfo/Enricher.cs +++ b/Serilog.Enrichers.CallerInfo/Enricher.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.IO; using System.Linq; using Serilog.Core; using Serilog.Events; @@ -11,12 +12,14 @@ namespace Serilog.Enrichers.CallerInfo public class Enricher : ILogEventEnricher { private readonly bool _includeFileInfo; + private readonly int _filePathDepth; private readonly ImmutableHashSet _allowedAssemblies; private readonly string _prefix; - public Enricher(bool includeFileInfo, IEnumerable allowedAssemblies, string prefix = "") + public Enricher(bool includeFileInfo, IEnumerable allowedAssemblies, string prefix = "",int filePathDepth=0) { _includeFileInfo = includeFileInfo; + _filePathDepth = filePathDepth; _allowedAssemblies = allowedAssemblies.ToImmutableHashSet(equalityComparer: StringComparer.OrdinalIgnoreCase) ?? ImmutableHashSet.Empty; _prefix = prefix ?? string.Empty; } @@ -47,7 +50,7 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) if (_includeFileInfo) { var fullFileName = frame.GetFileName(); - var fileName = GetCleanFileName(fullFileName); + var fileName = GetCleanFileName(fullFileName,_filePathDepth); if (fileName != null) { logEvent.AddPropertyIfAbsent(new LogEventProperty($"{_prefix}SourceFile", new ScalarValue(fileName))); @@ -57,26 +60,44 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) } } } - + /// - /// Gets at most 3 levels of the full file name to make it easier to read and avoid leaking sensitive information. + /// Gets a clean file name from a full file path, optionally including a specified number of parent directories. /// - /// - /// - private static string GetCleanFileName(string fullFileName) + /// The full file path. + /// The number of parent directories to include in the file name. If zero or negative, the full path is returned. If larger than the actual depth of the file, the full path is also returned. + /// A string representing the clean file name, or null if the full file path is null or whitespace. + private static string GetCleanFileName(string fullFileName, int depth=0) { if (string.IsNullOrWhiteSpace(fullFileName)) { return null; } - var split = fullFileName.Split('\\'); - if (split.Length < 3) + if (depth <= 0) // if the depth is zero or negative, return the full path { return fullFileName; } - return $"{split[split.Length - 3]}\\{split[split.Length - 2]}\\{split[split.Length - 1]}"; - + var fileName = Path.GetFileName(fullFileName); // get the file name + var dirName = Path.GetDirectoryName(fullFileName); // get the directory name + if (string.IsNullOrWhiteSpace(dirName)) + { + return fileName; + } + var pathSegments = new List { fileName }; // create a list to store the path segments and add the file name to the list + for (var i = 0; i < depth - 1; i++) // loop until the desired depth is reached or there are no more parent directories + { + var parentDirName = Path.GetFileName(dirName); // get the parent directory name + if (string.IsNullOrWhiteSpace(parentDirName)) // if there is no parent directory, break the loop + { + break; + } + pathSegments.Add(parentDirName); // add the parent directory name to the list + dirName = Path.GetDirectoryName(dirName); // get the grandparent directory name + } + pathSegments.Reverse(); // reverse the order of the list to get the correct path order + return Path.Combine(pathSegments.ToArray()); // join the path segments with the appropriate path separator and return the result } + } internal static class Extensions diff --git a/Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs b/Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs index 7f37452..bc881cc 100644 --- a/Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs +++ b/Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs @@ -15,14 +15,16 @@ public static class EnricherConfiguration /// Whether to include the caller's file information (file name, line number, column number). /// Which assemblies to consider when finding the calling method in the stack trace. /// An optional prefix to prepend to all property values. + /// /// The number of parent directories to include in the file name. If zero or negative, the full path is returned. If larger than the actual depth of the file, the full path is also returned. /// The modified logger configuration. public static LoggerConfiguration WithCallerInfo( this LoggerEnrichmentConfiguration enrichmentConfiguration, bool includeFileInfo, IEnumerable allowedAssemblies, - string prefix = "") + string prefix = "", + int filePathDepth=0) { - return enrichmentConfiguration.With(new Enricher(includeFileInfo, allowedAssemblies, prefix)); + return enrichmentConfiguration.With(new Enricher(includeFileInfo, allowedAssemblies, prefix,filePathDepth)); } /// @@ -33,6 +35,7 @@ public static LoggerConfiguration WithCallerInfo( /// The prefix of assemblies to allow when finding the calling method in the stack trace. /// An optional prefix to prepend to all property values. /// The optional name of the assembly from which to discover other related ones with the given prefix. If not provided, the calling assembly of this method is used as the starting point. + /// /// /// The number of parent directories to include in the file name. If zero or negative, the full path is returned. If larger than the actual depth of the file, the full path is also returned. /// Which assembly prefixes to exclude when finding the calling method in the stack trace. /// The modified logger configuration. public static LoggerConfiguration WithCallerInfo( @@ -41,12 +44,13 @@ public static LoggerConfiguration WithCallerInfo( string assemblyPrefix, string prefix = "", string startingAssembly = "", + int filePathDepth=0, IEnumerable excludedPrefixes = null) { var startAssembly = string.IsNullOrWhiteSpace(startingAssembly) ? Assembly.GetCallingAssembly() : Assembly.Load(startingAssembly); - var referencedAssemblies = GetAssemblies(startAssembly, asm => asm.Name?.StartsWith(assemblyPrefix, StringComparison.OrdinalIgnoreCase) ?? false, asm => - excludedPrefixes?.Any(excluded => asm?.Name?.StartsWith(excluded, StringComparison.OrdinalIgnoreCase) ?? false) ?? false); - return enrichmentConfiguration.WithCallerInfo(includeFileInfo, referencedAssemblies, prefix); + var referencedAssemblies = GetAssemblies(startAssembly, asm => asm.Name?.StartsWith(assemblyPrefix, StringComparison.OrdinalIgnoreCase) ?? false, + asm => excludedPrefixes?.Any(excluded => asm?.Name?.StartsWith(excluded, StringComparison.OrdinalIgnoreCase) ?? false) ?? false); + return enrichmentConfiguration.WithCallerInfo(includeFileInfo, referencedAssemblies, prefix,filePathDepth); } /// @@ -64,27 +68,45 @@ private static IEnumerable GetAssemblies(Assembly startingAssembly, Func var stack = new Stack(); stack.Push(startingAssembly); var entryAssembly = Assembly.GetEntryAssembly(); - if (!startingAssembly.FullName.Equals(entryAssembly?.FullName,StringComparison.OrdinalIgnoreCase)) + if (!startingAssembly.FullName?.Equals(entryAssembly?.FullName,StringComparison.OrdinalIgnoreCase) ?? false) { - stack.Push(entryAssembly); + // stack.Push(entryAssembly); } do { var asm = stack.Pop(); - var assemblyExistsInList = asmNames.Contains(asm.GetName().Name); - var assemblyIsNotExcluded = exclude == null || !exclude(asm.GetName()); - var assemblyIsIncluded = filter(asm.GetName()); - if (!assemblyExistsInList && assemblyIsIncluded &&assemblyIsNotExcluded ) + if (!AssemblyExistsInList(asmNames, asm) && IsAssemblyIncluded(filter, asm) && !IsAssemblyExcluded(exclude, asm) ) { asmNames.Add(asm.GetName().Name); } foreach (var reference in asm.GetReferencedAssemblies()) { + if (AssemblyExistsInList(asmNames, asm) || !IsAssemblyIncluded(filter, asm) || + IsAssemblyExcluded(exclude, asm)) continue; stack.Push(Assembly.Load(reference)); + asmNames.Add(reference.Name); + } } while (stack.Count > 0); return asmNames; } + + private static bool IsAssemblyExcluded(Func exclude, Assembly asm) + { + return exclude != null && exclude(asm.GetName()); + } + + private static bool IsAssemblyIncluded(Func filter, Assembly asm) + { + var assemblyIsIncluded = filter(asm.GetName()); + return assemblyIsIncluded; + } + + private static bool AssemblyExistsInList(HashSet asmNames, Assembly asm) + { + var assemblyExistsInList = asmNames.Contains(asm.GetName().Name); + return assemblyExistsInList; + } } }