Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add excludedPrefixes option and improve performance #3

Merged
merged 2 commits into from
Sep 13, 2023
Merged
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
24 changes: 23 additions & 1 deletion Serilog.Enrichers.CallerInfo.Tests/CallerInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);
}
}
}
57 changes: 50 additions & 7 deletions Serilog.Enrichers.CallerInfo/Enricher.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Serilog.Core;
using Serilog.Events;
Expand All @@ -10,13 +12,15 @@ namespace Serilog.Enrichers.CallerInfo
public class Enricher : ILogEventEnricher
{
private readonly bool _includeFileInfo;
private readonly IEnumerable<string> _allowedAssemblies;
private readonly int _filePathDepth;
private readonly ImmutableHashSet<string> _allowedAssemblies;
private readonly string _prefix;

public Enricher(bool includeFileInfo, IEnumerable<string> allowedAssemblies, string prefix = "")
public Enricher(bool includeFileInfo, IEnumerable<string> allowedAssemblies, string prefix = "",int filePathDepth=0)
{
_includeFileInfo = includeFileInfo;
_allowedAssemblies = allowedAssemblies ?? new List<string>();
_filePathDepth = filePathDepth;
_allowedAssemblies = allowedAssemblies.ToImmutableHashSet(equalityComparer: StringComparer.OrdinalIgnoreCase) ?? ImmutableHashSet<string>.Empty;
_prefix = prefix ?? string.Empty;
}

Expand Down Expand Up @@ -45,7 +49,8 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)

if (_includeFileInfo)
{
var fileName = frame.GetFileName();
var fullFileName = frame.GetFileName();
var fileName = GetCleanFileName(fullFileName,_filePathDepth);
if (fileName != null)
{
logEvent.AddPropertyIfAbsent(new LogEventProperty($"{_prefix}SourceFile", new ScalarValue(fileName)));
Expand All @@ -55,6 +60,44 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
}
}
}

