# 日志 (03_Logging)

最后更新: 2024-09-20


# 📚 概述

DarkM 日志模块基于 Serilog 构建,支持结构化日志记录,并集成 OpenTracing 实现分布式链路追踪(Jaeger)。

源代码位置: DarkM/src/Framework/Logging


# 🏗️ 模块架构

# 完整目录结构

Logging/
├── Logging.Serilog/                       # Serilog 日志核心
│   ├── WebHostBuilderExtensions.cs        # Web 主机扩展
│   └── Logging.Serilog.csproj
│
├── Logging.Serilog.Enricher/              # 日志增强器
│   ├── SerilogLoggerEnricher.cs           # 日志增强器
│   ├── SerilogLoggerExtensions.cs         # 日志扩展
│   └── SerilogLoggerConfiguration.cs      # 日志配置
│
├── Logging.Serilog.GenericHost/           # 通用主机日志
│   └── GenericHostBuilderExtensions.cs    # 通用主机扩展
│
└── Logging.Serilog.OpenTracing/           # OpenTracing 链路追踪
    ├── Options/
    │   ├── OpenTracingConfig.cs           # OpenTracing 配置
    │   └── JaegerConfig.cs                # Jaeger 配置
    ├── OpenTracing/
    │   ├── UpdatableReporter.cs           # 可更新报告器
    │   └── UpdatableSampler.cs            # 可更新采样器
    ├── Enrichers/
    │   ├── RequestContextLogEventEnricher.cs      # 请求上下文增强
    │   └── OpenTracingContextLogEventEnricher.cs  # 追踪上下文增强
    ├── OpenTracingSink.cs                 # OpenTracing Sink
    └── ServiceCollectionExtensions.cs     # DI 扩展
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

# 🔧 Serilog 配置

# appsettings.json 配置

{
  "Serilog": {
    // 日志输出最小等级
    "MinimumLevel": {
      "Default": "Error",
      "Override": {
        "Microsoft": "Error",
        "System": "Error"
      }
    },
    "WriteTo": [
      // 输出到控制台
      {
        "Name": "Console",
        "Args": {
          "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
        }
      },
      // 输出到文件
      {
        "Name": "File",
        "Args": {
          // 文件路径
          "path": "log/log.log",
          // 文件滚动方式
          "rollingInterval": "Day",
          // 消息输出格式
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
          // 文件数量
          "retainedFileCountLimit": 60,
          // 使用缓冲,提高写入效率
          "buffered": false
        }
      }
    ]
  }
}
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

# 配置项详解

配置项 类型 默认值 说明
MinimumLevel.Default LogEventLevel Error 默认日志等级
MinimumLevel.Override Object - 覆盖命名空间日志等级
WriteTo Array - 输出目标数组
WriteTo.Name string - 输出方式(Console/File/等)
WriteTo.Args Object - 输出方式参数

# 日志等级说明

等级 说明 使用场景
Verbose 最详细日志 调试模式
Debug 调试信息 开发环境
Information 一般信息 生产环境(推荐)
Warning 警告信息 需要注意的问题
Error 错误信息 生产环境(默认)
Fatal 致命错误 系统崩溃级别

# 🔧 OpenTracing 链路追踪

# OpenTracingConfig

文件位置: Logging.Serilog.OpenTracing/Options/OpenTracingConfig.cs

public class OpenTracingConfig
{
    /// <summary>
    /// 是否启用追踪
    /// </summary>
    public bool EnableTracing { get; set; }

    /// <summary>
    /// Jaeger 配置
    /// </summary>
    public JaegerConfig JaegerConfig { get; set; }
}

public class JaegerConfig
{
    public SamplerOption SamplerOption { get; set; }
    public SenderOption SenderOption { get; set; }
    public ReporterOption ReporterOption { get; set; }
}

public class SamplerOption
{
    /// <summary>
    /// 采样器类型(Const/RemoteControlled)
    /// </summary>
    public string SamplerType { get; set; }

    /// <summary>
    /// 采样参数(0-1 之间,表示采样比例)
    /// </summary>
    public int Param { get; set; }

    /// <summary>
    /// 采样端点
    /// </summary>
    public string SamplingEndpoint { get; set; }

    /// <summary>
    /// 管理器主机端口
    /// </summary>
    public int ManagerHostPort { get; set; }
}

public class SenderOption
{
    /// <summary>
    /// Agent 主机地址
    /// </summary>
    public string AgentHost { get; set; }

    /// <summary>
    /// Agent 端口
    /// </summary>
    public int AgentPort { get; set; }

    /// <summary>
    /// gRPC 目标地址
    /// </summary>
    public string GrpcTarget { get; set; }

    /// <summary>
    /// 端点地址
    /// </summary>
    public string Endpoint { get; set; }

