最近在研究全鏈路監控的實現方式,目的是計劃在項目中加入全鏈路日誌的支持,說到這個問題肯定有人會想到 APM,如:SkyWalking、Cat、Zipkin、Pinpoint 、Elastic APM 等,確實市面上已經存在現成的全鏈路監控框架可以直接使用,不過說實話,在免費領域 .NET 這方面的實現確實還不夠成熟,當然和很多組件本身實現方式也有關,並沒有預置埋點。SkyAPM-dotnet 是目前支持組件診斷分析較多的一個實現,如下圖:
基於對 SkyWalking 的 SkyAPM-dotnet 和 Elastic APM 的 apm-agent-dotnet 源碼閱讀,它們本質上都是基於 DiagnosticSource
來實現的診斷跟蹤,本身也定義了一套較規範的標準,如果需要實現更多組件的診斷跟蹤,基本上是可以直接基於這套標準擴展即可,所以本文就主要介紹 DiagnosticSource
的使用,初步瞭解實現原理。
DiagnosticSource 是什麼
簡單來說 DiagnosticSource
一個基於觀察者模式的日誌模塊,日誌寫入 DiagnosticSource
,然後供訂閱者消費。DiagnosticSource
只是一個抽象類,它定義了記錄事件所需的方法,實際核心的是 DiagnosticListener
實現類,每個 DiagnosticListener
都具有一個 Name
屬性(診斷器名),一個應用程序中可包含多個 DiagnosticListener
,每個 DiagnosticListener
有自己唯一的診斷器名標識。 DiagnosticListener
充當發佈者角色,通過 Write
向 DiagnosticSource
寫入日誌,同時提供了 Subscribe
方法設置訂閱者來消費 DiagnosticSource
中的日誌。
DiagnosticSource 事件發佈
- 在事件發佈前需要先創建
DiagnosticSource
,如下定義了一個診斷器名爲TestDiagnosticListener
的DiagnosticListener
:private static readonly DiagnosticSource testDiagnosticListener = new DiagnosticListener("TestDiagnosticListener");
- 判斷當前診斷器的某個事件名是否存在消費者監聽:
bool IsEnabled(string name);
- 攜帶數據對象寫入診斷器 DiagnosticSource 中:
使用示例:void Write(string name, object value);
if (testDiagnosticListener.IsEnabled("RequestStart")) { testDiagnosticListener.Write("RequestStart", "hello world"); }
DiagnosticSource 事件消費
-
定義
DiagnosticListener
事件消費處理接口,實現類中的ListenerName
必須與對應DiagnosticListener
的診斷器名一致:public interface IDiagnosticProcessor { string ListenerName { get; } }
-
定義診斷器名爲
TestDiagnosticListener
的DiagnosticListener
事件消費處理邏輯:public class TestDiagnosticProcessor : IDiagnosticProcessor { public string ListenerName { get; } = "TestDiagnosticListener"; [DiagnosticName("RequestStart")] public void RequestStart([Object]string name) { Console.WriteLine(name); } }
-
創建
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)); } }
-
事件訂閱者需要創建基於
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); } } }
最後需要通過
DiagnosticListener.AllListeners.Subscribe
設置DiagnosticListenerObserver
對象;-
執行結果:
總結
通過以上示例,我們完全可以參考基於這樣的標準在組件封裝(MongoDB、Dapper、Kafka、Redis ...)過程中自己埋點(創建相應的 DiagnosticListener
併發布事件),然後訂閱者根據需求監聽需要的事件,從而達到診斷日誌全鏈路收集的目的。
注:本文涉及的代碼主要是參考了 SkyAPM-dotnet 。