EngineeringSolution ArchitectSecurity and Observability
Session 01 - OpenTelemetry & Distributed Tracing
2 min read8 lessons in this module
Tổng quan OpenTelemetry
OpenTelemetry (OTel) là vendor-neutral standard cho observability, cung cấp 3 pillars:
| Pillar | Mục đích | .NET API |
|---|
| Traces | Theo dõi request flow qua services | System.Diagnostics.Activity |
| Metrics | Đo lường performance & business KPIs | System.Diagnostics.Metrics |
| Logs | Structured event data | ILogger + OTel bridge |
Tại sao chọn OpenTelemetry?
- Vendor lock-in elimination: Đổi backend (Jaeger → Zipkin → Azure Monitor) mà không sửa code
- W3C Trace Context: Standard propagation header (
traceparent, tracestate)
- Auto-instrumentation: HTTP, gRPC, EF Core, HttpClient tự động tạo spans
.NET Integration - Complete Setup
1. NuGet Packages
xml
<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.9.*" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.*" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.3.*" />
</ItemGroup>
2. Program.cs Configuration
csharp
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
using OpenTelemetry.Logs;
var builder = WebApplication.CreateBuilder(args);
var serviceName = "OrderService";
var serviceVersion = "1.0.0";
var resource = ResourceBuilder.CreateDefault()
.AddService(serviceName, serviceVersion: serviceVersion)
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = builder.Environment.EnvironmentName,
["host.name"] = Environment.MachineName
});
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.SetResourceBuilder(resource)
.AddAspNetCoreInstrumentation(opts =>
{
opts.RecordException = true;
opts.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health");
})
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation(opts =>
{
opts.SetDbStatementForText = true;
opts.RecordException = true;
})
.AddSource("OrderService.Activities")
.AddOtlpExporter(opts =>
{
opts.Endpoint = new Uri("http://otel-collector:4317");
});
})
.WithMetrics(metrics =>
{
metrics
.SetResourceBuilder(resource)
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddMeter("OrderService.Metrics")
.AddOtlpExporter();
});
builder.Logging.AddOpenTelemetry(logging =>
{
logging.SetResourceBuilder(resource);
logging.IncludeScopes = true;
logging.IncludeFormattedMessage = true;
logging.AddOtlpExporter();
});
3. Custom Spans & Attributes
csharp
using System.Diagnostics;
public class OrderService
{
private static readonly ActivitySource _activitySource = new("OrderService.Activities");
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
using var activity = _activitySource.StartActivity(
"CreateOrder",
ActivityKind.Internal);
activity?.SetTag("order.customer_id", request.CustomerId);
activity?.SetTag("order.item_count", request.Items.Count);
activity?.SetTag("order.total_amount", request.TotalAmount);
try
{
var order = await ProcessOrder(request);
activity?.SetTag("order.id", order.Id);
activity?.SetStatus(ActivityStatusCode.Ok);
return order;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
}
Trace Propagation
Correlation ID Middleware
csharp
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Activity.Current?.TraceId.ToString()
?? Guid.NewGuid().ToString();
context.Items["CorrelationId"] = correlationId;
context.Response.Headers["X-Correlation-Id"] = correlationId;
using (var scope = context.RequestServices
.GetRequiredService<ILogger<CorrelationIdMiddleware>>()
.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId }))
{
await _next(context);
}
}
}
Sampling Strategies
| Strategy | Mô tả | Use case |
|---|
| AlwaysOn | Sample 100% traces | Dev/staging |
| AlwaysOff | Không sample | Tắt tracing |
| TraceIdRatioBased | Sample theo tỷ lệ (VD: 10%) | Production high-traffic |
| ParentBased | Kế thừa quyết định từ parent span | Distributed systems |
csharp
tracing.SetSampler(new ParentBasedSampler(
new TraceIdRatioBasedSampler(0.1)));
Tail-based Sampling (Collector-side)
Thay vì quyết định lúc tạo trace, tail-based sampling chờ trace hoàn thành rồi mới quyết định:
- Giữ lại 100% traces có error
- Giữ lại traces có latency > P99
- Sample random cho traces bình thường
Exporters Comparison
| Exporter | Protocol | Use case |
|---|
| OTLP/gRPC | gRPC | OTel Collector, Jaeger, Tempo |
| OTLP/HTTP | HTTP/protobuf | Khi gRPC bị block bởi firewall |
| Azure Monitor | HTTPS | Azure Application Insights |
| Console | stdout | Local debugging |
| Zipkin | HTTP/JSON | Legacy Zipkin backends |
Trade-offs & Best Practices
Nên
- Dùng OTel Collector làm trung gian thay vì export trực tiếp từ app
- Đặt semantic conventions cho span names (
http.method, db.system)
- Luôn set
RecordException = true cho production
Tránh
- Over-instrumentation: Không tạo span cho mọi method call - chỉ tạo cho boundary operations
- Sensitive data trong tags: Không log PII trong span attributes
- Quá nhiều cardinality: Tránh dùng userId làm metric label - sẽ explode time series