    /// <summary>
    /// 认证令牌
    /// </summary>
    public string AuthToken { get; set; }
}

public class ReporterOption
{
    /// <summary>
    /// 是否记录 Span 日志
    /// </summary>
    public bool LogSpans { get; set; }

    /// <summary>
    /// 最大队列大小
    /// </summary>
    public int MaxQueueSize { get; set; }

    /// <summary>
    /// 刷新间隔(毫秒)
    /// </summary>
    public int FlushInterval { get; set; }
}
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

# 💡 配置说明

# appsettings.json 完整配置

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "log/log.log",
          "rollingInterval": "Day",
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
          "retainedFileCountLimit": 60,
          "buffered": false
        }
      }
    ]
  },
  "OpenTracing": {
    // 是否启用链路追踪
    "EnableTracing": true,
    
    // Jaeger 配置
    "JaegerConfig": {
      // 采样器配置
      "SamplerOption": {
        "SamplerType": "RemoteControlled",
        "Param": 1,
        "SamplingEndpoint": "",
        "ManagerHostPort": 0
      },
      
      // 发送器配置(UDP 方式)
      "SenderOption": {
        "AgentHost": "localhost",
        "AgentPort": 6831,
        "GrpcTarget": "",
        "GrpcRootCertificate": "",
        "Endpoint": "",
        "AuthToken": "",
        "AuthUsername": "",
        "AuthPassword": ""
      },
      
      // 报告器配置
      "ReporterOption": {
        "LogSpans": true,
        "MaxQueueSize": 100,
        "FlushInterval": 1000
      }
    }
  }
}
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

# OpenTracing 配置项详解

配置项 类型 默认值 说明
EnableTracing bool false 是否启用 OpenTracing
SamplerType string RemoteControlled 采样器类型
Param int 1 采样比例(1=100%)
AgentHost string localhost Jaeger Agent 地址
AgentPort int 6831 Jaeger Agent UDP 端口
LogSpans bool true 记录 Span 日志
MaxQueueSize int 100 最大队列大小
FlushInterval int 1000 刷新间隔(毫秒)

# 🏗️ 依赖注入

# ServiceCollectionExtensions

文件位置: Logging.Serilog.OpenTracing/ServiceCollectionExtensions.cs

public static class ServiceCollectionExtensions
{
    /// <summary>
    /// 添加 OpenTracing 日志
    /// </summary>
    public static IServiceCollection AddOpenTracingLog(
        this IServiceCollection services, 
        IConfiguration cfg)
    {
        var config = new OpenTracingConfig();
        var section = cfg.GetSection("OpenTracing");
        section?.Bind(config);

        if (config.EnableTracing)
        {
            services.AddSingleton(config);

            // 加入 OpenTracing
            services.AddOpenTracing();

            services.AddSingleton<ITracer>(serviceProvider =>
            {
                var loggerFactory = serviceProvider
                    .GetRequiredService<ILoggerFactory>();
                
                string serviceName = serviceProvider
                    .GetRequiredService<IHostingEnvironment>()
                    .ApplicationName;
                
                var sendOption = config.JaegerConfig.SenderOption;

                // 配置发送器(使用 UDP 发送器)
                Configuration.SenderConfiguration.DefaultSenderResolver = 
                    new SenderResolver(loggerFactory)
                        .RegisterSenderFactory<ThriftSenderFactory>();

                // 创建报告器
                var reporter = new RemoteReporter.Builder()
                    .WithLoggerFactory(loggerFactory)
                    .WithSender(new UdpSender(
                        sendOption.AgentHost, 
                        sendOption.AgentPort, 
                        0))
                    .Build();

                // 创建 Tracer
                var tracer = new Tracer.Builder(serviceName)
                    .WithLoggerFactory(loggerFactory)
                    .WithSampler(new RemoteControlledSampler
                        .Builder(serviceName)
                        .Build())
                    .WithReporter(reporter)
                    .Build();

                // 注册全局 Tracer
                GlobalTracer.Register(tracer);

                return tracer;
            });
        }

        return services;
    }
}
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

# 💡 使用示例

# 1. 启用 OpenTracing

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 添加 OpenTracing 日志
builder.Services.AddOpenTracingLog(builder.Configuration);

var app = builder.Build();
1
2
3
4
5
6
7

# 2. 手动创建 Span

using OpenTracing;
using OpenTracing.Util;

public class OrderService
{
    private readonly ITracer _tracer;
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
        _tracer = GlobalTracer.Instance;
    }

    public async Task<OrderEntity> CreateOrderAsync(CreateOrderRequest request)
    {
        // 创建 Span
        using var scope = _tracer.BuildSpan("CreateOrder")
            .WithTag("order.type", request.OrderType)
            .WithTag("order.amount", request.Amount)
            .StartActive(true);

        var span = scope.Span;

        try
        {
            _logger.LogInformation("开始创建订单:{OrderType}", request.OrderType);

            // 业务逻辑
            var order = await _orderRepository.InsertAsync(request.ToEntity());

            span.SetTag("order.id", order.Id);
            span.Log("订单创建成功");

            _logger.LogInformation("订单创建成功:{OrderId}", order.Id);

            return order;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true);
            span.Log($"订单创建失败:{ex.Message}");
            
            _logger.LogError(ex, "订单创建失败");
            throw;
        }
    }
}
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

