1. 引言
最近為了解決ABP集成CAP時(shí)無法通過攔截器啟用工作單元的問題漓帅,從小伙伴那里學(xué)了一招狭归。借助DiagnossticSource
,可以最小改動(dòng)完成需求将宪。關(guān)于DiagnosticSource曉東大佬18年在文章 在 .NET Core 中使用 Diagnostics (Diagnostic Source) 記錄跟蹤信息就有介紹,文章開頭就說明了Diagnostics 一直是一個(gè)被大多數(shù)開發(fā)者忽視的東西橡庞。是的较坛,我也忽略了,這個(gè)好東西扒最,有必要學(xué)習(xí)一下丑勤,下面就和大家簡單聊一聊System.Diagnostics.DiagnosticSource在.NET上的應(yīng)用。
2. System.Diagnostics.DiagnosticSource
Diagnostics
位于System
命名空間下扼倘,由此可見Diagnostics
在.NET 運(yùn)行時(shí)中的地位不可小覷确封。其中System.Diagnostics命名空間下又包含不同類庫,提供了允許與系統(tǒng)進(jìn)程再菊,事件日志和性能計(jì)數(shù)器進(jìn)行交互的類爪喘。如下圖所示:
其中System.Diagnostics.DiagnosticSource模塊,它允許對(duì)代碼進(jìn)行檢測纠拔,以在生產(chǎn)時(shí)記錄豐富的數(shù)據(jù)負(fù)載(可以傳遞不可序列化的數(shù)據(jù)類型)秉剑,以便在進(jìn)程內(nèi)進(jìn)行消耗。消費(fèi)者可以在運(yùn)行時(shí)動(dòng)態(tài)發(fā)現(xiàn)數(shù)據(jù)源并訂閱感興趣的數(shù)據(jù)源稠诲。
在展開之前侦鹏,有必要先梳理下涉及的以下核心概念:
- IObservable:可觀測對(duì)象
- IObserver:觀察者
- DiagnosticSource :診斷來源
- DiagnosticListener:診斷監(jiān)聽器
- Activity:活動(dòng)
3. 觀察者模式(IObservable & IObserver)
IObservable
和IObserver
位于System
命名空間下,是.NET中對(duì)觀察者模式的抽象臀叙。
觀察者設(shè)計(jì)模式使觀察者能夠從可觀察對(duì)象訂閱并接收通知略水。 它適用于需要基于推送通知的任何方案。 此模式定義可觀察對(duì)象劝萤,以及零個(gè)渊涝、一個(gè)或多個(gè)觀察者。 觀察者訂閱可觀察對(duì)象,并且每當(dāng)預(yù)定義的條件跨释、事件或狀態(tài)發(fā)生更改時(shí)胸私,該可觀察對(duì)象會(huì)通過調(diào)用其方法之一來自動(dòng)通知所有觀察者。 在此方法調(diào)用中鳖谈,該可觀察對(duì)象還可向觀察者提供當(dāng)前狀態(tài)信息岁疼。 在 .NET Framework 中,通過實(shí)現(xiàn)泛型 System.IObservable<T> 和 System.IObserver<T> 接口來應(yīng)用觀察者設(shè)計(jì)模式缆娃。 泛型類型參數(shù)表示提供通知信息的類型捷绒。 泛型類型參數(shù)表示提供通知信息的類型。
第一次學(xué)習(xí)觀察者模式龄恋,應(yīng)該是大學(xué)課本中基于事件燒水的例子疙驾,咱們就基于此實(shí)現(xiàn)個(gè)簡單的Demo吧。首先執(zhí)行dotnet new web -n Dotnet.Diagnostic.Demo
創(chuàng)建示例項(xiàng)目郭毕。
3.1. 定義可觀察對(duì)象(實(shí)現(xiàn)IObservable接口)
對(duì)于燒水的示例,主要關(guān)注水溫的變化函荣,因此先定義Temperature
來表示溫度變化:
public class Temperature
{
public Temperature(decimal temperature, DateTime date)
{
Degree = temperature;
Date = date;
}
public decimal Degree { get; }
public DateTime Date { get; }
}
接下來通過實(shí)現(xiàn)IObservable<T>
接口來定義可觀察對(duì)象显押。
public interface IObservable<out T>
{
IDisposable Subscribe(IObserver<T> observer);
}
從接口申明來看,只定義了一個(gè)Subscribe
方法傻挂,從觀察者模式講乘碑,觀察者應(yīng)該既能訂閱又能取消訂閱消息。為什么沒有定義一個(gè)UnSubscribe
方法呢金拒?其實(shí)這里方法申明已經(jīng)說明兽肤,期望通過返回IDisposable
對(duì)象的Dispose
方法來達(dá)到這個(gè)目的。
/// <summary>
/// 熱水壺
/// </summary>
public class Kettle : IObservable<Temperature>
{
private List<IObserver<Temperature>> observers;
private decimal temperature = 0;
public Kettle()
{
observers = new List<IObserver<Temperature>>();
}
public decimal Temperature
{
get => temperature;
private set
{
temperature = value;
observers.ForEach(observer => observer.OnNext(new Temperature(temperature, DateTime.Now)));
if (temperature == 100)
observers.ForEach(observer => observer.OnCompleted());
}
}
public IDisposable Subscribe(IObserver<Temperature> observer)
{
if (!observers.Contains(observer))
{
Console.WriteLine("Subscribed!");
observers.Add(observer);
}
//使用UnSubscriber包裝绪抛,返回IDisposable對(duì)象资铡,用于觀察者取消訂閱
return new UnSubscriber<Temperature>(observers, observer);
}
/// <summary>
/// 燒水方法
/// </summary>
public async Task StartBoilWaterAsync()
{
var random = new Random(DateTime.Now.Millisecond);
while (Temperature < 100)
{
Temperature += 10;
await Task.Delay(random.Next(5000));
}
}
}
//定義泛型取消訂閱對(duì)象,用于取消訂閱
internal class UnSubscriber<T> : IDisposable
{
private List<IObserver<T>> _observers;
private IObserver<T> _observer;
internal UnSubscriber(List<IObserver<T>> observers, IObserver<T> observer)
{
this._observers = observers;
this._observer = observer;
}
public void Dispose()
{
if (_observers.Contains(_observer))
{
Console.WriteLine("Unsubscribed!");
_observers.Remove(_observer);
}
}
}
以上代碼中List<IObserver<T>>存在線程安全問題幢码,因?yàn)楹唵蜠emo笤休,就不予優(yōu)化了。
3.2. 定義觀察者(實(shí)現(xiàn)IObserver<T>接口)
比如定義一個(gè)報(bào)警器症副,實(shí)時(shí)播報(bào)溫度店雅。
public class Alter : IObserver<Temperature>
{
public void OnCompleted()
{
Console.WriteLine("du du du !!!");
}
public void OnError(Exception error)
{
//Nothing to do
}
public void OnNext(Temperature value)
{
Console.WriteLine($"{value.Date.ToString()}: Current temperature is {value.Degree}.");
}
}
添加測試代碼,訪問localhost:5000/subscriber
控制臺(tái)輸出結(jié)果如下:
endpoints.MapGet("/subscriber", async context =>
{
var kettle = new Kettle();//初始化熱水壺
var subscribeRef = kettle.Subscribe(new Alter());//訂閱
var boilTask = kettle.StartBoilWaterAsync();//啟動(dòng)開始燒水任務(wù)
var timoutTask = Task.Delay(TimeSpan.FromSeconds(15));//定義15s超時(shí)任務(wù)
//等待贞铣,如果超時(shí)任務(wù)先返回則取消訂閱
var firstReturnTask = await Task.WhenAny(boilTask, timoutTask);
if (firstReturnTask == timoutTask)
subscribeRef.Dispose();
await context.Response.WriteAsync("Hello subscriber!");
});
------------------------------------------------------------------
Subscribed!
10/2/2020 4:53:20 PM: Current temperature is 10.
10/2/2020 4:53:20 PM: Current temperature is 20.
10/2/2020 4:53:21 PM: Current temperature is 30.
10/2/2020 4:53:21 PM: Current temperature is 40.
10/2/2020 4:53:24 PM: Current temperature is 50.
10/2/2020 4:53:25 PM: Current temperature is 60.
10/2/2020 4:53:26 PM: Current temperature is 70.
10/2/2020 4:53:30 PM: Current temperature is 80.
Unsubscribed!
4. DiagnosticSource & DiagnosticListener
4.1. 概念講解
DiagnosticSource
直譯就是診斷源闹啦,也就是它是診斷日志的來源入口。DiagnosticSource其是一個(gè)抽象類主要定義了以下方法:
//Provides a generic way of logging complex payloads
public abstract void Write(string name, object value);
//Verifies if the notification event is enabled.
public abstract bool IsEnabled(string name);
DiagnosticListener
直譯就是診斷監(jiān)聽器辕坝,繼承自DiagnosticSource
窍奋,同時(shí)實(shí)現(xiàn)了IObservable<KeyValuePair<string, object>>
接口,因此其本質(zhì)是一個(gè)可觀察對(duì)象。小結(jié)以下:
-
DiagnosticSource
作為診斷日志來源费变,提供接口摧扇,用于寫入診斷日志。 - 診斷日志的可觀察數(shù)據(jù)類型為
KeyValuePair<string, object>
挚歧。 -
DiagnosticListener
繼承自DiagnosticSource
扛稽,作為可觀察對(duì)象,可由其他觀察者訂閱滑负,以獲取診斷日志在张。
DiagnosticListener
其構(gòu)造函數(shù)接收一個(gè)name
參數(shù)。
private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
可以通過下面這種方式記錄診斷日志:
if (httpLogger.IsEnabled("RequestStart"))
httpLogger.Write("RequestStart", new { Url="http://clr", Request=aRequest });
然后需要實(shí)現(xiàn)IObserver<KeyValuePair<string, object>>
接口矮慕,以便消費(fèi)診斷數(shù)據(jù)帮匾。定義DiagnosticObserver
,進(jìn)行診斷日志消費(fèi):
public class DiagnosticObserver : IObserver<KeyValuePair<string, object>>
{
public void OnCompleted()
{
//Noting to do
}
public void OnError(Exception error)
{
Console.WriteLine($"{error.Message}");
}
public void OnNext(KeyValuePair<string, object> pair)
{
// 這里消費(fèi)診斷數(shù)據(jù)
Console.WriteLine($"{pair.Key}-{pair.Value}");
}
}
ASP.NET Core 項(xiàng)目中默認(rèn)就依賴了System.Diagnostics.DiagnosticSource
Nuget包痴鳄,同時(shí)在構(gòu)建通用Web主機(jī)時(shí)瘟斜,就注入了名為Microsoft.AspNetCore
的DiagnosticListener
。
//GenericWebHostBuilder.cs
DiagnosticListener instance = new DiagnosticListener("Microsoft.AspNetCore");
services.TryAddSingleton<DiagnosticListener>(instance);
services.TryAddSingleton<DiagnosticSource>((DiagnosticSource) instance);
因此我們可以直接通過注入DiagnosticListener
進(jìn)行診斷日志的訂閱:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DiagnosticListener diagnosticListener)
{
diagnosticListener.Subscribe(new DiagnosticObserver());//訂閱診斷日志
}
當(dāng)然也可以直接使用DiagnosticListener.AllListeners.Subscribe(IObserver<DiagnosticListener> observer);
進(jìn)行訂閱痪寻,不過區(qū)別是螺句,接收的參數(shù)類型為IObserver<DiagnosticListener>
。
運(yùn)行項(xiàng)目輸出:
Microsoft.AspNetCore.Hosting.HttpRequestIn.Start-Microsoft.AspNetCore.Http.DefaultHttpContext
Microsoft.AspNetCore.Hosting.BeginRequest-{ httpContext = Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp = 7526300014352 }
Microsoft.AspNetCore.Routing.EndpointMatched-Microsoft.AspNetCore.Http.DefaultHttpContext
Microsoft.AspNetCore.Hosting.EndRequest-{ httpContext = Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp = 7526300319214 }
Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop-Microsoft.AspNetCore.Http.DefaultHttpContext
從中可以看出橡类,ASP.NET Core Empty Web Project在一次正常的Http請(qǐng)求過程中分別在請(qǐng)求進(jìn)入蛇尚、請(qǐng)求處理、路由匹配都埋了點(diǎn)顾画,除此之外還有請(qǐng)求異常取劫、Action處理都有埋點(diǎn)。因此研侣,根據(jù)需要谱邪,可以實(shí)現(xiàn)比如請(qǐng)求攔截、耗時(shí)統(tǒng)計(jì)等系列操作义辕。
4.2. 耗時(shí)統(tǒng)計(jì)
基于以上知識(shí)虾标,下面嘗試完成一個(gè)簡單的耗時(shí)統(tǒng)計(jì)。從上面的內(nèi)容可知灌砖,ASP.NET Core在BeginRequest和EndRequest返回的診斷數(shù)據(jù)類型如下所示:
Microsoft.AspNetCore.Hosting.BeginRequest-{ httpContext = Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp = 7526300014352 }
Microsoft.AspNetCore.Hosting.EndRequest-{ httpContext = Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp = 7526300319214 }
因此只要拿到兩個(gè)timestamp就可以直接計(jì)算耗時(shí)璧函,修改DiagnosticObserver
的OnNext
方法如下:
private ConcurrentDictionary<string, long> startTimes = new ConcurrentDictionary<string, long>();
public void OnNext(KeyValuePair<string, object> pair)
{
//Console.WriteLine($"{pair.Key}-{pair.Value}");
//獲取httpContext
var context = pair.Value.GetType().GetTypeInfo().GetDeclaredProperty("httpContext")
?.GetValue(pair.Value) as DefaultHttpContext;
//獲取timestamp
var timestamp = pair.Value.GetType().GetTypeInfo().GetDeclaredProperty("timestamp")
?.GetValue(pair.Value) as long?;
switch (pair.Key)
{
case "Microsoft.AspNetCore.Hosting.BeginRequest":
Console.WriteLine($"Request {context.TraceIdentifier} Begin:{context.Request.GetUri()}");
startTimes.TryAdd(context.TraceIdentifier, timestamp.Value);//記錄請(qǐng)求開始時(shí)間
break;
case "Microsoft.AspNetCore.Hosting.EndRequest":
startTimes.TryGetValue(context.TraceIdentifier, out long startTime);
var elapsedMs = (timestamp - startTime) / TimeSpan.TicksPerMillisecond;//計(jì)算耗時(shí)
Console.WriteLine(
$"Request {context.TraceIdentifier} End: Status Code is {context.Response.StatusCode},Elapsed {elapsedMs}ms");
startTimes.TryRemove(context.TraceIdentifier, out _);
break;
}
}
輸出如下,大功告成:
Request 0HM37UNERKGF0:00000001 Begin:https://localhost:44330
Request 0HM37UNERKGF0:00000001 End: Status Code is 200,Elapsed 38ms
上面有通過反射去獲取診斷數(shù)據(jù)屬性的代碼(var timestamp = pair.Value.GetType().GetTypeInfo().GetDeclaredProperty("timestamp") ?.GetValue(pair.Value) as long?;
)基显,非常不優(yōu)雅蘸吓。但我們可以安裝Microsoft.Extensions.DiagnosticAdapter
包來簡化診斷數(shù)據(jù)的消費(fèi)。安裝后撩幽,添加HttpContextDiagnosticObserver
库继,通過添加DiagnosticName
指定監(jiān)聽的診斷名稱箩艺,即可進(jìn)行診斷數(shù)據(jù)消費(fèi)。
public sealed class HttpContextDiagnosticObserver
{
private ConcurrentDictionary<string, long> startTimes = new ConcurrentDictionary<string, long>();
[DiagnosticName("Microsoft.AspNetCore.Hosting.BeginRequest")]
public void BeginRequest(HttpContext httpContext,long timestamp)
{
Console.WriteLine($"Request {httpContext.TraceIdentifier} Begin:{httpContext.Request.GetUri()}");
startTimes.TryAdd(httpContext.TraceIdentifier, timestamp);//記錄請(qǐng)求開始時(shí)間
}
[DiagnosticName("Microsoft.AspNetCore.Hosting.EndRequest")]
public void EndRequest(HttpContext httpContext,long timestamp)
{
startTimes.TryGetValue(httpContext.TraceIdentifier, out long startTime);
var elapsedMs = (timestamp - startTime) / TimeSpan.TicksPerMillisecond;//計(jì)算耗時(shí)
Console.WriteLine(
$"Request {httpContext.TraceIdentifier} End: Status Code is {httpContext.Response.StatusCode},Elapsed {elapsedMs}ms");
startTimes.TryRemove(httpContext.TraceIdentifier, out _);
}
}
然后使用SubscribeWithAdapter
進(jìn)行訂閱即可宪萄。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DiagnosticListener diagnosticListener)
{
// diagnosticListener.Subscribe(new DiagnosticObserver());
diagnosticListener.SubscribeWithAdapter(new HttpContextDiagnosticObserver());
}
到這里可能也有小伙伴說艺谆,我用ActionFilter
也可以實(shí)現(xiàn),沒錯(cuò)拜英,但這兩種方式是完全不同的静汤,DiagnosticSource
是完全異步的。
4.3. 應(yīng)用場景思考
根據(jù)DiagnosticSource的特性居凶,可以運(yùn)用于以下場景 :
- AOP:因?yàn)镈iagnostics命名事件一般是成對(duì)出現(xiàn)的虫给,因此可以做些攔截操作。比如在Abp集成Cap時(shí)侠碧,若想默認(rèn)啟用Uow抹估,就可以消費(fèi)
DotNetCore.CAP.WriteSubscriberInvokeBefore
命名事件,創(chuàng)建Uow弄兜,再在命名事件DotNetCore.CAP.WriteSubscriberInvokeAfter
中提交事務(wù)药蜻,并Dispose。 - APM:SkyAPM-dotnet的實(shí)現(xiàn)就是通過消費(fèi)診斷日志替饿,進(jìn)行鏈路跟蹤谷暮。
- EventBus:充分利用其發(fā)布訂閱模式,可將其用于進(jìn)程內(nèi)事件的發(fā)布與消費(fèi)盛垦。
5. Activity(活動(dòng))
5.1. Activity 概述
那Activity又是何方神圣,用于解決什么問題呢瓤漏?關(guān)于Activity官方只有一句簡要介紹:Represents an operation with context to be used for logging腾夯。(表示包含上下文的操作,用于日志記錄蔬充。)
Activity用來存儲(chǔ)和訪問診斷上下文蝶俱,并由日志系統(tǒng)進(jìn)行消費(fèi)。當(dāng)應(yīng)用程序開始處理操作時(shí)饥漫,例如HTTP請(qǐng)求或隊(duì)列中的任務(wù)榨呆,它會(huì)在處理請(qǐng)求時(shí)創(chuàng)建Activity以在系統(tǒng)中跟蹤該Activity。Activity中存儲(chǔ)的上下文可以是HTTP請(qǐng)求路徑庸队,方法积蜻,用戶代理或關(guān)聯(lián)ID:所有重要信息都應(yīng)與每個(gè)跟蹤一起記錄。當(dāng)應(yīng)用程序調(diào)用外部依賴關(guān)系以完成操作時(shí)彻消,它可能需要傳遞一些上下文(例如竿拆,關(guān)聯(lián)ID)以及依賴關(guān)系調(diào)用,以便能夠關(guān)聯(lián)來自多個(gè)服務(wù)的日志宾尚。
先來看下Activity主要以下核心屬性:
Tags(標(biāo)簽)
IEnumerable<KeyValuePair<string, string>> Tags { get; }
- 表示與活動(dòng)一起記錄的信息丙笋。標(biāo)簽的好例子是實(shí)例/機(jī)器名稱谢澈,傳入請(qǐng)求HTTP方法,路徑御板,用戶/用戶代理等锥忿。標(biāo)簽不傳遞給子活動(dòng)。
典型的標(biāo)簽用法包括添加一些自定義標(biāo)簽怠肋,并通過它們進(jìn)行枚舉以填充日志事件的有效負(fù)載敬鬓。可通過Activity AddTag(string key, string value)
添加Tag灶似,但不支持通過Key檢索標(biāo)簽列林。Baggage(行李)
IEnumerable<KeyValuePair<string, string>> Baggage { get; }
- 表示要與活動(dòng)一起記錄并傳遞給其子項(xiàng)的信息。行李的例子包括相關(guān)ID酪惭,采樣和特征標(biāo)記希痴。
Baggage被序列化并與外部依賴項(xiàng)請(qǐng)求一起傳遞。
典型的Baggage用法包括添加一些Baggage屬性春感,并通過它們進(jìn)行枚舉以填充日志事件的有效負(fù)載砌创。
可通過Activity AddBaggage(string key, string value)
添加Baggage。并通過string GetBaggageItem(string key)
獲取指定Key的Baggage鲫懒。OperationName(操作名稱)
string OperationName { get; }
- 活動(dòng)名稱嫩实,必須在構(gòu)造函數(shù)中指定。StartTimeUtc
DateTime StartTimeUtc { get; private set; }
- UTC格式的啟動(dòng)時(shí)間窥岩,如果不指定甲献,則在啟動(dòng)時(shí)默認(rèn)指定為DateTime.UtcNow
∷桃恚可通過Activity SetStartTime(DateTime startTimeUtc)
指定晃洒。Duration
TimeSpan Duration { get; private set; }
- 如果活動(dòng)已停止,則代表活動(dòng)持續(xù)時(shí)間朦乏,否則為0球及。Id
string Id { get; private set; }
- 表示特定的活動(dòng)標(biāo)識(shí)符。過濾特定ID可確保您僅獲得與操作中特定請(qǐng)求相關(guān)的日志記錄呻疹。該Id在活動(dòng)開始時(shí)生成吃引。Id傳遞給外部依賴項(xiàng),并被視為新的外部活動(dòng)的[ParentId]刽锤。ParentId
string ParentId { get; private set; }
- 如果活動(dòng)是根據(jù)請(qǐng)求反序列化的镊尺,則該活動(dòng)可能具有進(jìn)程中的[Parent]或外部Parent。 ParentId和Id代表日志中的父子關(guān)系姑蓝,并允許您關(guān)聯(lián)傳出和傳入請(qǐng)求鹅心。RootId
string RootId { get; private set; }
- 代表根IdCurrent
static Activity Current { get; }
- 返回在異步調(diào)用之間流動(dòng)的當(dāng)前Activity。Parent
Activity Parent { get; private set; }
- 如果活動(dòng)是在同一過程中從另一個(gè)活動(dòng)創(chuàng)建的纺荧,則可以使用Partent
獲得該活動(dòng)旭愧。但是颅筋,如果“活動(dòng)”是根活動(dòng)或父項(xiàng)來自流程外部,則此字段可能為null输枯。Start()
Activity Start()
- 啟動(dòng)活動(dòng):設(shè)置活動(dòng)的Activity.Current和Parent议泵,生成唯一的ID并設(shè)置StartTimeUtc(如果尚未設(shè)置)。Stop()
void Stop()
- 停止活動(dòng):設(shè)置活動(dòng)的Activity.Current桃熄,并使用Activity SetEndTime(DateTime endTimeUtc)
或DateTime.UtcNow中提供的時(shí)間戳計(jì)算Duration先口。
另外DiagnosticSource
中也定義了兩個(gè)相關(guān)方法:
- StartActivity
Activity StartActivity(Activity activity, object args)
- 啟動(dòng)給定的Activity,并將DiagnosticSource
事件消息寫入OperationName.Start
格式的命名事件中瞳收。 - StopActivity
void StopActivity(Activity activity, object args)
- 停止給定的Activity碉京,并將DiagnosticSource
事件消息寫入{OperationName}.Stop
格式的命名事件中。
5.2. Activity在ASP.NET Core中的應(yīng)用
要想弄懂Activity螟深,我們還是得向源碼學(xué)習(xí)谐宙,看一下HostingApplicationDiagnostics的實(shí)現(xiàn)。首先來看下BeginRequst
中的StartActivity
方法界弧。
private Activity StartActivity(HttpContext httpContext, out bool hasDiagnosticListener)
{
Activity activity = new Activity("Microsoft.AspNetCore.Hosting.HttpRequestIn");
hasDiagnosticListener = false;
IHeaderDictionary headers = httpContext.Request.Headers;
StringValues stringValues1;
if (!headers.TryGetValue(HeaderNames.TraceParent, out stringValues1))
headers.TryGetValue(HeaderNames.RequestId, out stringValues1);
if (!StringValues.IsNullOrEmpty(stringValues1))
{
activity.SetParentId((string) stringValues1);
StringValues stringValues2;
if (headers.TryGetValue(HeaderNames.TraceState, out stringValues2))
activity.TraceStateString = (string) stringValues2;
string[] commaSeparatedValues = headers.GetCommaSeparatedValues(HeaderNames.CorrelationContext);
if (commaSeparatedValues.Length != 0)
{
foreach (string str in commaSeparatedValues)
{
NameValueHeaderValue parsedValue;
if (NameValueHeaderValue.TryParse((StringSegment) str, out parsedValue))
activity.AddBaggage(parsedValue.Name.ToString(), parsedValue.Value.ToString());
}
}
}
this._diagnosticListener.OnActivityImport(activity, (object) httpContext);
if (this._diagnosticListener.IsEnabled("Microsoft.AspNetCore.Hosting.HttpRequestIn.Start"))
{
hasDiagnosticListener = true;
this.StartActivity(activity, httpContext);
}
else
activity.Start();
return activity;
}
從中可以看出凡蜻,在ASP.NET Core 開始處理請(qǐng)求之前:
- 首先,創(chuàng)建了名為
Microsoft.AspNetCore.Hosting.HttpRequestIn
的Activity垢箕,該Activity首先嘗試從HTTP請(qǐng)求頭中獲取TraceParent/euqstId作為當(dāng)前Activity的ParentId划栓,這個(gè)很顯然,是用來鏈路跟蹤的条获。 - 其次忠荞,嘗試從
CorrelationContext
中獲取關(guān)聯(lián)上下文信息,然后將其添加到創(chuàng)建的Activity的Baggage中帅掘,進(jìn)行關(guān)聯(lián)上下文的繼續(xù)傳遞钻洒。 - 然后,啟動(dòng)Activity锄开,然后向Name為
Microsoft.AspNetCore.Hosting.HttpRequestIn.Start
中寫入診斷日志。
這里大家可能有個(gè)疑問称诗,這個(gè)關(guān)聯(lián)上下文信息CorrelationContext
又是何時(shí)添加到Http請(qǐng)求頭中的呢萍悴?在System.Net.Http
中的DiagnosticsHandler中添加的。
因此我們應(yīng)該明白了寓免,整個(gè)關(guān)聯(lián)上下文的傳遞機(jī)制癣诱。
緊接著再來看一看RequestEnd
中的StopActivity
方法。
private void StopActivity(Activity activity, HttpContext httpContext)
{
if (activity.Duration == TimeSpan.Zero)
activity.SetEndTime(DateTime.UtcNow);
this._diagnosticListener.Write("Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", (object) httpContext);
activity.Stop();
}
從中可以看出主要是先SetEndTime
袜香,再寫入Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop
命名事件撕予;最后調(diào)用Stop
方法停止當(dāng)前Activity。
簡單總結(jié)一下蜈首,借助Activity中附加的Baggage信息可以實(shí)現(xiàn)請(qǐng)求鏈路上上下文數(shù)據(jù)的共享实抡。
5.3. 應(yīng)用場景思考
從上面的命名事件中可以看出欠母,其封送的數(shù)據(jù)類型是特定的,因此可以借助Activity的Tags或Baggage添加自定義的數(shù)據(jù)進(jìn)行共享吆寨。
按照上面我們的耗時(shí)統(tǒng)計(jì)赏淌,只能統(tǒng)計(jì)到整個(gè)http請(qǐng)求的耗時(shí),但對(duì)于我們定位問題來說還是有困難啄清,比如六水,某個(gè)api即有調(diào)用redis枣耀,又操作了消息隊(duì)列俏脊,同時(shí)又訪問了數(shù)據(jù)庫禽捆,那到底是那一段超時(shí)了呢西潘?顯然不好直接定位筏养,借助activity丧慈,我們就可以很好的實(shí)現(xiàn)細(xì)粒度的鏈路跟蹤输玷。通過activity攜帶的信息山叮,可以將一系列的操作關(guān)聯(lián)起來计露,記錄日志博脑,再借助AMP進(jìn)行可視化快速定位跟蹤。
6. 參考資料
- 在 .NET Core 中使用 Diagnostics (Diagnostic Source) 記錄跟蹤信息
- Logging using DiagnosticSource in ASP.NET Core
- .Net Core中的診斷日志DiagnosticSource講解
- Observer Design Pattern
- DiagnosticSource User Guide
- Activity User Guide
- DiagnosticSourcery 101 - Mark Rendle
- Improvements in .NET Core 3.0 for troubleshooting and monitoring distributed apps