前言

三年前寫了一篇跟 Serilog 有關的極致入門文章後就把這個主題閒置到現在,他的很多特色不要說點到為止,而是根本沒提到,但這樣對於這樣一個好用的 Library 來說實在太糟蹋,所以打算重啟這個主題。因為當時就有位這個主題開個 repo 所以我們也就不要浪費網路的空間了,直接回收利用吧(還順便把 solution 檔給升級了)~

source code

這次將會針對設定做一點初步的介紹,雖然近期事情開始漸漸變多,但看看能抽出多少時間來把相關的內容介紹一下,也許有機會變成一個小的系列 (也許啦 XD)。

內文

這邊會展示在進行 Serilog 設置時可以採行什麼樣的寫法在 Production 環境下可以有比較好的閱讀性與維護性,還有我們可以針對需要寫 Log 的情境給予一些過濾條件,讓這個系統更有彈性,最後我們整合目前官方網站上有提供的一些 Sink 來稍稍展示 Serilog 搭配上 Sink 之後可以展現的可能性。

在先前的文章之中我們直接針對 Serilog 的 LoggerConfiguration 類別來設定,但若是需要設定的條件眾多那麼光是這個設定就會讓程式看起來很複雜。這邊會使用一個小技巧來讓設定看起來比較簡潔一點。

首先建立一個給 Serilog 做初始的一個類別,並使用最簡單的設定方式來為 Logger 做設定。

ps. 這邊的 LogFactory 類別只是作範例簡單使用,重點在於 InitMyLogger 方法內的寫法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class LogFactory
{
    public static void InitMyLogger()
    {
        Log.Logger = new LoggerConfiguration()
                            .WriteTo.NLog(
                                Serilog.Events.LogEventLevel.Error,
                                "[AppId:{AppId}]{Message:lj}{NewLine}{Exception}")
                            .WriteTo.Console(
                                restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information,
                                outputTemplate: "{Timestamp:HH:mm:ss} [AppId:{AppId} {Level:u3}] {Message:lj}{NewLine}{Exception}")
                            .WriteTo.Logger(configer =>
                            {
                                configer.Filter
                                        .ByIncludingOnly(Matching.FromSource<BusinessLogic>())
                                        .WriteTo.Console(
                                            Serilog.Events.LogEventLevel.Warning,
                                            "{Timestamp:HH:mm:ss} [AppId:{AppId2}{AppId} {Level:u3}]$ {Message:lj}{NewLine}{Exception}");
                            }).CreateLogger();
    }
}

這邊不難看出目前雖然只是簡單的三樣設定就已將讓這個設定開始顯得有些複雜了。若這是一段需要被維護的程式碼不難想像應該會是個痛苦的開始。

這邊會利用到的是 LoggerSinkConfigurationLogger 方法,由於它可以接受一個 Action delegate 並且傳入一個 LoggerConfiguration 型別的參數,我們就可以利用這一點來將程式法做一個整理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class LogFactory
{
    public static void InitMyLogger()
    {
        Log.Logger = new LoggerConfiguration()
                         .WriteTo.Logger(MyDefaultConsoleSinkConfiger)
                         .WriteTo.Logger(MyDefaultNLogSinkConfiger)
                         .WriteTo.Logger(ConsoleLogWithPropAndFilterWithClassSinkConfiger)
                         .CreateLogger();
    }

    private static void MyDefaultNLogSinkConfiger(LoggerConfiguration lscf)
    {
        lscf.WriteTo
            .NLog(Serilog.Events.LogEventLevel.Error, "[AppId:{AppId}] {Message:lj}{NewLine}{Exception}");
    }

    private static void MyDefaultConsoleSinkConfiger(LoggerConfiguration lscf)
    {
        lscf.WriteTo.Console(
            restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information,
            outputTemplate: "{Timestamp:HH:mm:ss} [AppId:{AppId} {Level:u3}] {Message:lj}{NewLine}{Exception}");
    }

    private static void ConsoleLogWithPropAndFilterWithClassSinkConfiger(LoggerConfiguration lscf)
    {
        var isBSLN = Matching.FromSource<BusinessLogic>();

        lscf.Enrich
            .Filter
            .ByIncludingOnly(isBSLN)
            .WriteTo.Console(
                Serilog.Events.LogEventLevel.Warning,
                "{Timestamp:HH:mm:ss} [AppId:{AppId2}{AppId} {Level:u3}]$ {Message:lj}{NewLine}{Exception}");
    }
}

