Serilog組態設定紀錄-混合型

Serilog用的越來越專業組態設定就越來越複雜。在這複雜的組態中簡化的方案就是混合 appsettings.json 的靜態組態與程式碼的動態(參數)組態。

開發環境

  • Visual Studio 2022

  • .NET6

  • Blazor Server App

上一版文章參考

Serilog 導入紀錄 for ASP.NET Core

程式碼紀錄

appsettings.json
{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    }
  },
  "SystemID": "MONEY",  
  "LogFolder": "C:\\Log",
  ...
}
Program.cs
using Serilog;
using Serilog.Sinks.MSSqlServer;

try
{
    var builder = WebApplication.CreateBuilder(args);
    IConfiguration Configuration = builder.Configuration;

    #region ## Prefix system initialization
    
    // 取得系統名稱
    string systemName = Assembly.GetAssembly(typeof(Program)).GetName().Name;
    
    #endregion

    #region ## Serilog configuration. ------------------------------------------
    var columnOptions = new ColumnOptions();
    columnOptions.Store.Remove(StandardColumn.Properties);
    columnOptions.Store.Remove(StandardColumn.MessageTemplate);
    columnOptions.Store.Add(StandardColumn.LogEvent);
    columnOptions.AdditionalColumns = new Collection<SqlColumn>
    {
        new SqlColumn{ColumnName = "SysName", DataType = SqlDbType.NVarChar, DataLength = 128},
        new SqlColumn{ColumnName = "ClassName", DataType = SqlDbType.NVarChar, DataLength = 128},
        new SqlColumn{ColumnName = "MethodName", DataType = SqlDbType.NVarChar, DataLength = 128},
        new SqlColumn{ColumnName = "AuthUser", DataType = SqlDbType.NVarChar, DataLength = 128},
        new SqlColumn{ColumnName = "ClientIP", DataType = SqlDbType.NVarChar, DataLength = 128}
    };

    Log.Logger = new LoggerConfiguration()
        .ReadFrom.Configuration(Configuration) // 先取靜態組態,接下來再混合動態(參數)組態。
        .Enrich.WithProperty("SysName", Configuration["SystemID"])
        .Enrich.FromLogContext()
        .WriteTo.Async(cfg =>
        {
            //# 文字檔
            cfg.File($"{Configuration["LogFolder"] ?? "Log"}/{systemName}Log.txt",
                rollingInterval: RollingInterval.Day);
            //# JSON 檔
            cfg.File(new Serilog.Formatting.Json.JsonFormatter(), $"{Configuration["LogFolder"] ?? "Log"}/{systemName}Log.json",
                rollingInterval: RollingInterval.Day);
            //# SQL Server
            cfg.MSSqlServer(
                connectionString: YourSecureModule["LOGDB"],
                sinkOptions: new MSSqlServerSinkOptions() { TableName = "Serilog" },
                columnOptions: columnOptions);
            //# 自訂 LoggerSink : ActionLog WebApi
            cfg.CustomSerilogSink(Configuration);
        }).CreateLogger();
    #endregion

    builder.Host.UseSerilog();
    Log.Information("Web host start.");

    #region ## Add services to the container. ----------------------------------
    ...
    #endregion

    var app = builder.Build();

    #region ## Configure the HTTP request pipeline. ----------------------------

    ...

    // custom middleware:用於強化Serilog取得環境參數。
    app.UseMiddleware<CustomMiddleware>();

    //## Endpoints
    app.MapBlazorHub();
    app.MapFallbackToPage("/_Host");

    #endregion

    app.Run();
    Log.Information("Web host exit.");
}
catch (Exception ex)
{
    Log.Fatal(ex, "Web host terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}
CustomMiddleware.cs
using Microsoft.AspNetCore.Http;
using Serilog.Context;

namespace YourProject.Services;

/// <summary>
/// 中介軟體(ASP.NET Core middleware)。用於強化Serilog取得環境參數。
/// </summary>
/// <see cref="ASP.NET Core 中介軟體(https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1)"/>
public class CustomMiddleware
{
    private readonly RequestDelegate _next;

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

    [System.Diagnostics.DebuggerHidden]
    public async Task Invoke(HttpContext context)
    {
        try
        {
            using (LogContext.PushProperty("AuthUser", context.User.Identity?.Name))
            using (LogContext.PushProperty("ClientIP", context.Connection.RemoteIpAddress))
            using (LogContext.PushProperty("UserAgent", context.Request.Headers["User-Agent"].FirstOrDefault()))
            using (LogContext.PushProperty("DomainUserName", $"{Environment.UserDomainName}\\{Environment.UserName}"))
            using (LogContext.PushProperty("MachineName", Environment.MachineName))
            {
                // Do work that doesn't write to the Response.
                await _next.Invoke(context);
                // Do logging or other work that doesn't write to the Response.
            }
        }
        catch (Exception ex)
        {
            throw new ApplicationException("CustomMiddleware.Invoke 出現例外!", ex);
        }
    }
}
Serilog.table.sql
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Serilog](
	[Id] [bigint] IDENTITY(1,1) NOT NULL,
	[TimeStamp] [datetime2](7) NOT NULL,
	[Level] [nvarchar](128) NULL,
	[Message] [nvarchar](max) NULL,
	[AuthUser] [nvarchar](128) NULL,
	[SysName] [nvarchar](128) NULL,
	[ClassName] [nvarchar](128) NULL,
	[MethodName] [nvarchar](128) NULL,
	[ClientIP] [nvarchar](128) NULL,
	[Exception] [nvarchar](max) NULL,
	[LogEvent] [nvarchar](max) NULL,
 CONSTRAINT [PK_Serilog] PRIMARY KEY CLUSTERED 
(
	[Id] DESC
))
GO
ILoggerClassExtensions.cs
using Microsoft.Extensions.Logging;
using Serilog.Context;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

public static class ILoggerClassExtensions
{
    public static void LogEx(this ILogger logger, LogLevel logLevel, string message, Exception exception = null,
       [CallerMemberName] string memberName = "",
       [CallerFilePath] string sourceFilePath = "",
       [CallerLineNumber] int sourceLineNumber = 0)
    {
        using (LogContext.PushProperty("ClassName", logger.GetType().GenericTypeArguments[0].Name))
        using (LogContext.PushProperty("MethodName", memberName))
        using (LogContext.PushProperty("SourceFilePath", sourceFilePath))
        using (LogContext.PushProperty("SourceLineNumber", sourceLineNumber))
        {
            logger.Log(logLevel, exception, message);
        }
    }

    public static void CriticalEx(this ILogger logger, string message, Exception exception = null,
       [CallerMemberName] string memberName = "",
       [CallerFilePath] string sourceFilePath = "",
       [CallerLineNumber] int sourceLineNumber = 0)
    {
        using (LogContext.PushProperty("ClassName", logger.GetType().GenericTypeArguments[0].Name))
        using (LogContext.PushProperty("MethodName", memberName))
        using (LogContext.PushProperty("SourceFilePath", sourceFilePath))
        using (LogContext.PushProperty("SourceLineNumber", sourceLineNumber))
        {
            logger.LogCritical(exception, message);
        }
    }

    public static void DebugEx(this ILogger logger, string message, Exception exception = null,
       [CallerMemberName] string memberName = "",
       [CallerFilePath] string sourceFilePath = "",
       [CallerLineNumber] int sourceLineNumber = 0)
    {
        using (LogContext.PushProperty("ClassName", logger.GetType().GenericTypeArguments[0].Name))
        using (LogContext.PushProperty("MethodName", memberName))
        using (LogContext.PushProperty("SourceFilePath", sourceFilePath))
        using (LogContext.PushProperty("SourceLineNumber", sourceLineNumber))
        {
            logger.LogDebug(exception, message);
        }
    }

    public static void ErrorEx(this ILogger logger, string message, Exception exception = null,
       [CallerMemberName] string memberName = "",
       [CallerFilePath] string sourceFilePath = "",
       [CallerLineNumber] int sourceLineNumber = 0)
    {
        using (LogContext.PushProperty("ClassName", logger.GetType().GenericTypeArguments[0].Name))
        using (LogContext.PushProperty("MethodName", memberName))
        using (LogContext.PushProperty("SourceFilePath", sourceFilePath))
        using (LogContext.PushProperty("SourceLineNumber", sourceLineNumber))
        {
            logger.LogError(exception, message);
        }
    }

    public static void InfoEx(this ILogger logger, string message, Exception exception = null,
       [CallerMemberName] string memberName = "",
       [CallerFilePath] string sourceFilePath = "",
       [CallerLineNumber] int sourceLineNumber = 0)
    {
        using (LogContext.PushProperty("ClassName", logger.GetType().GenericTypeArguments[0].Name))
        using (LogContext.PushProperty("MethodName", memberName))
        using (LogContext.PushProperty("SourceFilePath", sourceFilePath))
        using (LogContext.PushProperty("SourceLineNumber", sourceLineNumber))
        {
            logger.LogInformation(exception, message);
        }
    }

    public static void TraceEx(this ILogger logger, string message, Exception exception = null,
       [CallerMemberName] string memberName = "",
       [CallerFilePath] string sourceFilePath = "",
       [CallerLineNumber] int sourceLineNumber = 0)
    {
        using (LogContext.PushProperty("ClassName", logger.GetType().GenericTypeArguments[0].Name))
        using (LogContext.PushProperty("MethodName", memberName))
        using (LogContext.PushProperty("SourceFilePath", sourceFilePath))
        using (LogContext.PushProperty("SourceLineNumber", sourceLineNumber))
        {
            logger.LogTrace(exception, message);
        }
    }

    public static void WarnEx(this ILogger logger, string message, Exception exception = null,
       [CallerMemberName] string memberName = "",
       [CallerFilePath] string sourceFilePath = "",
       [CallerLineNumber] int sourceLineNumber = 0)
    {
        using (LogContext.PushProperty("ClassName", logger.GetType().GenericTypeArguments[0].Name))
        using (LogContext.PushProperty("MethodName", memberName))
        using (LogContext.PushProperty("SourceFilePath", sourceFilePath))
        using (LogContext.PushProperty("SourceLineNumber", sourceLineNumber))
        {
            logger.LogWarning(exception, message);
        }
    }
}

Last updated