Serilog MSSqlServer 没有记录错误

Serilog MSSqlServer is not logging errors

在进入问题之前,我必须解释一下我在哪里以及为什么按照我的方式创建代码。 我正在学习关于 PluralSight 的教程,其中解释了日志记录:

https://app.pluralsight.com/library/courses/dotnet-logging-using-serilog-opinionated-approach/discussion

所以,我创建了一个静态的 class 并稍微修改了它(因为课程已经过时了)并想出了这个:

public static class Logger
{
    private static ILogger _diagnosticLogger;
    private static ILogger _errorLogger;
    private static ILogger _performanceLogger;
    private static ILogger _usageLogger;

    public static void AddLogging(this IServiceCollection services, IConfiguration configuration, string sectionName = "Logging")
    {
        services.Configure<LoggerConfig>(configuration.GetSection(sectionName));
        services.AddSingleton(m => m.GetRequiredService<IOptions<LoggerConfig>>().Value);

        var scope = services.BuildServiceProvider().CreateScope();
        var config = scope.ServiceProvider.GetRequiredService<LoggerConfig>();

        _diagnosticLogger = CreateLogger("DiagnosticLogs", config.ConnectionString);
        _errorLogger = CreateLogger("ErrorLogs", config.ConnectionString);
        _performanceLogger = CreateLogger("PerformanceLogs", config.ConnectionString);
        _usageLogger = CreateLogger("UsageLogs", config.ConnectionString);
    }

    public static void LogDiagnostic(Log log)
    {
        var shouldWrite = Convert.ToBoolean(Environment.GetEnvironmentVariable("LOG_DIAGNOSTICS"));
        if (!shouldWrite) return;

        _diagnosticLogger.Write(LogEventLevel.Information, "{@Log}", log);
    }

    public static void LogError(Log log)
    {
        log.Message = GetMessageFromException(log.Exception);
        _errorLogger.Write(LogEventLevel.Information, "{@Log}", log);
    }

    public static void LogPerformance(Log log) =>
        _performanceLogger.Write(LogEventLevel.Information, "{@Log}", log);

    public static void LogUsage(Log log) =>
        _usageLogger.Write(LogEventLevel.Information, "{@Log}", log);

    private static string GetMessageFromException(Exception exception)
    {
        while (true)
        {
            if (exception.InnerException == null) return exception.Message;
            exception = exception.InnerException;
        }
    }

    private static ILogger CreateLogger(string name, string connectionString) =>
        new LoggerConfiguration()
            //.WriteTo.File(path: Environment.GetEnvironmentVariable(name))
            .WriteTo.MSSqlServer(connectionString, 
                sinkOptions: GetSinkOptions(name),
                columnOptions: GetColumnOptions())
            .CreateLogger();

    private static ColumnOptions GetColumnOptions()
    {
        var columnOptions = new ColumnOptions();

        columnOptions.Store.Remove(StandardColumn.Exception);
        columnOptions.Store.Remove(StandardColumn.Level);
        columnOptions.Store.Remove(StandardColumn.Message);
        columnOptions.Store.Remove(StandardColumn.MessageTemplate);
        columnOptions.Store.Remove(StandardColumn.Properties);
        columnOptions.Store.Remove(StandardColumn.TimeStamp);

        columnOptions.AdditionalColumns = new Collection<SqlColumn>
        {
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "AdditionalInformation"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "CorrelationId"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "CustomException"},
            new SqlColumn { DataType = SqlDbType.Int, ColumnName = "ElapsedMilliseconds"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Exception"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Hostname"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Layer"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Location"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Message"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Model"},
            new SqlColumn { DataType = SqlDbType.DateTime, ColumnName = "Timestamp"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "UserId"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "UserEmail"}
        };

        return columnOptions;
    }

    private static SinkOptions GetSinkOptions(string name)
    {
        return new SinkOptions
        {
            TableName = name,
            AutoCreateSqlTable = true,
            BatchPostingLimit = 1
        };
    }
}

