🔍 微服務可觀測性:日誌、追蹤與指標
📌 可觀測性三大支柱
可觀測性 (Observability)
├── 📋 Logs(日誌):記錄發生了什麼事
│ └── "訂單 #123 建立成功"
├── 🔗 Traces(追蹤):追蹤請求在服務間的流動
│ └── Gateway → OrderService → InventoryService → PaymentService
└── 📊 Metrics(指標):量化系統的狀態
└── 請求數/秒、延遲 P99、錯誤率
| 支柱 | 回答的問題 | 工具 |
|---|---|---|
| Logs | 發生了什麼?為什麼出錯? | Serilog + Seq / ELK |
| Traces | 請求經過了哪些服務?哪裡慢? | OpenTelemetry + Jaeger |
| Metrics | 系統整體表現如何?需要擴展嗎? | Prometheus + Grafana |
📌 集中式日誌:Serilog
為什麼需要集中式日誌?
微服務的日誌分散在多個容器中,需要集中收集才能有效除錯。
// ── 安裝 Serilog ──
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.Seq
// dotnet add package Serilog.Enrichers.Environment
// dotnet add package Serilog.Enrichers.Thread
// Program.cs
builder.Host.UseSerilog((context, config) =>
{
config
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.Enrich.WithProperty("ServiceName", "OrderService")
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {ServiceName} | {Message:lj}{NewLine}{Exception}")
.WriteTo.Seq("http://seq:5341"); // 集中式日誌伺服器
});
// 使用結構化日誌
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public async Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
_logger.LogInformation("開始建立訂單 CustomerId={CustomerId}, Items={ItemCount}",
cmd.CustomerId, cmd.Items.Count);
try
{
var order = new Order { /* ... */ };
await _repo.AddAsync(order);
_logger.LogInformation("訂單建立成功 OrderId={OrderId}, Total={Total}",
order.Id, order.TotalAmount);
return order;
}
catch (Exception ex)
{
_logger.LogError(ex, "訂單建立失敗 CustomerId={CustomerId}",
cmd.CustomerId);
throw;
}
}
}
📌 關聯 ID (Correlation ID) 追蹤請求鏈
// ── 中介軟體:為每個請求加上 Correlation ID ──
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
private const string CorrelationHeader = "X-Correlation-ID";
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
// 從上游取得 Correlation ID,沒有就建立新的
if (!context.Request.Headers.TryGetValue(
CorrelationHeader, out var correlationId))
{
correlationId = Guid.NewGuid().ToString();
}
context.Items[CorrelationHeader] = correlationId.ToString();
// 加入回應標頭
context.Response.OnStarting(() =>
{
context.Response.Headers[CorrelationHeader] = correlationId;
return Task.CompletedTask;
});
// 加入日誌上下文
using (LogContext.PushProperty("CorrelationId", correlationId.ToString()))
{
await _next(context);
}
}
}
// 呼叫下游服務時傳遞 Correlation ID
public class CorrelationIdDelegatingHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CorrelationIdDelegatingHandler(
IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
var correlationId = _httpContextAccessor.HttpContext?
.Items["X-Correlation-ID"]?.ToString();
if (!string.IsNullOrEmpty(correlationId))
request.Headers.Add("X-Correlation-ID", correlationId);
return await base.SendAsync(request, ct);
}
}
📌 分散式追蹤:OpenTelemetry
// ── 安裝 ──
// dotnet add package OpenTelemetry.Extensions.Hosting
// dotnet add package OpenTelemetry.Instrumentation.AspNetCore
// dotnet add package OpenTelemetry.Instrumentation.Http
// dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore
// dotnet add package OpenTelemetry.Exporter.OtlpProtobuf
// Program.cs
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService("OrderService"))
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("OrderService.Activities")
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri("http://jaeger:4317");
});
})
.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddPrometheusExporter();
});
// 使用 Prometheus 端點
app.MapPrometheusScrapingEndpoint();
自訂追蹤 Span
public class OrderService
{
private static readonly ActivitySource _activitySource =
new("OrderService.Activities");
public async Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
using var activity = _activitySource.StartActivity("CreateOrder");
activity?.SetTag("order.customer_id", cmd.CustomerId.ToString());
activity?.SetTag("order.item_count", cmd.Items.Count);
// 驗證步驟
using (var validateActivity = _activitySource.StartActivity("ValidateOrder"))
{
await ValidateAsync(cmd);
validateActivity?.SetTag("validation.result", "success");
}
// 儲存步驟
using (var saveActivity = _activitySource.StartActivity("SaveOrder"))
{
var order = new Order { /* ... */ };
await _repo.AddAsync(order);
activity?.SetTag("order.id", order.Id.ToString());
return order;
}
}
}
📌 健康指標:Prometheus + Grafana
// 自訂業務指標
public class OrderMetrics
{
private readonly Counter<long> _ordersCreated;
private readonly Histogram<double> _orderProcessingDuration;
private readonly UpDownCounter<long> _activeOrders;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("OrderService");
_ordersCreated = meter.CreateCounter<long>(
"orders.created",
description: "建立的訂單數量");
_orderProcessingDuration = meter.CreateHistogram<double>(
"orders.processing.duration",
unit: "ms",
description: "訂單處理耗時");
_activeOrders = meter.CreateUpDownCounter<long>(
"orders.active",
description: "進行中的訂單數量");
}
public void OrderCreated(string category)
{
_ordersCreated.Add(1, new KeyValuePair<string, object?>("category", category));
_activeOrders.Add(1);
}
public void RecordDuration(double milliseconds)
=> _orderProcessingDuration.Record(milliseconds);
public void OrderCompleted()
=> _activeOrders.Add(-1);
}
📌 Docker Compose:完整可觀測性堆疊
# 加入可觀測性服務
services:
# 集中日誌
seq:
image: datalust/seq:latest
environment:
- ACCEPT_EULA=Y
ports:
- "5341:5341" # 接收日誌
- "8081:80" # Web UI
# 分散式追蹤
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Web UI
- "4317:4317" # OTLP gRPC
# 指標收集
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
# 指標儀表板
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
下一章: 我們將學習如何將微服務部署到 Kubernetes,建立完整的 CI/CD Pipeline。