/// <summary>
/// Gets a clean file name from a full file path, optionally including a specified number of parent directories.
/// </summary>
/// <param name="fullFileName">The full file path.</param>
/// <param name="depth">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.</param>
/// <returns>A string representing the clean file name, or null if the full file path is null or whitespace.</returns>
private static string GetCleanFileName(string fullFileName, int depth=0)
{
if (string.IsNullOrWhiteSpace(fullFileName))
{
return null;
}
if (depth <= 0) // if the depth is zero or negative, return the full path
{
return fullFileName;
}
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<string> { 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
Expand All @@ -63,15 +106,15 @@ internal static class Extensions
/// Determines whether the resolved method originates in one of the allowed assemblies.
/// </summary>
/// <param name="method">The method to look up.</param>
/// <param name="allowedAssemblies">A list of fully qualified assembly names to check against.</param>
/// <param name="allowedAssemblies">A HashSet of fully qualified assembly names to check against.</param>
/// <returns>True if the method originates from one of the allowed assemblies, false otherwise.</returns>
internal static bool IsInAllowedAssembly(this ResolvedMethod method, IEnumerable<string> allowedAssemblies)
internal static bool IsInAllowedAssembly(this ResolvedMethod method, ImmutableHashSet<string> 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;
Expand Down
69 changes: 46 additions & 23 deletions Serilog.Enrichers.CallerInfo/EnricherConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ public static class EnricherConfiguration
/// <param name="includeFileInfo">Whether to include the caller's file information (file name, line number, column number).</param>
/// <param name="allowedAssemblies">Which assemblies to consider when finding the calling method in the stack trace.</param>
/// <param name="prefix">An optional prefix to prepend to all property values.</param>
/// /// <param name="filePathDepth">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.</param>
/// <returns>The modified logger configuration.</returns>
public static LoggerConfiguration WithCallerInfo(
this LoggerEnrichmentConfiguration enrichmentConfiguration,
bool includeFileInfo,
IEnumerable<string> 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));
}

/// <summary>
Expand All @@ -33,57 +35,78 @@ public static LoggerConfiguration WithCallerInfo(
/// <param name="assemblyPrefix">The prefix of assemblies to allow when finding the calling method in the stack trace.</param>
/// <param name="prefix">An optional prefix to prepend to all property values.</param>
/// <param name="startingAssembly">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.</param>
/// /// /// <param name="filePathDepth">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.</param>
/// <param name="excludedPrefixes">Which assembly prefixes to exclude when finding the calling method in the stack trace.</param>
/// <returns>The modified logger configuration.</returns>
public static LoggerConfiguration WithCallerInfo(
this LoggerEnrichmentConfiguration enrichmentConfiguration,
bool includeFileInfo,
string assemblyPrefix,
string prefix = "",
string startingAssembly = "")
string startingAssembly = "",
int filePathDepth=0,
IEnumerable<string> excludedPrefixes = null)
{
var startAssembly = string.IsNullOrWhiteSpace(startingAssembly) ? Assembly.GetCallingAssembly() : Assembly.Load(startingAssembly);
var referencedAssemblies = GetAssemblies(startAssembly, asm => asm.Name?.StartsWith(assemblyPrefix, StringComparison.OrdinalIgnoreCase) ?? 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);
}

/// <summary>
/// Find the assemblies that a starting Assembly references, filtering with some predicate.<br/>
/// Adapted from <see href="https://stackoverflow.com/a/10253634/2102106"/>
/// </summary>
/// <param name="start">The starting assembly.</param>
/// <param name="startingAssembly">The starting assembly.</param>
/// <param name="filter">A filtering predicate based on the AssemblyName</param>
/// <param name="exclude">An exclusion predicate based on the AssemblyName</param>
/// <returns>The list of referenced Assembly names</returns>
private static IEnumerable<string> GetAssemblies(Assembly start, Func<AssemblyName, bool> filter)
private static IEnumerable<string> GetAssemblies(Assembly startingAssembly, Func<AssemblyName, bool> filter, Func<AssemblyName, bool> exclude=null)
{
var asmNames = new List<string>();

var asmNames = new HashSet<string>(comparer:StringComparer.OrdinalIgnoreCase);
var stack = new Stack<Assembly>();
stack.Push(start);

stack.Push(startingAssembly);
var entryAssembly = Assembly.GetEntryAssembly();
if (!startingAssembly.FullName?.Equals(entryAssembly?.FullName,StringComparison.OrdinalIgnoreCase) ?? false)
{
// stack.Push(entryAssembly);
}
do
{
var asm = stack.Pop();
if (!filter(asm.GetName()))
if (!AssemblyExistsInList(asmNames, asm) && IsAssemblyIncluded(filter, asm) && !IsAssemblyExcluded(exclude, asm) )
{
continue;
asmNames.Add(asm.GetName().Name);
}

asmNames.Add(asm.GetName().Name);
foreach (var reference in asm.GetReferencedAssemblies())
{
if (!filter(reference))
{
continue;
}
if (AssemblyExistsInList(asmNames, asm) || !IsAssemblyIncluded(filter, asm) ||
IsAssemblyExcluded(exclude, asm)) continue;
stack.Push(Assembly.Load(reference));
asmNames.Add(reference.Name);

if (!asmNames.Contains(reference.Name))
{
stack.Push(Assembly.Load(reference));
asmNames.Add(reference.Name);
}
}
} while (stack.Count > 0);

return asmNames;
}

private static bool IsAssemblyExcluded(Func<AssemblyName, bool> exclude, Assembly asm)
{
return exclude != null && exclude(asm.GetName());
}

private static bool IsAssemblyIncluded(Func<AssemblyName, bool> filter, Assembly asm)
{
var assemblyIsIncluded = filter(asm.GetName());
return assemblyIsIncluded;
}

private static bool AssemblyExistsInList(HashSet<string> asmNames, Assembly asm)
{
var assemblyExistsInList = asmNames.Contains(asm.GetName().Name);
return assemblyExistsInList;
}
}
}