如何在引用的 class 库中使用 serilog 并与 CallerMember、CallerFilePath 一起使用(不是 asp.net 核心)

How to use serilog in a referenced class library and use with CallerMember, CallerFilePath (not asp.net core)

我正在开发一个 WPF 应用程序,可以成功设置 serilog 并通过将其放在 App.xaml.cs、

中来访问它的静态方法

但是我还想登录项目中引用的各种 class 库。

由于循环依赖,我无法从 class 库本身引用主应用程序,在这些 class 库中执行日志记录的最佳方法是什么?

public class LogManager : IMLogManager
    {
        #region Constructor

        public LogManager()
        {
            Log.Logger = new LoggerConfiguration()
                         .MinimumLevel.Debug()
                         .Enrich.FromLogContext()
                         .WriteTo.Console(LogEventLevel.Debug, OutputTemplate, theme: AnsiConsoleTheme.Code)
                         .WriteTo.Async(a => a.File("./log/log.log",
                                                    LogEventLevel.Debug,
                                                    OutputTemplate,
                                                    rollingInterval: RollingInterval.Day,
                                                    encoding: Encoding.UTF8,
                                                    buffered: true,
                                                    rollOnFileSizeLimit: true))
                         .CreateLogger();
        }

        #endregion

        #region Private properties

        /// <summary>
        ///     Template ghi log.
        /// </summary>
        public static string OutputTemplate =>
            "[{Timestamp:dd-MM-yyyy HH:mm:ss.fff} {Level:u3}] source {SourceContext} ::: {Message:lj}{NewLine}{Exception}{NewLine}";

        #endregion

        #region Private methods

        private static string FormatMessage(string message,
                                            string memberName = "",
                                            string filePath = "",
                                            int lineNumber = 0)
            => $"in method [{Path.GetFileNameWithoutExtension(filePath)} > {memberName}]\r\n\tat {filePath}:{lineNumber}\r\n{message}";

        #endregion

        #region Private fields

        #endregion

        #region Implementation of IMLogManager

        /// <summary>
        ///     Write app log.
        /// </summary>
        /// <param name="level"></param>
        /// <param name="message"></param>
        /// <param name="exception"></param>
        /// <param name="memberName"></param>
        /// <param name="filePath"></param>
        /// <param name="lineNumber"></param>
        public void WriteLog(LogEventLevel level,
                             string message,
                             Exception exception = null,
                             [CallerMemberName] string memberName = "",
                             [CallerFilePath] string filePath = "",
                             [CallerLineNumber] int lineNumber = 0)
        {
            switch (level)
            {
                case LogEventLevel.Verbose:
                    Log.Verbose(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Debug:
                    Log.Debug(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Information:
                    Log.Information(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Warning:
                    Log.Warning(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Error:
                    Log.Error(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Fatal:
                    Log.Fatal(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(level), level, null);
            }
        }

        public void WriteLog<T>(LogEventLevel level,
                                string message,
                                Exception exception = null,
                                [CallerMemberName] string memberName = "",
                                [CallerFilePath] string filePath = "",
                                [CallerLineNumber] int lineNumber = 0) where T : class
        {
            var log = Log.ForContext<T>()
                         .ForContext("MemberName", memberName)
                         .ForContext("FilePath", filePath)
                         .ForContext("FileName", Path.GetFileNameWithoutExtension(filePath))
                         .ForContext("LineNumber", lineNumber);
            switch (level)
            {
                case LogEventLevel.Verbose:
                    log.Verbose(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Debug:
                    log.Debug(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Information:
                    log.Information(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Warning:
                    log.Warning(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Error:
                    log.Error(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                case LogEventLevel.Fatal:
                    log.Fatal(exception, FormatMessage(message, memberName, filePath, lineNumber));

                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(level), level, null);
            }
        }

        public void CloseAndFlush()
        {
            Log.CloseAndFlush();
        }

        #endregion
    }

通常,需要通过 Serilog 公开日志记录的库将仅依赖于 Serilog 库,而任何希望执行日志记录的 class 要么依赖于 ILogger 通过构造函数参数或将使用静态 Log,例如

// Constructor
public MyClass(ILogger logger)
{
    _logger = logger.ForContext(GetType());
}
// Constructor
public MyClass()
{
    _logger = Log.ForContext(GetType());
}

由于您似乎想将调用者信息添加到每条日志消息中,我建议将此功能提取到一个共享库中,您的所有 "referenced class libraries" 都可以使用,例如作为扩展方法,如下所示:

public static ILogger WithCallerContext(
    this ILogger logger,
    [CallerMemberName] string memberName = "",   
    [CallerFilePath] string filePath = "",    
    [CallerLineNumber] int lineNumber = 0)
{
    return logger.ForContext("MemberName", memberName)
        .ForContext("FilePath", filePath)
        .ForContext("FileName", Path.GetFileNameWithoutExtension(filePath))
        .ForContext("LineNumber", lineNumber);
}

然后在您的库中您可以按如下方式登录:

_logger.WithCallerContext().Information("Hello, world!");

这种方法的另一个巨大优势是它将添加调用者上下文和记录消息的操作分开,因此您可以使用 ILogger.Information 等方法及其完整的消息模板支持,而不是总是有通过自定义 WriteLog 方法。然后你可以做这样的事情:

_logger.WithCallerContext().Information("Hello from {Name}!", name);

请注意,即使使用此方法调用者上下文不会包含在 Message 属性 中,您仍然可以通过引用 属性 名称将其包含在输出中直接在输出模板中,例如{FilePath}, {LineNumber}.

最后的提示是,在您的 WriteLog 方法中(如果您最终使用它们),而不是在各种日志事件级别之间切换并调用适当的 Log.<Level> 方法,您可以简单地使用以下 Log.Write 重载:

Log.Write(level, exception, FormatMessage(message, memberName, filePath, lineNumber));