
Bài viết này trình bày cách tích hợp Zipkin để xây dựng hệ thống theo dõi luồng yêu cầu phân tán trong ứng dụng .NET Core. Với sự gia tăng quy mô hệ thống và số lượng dịch vụ triển khai trên Kubernetes, việc phát hiện lỗi do mạng hoặc hiệu suất không ổn định trở nên khó khăn hơn khi thiếu công cụ theo dõi. Do đó, giải pháp được lựa chọn là sử dụng Zipkin – một framework nhẹ, phù hợp với môi trường microservices.
Mô hình triển khai
Chuỗi gọi dịch vụ gồm:
Gateway (SimpleZipKin) → API Web (WebApi) → Dịch vụ Đơn hàng (OrderApi)
Để đảm bảo tương thích, nếu dùng MySQL làm cơ sở lưu trữ cho Zipkin, cần kiểm tra phiên bản MySQL không vượt quá 8.0 vì Zipkin chưa hỗ trợ đầy đủ các tính năng của phiên bản mới nhất.
Cài đặt thư viện
Tạo một thư viện chung và thêm các gói sau (dùng phiên bản 1.5.0):
<PackageReference Include="zipkin4net" Version="1.5.0" />
<PackageReference Include="zipkin4net.middleware.aspnetcore" Version="1.5.0" />
Tạo lớp hỗ trợ Zipkin
public static class ZipkinExtensions
{
public static IServiceCollection AddZipkin(this IServiceCollection services)
{
services.AddSingleton<HttpDiagnosticSourceObserver>();
return services;
}
public static IApplicationBuilder UseZipkin(this IApplicationBuilder app,
IHostApplicationLifetime lifetime, ILoggerFactory loggerFactory,
string serviceName, string zipkinEndpoint)
{
DiagnosticListener.AllListeners.Subscribe(app.ApplicationServices.GetService<TraceObserver>());
lifetime.ApplicationStarted.Register(() =>
{
TraceManager.SamplingRate = 1.0f; // Ghi lại mọi yêu cầu
var logger = new TracingLogger(loggerFactory, "zipkin4net");
var sender = new HttpZipkinSender(zipkinEndpoint, "application/json");
var tracer = new ZipkinTracer(sender, new JSONSpanSerializer(), new Statistics());
var consoleTracer = new zipkin4net.Tracers.ConsoleTracer();
TraceManager.RegisterTracer(tracer);
TraceManager.RegisterTracer(consoleTracer);
TraceManager.Start(logger);
});
lifetime.ApplicationStopped.Register(TraceManager.Stop);
app.UseTracing(serviceName); // Tên service tùy chỉnh
return app;
}
}
Mở rộng ghi log với thông tin trace ID
public static class LogExtensions
{
public static void InfoWithTrace(this ILogger logger, string message)
{
var traceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
logger.LogInformation("tranceId={TraceId}, nội dung: {Message}", traceId, message);
}
public static void DebugWithTrace(this ILogger logger, string message)
{
var traceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
logger.LogDebug("tranceId={TraceId}, nội dung: {Message}", traceId, message);
}
public static void ErrorWithTrace(this ILogger logger, string message)
{
var traceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
logger.LogError("tranceId={TraceId}, nội dung: {Message}", traceId, message);
}
public static void WarnWithTrace(this ILogger logger, string message)
{
var traceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
logger.LogWarning("tranceId={TraceId}, nội dung: {Message}", traceId, message);
}
public static void TraceWithTrace(this ILogger logger, string message)
{
var traceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
logger.LogTrace("tranceId={TraceId}, nội dung: {Message}", traceId, message);
}
}
Cấu hình khởi tạo ứng dụng
Trong Program.cs:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging((context, config) =>
{
config.AddConfiguration(context.Configuration.GetSection("Logging"));
config.AddConsole();
config.AddDebug();
config.AddExceptionless();
ExceptionlessClient.Default.Configuration.SetDefaultMinLogLevel(Exceptionless.Logging.LogLevel.Debug);
config.SetMinimumLevel(LogLevel.Debug);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Cấu hình Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddZipkin();
services.AddSingleton<IDiagnosticSource, HttpDiagnosticSourceDemo>();
services.AddHttpClient("webapi", client =>
client.BaseAddress = new Uri("http://localhost:5001")
);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "SimpleZipkin", Version = "v1" });
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
ILoggerFactory loggerFactory, IHostApplicationLifetime lifetime)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SimpleZipkin v1"));
}
app.UseZipkin(lifetime, loggerFactory, "SimpleZipkinService", "http://127.0.0.1:9411");
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
Controller mẫu
[Route("api/[controller]")]
public class HomeController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
public HomeController(IHttpClientFactory httpClientFactory, ILogger<HomeController> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
[HttpGet("trace")]
public async Task<IActionResult> GetTrace()
{
_logger.InfoWithTrace("Yêu cầu bắt đầu từ Gateway");
var client = _httpClientFactory.CreateClient("webapi");
var response = await client.GetAsync("api/order/getorder");
var content = await response.Content.ReadAsStringAsync();
_logger.InfoWithTrace($"Kết quả từ OrderApi: {content}");
return Ok(content);
}
}
Cấu hình Exceptionless
Thêm vào appsettings.json:
{
"ExceptionLess": {
"ApiKey": "your-api-key-here",
"ServerUrl": "http://127.0.0.1:5000"
}
}
Theo dõi sự kiện HTTP qua DiagnosticSource
Tạo lớp ghi nhận sự kiện HTTP:
public class HttpDiagnosticSourceDemo : IDiagnosticSource
{
public string DiagnosticName => "HttpDiagnosticSourceDemo";
private ClientTrace _currentTrace;
private readonly IInjector<HttpHeaders> _injector = Propagations.B3String.Injector<HttpHeaders>(
(carrier, key, value) => carrier.Add(key, value)
);
[DiagnosticName("System.Net.Http.Request")]
public void OnRequest(HttpRequestMessage request)
{
_currentTrace = new ClientTrace("simplezipkin", request.Method.ToString());
if (_currentTrace.Trace != null)
{
_injector.Inject(_currentTrace.Trace.CurrentSpan, request.Headers);
}
}
[DiagnosticName("System.Net.Http.Response")]
public void OnResponse(HttpResponseMessage response)
{
if (_currentTrace?.Trace != null)
{
_currentTrace.AddAnnotation(Annotations.Tag("http.path", response.RequestMessage.RequestUri.LocalPath));
_currentTrace.AddAnnotation(Annotations.Tag("http.method", response.RequestMessage.Method.ToString()));
_currentTrace.AddAnnotation(Annotations.Tag("http.host", response.RequestMessage.RequestUri.Host));
if (!response.IsSuccessStatusCode)
{
_currentTrace.AddAnnotation(Annotations.Tag("http.status_code", ((int)response.StatusCode).ToString()));
}
}
}
[DiagnosticName("System.Net.Http.Exception")]
public void OnException(HttpRequestMessage request, Exception ex)
{
// Ghi log lỗi nếu cần
}
}
Observer cho DiagnosticSource
public class HttpDiagnosticSourceObserver : IObserver<DiagnosticListener>
{
private readonly IEnumerable<IDiagnosticSource> _sources;
public HttpDiagnosticSourceObserver(IEnumerable<IDiagnosticSource> sources)
{
_sources = sources;
}
public void OnNext(DiagnosticListener listener)
{
var source = _sources.FirstOrDefault(s => s.DiagnosticName == listener.Name);
if (source != null)
{
listener.SubscribeWithAdapter(source);
}
}
public void OnError(Exception error) { }
public void OnCompleted() { }
}
Kết quả
Sau khi chạy, tất cả các yêu cầu qua hệ thống sẽ được ghi nhận bởi Zipkin với:
- Trace ID duy nhất
- Thời gian thực hiện từng bước
- Các annotation như phương thức HTTP, URL, trạng thái mã
- Liên kết giữa các dịch vụ
Thông tin chi tiết cũng được ghi lại qua Exceptionless, giúp xác định chính xác máy chủ, môi trường và lỗi phát sinh.