藉由使用這個技巧可以讓你的設定更好整理。

當然這次的範例不會只有 Console 和 NLog 而已,目前的系統很常是跑在 Cloud 上,這部分通常又以 AWS, Azure, GCP 最為常見,而這些服務又會整合一些維運時必要的系統,也就是 Log 的蒐集與警示,這邊就與 AWS 的 Cloudwatch 系統做界接的 Sink 為範例。

Serilog 整合 AWS Cloudwatch

  1. 安裝 Sink

Install-Package Serilog.Sinks.AwsCloudWatch

這個套件會相依於 AWSSDK.CloudWatchLogs, Serilog, Serilog.Sinks.PeriodicBatchingAWSSDK.Core

  1. 設定 Sink
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static void AwsCloudwatchSinkConfiger(LoggerConfiguration lscf)
{
    // AWS Cloudwatch 要記錄 Log 的位置
    var logGroupName = "myLogGroup/dev";

    // 詳細的設定細節請參考原作 https://github.com/Cimpress-MCP/serilog-sinks-awscloudwatch/blob/master/src/Serilog.Sinks.AwsCloudWatch/CloudWatchSinkOptions.cs
    var options = new CloudWatchSinkOptions
    {
        // 請一定要提供 text formatter 不然會噴錯
        TextFormatter = new Serilog.Formatting.Json.JsonFormatter(),

        LogGroupName = logGroupName,

        // other defaults defaults
        MinimumLogEventLevel = Serilog.Events.LogEventLevel.Information,
        BatchSizeLimit = 100,
        QueueSizeLimit = 10000,
        Period = TimeSpan.FromSeconds(10),
        CreateLogGroup = true,
        LogStreamNameProvider = new DefaultLogStreamProvider(),
        RetryAttempts = 5
    };

    // setup AWS CloudWatch client, 請確認 IAM 使用者有 Cloudwatch 寫入的權限
    var client = new AmazonCloudWatchLogsClient("yourAwsAccessKey", "yourAwsSecretAccessKey", RegionEndpoint.USWest2);

    // 這個 Sink 把設定都擺在 CloudWatchSinkOptions 類別裡
    lscf.WriteTo
        .AmazonCloudWatch(options, client);
}

而在 Init 方法裡就用一樣的方式註冊進去

1
2
3
4
5
6
Log.Logger = new LoggerConfiguration()
                .WriteTo.Logger(MyDefaultConsoleSinkConfiger)
                .WriteTo.Logger(MyDefaultNLogSinkConfiger)
                .WriteTo.Logger(ConsoleLogWithPropAndFilterWithClassSinkConfiger)
                .WriteTo.Logger(AwsCloudwatchSinkConfiger)
                .CreateLogger();

