DiagnosticSource DiagnosticListener 無侵入式分散式跟蹤

chester·chen發表於2024-03-14

ASP.NET Core 中的框架中發出大量診斷事件,包括當前請求進入請求完成事件,HttpClient發出收到與響應,EFCore查詢等等。

我們可以利用DiagnosticListener來選擇性地監聽這些事件,然後透過自己的方式組織這些日誌,實現無侵入的分散式跟蹤。

下面我們透過DiagnosticSource監聽EFCore,與HTTPClient,實現鏈路追蹤。

建立監聽

現在我們將配置一個DiagnosticListener來監聽全部事件。

首先,我們需要一個IObserver<DiagnosticListener>,我們將使用它來訂閱所有事件。

public class TestDiagnosticObserver : IObserver<DiagnosticListener>
{
    public void OnNext(DiagnosticListener value)
    {
        value.Subscribe(new TestKeyValueObserver());
    }
    public void OnCompleted() { }
    public void OnError(Exception error) { }
}
其中重要的方法是OnNext。
然後我們傳入另一個自定義型別TestKeyValueObserver,這是實際接收例項發出的事件的類DiagnosticListener。
該事件會接受KeyValuePair<string, object>引數,我們後續可針對此引數做業務相關的篩選。
public class TestKeyValueObserver : IObserver<KeyValuePair<string, object?>>
{
    public void OnNext(KeyValuePair<string, object?> value)
    {
        var activity = Activity.Current;

        Console.WriteLine($"traceId {activity?.TraceId} Received event: {value.Key}");
    }
    public void OnCompleted() { }
    public void OnError(Exception error) { }
}

最後一步是在應用程式中註冊我們的程式TestDiagnosticObserver。

DiagnosticListener.AllListeners.Subscribe(new TestDiagnosticObserver());

建立HTTP請求與EFCore查詢

我們新建一個介面,用來整合EF與HttpClient。並呼叫這個介面檢視DiagnosticListener 監聽到的內容

[HttpGet]
public async Task<string> GetAsync()
{
    //HTTP
    await _httpClient.GetAsync("https://www.baidu.com");

    //EF
    Item item = new Item()
    {
        Barcode = Guid.NewGuid().ToString(),
        Brand = "Milky Way",
        Name = "Milk",
        PruchasePrice = 20.5,
        SellingPrice = 25.5
    };
    _productsContext.Items.Add(item);
    _productsContext.SaveChanges();
    return "OK";
}

呼叫此介面來看看我們的DiagnosticListener的效果。

可以看到收到了很多Event,包括當前請求的各個階段,HttpClient的各個階段,與EFCore查詢的各個階段。

解析Event

然後修改TestKeyValueObserver,我們從中挑選我們需要的HTTPClient與EFCore相關的事件。

public class TestKeyValueObserver : IObserver<KeyValuePair<string, object?>>
{
    public void OnNext(KeyValuePair<string, object?> value)
    {
        var activity = Activity.Current;

        //Console.WriteLine($"traceId {activity?.TraceId} Received event: {value.Key}");
        if (value.Key.StartsWith("System.Net.Http.Request"))
        {
            var cEventStr = JsonConvert.SerializeObject(value.Value);
            var cEvent = JsonConvert.DeserializeAnonymousType(cEventStr, new { Request = new { RequestUri = ""} , Timestamp = 2879029490722 });
            Console.WriteLine($"traceId {activity?.TraceId} Request.Start: {cEvent.Timestamp} ");
            Console.WriteLine($"traceId {activity?.TraceId} Request.Uri: {cEvent.Request.RequestUri} ");
        }
        if (value.Key.StartsWith("System.Net.Http.Response"))
        {
            var cEventStr = JsonConvert.SerializeObject(value.Value);
            var cEvent = JsonConvert.DeserializeAnonymousType(cEventStr, new { Request = new { RequestUri = "" }, Timestamp = 2879029490722 });
            Console.WriteLine($"traceId {activity?.TraceId} Http.Response: {cEvent.Timestamp} ");
        }


        if (value.Key.StartsWith("Microsoft.EntityFrameworkCore.Database.Connection.ConnectionOpening"))
        {
            var cEvent = (Microsoft.EntityFrameworkCore.Diagnostics.ConnectionEventData)value.Value;
            Console.WriteLine($"traceId {activity?.TraceId} Connection.ConnectionOpening: {cEvent?.StartTime.ToString("yyyy-MM-dd HH:mm:ss:fff")} ");
        }
        if (value.Key.StartsWith("Microsoft.EntityFrameworkCore.Database.Command.CommandExecuting"))
        {
            var cEvent = (Microsoft.EntityFrameworkCore.Diagnostics.CommandEventData)value.Value;
            Console.WriteLine($"traceId {activity?.TraceId}  {cEvent?.Command.CommandText} ");
        }
        if (value.Key.StartsWith("Microsoft.EntityFrameworkCore.Database.Connection.ConnectionClosed"))
        {
            var cEvent = (Microsoft.EntityFrameworkCore.Diagnostics.ConnectionEventData)value.Value;
            Console.WriteLine($"traceId {activity?.TraceId} Connection.ConnectionClosed: {cEvent?.StartTime.ToString("yyyy-MM-dd HH:mm:ss:fff")} ");
        }
    }
    public void OnCompleted() { }
    public void OnError(Exception error) { }
}

再次啟動,檢視效果,可以看到已經獲取到了http請求的開始結束事件,EF的查詢語句,開始事件等。

最後我們可以結構化這些資料,並將其持久化到自己的監控體系中,實現鏈路跟蹤。

相關文章