在我的 Startup.cs 中,我通过调用 AddLogging 方法初始化了 Loggers

services.AddLogging(Configuration, nameof(DataConfig));

哪个执行这个方法:

public static void AddLogging(this IServiceCollection services, IConfiguration configuration, string sectionName = "Logging")
{
    services.Configure<LoggerConfig>(configuration.GetSection(sectionName));
    services.AddSingleton(m => m.GetRequiredService<IOptions<LoggerConfig>>().Value);

    var scope = services.BuildServiceProvider().CreateScope();
    var config = scope.ServiceProvider.GetRequiredService<LoggerConfig>();

    _diagnosticLogger = CreateLogger("DiagnosticLogs", config.ConnectionString);
    _errorLogger = CreateLogger("ErrorLogs", config.ConnectionString);
    _performanceLogger = CreateLogger("PerformanceLogs", config.ConnectionString);
    _usageLogger = CreateLogger("UsageLogs", config.ConnectionString);
}

private static ILogger CreateLogger(string name, string connectionString) =>
    new LoggerConfiguration()
        //.WriteTo.File(path: Environment.GetEnvironmentVariable(name))
        .WriteTo.MSSqlServer(connectionString, 
            sinkOptions: GetSinkOptions(name),
            columnOptions: GetColumnOptions())
        .CreateLogger();

如您所见,应该可以很好地创建记录器。 然后我添加了一个自定义中间件:

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        switch (exception)
        {
            case NotFoundException _:
                context.Response.StatusCode = (int)HttpStatusCode.NotFound;
                break;
            case BadRequestException _:
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                break;
        }

        WebHelper.LogWebError(null, "Core API", exception, context);

        var errorId = Activity.Current?.Id ?? context.TraceIdentifier;
        var response = JsonConvert.SerializeObject(new ErrorResponse
        {
            ErrorId = errorId,
            Message = "An error occurred in the API."
        });
        await context.Response.WriteAsync(response, Encoding.UTF8);
    }
}

我有一个扩展方法可以添加到我的项目中:

public static class ErrorHandlerMiddlewareExtensions
{
    public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder)
        => builder.UseMiddleware<ErrorHandlerMiddleware>();
}

当我使用 postman 调用一个方法得到一个错误时,我得到一个格式很好的错误,如下所示:

{
    "ErrorId": "|e55d889c-463ad56fc704d7fb.",
    "Message": "An error occurred in the API."
}

我在我的代码中设置了断点,逐步执行了所有步骤,日志显示它已创建(通过调用 LogError 方法)并且一切似乎都很好。

如果我检查我的数据库;我可以看到表已经创建,但是没有记录。

我在我的 LogError 方法中添加了一个 try/catch 块,如下所示:

public static void LogError(Log log)
{
    try
    {

        log.Message = GetMessageFromException(log.Exception);
        _errorLogger.Write(LogEventLevel.Information, "{@Log}", log);
    }
    catch (Exception ex)
    {

    }
}

并且没有抛出任何错误,一切都按预期执行,但我的数据库中没有添加任何记录。 有谁知道为什么?


更新

玩了一会儿后我注意到它实际上是在尝试记录数据,只是所有列都是空的。

如果我删除了我的列选项,它实际上会将日志添加到数据库中。所以这与映射有关。有人可以帮我吗?

这是我的日志的样子:

public class Log
{
    public DateTime TimeStamp { get; }
    public string Message { get; set; }

    public string Model { get; set; }
    public string Layer { get; set; }
    public string Location { get; set; }
    public string HostName { get; set; }
    public string UserId { get; set; }
    public string UserEmail { get; set; }
    public long? ElapsedMilliseconds { get; set; }
    public string CorrelationId { get; set; }
    public Exception Exception { get; set; }
    public Dictionary<string, string> AdditionalInformation { get; set; }

    public Log() => TimeStamp = DateTime.UtcNow;
}

如您所见,我正在尝试将其记录到此:

private static ColumnOptions GetColumnOptions()
{
    var columnOptions = new ColumnOptions();

    columnOptions.Store.Remove(StandardColumn.Exception);
    columnOptions.Store.Remove(StandardColumn.Level);
    columnOptions.Store.Remove(StandardColumn.Message);
    columnOptions.Store.Remove(StandardColumn.MessageTemplate);
    columnOptions.Store.Remove(StandardColumn.Properties);
    columnOptions.Store.Remove(StandardColumn.TimeStamp);

    columnOptions.AdditionalColumns = new Collection<SqlColumn>
    {
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "AdditionalInformation"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "CorrelationId"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "CustomException"},
        new SqlColumn { DataType = SqlDbType.Int, ColumnName = "ElapsedMilliseconds", AllowNull = true},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Exception"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Hostname"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Layer"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Location"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Message"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Model"},
        new SqlColumn { DataType = SqlDbType.DateTime, ColumnName = "Timestamp"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "UserId"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "UserEmail"}
    };

    return columnOptions;
}

我设法让它工作了。我不确定为什么我在任何地方都找不到它的任何文档,但这就是我所做的。 我做的第一件事是将我的列选项修改为:

private static ColumnOptions GetColumnOptions()
{
    var columnOptions = new ColumnOptions();

    columnOptions.Store.Remove(StandardColumn.Level);
    columnOptions.Store.Remove(StandardColumn.Message);
    columnOptions.Store.Remove(StandardColumn.MessageTemplate);
    columnOptions.Store.Remove(StandardColumn.Properties);

    columnOptions.AdditionalColumns = new Collection<SqlColumn>
    {
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "AdditionalInformation"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "CorrelationId"},
        new SqlColumn { DataType = SqlDbType.Int, ColumnName = "ElapsedMilliseconds", AllowNull = true},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Hostname"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Layer"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Location"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Message"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Model"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "UserId"},
        new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "UserEmail"}
    };

    return columnOptions;
}

我打算稍后设置 DataLength,但如您所见,我删除了一些不感兴趣的列,然后添加了我自己的列。

然后我修改了我写日志的方式:

public static void LogError(Log log)
{
    log.Message = GetMessageFromException(log.Exception);
    _errorLogger.PopulateCustomColumns(log).Write(LogEventLevel.Information, log.Exception, "{@Log}", log);
}

这里有两件事发生了变化,首先我在调用中添加了 Exception,其次,我添加了一个名为 PopulateCustomColumns 的新方法,如下所示:

private static ILogger PopulateCustomColumns(this ILogger logger, Log log)
{
    var additionalInformation = JsonConvert.SerializeObject(log.AdditionalInformation);
    return logger
        .ForContext("AdditionalInformation", additionalInformation)
        .ForContext("CorrelationId", log.CorrelationId)
        .ForContext("ElapsedMilliseconds", log.ElapsedMilliseconds)
        .ForContext("Hostname", log.HostName)
        .ForContext("Layer", log.Layer)
        .ForContext("Location", log.Location)
        .ForContext("Message", log.Message)
        .ForContext("Model", log.Model)
        .ForContext("UserId", log.UserId)
        .ForContext("UserEmail", log.UserEmail);
}

值得注意的是,如果不添加 messageTemplate,它不会记录任何内容(时间戳除外)。我希望这对其他人有帮助。

为了完成,这里是整个 Logger class:

public static class Logger
{
    private static ILogger _diagnosticLogger;
    private static ILogger _errorLogger;
    private static ILogger _performanceLogger;
    private static ILogger _usageLogger;