完整的設定類別如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class LogFactory
{
    public static void InitMyLogger()
    {
        Log.Logger = new LoggerConfiguration()
                        .Enrich.WithProperty("AppId", new { Id = 1, Name = "Ben" })
                        .WriteTo.Logger(MyDefaultConsoleSinkConfiger)
                        .WriteTo.Logger(MyDefaultNLogSinkConfiger)
                        .WriteTo.Logger(ConsoleLogWithPropAndFilterWithClassSinkConfiger)
                        .WriteTo.Logger(AwsCloudwatchSinkConfiger)
                        .CreateLogger();
    }

    private static void MyDefaultNLogSinkConfiger(LoggerConfiguration lscf)
    {
        lscf.WriteTo
            .NLog(Serilog.Events.LogEventLevel.Error, "[AppId:{AppId}] {Message:lj}{NewLine}{Exception}");
    }

    private static void MyDefaultConsoleSinkConfiger(LoggerConfiguration lscf)
    {
        lscf.WriteTo.Console(
            restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information,
            outputTemplate: "{Timestamp:HH:mm:ss} [AppId:{AppId} {Level:u3}] {Message:lj}{NewLine}{Exception}");
    }

    private static void ConsoleLogWithPropAndFilterWithClassSinkConfiger(LoggerConfiguration lscf)
    {
        var isBSLN = Matching.FromSource<BusinessLogic>();

        lscf.Enrich
            .WithProperty("AppId2", 10)
            .Filter
            .ByIncludingOnly(isBSLN)
            .WriteTo.Console(
                Serilog.Events.LogEventLevel.Warning,
                "{Timestamp:HH:mm:ss} [AppId:{AppId2}{AppId} {Level:u3}]$ {Message:lj}{NewLine}{Exception}");
    }

    private static void AwsCloudwatchSinkConfiger(LoggerConfiguration lscf)
    {
        // AWS Cloudwatch 要記錄 Log 的位置
        var logGroupName = "myLogGroup/dev";

        // 詳細的設定細節請參考原作 https://github.com/Cimpress-MCP/serilog-sinks-awscloudwatch/blob/master/src/Serilog.Sinks.AwsCloudWatch/CloudWatchSinkOptions.cs
        var options = new CloudWatchSinkOptions
        {
            // 請一定要提供 text formatter 不然會噴錯
            TextFormatter = new Serilog.Formatting.Json.JsonFormatter(),

            LogGroupName = logGroupName,

            // other defaults defaults
            MinimumLogEventLevel = Serilog.Events.LogEventLevel.Information,
            BatchSizeLimit = 100,
            QueueSizeLimit = 10000,
            Period = TimeSpan.FromSeconds(10),
            CreateLogGroup = true,
            LogStreamNameProvider = new DefaultLogStreamProvider(),
            RetryAttempts = 5
        };

        // setup AWS CloudWatch client, 請確認 IAM 使用者有 Cloudwatch 寫入的權限
        var client = new AmazonCloudWatchLogsClient("yourAwsAccessKey", "yourAwsSecretAccessKey", RegionEndpoint.USWest2);

        // 這個 Sink 把設定都擺在 CloudWatchSinkOptions 類別裡
        lscf.WriteTo
            .AmazonCloudWatch(options, client);
    }
}

以 Github 裡的 source code 為例,在 Cloudwatch 中的 Log 看起來如下 (共四筆):

1
2
3
4
5
6
7
8
9
{
    "Timestamp": "2020-04-07T17:56:27.9702533+08:00",
    "Level": "Information",
    "MessageTemplate": "Starting up",
    "Properties": {
        "SourceContext": "BenSerilogNlog.BusinessLogic",
        "AppId": "{ Id = 1, Name = Ben }"
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "Timestamp": "2020-04-07T17:56:28.1162511+08:00",
    "Level": "Information",
    "MessageTemplate": "In my bowl I have {Fruit}",
    "Properties": {
        "Fruit": {
            "Apple": 1,
            "Pear": 5
        },
        "SourceContext": "BenSerilogNlog.BusinessLogic",
        "AppId": "{ Id = 1, Name = Ben }"
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "Timestamp": "2020-04-07T17:56:28.2832500+08:00",
    "Level": "Warning",
    "MessageTemplate": "Processed {@Position}",
    "Properties": {
        "Position": {
            "Latitude": 25,
            "Longitude": 134
        },
        "SourceContext": "BenSerilogNlog.BusinessLogic",
        "AppId": "{ Id = 1, Name = Ben }"
    }
}
1
2
3
4
5
6
7
8
9
{
    "Timestamp": "2020-04-07T17:56:28.6452494+08:00",
    "Level": "Error",
    "MessageTemplate": "This is a error log",
    "Properties": {
        "SourceContext": "BenSerilogNlog.BusinessLogic",
        "AppId": "{ Id = 1, Name = Ben }"
    }
}

Enrich WithProperty 的一些小想法

在完整範例這邊我又多加了一個 Property 這個東西, 這個可以加在全部的 Log 也可以加在個別的 Log 之上。在最外圍的 Property: AppId 就是可以在每一個 Log 裡都附加的一個屬性,這個很適合用在系統龐大但又分成數個子系統共同 co-work 且會把 Log 統一蒐集的系統中像是 GrayLog, Kibana 等,我們可以藉由添加這個屬性,讓這些視覺化的系統可以用一些 Filter 過濾不同系統的 Log。而這個 Property 也可以設在 Logger 裡,讓不同的 Logger 可以攜帶不同的 property 使資訊量更豐富。

Reference