在 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 可实现日志的集中管理、查询和可视化,非常适合分布式系统的日志监控。