C# 日志写入loki

发布于:2025-08-31 ⋅ 阅读:(20) ⋅ 点赞:(0)

在 C# 中实现日志写入 Loki 最常用的方式是结合 Serilog 日志框架和 Serilog.Sinks.Grafana.Loki 扩展包。这种方式支持结构化日志、自定义标签和灵活的配置,以下是完整的实现步骤:

一、准备工作

  1. 安装 NuGet 包
    在项目中安装必要的依赖包(通过 NuGet 包管理器或命令行):

    # Serilog 核心包
    Install-Package Serilog -Version 3.1.1
    Install-Package Serilog.AspNetCore -Version 8.0.0  # 集成 ASP.NET Core
    
    # Loki 接收器(用于将日志发送到 Loki)
    Install-Package Serilog.Sinks.Grafana.Loki -Version 8.0.0
    
  2. 确保 Loki 服务可用
    确认 Loki 已启动并可访问(默认地址:http://localhost:3100),可通过访问 http://localhost:3100/ready 验证,返回 ready 即表示正常运行。

二、配置 Serilog 连接 Loki

Program.cs 中配置 Serilog,设置 Loki 服务地址、日志标签、租户信息(可选)和日志格式:

using Serilog;
using Serilog.Sinks.Grafana.Loki;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

var builder = WebApplication.CreateBuilder(args);

// 1. 从配置文件读取 Loki 地址和租户 ID(建议通过 appsettings.json 配置)
var lokiUri = builder.Configuration["Loki:Uri"] ?? "http://localhost:3100";
var tenantId = builder.Configuration["Loki:TenantId"] ?? "default-tenant"; // 多租户标识

// 2. 配置 Serilog 日志系统
List<LokiLabel> labels = new List<LokiLabel>();
labels.Add(new LokiLabel { Key = "App", Value = "testproject" });

// 1.获取应用程序名称和版本(用于 Elasticsearch 索引命名)
var appName = Assembly.GetExecutingAssembly().GetName().Name;
var appVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0";

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()  // 从日志上下文获取属性(如追踪 ID)
    .Enrich.WithProperty("service", "ServiceB")  // 全局标签:服务名
    .Enrich.WithProperty("environment", "development") // 全局标签:环境
    .WriteTo.GrafanaLoki("http://localhost:3100", labels)
    .WriteTo.Console()
    // 设置最小日志级别(Information 及以上)
    .MinimumLevel.Information()
    // 针对特定命名空间调整日志级别(可选)
    .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
    .CreateLogger();
Log.Information("Hello, Grafana Loki!");
// 替换默认日志工厂为 Serilog
builder.Host.UseSerilog();

// 3. 替换 ASP.NET Core 默认日志系统为 Serilog
builder.Host.UseSerilog();

// 4. 注册服务和中间件
builder.Services.AddControllers();
var app = builder.Build();

// 5. 可选:添加分布式追踪 ID 到日志(便于链路追踪)
app.Use(async (context, next) =>
{
    var activity = System.Diagnostics.Activity.Current;
    if (activity != null)
    {
        // 将 TraceId 和 SpanId 注入日志上下文
        using (LogContext.PushProperty("trace_id", activity.TraceId.ToString()))
        using (LogContext.PushProperty("span_id", activity.SpanId.ToString()))
        {
            await next();
        }
    }
    else
    {
        await next();
    }
});

app.MapControllers();
app.Run();

// 自定义 HTTP 处理器:添加 Loki 租户头(多租户场景)
public class LokiTenantHandler : DelegatingHandler
{
    private readonly string _tenantId;

    public LokiTenantHandler(string tenantId)
    {
        _tenantId = tenantId;
        InnerHandler = new HttpClientHandler(); // 基础 HTTP 处理器
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        // 添加 Loki 租户标识头(多租户必需)
        request.Headers.Add("X-Scope-OrgID", _tenantId);
        return base.SendAsync(request, cancellationToken);
    }
}

三、在业务代码中记录日志

通过 ILogger<T> 接口记录日志,日志会自动序列化为 JSON 并发送到 Loki:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace LokiLoggingDemo.Controllers;

[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;

    // 构造函数注入日志接口
    public OrderController(ILogger<OrderController> logger)
    {
        _logger = logger;
    }

    [HttpPost]
    public IActionResult CreateOrder([FromBody] OrderRequest request)
    {
        // 1. 记录信息日志(包含结构化参数)
        _logger.LogInformation(
            "用户 {UserId} 发起订单创建请求,商品 ID:{ProductId},数量:{Quantity}",
            request.UserId, request.ProductId, request.Quantity
        );

        try
        {
            if (request.Quantity <= 0)
            {
                throw new ArgumentException("商品数量必须大于 0");
            }

            // 模拟订单创建逻辑
            var orderId = Guid.NewGuid().ToString();
            
            // 2. 记录成功日志(包含复杂对象)
            var orderResult = new 
            { 
                OrderId = orderId, 
                Status = "Created", 
                TotalAmount = request.Quantity * 99.99m 
            };
            _logger.LogInformation("订单创建成功:{OrderResult}", orderResult);

            return Ok(new { OrderId = orderId });
        }
        catch (Exception ex)
        {
            // 3. 记录错误日志(包含异常堆栈)
            _logger.LogError(
                ex, 
                "用户 {UserId} 订单创建失败,商品 ID:{ProductId}",
                request.UserId, request.ProductId
            );
            return BadRequest(ex.Message);
        }
    }
}

// 订单请求模型
public class OrderRequest
{
    public string UserId { get; set; }
    public string ProductId { get; set; }
    public int Quantity { get; set; }
}

四、验证日志是否写入 Loki

  1. 运行应用程序
    调用 OrderController.CreateOrder 接口(可通过 Postman 或 Swagger 发送请求),生成测试日志。

  2. 在 Grafana 中查询日志 在这里插入图片描述

    1. 打开 Grafana(默认地址:http://localhost:3000),添加 Loki 数据源(地址填写 Loki 的 HTTP 地址,如 http://localhost:3100)。
    2. 进入 Explore 页面,选择 Loki 数据源,使用标签筛选日志:
      # 筛选 order-service 的日志
      {service="order-service", environment="Development"}
      
    3. 若日志为 JSON 格式,可通过 | json 解析字段并筛选:
      # 筛选用户 ID 为 123 的错误日志
      {service="order-service"} | json | UserId="123" and Level="Error"
      

![(https://i-blog.csdnimg.cn/direct/7aced4b706fa458784f296f279635a87.png)

五、关键配置说明

  1. Loki 地址与租户

    • uri:Loki 的 HTTP 接口地址(默认 http://localhost:3100),若 Loki 部署在远程服务器,需替换为实际 IP 或域名。
    • 多租户场景:通过 X-Scope-OrgID 头指定 tenantId,实现不同租户日志隔离。
  2. 日志标签(labels)
    标签是 Loki 日志筛选的核心,建议包含 service(服务名)、environment(环境)等固定标识,便于后续按服务、环境查询日志。

  3. JSON 格式化
    使用 JsonFormatter 输出 JSON 格式日志,Loki 可直接解析其中的字段(如 UserIdOrderId),支持复杂查询(如按用户 ID 筛选)。

  4. 批量发送
    通过 batchPostingLimitperiod 控制日志批量发送策略,减少网络请求次数,优化性能。

六、常见问题解决

  1. 日志未发送到 Loki

    • 检查 Loki 地址是否正确,确保 http://localhost:3100/ready 可访问。
    • 查看应用程序控制台输出,是否有 Failed to send log batch to Loki 错误(通常是网络不通或 Loki 未启动)。
    • 确认防火墙未拦截 3100 端口。
  2. 多租户日志隔离问题

    • 若租户日志混淆,检查 X-Scope-OrgID 头是否正确添加(可通过抓包工具验证请求头)。
    • 在 Loki 配置中设置 allow_empty_org_id: false,强制客户端必须指定租户 ID。
  3. 日志字段解析失败

    • 若 JSON 日志字段未被 Loki 解析,确保 textFormatter 使用 JsonFormatter,且日志格式为标准 JSON。

通过以上配置,C# 应用程序的日志可无缝发送到 Loki,结合 Grafana 可实现日志的集中管理、查询和可视化,非常适合分布式系统的日志监控。