# 3. 链路追踪上下文传递

public class PaymentService
{
    private readonly ITracer _tracer;

    public PaymentService()
    {
        _tracer = GlobalTracer.Instance;
    }

    public async Task<bool> ProcessPaymentAsync(int orderId, decimal amount)
    {
        // 获取当前 Span 上下文
        var parentSpan = _tracer.ActiveSpan;

        // 创建子 Span
        using var scope = _tracer.BuildSpan("ProcessPayment")
            .AsChildOf(parentSpan)
            .WithTag("payment.orderId", orderId)
            .WithTag("payment.amount", amount)
            .StartActive(true);

        var span = scope.Span;

        try
        {
            // 调用第三方支付接口
            var result = await _paymentGateway.PayAsync(orderId, amount);

            span.Log($"支付成功:{result.TransactionId}");
            
            return result.Success;
        }
        catch (Exception ex)
        {
            span.SetTag("error", true);
            span.Log($"支付失败:{ex.Message}");
            throw;
        }
    }
}
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

# 🔧 Jaeger 部署

# Docker 部署 Jaeger(All-in-One)

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 14250:14250 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.46
1
2
3
4
5
6
7
8
9
10
11

访问 Jaeger UI: http://localhost:16686

# Docker Compose 部署

version: '3.8'

services:
  jaeger:
    image: jaegertracing/all-in-one:1.46
    environment:
      - COLLECTOR_ZIPKIN_HOST_PORT=:9411
    ports:
      - "5775:5775/udp"
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
      - "16686:16686"
      - "14268:14268"
      - "14250:14250"
      - "9411:9411"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 📊 链路追踪示例

# 典型请求链路

[API Gateway]
    ↓
[Web Controller] ── Span: GET /api/orders
    ↓
[Order Service] ──── Span: CreateOrder
    ↓
[Payment Service] ─ Span: ProcessPayment
    ↓
[Payment Gateway] ─ Span: HTTP POST /pay
    ↓
[Database] ──────── Span: DB.Insert
1
2
3
4
5
6
7
8
9
10
11

# Jaeger UI 查看步骤

  1. 访问 Jaeger UI:http://localhost:16686
  2. 选择 Service(应用名称)
  3. 选择 Operation(操作名称)
  4. 点击 Find Traces
  5. 展开 Trace 查看详细 Span

# 🔍 常见问题

# Q1: 如何配置采样率?

解决方案:

{
  "OpenTracing": {
    "JaegerConfig": {
      "SamplerOption": {
        "SamplerType": "Probabilistic",
        "Param": 0.1  // 10% 采样率
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10

采样类型:

  • Const - 固定采样(0=不采样,1=全部采样)
  • Probabilistic - 概率采样(0-1 之间)
  • RateLimiting - 速率限制采样
  • RemoteControlled - 远程控制采样(推荐)

# Q2: 如何配置 Jaeger Agent 地址?

解决方案:

{
  "OpenTracing": {
    "JaegerConfig": {
      "SenderOption": {
        "AgentHost": "jaeger-agent",
        "AgentPort": 6831
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10

# Q3: 如何禁用某些请求的追踪?

解决方案:

services.Configure<HttpHandlerDiagnosticOptions>(options =>
{
    options.IgnorePatterns.Add(req => 
        req.RequestUri.AbsolutePath.Contains("/health"));
    
    options.IgnorePatterns.Add(req => 
        req.RequestUri.AbsolutePath.Contains("/metrics"));
});
1
2
3
4
5
6
7
8

# Q4: 如何更改日志等级?

解决方案:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug"  // 修改为 Debug 记录更详细的日志
    }
  }
}
1
2
3
4
5
6
7

# Q5: Serilog 支持哪些输出方式?

Serilog 提供了很多种日志输出方式(Sinks):

Sink 说明 NuGet 包
Console 控制台输出 Serilog.Sinks.Console
File 文件输出 Serilog.Sinks.File
Database 数据库输出 Serilog.Sinks.MSSqlServer
Elasticsearch Elasticsearch 输出 Serilog.Sinks.Elasticsearch
Seq Seq 日志服务器 Serilog.Sinks.Seq
Redis Redis 输出 Serilog.Sinks.Redis

参考:Provided Sinks (opens new window)


# 📚 相关文档


# 🔗 参考链接


最后更新: 2024-09-20