ASP.NET Core 診斷跟蹤:DiagnosticSource

最近在研究全鏈路監控的實現方式,目的是計劃在項目中加入全鏈路日誌的支持,說到這個問題肯定有人會想到 APM,如:SkyWalkingCatZipkinPinpointElastic APM 等,確實市面上已經存在現成的全鏈路監控框架可以直接使用,不過說實話,在免費領域 .NET 這方面的實現確實還不夠成熟,當然和很多組件本身實現方式也有關,並沒有預置埋點。SkyAPM-dotnet 是目前支持組件診斷分析較多的一個實現,如下圖:

基於對 SkyWalking 的 SkyAPM-dotnet 和 Elastic APM 的 apm-agent-dotnet 源碼閱讀,它們本質上都是基於 DiagnosticSource 來實現的診斷跟蹤,本身也定義了一套較規範的標準,如果需要實現更多組件的診斷跟蹤,基本上是可以直接基於這套標準擴展即可,所以本文就主要介紹 DiagnosticSource 的使用,初步瞭解實現原理。

DiagnosticSource 是什麼

簡單來說 DiagnosticSource 一個基於觀察者模式的日誌模塊,日誌寫入 DiagnosticSource,然後供訂閱者消費。DiagnosticSource 只是一個抽象類,它定義了記錄事件所需的方法,實際核心的是 DiagnosticListener 實現類,每個 DiagnosticListener 都具有一個 Name 屬性(診斷器名),一個應用程序中可包含多個 DiagnosticListener ,每個 DiagnosticListener 有自己唯一的診斷器名標識。 DiagnosticListener 充當發佈者角色,通過 WriteDiagnosticSource 寫入日誌,同時提供了 Subscribe 方法設置訂閱者來消費 DiagnosticSource 中的日誌。

DiagnosticSource 事件發佈

  1. 在事件發佈前需要先創建 DiagnosticSource,如下定義了一個診斷器名爲 TestDiagnosticListenerDiagnosticListener
    private static readonly DiagnosticSource testDiagnosticListener = new DiagnosticListener("TestDiagnosticListener"); 
    
  2. 判斷當前診斷器的某個事件名是否存在消費者監聽:
    bool IsEnabled(string name); 
    
  3. 攜帶數據對象寫入診斷器 DiagnosticSource 中:
    void Write(string name, object value); 
    
    使用示例:
    if (testDiagnosticListener.IsEnabled("RequestStart"))
    {
      testDiagnosticListener.Write("RequestStart", "hello world");
    }
    

DiagnosticSource 事件消費

  1. 定義 DiagnosticListener 事件消費處理接口,實現類中的 ListenerName 必須與對應 DiagnosticListener 的診斷器名一致:

    public interface IDiagnosticProcessor
    {
      string ListenerName { get; }
    } 
    
  2. 定義診斷器名爲 TestDiagnosticListenerDiagnosticListener 事件消費處理邏輯:

    public class TestDiagnosticProcessor : IDiagnosticProcessor
    {
      public string ListenerName { get; } = "TestDiagnosticListener";
    
      [DiagnosticName("RequestStart")]
      public void RequestStart([Object]string name)
      {
        Console.WriteLine(name);
      }
    }
    
  3. 創建 IObserver<DiagnosticListener> 實現類訂閱所有類型的 DiagnosticListener,通過 OnNext 方法的 DiagnosticListener 對象獲取當前的診斷器名,不同(診斷器名不同) DiagnosticListener 發佈的事件設置不同的訂閱者,主要代碼如下(完整代碼):

    public class DiagnosticListenerObserver : IObserver<DiagnosticListener>
    {
      private readonly IEnumerable<IDiagnosticProcessor> _diagnosticProcessors;
    
      public DiagnosticListenerObserver(IEnumerable<IDiagnosticProcessor> diagnosticProcessors)
      {
        _diagnosticProcessors = diagnosticProcessors;
      }
    
      public void OnNext(DiagnosticListener value)
      {
        var diagnosticProcessor = _diagnosticProcessors?.FirstOrDefault(_ => _.ListenerName == value.Name);
        if (diagnosticProcessor == null) return;
    
        value.Subscribe(new DiagnosticEventObserver(diagnosticProcessor));
      }
    }
    
  4. 事件訂閱者需要創建基於 IObserver<KeyValuePair<string, object>> 的實現類,根據觸發的事件名(value.Key)和已訂閱的事件處理集合(_eventCollection)進行匹對查找,匹配上的通過反射執行對應的消費方法,主要代碼如下(完整代碼):

    public class DiagnosticEventObserver : IObserver<KeyValuePair<string, object>>
    {
      private readonly DiagnosticEventCollection _eventCollection;
    
      public DiagnosticEventObserver(IDiagnosticProcessor diagnosticProcessor)
      {
        _eventCollection = new DiagnosticEventCollection(diagnosticProcessor);
      }
      
      public void OnNext(KeyValuePair<string, object> value)
      {
        var diagnosticEvent = _eventCollection.GetDiagnosticEvent(value.Key);
        if (diagnosticEvent == null) return;
    
        try
        {
          diagnosticEvent.Invoke(value.Value);
        }
        catch (Exception ex)
        {
          Console.WriteLine(ex.Message);
        }
      }
    }
    
  5. 最後需要通過 DiagnosticListener.AllListeners.Subscribe 設置 DiagnosticListenerObserver 對象;

  6. 執行結果:

總結

通過以上示例,我們完全可以參考基於這樣的標準在組件封裝(MongoDB、Dapper、Kafka、Redis ...)過程中自己埋點(創建相應的 DiagnosticListener 併發布事件),然後訂閱者根據需求監聽需要的事件,從而達到診斷日誌全鏈路收集的目的。

注:本文涉及的代碼主要是參考了 SkyAPM-dotnet

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章