Trong khi thực hiện lập trình ghép đôi để tích hợp OpenTelemetry tracing vào một ứng dụng .NET, chúng tôi đã có một cuộc thảo luận về cách sử dụng ActivitySource. Nó trông có vẻ đơn giản. Bạn tạo một thể hiện mới, đặt tên cho nó, bắt đầu một activity, hoàn thành.
Cuộc thảo luận bắt đầu khi chúng tôi thêm một ActivitySource thứ hai với tên chính xác giống hệt trong một class khác. Điều này khiến chúng tôi tự hỏi: “Liệu bây giờ chúng ta có đang nhân bản các trace không? Đây có phải là rò rỉ bộ nhớ không? Liệu chúng ta có cần một singleton không?”
Vì vậy, chúng tôi quyết định tìm hiểu sâu hơn. Bài viết này là những gì chúng tôi đã học được…
Mục lục
ActivitySource thực sự là gì
ActivitySource là một phần của System.Diagnostics, không phải là một phần của các gói NuGet OpenTelemetry. Microsoft đã xây dựng các nguyên tắc tracing trực tiếp vào BCL (Base Class Library), và SDK .NET của OpenTelemetry chỉ đơn giản là lắng nghe chúng. Đây là lý do tại sao bạn có thể thêm distributed tracing vào một thư viện mà không cần phụ thuộc vào OpenTelemetry.
Một ActivitySource là một nhà máy tạo ra các đối tượng Activity, và Activity là tên gọi của .NET cho khái niệm mà OpenTelemetry gọi là span (đừng hỏi tôi tại sao họ quyết định sử dụng tên gọi khác cho cùng một khái niệm). Không có gì được ghi lại, xuất ra, hoặc thậm chí được tạo trong bộ nhớ như một đối tượng thực tế trừ khi có thứ gì đó đang lắng nghe.
“Thứ đó” chính là cấu hình TracerProviderBuilder trong thiết lập OpenTelemetry của bạn.
public static class TelemetryConstants
{
public const string ServiceName = "OrderProcessing.Api";
}
public class OrderService
{
private static readonly ActivitySource ActivitySource = new(TelemetryConstants.ServiceName);
public async Task ProcessOrderAsync(int orderId)
{
using var activity = ActivitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);
// ... business logic
activity?.SetStatus(ActivityStatusCode.Ok);
}
}
Và phía OpenTelemetry:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource(TelemetryConstants.ServiceName)
.AddAspNetCoreInstrumentation()
.AddOtlpExporter());
Gọi AddSource(...) mới là phần thực sự quan trọng. Nếu tên bạn truyền vào không khớp với tên của ActivitySource của bạn, các activity của bạn sẽ được tạo, được gắn tag, được giải phóng… và im lặng bị bỏ qua. Không có lỗi, không có cảnh báo. Đây là nguyên nhân số một khiến mọi người nghĩ “OpenTelemetry không hoạt động” trong khi thực tế tracing đang hoạt động chính xác như được cấu hình — chỉ là không lắng nghe đúng tên.
Gốc rễ của cuộc thảo luận: việc tạo nhiều lần không sao cả
Đây là phát biểu bắt đầu cuộc thảo luận:
“Phải có đúng một thể hiện ActivitySource cho mỗi tên trong toàn bộ ứng dụng, nếu không sẽ có vấn đề.”
Điều đó hóa ra KHÔNG đúng. Bạn có thể tạo bao nhiêu thể hiện ActivitySource tùy thích với cùng một tên, trong bao nhiêu class, assembly, hoặc gói NuGet tùy thích. SDK OpenTelemetry không quan tâm đến danh tính thể hiện. Nó đăng ký theo tên (và tùy chọn phiên bản), không phải theo tham chiếu đối tượng.
// File A
private static readonly ActivitySource Source = new("OrderProcessing.Api");
// File B, class hoàn toàn không liên quan
private static readonly ActivitySource Source = new("OrderProcessing.Api");
Cả hai đều được chấp nhận bởi cùng một gọi AddSource("OrderProcessing.Api"). Các activity từ một trong hai đều đi vào cùng một pipeline trace, với cùng các thuộc tính tài nguyên. Không có sự trùng lặp, không xuất ra gấp đôi, không có rò rỉ.
Tại sao điều này quan trọng trong thực tế?
- Các thư viện không cần phải expose
ActivitySourcenội bộ của chúng. Một gói NuGet có thể khai báo source của riêng mình với một tên được biết đến, và người sử dụng chỉ cần gọiAddSource("TênThưViện")mà không bao giờ chạm vào bên trong thư viện đó. - Bạn không cần một class static chung được truyền qua DI chỉ để truy cập một thể hiện
ActivitySource. Mỗi thành phần có thể khai báo source của riêng mình, sử dụng một quy ước đặt tên chung (thường là tên assembly hoặc một hằng số). - Các modular monoliths và Razor Class Libraries có thể sở hữu thiết lập tracing riêng mà không cần phối hợp một nguồn sự thật duy nhất cho thể hiện — chỉ cần thống nhất về tên.
Một điều cần phải khớp nhau là chuỗi ký tự. Nhầm lẫn chính là kiểu lỗi thực sự ở đây, không phải là tạo nhiều thể hiện.
Một lần thử sai ban đầu
Một mẫu mà chúng tôi nhận thấy trong codebase ban đầu: ai đó cố gắng “đúng” bằng cách inject ActivitySource qua DI như một singleton, vì điều đó có vẻ an toàn, có thể kiểm tra được.
// Hoạt động, nhưng thêm nghi lễ bạn không cần
services.AddSingleton(new ActivitySource("OrderProcessing.Api"));
public class OrderService(ActivitySource activitySource)
{
public async Task ProcessOrderAsync(int orderId)
{
using var activity = activitySource.StartActivity("ProcessOrder");
// ...
}
}
Không có gì sai với cách này, nhưng đây là thêm cơ cấu không cần thiết vì không mang lại lợi ích thực sự, vì các thể hiện ActivitySource với cùng tên có thể thay thế cho nhau. Mẫu thông dụng trong mã code tracing .NET và những gì bạn sẽ thấy trong các thư viện của chính Microsoft, là một trường private static readonly ActivitySource cho mỗi class hoặc mỗi assembly, đặt tên theo thành phần thuộc về:
internal static class Telemetry
{
public static readonly ActivitySource Source = new("OrderProcessing.Api.Orders");
}
Đơn giản, không cần đăng ký DI, và nó hoạt động cho dù class sử dụng nó đến từ ứng dụng của bạn, một class library, hay một gói bên thứ ba áp dụng cùng quy ước.
Đặt tên và phiên bản hóa
ActivitySource cũng chấp nhận một phiên bản tùy chọn:
private static readonly ActivitySource ActivitySource = new("OrderProcessing.Api.Orders", "1.2.0");
Phiên bản đi cùng với các span kết quả như metadata nhưng không ảnh hưởng đến việc SDK có lắng nghe nó không — AddSource vẫn khớp theo tên. Sử dụng nó theo cách tương tự như bạn sử dụng phiên bản assembly: thông tin để gỡ lỗi xem bản dựng nào đã tạo ra một trace, không phải là cơ chế cô lập.
Một quy ước đáng áp dụng: đặt tên các source của bạn giống như namespace (Company.Product.Component), khớp với cách bạn cấu trúc các danh mục logger với ILogger<T>. Điều này giúp các gọi AddSource trở nên có thể dự đoán được và khiến việc khớp wildcard (AddSource("OrderProcessing.*") trong một số exporters) thực sự hữu ích.
Kết luận
ActivitySource là điểm đầu vào vào câu chuyện tracing gốc của .NET, và OpenTelemetry chỉ là một trong số (rất phổ biến) các listener trên đó. Tên là hợp đồng; hãy đúng tên, đăng ký nó với AddSource, và bạn có thể tạo bao nhiêu thể hiện tùy thích theo cách cấu trúc codebase của bạn tự nhiên đòi hỏi. Đừng để nỗi sợ “các thể hiện trùng lặp” đẩy bạn vào nghi lễ DI mà bạn không cần.



