C# 日志写入loki
在 C# 中实现日志写入 Loki 最常用的方式是结合 Serilog 日志框架和 Serilog.Sinks.Grafana.Loki
扩展包。这种方式支持结构化日志、自定义标签和灵活的配置,以下是完整的实现步骤:
一、准备工作
-
安装 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
-
确保 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
-
运行应用程序
调用OrderController.CreateOrder
接口(可通过 Postman 或 Swagger 发送请求),生成测试日志。 -
在 Grafana 中查询日志
- 打开 Grafana(默认地址:
http://localhost:3000
),添加 Loki 数据源(地址填写 Loki 的 HTTP 地址,如http://localhost:3100
)。 - 进入 Explore 页面,选择 Loki 数据源,使用标签筛选日志:
# 筛选 order-service 的日志 {service="order-service", environment="Development"}
- 若日志为 JSON 格式,可通过
| json
解析字段并筛选:# 筛选用户 ID 为 123 的错误日志 {service="order-service"} | json | UserId="123" and Level="Error"
- 打开 Grafana(默认地址:
![(https://i-blog.csdnimg.cn/direct/7aced4b706fa458784f296f279635a87.png)
五、关键配置说明
-
Loki 地址与租户
uri
:Loki 的 HTTP 接口地址(默认http://localhost:3100
),若 Loki 部署在远程服务器,需替换为实际 IP 或域名。- 多租户场景:通过
X-Scope-OrgID
头指定tenantId
,实现不同租户日志隔离。
-
日志标签(labels)
标签是 Loki 日志筛选的核心,建议包含service
(服务名)、environment
(环境)等固定标识,便于后续按服务、环境查询日志。 -
JSON 格式化
使用JsonFormatter
输出 JSON 格式日志,Loki 可直接解析其中的字段(如UserId
、OrderId
),支持复杂查询(如按用户 ID 筛选)。 -
批量发送
通过batchPostingLimit
和period
控制日志批量发送策略,减少网络请求次数,优化性能。
六、常见问题解决
-
日志未发送到 Loki
- 检查 Loki 地址是否正确,确保
http://localhost:3100/ready
可访问。 - 查看应用程序控制台输出,是否有
Failed to send log batch to Loki
错误(通常是网络不通或 Loki 未启动)。 - 确认防火墙未拦截 3100 端口。
- 检查 Loki 地址是否正确,确保
-
多租户日志隔离问题
- 若租户日志混淆,检查
X-Scope-OrgID
头是否正确添加(可通过抓包工具验证请求头)。 - 在 Loki 配置中设置
allow_empty_org_id: false
,强制客户端必须指定租户 ID。
- 若租户日志混淆,检查
-
日志字段解析失败
- 若 JSON 日志字段未被 Loki 解析,确保
textFormatter
使用JsonFormatter
,且日志格式为标准 JSON。
- 若 JSON 日志字段未被 Loki 解析,确保
通过以上配置,C# 应用程序的日志可无缝发送到 Loki,结合 Grafana 可实现日志的集中管理、查询和可视化,非常适合分布式系统的日志监控。