.Net Core中的诊断日志DiagnosticSource讲解
前言
????近期由于需要進行分布式鏈路跟蹤系統的技術選型,所以一直在研究鏈路跟蹤相關的框架。作為能在.Net Core中使用的APM,SkyWalking自然成為了首選。SkyAPM-dotnet是SkyWalking在.Net Core端的探針實現,其主要的收集日志的手段就是基于DiagnosticSource來進行診斷跟蹤的。不得不說SkyAPM-dotnet的設計還是非常優秀的,它本身定義了一套非常規范的標準,而且提供了非常良好的擴展性,雖然框架本身可支持的采集端有限,但是基于這套標準擴展起來還是非常方便的。
概念介紹
????關于DiagnosticSource它本身是一個基于發布訂閱模式的工作模式,由于它本身的實現方式是異步的,所以不僅僅可以把它用到日志上,還可以用它實現異步操作,或者用它簡化實現發布訂閱的功能。DiagnosticSource本身是一個抽象類,我們最常用到的是它的子類DiagnosticListener,通過DiagnosticSource的Write方法實現發布一條有具體名稱的消息,然后通過IObserver去訂閱消息。DiagnosticListener可以實現不同的實例,每個實例可以有自己的名稱,每個實例還可以發布不同名稱的消息,好比一個在寫代碼的時候我們可以定義多個程序集,一個程序集下面可以包含多個命名空間。
使用方式
上面我們大致的介紹了關于DiagnosticSource相關的概念,相信大家已經有了初步的了解,接下來我們就來看一下在代碼中如何使用DiagnosticSource,還說到了它一個重要的子類DiagnosticListener,基本上關于DiagnosticSource的工作方式都是圍繞著DiagnosticListener實現的,首先我們來看一下如何發布一條消息
//聲明DiagnosticListener并命名為MyTest DiagnosticSource diagnosticSource = new DiagnosticListener("MyTest"); string pubName = "MyTest.Log"; //判斷是否存在MyTest.Log的訂閱者 if (diagnosticSource.IsEnabled(pubName)) {//發送名為MyTest.Log的消息,包含Name,Address兩個屬性diagnosticSource.Write(pubName, new { Name = "old王", Address="隔壁" }); }通過這種方式,我們就可以完成針對消息的發布,其中用到了IsEnabled方法,這個方法是在實際使用DiagnosticSource過程中比較常用的方法,用于判斷是夠存在對應名稱的消費者,這樣可以有效的避免發送消息浪費。
發送相對還是比較簡單的,接下來我們看一下如何訂閱發布的消息。上面我們提到了訂閱消息是通過IObserver接口實現的,IObserver代表了訂閱者。雖然我們通過DiagnosticSource去發布消息,但是真正描述發布者身份的是IObservable接口,IObservable的唯一方法Subscribe是用來注冊訂閱者IObserver,但是默認系統并沒有為我們提供一個具體的實現類,所以我們需要定義一個IObserver訂閱者的實現類
有了具體的訂閱者實現類,我們就可以為發布者注冊訂閱者了,同樣是使用DiagnosticListener,個人認為雖然操作都是通過DiagnosticSource來完成的,但它只是一個外觀類,但是并不能直接描述發布者和訂閱者本身。接下來我們看一下具體實現
//AllListeners獲取所有發布者,Subscribe為發布者注冊訂閱者MyObserver DiagnosticListener.AllListeners.Subscribe(new MyObserver<DiagnosticListener>(listener => {//判斷發布者的名字if (listener.Name == "MyTest"){//獲取訂閱信息listener.Subscribe(new MyObserver<KeyValuePair<string, object>>(listenerData =>{System.Console.WriteLine($"監聽名稱:{listenerData.Key}");dynamic data = listenerData.Value;//打印發布的消息System.Console.WriteLine($"獲取的信息為:{data.Name}的地址是{data.Address}");}));listener.SubscribeWithAdapter(new MyDiagnosticListener());} }));具體實現可總結為兩步,首先為發布者注冊訂閱者,然后獲取訂閱者獲取發布的消息。這種寫法還是比較復雜的,首先需要實現訂閱者類,然后通過一系列復雜的操作,才能完成消息訂閱,然后還要自己獲取發布的消息,解析具體的消息值,總之操作流程非常繁瑣。微軟似乎也意識到了這個問題,于是乎給我提供了一個關于實現訂閱者的便利方法,編輯項目文件引入DiagnosticAdapter包
<PackageReference Include="Microsoft.Extensions.DiagnosticAdapter" Version="3.1.7" />或者通過包管理器直接搜索安裝,道路千萬條都是通羅馬。通過這個包解決了我們兩個痛點,首先是關于訂閱者的注冊難問題,其次解決了關于發布消息解析難的痛點。我們可以直接訂閱一個適配類來充當訂閱者的載體,其次我們可以定義方法模擬訂閱去訂閱消息,而這個方法的參數就是我們發布的消息內容。說了這么多,不如直接上代碼
public class MyDiagnosticListener {//發布的消息主題名稱[DiagnosticName("MyTest.Log")]//發布的消息參數名稱和發布的屬性名稱要一致public void MyLog(string name,string address){System.Console.WriteLine($"監聽名稱:MyTest.Log");System.Console.WriteLine($"獲取的信息為:{name}的地址是{address}");} }我們可以隨便定義一個類來充當訂閱者載體,類里面可以自定義方法來實現獲取解析消息的實現。想要讓方法可以訂閱消息,需要在方法上聲明DiagnosticName,然后名稱就是你要訂閱消息的名稱,而方法的參數就是你發布消息的字段屬性名稱,這里需要注意的是訂閱的參數名稱需要和發布聲明屬性名稱一致。
然后我們直接可以通過這個類去接收訂閱消息
可能你覺得這樣還是不夠好,因為還是沒有脫離需要自定義訂閱者,這里還有更簡潔的實現方式。細心的你可能已經發現了SubscribeWithAdapter是DiagnosticListener的擴展方法,而我們聲明DiagnosticSource就是使用的DiagnosticListener實例,所以上面的代碼可以簡化為一下方式
DiagnosticListener diagnosticListener = new DiagnosticListener("MyTest"); DiagnosticSource diagnosticSource = diagnosticListener; //直接去適配訂閱者 diagnosticListener.SubscribeWithAdapter(new MyDiagnosticListener());string pubName = "MyTest.Log"; if (diagnosticSource.IsEnabled(pubName)) {diagnosticSource.Write(pubName, new { Name = "old王", Address="隔壁" }); }這種方式也是我們比較推薦的使用方式,極大的節省了工作的方式,而且代碼非常的簡潔。但是存在唯一的不足,這種寫法只能針對特定的DiagnosticListener進行訂閱處理,如果你需要監聽所有發布者,就需要使用DiagnosticListener.AllListeners.Subscribe的方式。
DotNetCore源碼中診斷日志的埋點
在.Net Core的源碼中,微軟默認在涉及到網絡請求或處理請求等許多重要的節點都使用了DiagnosticListener來發布攔截的消息,接下來就羅列一些我知道的比較常見的埋點,通過這些操作我們就可以看出,診斷日志還是很便利的,而且微軟在.Net Core中也非常重視它的使用。
在ASP.NET Core中
當我們通過ConfigureWebHostDefaults配置Web主機的時候,程序就已經默認給我們注入了診斷名稱為Microsoft.AspNetCore的DiagnosticListener和DiagnosticSource,這樣我們就可以很方便的在程序中直接獲取DiagnosticListener實例去發布消息或者監聽發布的內部消息,具體注入邏輯位于可以去GenericWebHostBuilder類中查看[點擊查看源碼????]
var listener = new DiagnosticListener("Microsoft.AspNetCore"); services.TryAddSingleton<DiagnosticListener>(listener); services.TryAddSingleton<DiagnosticSource>(listener);然后在Server啟動的時候傳遞了DiagnosticListener實例[點擊查看源碼????]
var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, HttpContextFactory); await Server.StartAsync(httpApplication, cancellationToken);這樣在Server運行期間我們可以通過DiagnosticListener診斷跟蹤請求相關的信息,我們可以看下在處理請求的過程中DiagnosticListener都發布了哪些消息,我們找到發送診斷跟蹤的位置位于HostingApplicationDiagnostics中[點擊查看源碼????],這事集中處理請求相關的診斷跟蹤,接下來我們就大致查看一下它發布了哪些事件消息,首先找到定義發布名稱的屬性
private const string ActivityName = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; private const string ActivityStartKey = ActivityName + ".Start"; private const string ActivityStopKey = ActivityName + ".Stop";private const string DeprecatedDiagnosticsBeginRequestKey = "Microsoft.AspNetCore.Hosting.BeginRequest"; private const string DeprecatedDiagnosticsEndRequestKey = "Microsoft.AspNetCore.Hosting.EndRequest"; private const string DiagnosticsUnhandledExceptionKey = "Microsoft.AspNetCore.Hosting.UnhandledException";通過這些發布消息的名稱我們就可以看出,在請求開始、請求進入、請求結束、請求停止、請求異常等都發布了診斷消息,我們以BeginRequest為例查看一下具體發送的消息
if (_diagnosticListener.IsEnabled(DeprecatedDiagnosticsBeginRequestKey)) {startTimestamp = Stopwatch.GetTimestamp();RecordBeginRequestDiagnostics(httpContext, startTimestamp); }找到RecordBeginRequestDiagnostics方法的實現
[MethodImpl(MethodImplOptions.NoInlining)] private void RecordBeginRequestDiagnostics(HttpContext httpContext, long startTimestamp) {_diagnosticListener.Write(DeprecatedDiagnosticsBeginRequestKey,new{httpContext = httpContext,timestamp = startTimestamp}); }從這里我們可以看出在BeginRequest中診斷日志發出的消息中包含了HttpContext和開始時間戳信息,然后再來看一下請求結束發布的診斷消息
[MethodImpl(MethodImplOptions.NoInlining)] private void RecordEndRequestDiagnostics(HttpContext httpContext, long currentTimestamp) {_diagnosticListener.Write(DeprecatedDiagnosticsEndRequestKey,new{httpContext = httpContext,timestamp = currentTimestamp}); }通過發布的這些跟蹤日志我們可以獲取請求信息,請求時間并且能得到輸出信息和結束時間,有了這些關鍵信息,我們就可以監聽請Asp.Net Core處理請求的情況,我們上面提到過SkyAPM-dotnet正是通過這些發出診斷跟蹤日志,來實現對程序無入侵的方式來處理應用系統監控的,具體我們可以查看相關實現,我們找到訂閱這些消息的地方
[點擊查看源碼????],拿出來類的結構,大致如下
不得不承認SkyAPM-dotnet非常巧妙的利用了系統內部發出的診斷跟蹤日志,實現了對請求的處理跟蹤,真的是非常優秀。
在HttpClient中
上面我們看到的是AspNetCore處理請求的診斷日志埋點,在發出請求的HttpClient中,微軟也做了埋點處理。我們在之前的文章.NET Core HttpClient源碼探究中提到過HttpClient通過HttpClientHandler發送請求的,在HttpClientHandler SendAsync方法中我們可以看到如下實現
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken cancellationToken) {return DiagnosticsHandler.IsEnabled() && _diagnosticsHandler != null ?_diagnosticsHandler.SendAsync(request, cancellationToken) :_underlyingHandler.SendAsync(request, cancellationToken); }也就是說如果滿足DiagnosticsHandler.IsEnabled()并且_diagnosticsHandler不為空的情況下將會使用DiagnosticsHandler發送請求,關于DiagnosticsHandler.IsEnabled()的大致實現邏輯如下
if (AppContext.TryGetSwitch("System.Net.Http.EnableActivityPropagation", out bool enableActivityPropagation)) {return enableActivityPropagation; }string? envVar = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_ENABLEACTIVITYPROPAGATION"); if (envVar != null && (envVar.Equals("false", StringComparison.OrdinalIgnoreCase) || envVar.Equals("0"))) {return false; } return true;通過這個邏輯可以看出,默認情況下我們不做特殊處理返回的就是true,也就是說發送請求會通過DiagnosticsHandler,我們找到DiagnosticsHandler的實現[點擊查看源碼????],抽離出來SendAsyncCore方法中關于診斷跟蹤的核心實現邏輯,大致如下
DiagnosticListener diagnosticListener = new DiagnosticListener("HttpHandlerDiagnosticListener"); if (diagnosticListener.IsEnabled("System.Net.Http.Request")) {long timestamp = Stopwatch.GetTimestamp();loggingRequestId = Guid.NewGuid();//請求開始之前發送診斷日志diagnosticListener.Write("System.Net.Http.Request",new RequestData(request, loggingRequestId, timestamp)); } HttpResponseMessage? response = null; TaskStatus taskStatus = TaskStatus.RanToCompletion; try {response = async ?await base.SendAsync(request, cancellationToken).ConfigureAwait(false) :base.Send(request, cancellationToken);return response; } catch (OperationCanceledException) {taskStatus = TaskStatus.Canceled;throw; } catch (Exception ex) {taskStatus = TaskStatus.Faulted;if (diagnosticListener.IsEnabled("System.Net.Http.Exception")){ //如果請求出現異常發出異常消息診斷日志diagnosticListener.Write("System.Net.Http.Exception", new ExceptionData(ex, request));}throw; } finally {if (activity != null){diagnosticListener.StopActivity(activity, new ActivityStopData(response,request,taskStatus));}if (diagnosticListener.IsEnabled("System.Net.Http.Response")){long timestamp = Stopwatch.GetTimestamp();//得到輸出結果后發送診斷日志diagnosticListener.Write("System.Net.Http.Response",new ResponseData(response,loggingRequestId,timestamp,taskStatus));} }同樣的思路HttpClient會在發送請求之前發出請求信息相關的診斷跟蹤,會在得到相應之后發送響應相關診斷跟蹤,通過這些信息我們可以捕獲到由程序發出的Http請求相關的信息,從而監控請求相關的數據,我們來看一下SkyAPM-dotnet訂閱Http請求相關的實現,在HttpClientTracingDiagnosticProcessor類中[點擊查看源碼????],抽離實現的框架大致如下
public class HttpClientTracingDiagnosticProcessor : ITracingDiagnosticProcessor {public string ListenerName { get; } = "HttpHandlerDiagnosticListener";[DiagnosticName("System.Net.Http.Request")]public void HttpRequest([Property(Name = "Request")] HttpRequestMessage request){}[DiagnosticName("System.Net.Http.Response")]public void HttpResponse([Property(Name = "Response")] HttpResponseMessage response){}[DiagnosticName("System.Net.Http.Exception")]public void HttpException([Property(Name = "Request")] HttpRequestMessage request,[Property(Name = "Exception")] Exception exception){} }這里正是監聽的HttpClient發出的診斷日志。假如存在系統A和系統B,系統A通過HttpClient發送請求調用Asp.Net Core系統B,通過訂閱他們發出的診斷跟蹤日志,而這些數據正是實現系統監控和鏈路跟蹤重要依據。
其他
????在.Net Core相關的源碼中還有許多其他關于DiagnosticListener的埋點信息比如請求執行到Action的時候或者出現全局異常的時候都有類似的處理。同樣在EFCore中也存在這些埋點信息,有興趣的可以自行查閱相關源碼和SkyAPM-dotnet源碼,了解DiagnosticSource工作方式,以及如何通過這些信息實現APM系統。雖然SkyAPM-dotnet本身實現的框架個數有限,但是它給我們實現了良好的擴展性,我們可以通過DiagnosticSource和DiagnosticListener自行實現SkyAPM-dotnet的擴展,比如你可以擴展Redis MongoDb等其它中間件。比如SkyApm.Diagnostics.CAP是CAP納入SkyAPM中程序包,正是楊總參與了相關代碼的實現。
總結
??? DiagnosticSource診斷跟蹤涉及到的概念雖然不是很多,但是在.Net Core相關的框架中使用的還是非常廣泛的,通過這些信息我們可以拿到框架執行過程中關鍵節點得到的信息,為我們提供了很大的便利。加上SkyAPM-dotnet巧妙的使用了這一特點使得DiagnosticSource更變得強大而且通用。上面我們講述的只是冰山一角,還有更多更深的應用,比如Azure監控.Net Core應用程序也是利用了這些。有興趣的可以查看相關源碼,也可以學習一下SkyAPM-dotnet相關源碼,體會一下DiagnosticSource精髓所在。
????歡迎掃碼關注我的公眾號????
總結
以上是生活随笔為你收集整理的.Net Core中的诊断日志DiagnosticSource讲解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【LeetCode】1. 盛最多水的容器
- 下一篇: [Hei.Captcha] Asp.Ne