Thực hiện theo dõi chuỗi phân tán trong .NET Core với Zipkin

Hình minh họa Zipkin

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.


Tài liệu tham khảo

Thẻ: zipkin .net core distributed tracing diagnosticsource HttpClient

Đăng vào ngày 2 tháng 9 lúc 06:53