    public static void AddLogging(this IServiceCollection services, IConfiguration configuration, string sectionName = "Logging")
    {
        services.Configure<LoggerConfig>(configuration.GetSection(sectionName));
        services.AddSingleton(m => m.GetRequiredService<IOptions<LoggerConfig>>().Value);

        var scope = services.BuildServiceProvider().CreateScope();
        var config = scope.ServiceProvider.GetRequiredService<LoggerConfig>();

        var t = Path.Combine(Directory.GetCurrentDirectory(), "self.log");
        var file = File.CreateText(Path.Combine(Directory.GetCurrentDirectory(), "self.log"));

        Serilog.Debugging.SelfLog.Enable(TextWriter.Synchronized(file));

        _diagnosticLogger = CreateLogger("DiagnosticLogs", config.ConnectionString);
        _errorLogger = CreateLogger("ErrorLogs", config.ConnectionString);
        _performanceLogger = CreateLogger("PerformanceLogs", config.ConnectionString);
        _usageLogger = CreateLogger("UsageLogs", config.ConnectionString);
    }

    public static void LogDiagnostic(Log log)
    {
        var shouldWrite = Convert.ToBoolean(Environment.GetEnvironmentVariable("LOG_DIAGNOSTICS"));
        if (!shouldWrite) return;

        _diagnosticLogger.PopulateCustomColumns(log).Write(LogEventLevel.Information, log.Exception, "{@Log}", log);
    }

    public static void LogError(Log log)
    {
        log.Message = GetMessageFromException(log.Exception);
        _errorLogger.PopulateCustomColumns(log).Write(LogEventLevel.Information, log.Exception, "{@Log}", log);
    }

    public static void LogPerformance(Log log) =>
        _performanceLogger.PopulateCustomColumns(log).Write(LogEventLevel.Information, log.Exception, "{@Log}", log);

    public static void LogUsage(Log log) =>
        _usageLogger.PopulateCustomColumns(log).Write(LogEventLevel.Information, log.Exception, "{@Log}", log);

    private static string GetMessageFromException(Exception exception)
    {
        while (true)
        {
            if (exception.InnerException == null) return exception.Message;
            exception = exception.InnerException;
        }
    }

    private static ILogger CreateLogger(string name, string connectionString) =>
        new LoggerConfiguration()
            //.WriteTo.File(path: Environment.GetEnvironmentVariable(name))
            .WriteTo.MSSqlServer(connectionString, 
                sinkOptions: GetSinkOptions(name),
                columnOptions: GetColumnOptions())
            .CreateLogger();

    private static ColumnOptions GetColumnOptions()
    {
        var columnOptions = new ColumnOptions();

        columnOptions.Store.Remove(StandardColumn.Level);
        columnOptions.Store.Remove(StandardColumn.Message);
        columnOptions.Store.Remove(StandardColumn.MessageTemplate);
        columnOptions.Store.Remove(StandardColumn.Properties);

        columnOptions.AdditionalColumns = new Collection<SqlColumn>
        {
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "AdditionalInformation"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "CorrelationId"},
            new SqlColumn { DataType = SqlDbType.Int, ColumnName = "ElapsedMilliseconds", AllowNull = true},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Hostname"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Layer"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Location"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Message"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "Model"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "UserId"},
            new SqlColumn { DataType = SqlDbType.VarChar, ColumnName = "UserEmail"}
        };

        return columnOptions;
    }

    private static ILogger PopulateCustomColumns(this ILogger logger, Log log)
    {
        var additionalInformation = JsonConvert.SerializeObject(log.AdditionalInformation);
        return logger
            .ForContext("AdditionalInformation", additionalInformation)
            .ForContext("CorrelationId", log.CorrelationId)
            .ForContext("ElapsedMilliseconds", log.ElapsedMilliseconds)
            .ForContext("Hostname", log.HostName)
            .ForContext("Layer", log.Layer)
            .ForContext("Location", log.Location)
            .ForContext("Message", log.Message)
            .ForContext("Model", log.Model)
            .ForContext("UserId", log.UserId)
            .ForContext("UserEmail", log.UserEmail);
    }

    private static SinkOptions GetSinkOptions(string name)
    {
        return new SinkOptions
        {
            TableName = name,
            AutoCreateSqlTable = true,
            BatchPostingLimit = 1
        };
    }
}