# OpenTelemetry trong .NET: Distributed Tracing, TraceId và OTLP từ Zero đến Production
## Giới thiệu
Khi một hệ thống chỉ có một API và một database, việc debug tương đối đơn giản.
Bạn có thể xem log:
“`text
Request failed at PaymentService
“`
và lần theo code để tìm nguyên nhân.
Nhưng khi hệ thống bắt đầu có nhiều thành phần:
“`text
Mobile App
↓
API Gateway
↓
Order API
↓
Payment API
↓
PostgreSQL
↓
Service Bus
↓
Background Worker
“`
một request có thể đi qua nhiều service, database query và background process khác nhau.
Nếu Payment API xảy ra timeout, câu hỏi quan trọng không còn chỉ là:
> Service nào bị lỗi?
Mà là:
> Log này thuộc request nào? Request đó đã đi qua những service nào? Query nào chậm? Service downstream nào gây ra latency?
Đây là bài toán mà **Distributed Tracing** giải quyết.
Trong .NET, một trong những cách chuẩn nhất để triển khai distributed tracing hiện nay là sử dụng:
“`text
OpenTelemetry
+
System.Diagnostics.Activity
+
OTLP
“`
—
# 1. Trước tiên: OTLP không phải OLTP
Hai thuật ngữ này khá dễ nhầm.
## OLTP
OLTP là:
“`text
Online Transaction Processing
“`
Nó liên quan đến workload database có nhiều transaction ngắn như:
“`text
Create Order
Update Wallet
Register Event
Redeem Coupon
“`
Ví dụ hệ thống:
“`text
.NET API
↓
SQL Server
“`
có thể là một OLTP system.
## OTLP
OTLP là:
“`text
OpenTelemetry Protocol
“`
Đây là protocol được sử dụng để truyền telemetry như:
“`text
Traces
Metrics
Logs
“`
Ví dụ:
“`text
.NET API
↓
OpenTelemetry SDK
↓
OTLP
↓
OpenTelemetry Collector
↓
Observability Backend
“`
Trong bài này chúng ta tập trung vào **OTLP và distributed tracing**.
—
# 2. Trace là gì?
Giả sử client gọi:
“`http
POST /api/orders
“`
Request này chạy qua:
“`text
Order API
↓
Payment API
↓
Database
↓
Message Broker
“`
Toàn bộ flow đó được gọi là một:
“`text
Trace
“`
Một trace có thể chứa nhiều:
“`text
Span
“`
Ví dụ:
“`text
Trace
TraceId = ABC123
├── POST /api/orders
│
├── ValidateOrder
│
├── SELECT Product
│
├── Payment API
│
├── INSERT Order
│
└── Publish OrderCreated
“`
Mỗi operation là một span.
—
# 3. TraceId và SpanId
Một trace có một:
“`text
TraceId
“`
Ví dụ:
“`text
TraceId
=
2d5f762c7dc947fbb62a328030fb3db2
“`
Mỗi span trong trace có:
“`text
SpanId
“`
Ví dụ:
“`text
TraceId = ABC123
POST /orders
SpanId = 001
SELECT Order
SpanId = 002
Payment API
SpanId = 003
“`
Tất cả span có cùng TraceId nhưng SpanId khác nhau.
Đó chính là cách observability backend biết:
“`text
những operation này thuộc cùng một request
“`
—
# 4. OpenTelemetry mapping sang .NET như thế nào?
Trong .NET, OpenTelemetry tracing dựa nhiều vào:
“`csharp
System.Diagnostics.Activity
“`
Mapping có thể hiểu đơn giản như sau:
| OpenTelemetry | .NET |
|—|—|
| Trace | chuỗi các Activity |
| Span | `Activity` |
| Tracer | `ActivitySource` |
| TraceId | `Activity.TraceId` |
| SpanId | `Activity.SpanId` |
Ví dụ:
“`csharp
var traceId = Activity.Current?.TraceId;
“`
Nếu code đang chạy trong một request được trace, `Activity.Current` sẽ đại diện cho span hiện tại.
—
# 5. Không nên tự tạo TraceId bằng GUID
Một lỗi khá phổ biến là:
“`csharp
var traceId = Guid.NewGuid().ToString();
“`
sau đó:
“`csharp
logger.LogInformation(
“TraceId={TraceId}”,
traceId);
“`
Cách này tạo một custom correlation ID.
Nhưng nó không tự động liên kết với:
“`text
ASP.NET Core
HttpClient
Database
OpenTelemetry
Message Broker
Distributed Trace
“`
Nếu hệ thống đã sử dụng OpenTelemetry thì nên dùng:
“`csharp
Activity.Current?.TraceId
“`
Ví dụ:
“`csharp
var traceId =
Activity.Current?.TraceId.ToString();
“`
Điều này giúp giữ nguyên trace context xuyên suốt request.
—
# 6. Các package cần thiết
Một ASP.NET Core application cơ bản có thể cần:
“`bash
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
“`
Nếu cần instrument EF Core:
“`bash
dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore
“`
Các package có vai trò khác nhau:
“`text
OpenTelemetry.Extensions.Hosting
↓
Integrate OpenTelemetry với .NET DI
AspNetCoreInstrumentation
↓
Trace incoming HTTP requests
HttpInstrumentation
↓
Trace outgoing HttpClient calls
EntityFrameworkCoreInstrumentation
↓
Trace EF Core operations
OTLP Exporter
↓
Send telemetry tới Collector/backend
“`
—
# 7. Cấu hình OpenTelemetry trong ASP.NET Core
Một setup cơ bản:
“`csharp
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOpenTelemetry()
.ConfigureResource(resource =>
{
resource.AddService(
serviceName: “Order.Api”,
serviceVersion: “1.0.0”);
})
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource(“Order.Api”)
.AddOtlpExporter(options =>
{
options.Endpoint =
new Uri(“http://otel-collector:4317”);
});
});
var app = builder.Build();
app.Run();
“`
Flow lúc này:
“`text
HTTP Request
↓
ASP.NET Core Instrumentation
↓
Activity
↓
TracerProvider
↓
OTLP Exporter
↓
OpenTelemetry Collector
“`
—
# 8. OTLP đóng vai trò gì?
OpenTelemetry và OTLP không phải cùng một thứ.
Có thể hiểu:
“`text
OpenTelemetry
=
Telemetry standard + API + SDK + instrumentation
“`
Còn:
“`text
OTLP
=
Protocol dùng để truyền telemetry
“`
Architecture phổ biến:
“`text
ASP.NET Core
↓
OpenTelemetry SDK
↓
OTLP
↓
OpenTelemetry Collector
↓
Jaeger / Tempo / Azure Monitor / Other Backend
“`
OTLP thường hỗ trợ:
“`text
gRPC
HTTP/Protobuf
“`
—
# 9. Không nên hard-code OTLP endpoint
Development có thể viết:
“`csharp
.AddOtlpExporter(options =>
{
options.Endpoint =
new Uri(“http://localhost:4317”);
});
“`
Nhưng production tốt hơn nên đưa config ra environment.
Ví dụ:
“`text
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
“`
Code:
“`csharp
.AddOtlpExporter();
“`
Lợi ích là cùng một Docker image có thể chạy trên:
“`text
Development
Staging
Production
“`
mà không cần rebuild.
—
# 10. Auto instrumentation chưa đủ
OpenTelemetry có thể tự trace được nhiều infrastructure operation như:
“`text
Incoming HTTP
Outgoing HTTP
Database
“`
Nhưng framework không hiểu business logic của bạn.
Ví dụ:
“`text
RedeemCoupon
CheckInEvent
CalculateReward
ApproveMerchant
“`
Nếu muốn trace những operation này, cần custom span.
—
# 11. Tạo ActivitySource
Một cách tổ chức tốt:
“`csharp
using System.Diagnostics;
public static class Telemetry
{
public const string ServiceName =
“CivicPoints.Api”;
public static readonly ActivitySource ActivitySource =
new(ServiceName);
}
“`
Sau đó register:
“`csharp
builder.Services
.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource(Telemetry.ServiceName)
.AddOtlpExporter();
});
“`
—
# 12. Tạo custom business span
Ví dụ operation:
“`text
RedeemCoupon
“`
Code:
“`csharp
using var activity =
Telemetry.ActivitySource.StartActivity(
“Coupon.Redeem”);
activity?.SetTag(
“coupon.id”,
couponId);
activity?.SetTag(
“merchant.id”,
merchantId);
“`
Trace có thể hiển thị:
“`text
POST /coupon/redeem
│
├── Coupon.Redeem
│
├── SELECT Coupon
│
├── UPDATE Wallet
│
├── INSERT RewardHistory
│
└── UPDATE Coupon
“`
Từ đó bạn thấy rất rõ phần nào của request đang chậm.
—
# 13. Một lỗi dễ gặp với ActivitySource
Giả sử:
“`csharp
new ActivitySource(“CivicPoints.Api”);
“`
nhưng register:
“`csharp
.AddSource(“Order.Api”);
“`
Hai tên không giống nhau.
Kết quả custom Activity có thể không được collect.
Nên đảm bảo:
“`csharp
public const string ServiceName =
“CivicPoints.Api”;
“`
được dùng ở cả hai nơi:
“`csharp
new ActivitySource(ServiceName);
“`
và:
“`csharp
.AddSource(ServiceName);
“`
—
# 14. TraceId trong ILogger
Trace chỉ thực sự hữu ích khi có thể correlate với logs.
Ví dụ:
“`csharp
logger.LogInformation(
“Redeeming coupon {CouponId}. TraceId={TraceId}”,
couponId,
Activity.Current?.TraceId);
“`
Log:
“`text
Redeeming coupon C123
TraceId=2d5f762c7dc947fbb62a328030fb3db2
“`
Khi production có lỗi, bạn search TraceId và xem toàn bộ flow.
—
# 15. Có nên trả TraceId về client?
Trong nhiều hệ thống, câu trả lời là có.
Ví dụ response:
“`json
{
“error”: “Internal server error”,
“traceId”: “2d5f762c7dc947fbb62a328030fb3db2”
}
“`
User hoặc QA báo:
“`text
I received error with TraceId:
2d5f762c7dc947fbb62a328030fb3db2
“`
Developer có thể search trực tiếp trace.
Một middleware đơn giản:
“`csharp
app.Use(async (context, next) =>
{
await next();
var traceId =
Activity.Current?.TraceId.ToString();
if (!string.IsNullOrEmpty(traceId))
{
context.Response.Headers[
“X-Trace-Id”] = traceId;
}
});
“`
—
# 16. TraceId, RequestId và CorrelationId khác nhau thế nào?
Đây là một câu khá hay trong interview.
## RequestId
Thường đại diện cho một HTTP request cụ thể.
“`text
Request A -> RequestId 1
Request B -> RequestId 2
“`
## CorrelationId
Thường là ID do application hoặc business tự định nghĩa.
Ví dụ:
“`text
OrderId
PaymentId
WorkflowId
“`
Hoặc:
“`text
CorrelationId = checkout-123
“`
## TraceId
TraceId thuộc distributed tracing.
Ví dụ:
“`text
Mobile
↓
API Gateway
↓
Order API
↓
Payment API
↓
SQL
“`
Tất cả có thể dùng cùng TraceId.
Bạn không nên dùng TraceId thay thế business ID.
Thực tế log tốt thường có cả:
“`text
TraceId
OrderId
UserId
EventId
OrganizationId
“`
—
# 17. Distributed Context Propagation
Giả sử:
“`text
Service A
↓
Service B
“`
Service A đang có:
“`text
TraceId = ABC
SpanId = 001
“`
Khi A gọi B, trace context cần được gửi theo request.
Với HTTP, một standard phổ biến là:
“`text
W3C Trace Context
“`
Header quan trọng:
“`text
traceparent
“`
Concept:
“`text
00-TRACE_ID-SPAN_ID-FLAGS
“`
Service B nhận context và tạo span:
“`text
TraceId = ABC
SpanId = 002
ParentSpanId = 001
“`
Kết quả:
“`text
Service A
↓
Service B
“`
thuộc cùng một trace.
—
# 18. HttpClient instrumentation
Nếu sử dụng:
“`csharp
.AddHttpClientInstrumentation()
“`
OpenTelemetry có thể trace outgoing `HttpClient` request.
Ví dụ:
“`csharp
await httpClient.PostAsync(
“/payment”,
content);
“`
Trace có thể hiển thị:
“`text
POST /orders
↓
HTTP POST Payment API
↓
Payment API processing
“`
Điều này hữu ích hơn rất nhiều so với chỉ thấy:
“`text
HTTP call took 2.3 seconds
“`
vì bạn biết downstream service cụ thể nào gây latency.
—
# 19. Message Queue thì sao?
HTTP call thường synchronous:
“`text
Producer
↓
Consumer
“`
Nhưng message broker thì khác:
“`text
Producer
↓
Service Bus
↓
5 seconds
↓
Worker
“`
Vẫn cần propagate trace context.
Concept:
“`text
Producer Trace
↓
Message Metadata
↓
Consumer
“`
Trace context thường được inject vào message properties.
Ví dụ metadata:
“`text
traceparent
tracestate
“`
Consumer sau đó extract context và tạo child span.
—
# 20. Không nên tạo TraceId mới ở Worker
Giả sử request:
“`text
API
↓
Publish Event
↓
Worker
“`
Worker là continuation của workflow đó.
Nếu worker làm:
“`csharp
Guid.NewGuid()
“`
thì:
“`text
API Trace = AAA
Worker Trace = BBB
“`
Observability system không còn thấy mối quan hệ.
Tốt hơn là propagate context từ producer sang message.
—
# 21. Error tracing
Custom activity nên ghi nhận error.
Ví dụ:
“`csharp
using var activity =
Telemetry.ActivitySource
.StartActivity(“Coupon.Redeem”);
try
{
await RedeemAsync();
activity?.SetStatus(
ActivityStatusCode.Ok);
}
catch (Exception ex)
{
activity?.SetStatus(
ActivityStatusCode.Error,
ex.Message);
activity?.AddException(ex);
throw;
}
“`
Trace UI sau đó có thể hiển thị:
“`text
Coupon.Redeem
████████████ ERROR
“`
Developer có thể click vào span để xem exception.
—
# 22. Nên tag những gì?
Business span có thể chứa:
“`csharp
activity?.SetTag(
“event.id”,
eventId);
activity?.SetTag(
“organization.id”,
organizationId);
activity?.SetTag(
“operation.type”,
“event.checkin”);
“`
Nhưng không nên đưa sensitive data như:
“`text
Password
Access Token
Refresh Token
Credit Card
Private Personal Data
“`
Telemetry cũng là production data và cần được bảo vệ.
—
# 23. Sampling
Nếu application xử lý:
“`text
10,000 requests / second
“`
trace 100% traffic có thể gây:
“`text
High network usage
High storage usage
Collector load
Observability cost
“`
Có thể chỉ sample:
“`text
10%
“`
Nhưng vấn đề là request lỗi cũng có thể bị sample out.
Hệ thống lớn hơn có thể sử dụng:
“`text
Tail-based sampling
“`
để ưu tiên giữ:
“`text
Error traces
Slow traces
Important transactions
“`
—
# 24. Architecture production phù hợp
Một architecture phổ biến:
“`text
ASP.NET Core
│
OpenTelemetry
│
┌───────┴───────┐
│ │
Traces Logs
│ │
└───────┬───────┘
│
OTLP
│
OpenTelemetry
Collector
│
┌───────────┼───────────┐
│ │ │
Jaeger Tempo Azure Monitor
“`
Lợi ích của OpenTelemetry Collector là application không phụ thuộc quá chặt vào một vendor.
—
# 25. Cách tổ chức code trong .NET
Một structure đơn giản:
“`text
Observability
│
├── Telemetry.cs
├── OpenTelemetryExtensions.cs
└── TelemetryConstants.cs
“`
`Telemetry.cs`:
“`csharp
using System.Diagnostics;
public static class Telemetry
{
public const string ServiceName =
“CivicPoints.Api”;
public static readonly ActivitySource ActivitySource =
new(ServiceName);
}
“`
—
# 26. Tách OpenTelemetry config thành extension
“`csharp
public static class OpenTelemetryExtensions
{
public static IServiceCollection AddObservability(
this IServiceCollection services,
IConfiguration configuration)
{
services
.AddOpenTelemetry()
.ConfigureResource(resource =>
{
resource.AddService(
Telemetry.ServiceName);
})
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource(
Telemetry.ServiceName)
.AddOtlpExporter();
});
return services;
}
}
“`
Trong `Program.cs`:
“`csharp
builder.Services
.AddObservability(
builder.Configuration);
“`
Business code:
“`csharp
using var activity =
Telemetry.ActivitySource.StartActivity(
“Event.CheckIn”);
“`
Cách này tránh việc OpenTelemetry configuration nằm rải rác khắp project.
—
# 27. Một trace thực tế có thể trông như thế nào?
Giả sử user check-in một event.
Request:
“`http
POST /v1/events/{id}/checkin
“`
Trace:
“`text
POST /events/{id}/checkin
│
├── Event.CheckIn
│
├── SELECT Event
│
├── SELECT UserEventStatus
│
├── UPDATE UserEventStatus
│
├── UPDATE Wallet
│
├── INSERT RewardHistory
│
└── Publish AttendanceUpdated
“`
Nếu request mất 3 giây:
“`text
POST request 3000 ms
Event.CheckIn 2950 ms
SELECT Event 20 ms
UPDATE Attendance 30 ms
UPDATE Wallet 20 ms
Publish Message 2700 ms
“`
Ngay lập tức bạn thấy:
“`text
Publish Message
“`
là bottleneck.
Đây chính là sức mạnh của distributed tracing.
—
# 28. Logs, Metrics và Traces
Observability thường có ba thành phần chính:
“`text
Logs
Metrics
Traces
“`
Logs trả lời:
> Điều gì đã xảy ra?
Metrics trả lời:
> Hệ thống đang hoạt động như thế nào theo thời gian?
Ví dụ:
“`text
Request rate
Error rate
CPU
Memory
Latency P95
“`
Traces trả lời:
> Request cụ thể này đã đi qua đâu và mất thời gian ở đâu?
Ba loại telemetry bổ sung cho nhau.
—
# 29. Một workflow debug production tốt hơn
Thay vì:
“`text
User reports error
↓
Search random logs
↓
Guess
“`
có thể làm:
“`text
User reports TraceId
↓
Find Trace
↓
Find failed Span
↓
Check logs for same TraceId
↓
Inspect downstream dependency
↓
Find root cause
“`
Ví dụ:
“`text
TraceId
↓
Order API
↓
Payment API
↓
SQL timeout
“`
Bạn biết ngay root cause nằm ở đâu.
—
# 30. Interview Question: How would you implement distributed tracing?
Một câu trả lời tốt:
> I would use OpenTelemetry with .NET’s Activity and ActivitySource APIs. ASP.NET Core instrumentation creates spans for incoming requests, while HttpClient instrumentation propagates W3C trace context to downstream services. For important business operations, I create custom activities. Telemetry is exported through OTLP, preferably to an OpenTelemetry Collector, and then forwarded to the observability backend. I also correlate application logs with the current TraceId so production incidents can be investigated across multiple services.
—
# 31. Interview Question: Would you generate your own TraceId?
Không nên trả lời:
> Yes, I use Guid.NewGuid().
Một câu trả lời tốt hơn:
> If OpenTelemetry is already managing distributed tracing, I use `Activity.Current.TraceId`. That preserves the trace context across service boundaries. A custom GUID can still be useful as a business correlation ID, but it shouldn’t replace the OpenTelemetry TraceId.
—
# 32. Interview Question: TraceId vs CorrelationId?
Có thể trả lời:
> TraceId represents one distributed trace and is managed by the tracing system. A correlation ID is an application-defined identifier that may represent a business workflow. They can coexist. For example, an order can have an OrderId while all services handling that operation share the same TraceId.
—
# 33. Những lỗi thường gặp
Một số lỗi nên tránh:
“`text
Generate GUID rồi gọi nó là TraceId
ActivitySource name không match AddSource
Không instrument HttpClient
Không propagate trace context qua message broker
Đưa password/token vào span tags
Trace 100% production traffic mà không nghĩ đến sampling
Hard-code OTLP endpoint
Tạo span cho mọi method nhỏ
Không correlate logs với TraceId
“`
Custom span nên dành cho operation có ý nghĩa.
Không cần:
“`text
ValidateString
MapDto
ConvertDate
“`
Nên trace:
“`text
RedeemCoupon
ProcessPayment
CheckInEvent
GenerateReward
PublishNotification
“`
—
# 34. Thứ tự học OpenTelemetry cho .NET
Nếu mới bắt đầu, nên học theo flow:
“`text
Activity / ActivitySource
↓
Trace / Span / TraceId
↓
ASP.NET Core Instrumentation
↓
HttpClient Instrumentation
↓
Custom Business Spans
↓
OTLP Exporter
↓
OpenTelemetry Collector
↓
Log Correlation
↓
Message Broker Propagation
↓
Sampling
“`
Đừng bắt đầu bằng việc dựng Jaeger hoặc Grafana trước khi hiểu:
“`text
Trace
Span
Parent Span
TraceId
Context Propagation
“`
Nếu không, bạn sẽ biết cách bật tool nhưng không hiểu tracing thực sự hoạt động như thế nào.
—
# Kết luận
Distributed tracing không đơn giản là:
“`text
add TraceId vào log
“`
Một implementation tốt cần cả:
“`text
Activity
+
ActivitySource
+
Trace Context Propagation
+
OpenTelemetry Instrumentation
+
OTLP
+
Collector
+
Log Correlation
“`
Architecture cuối cùng:
“`text
Client
↓
ASP.NET Core
↓
Activity / OpenTelemetry
↓
Downstream Services
↓
Database / Message Broker
↓
OTLP
↓
OpenTelemetry Collector
↓
Observability Backend
“`
Điểm quan trọng nhất cần nhớ:
> **Nếu OpenTelemetry đã quản lý distributed trace, hãy sử dụng TraceId từ `Activity.Current.TraceId` thay vì tự generate GUID và gọi nó là TraceId.**
Custom correlation ID vẫn có giá trị cho business workflow, nhưng nó giải quyết một bài toán khác.
Với một Senior .NET Backend Engineer, mục tiêu không chỉ là biết cách thêm package OpenTelemetry mà phải hiểu:
“`text
Request đi đâu?
Context propagate thế nào?
Service nào tạo span?
TraceId đến từ đâu?
Logs correlate bằng cách nào?
Queue continuation hoạt động thế nào?
Telemetry được export như thế nào?
Production cost được kiểm soát ra sao?
“`
Khi trả lời được những câu hỏi đó, bạn mới thực sự hiểu distributed tracing thay vì chỉ biết cấu hình một observability library.