診斷日志知多少 | DiagnosticSource 在.NET上的應(yīng)用

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)行交互的類爪喘。如下圖所示:

Diagnostic Namespace

其中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)

IObservableIObserver位于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é)以下:

  1. DiagnosticSource 作為診斷日志來源费变,提供接口摧扇,用于寫入診斷日志。
  2. 診斷日志的可觀察數(shù)據(jù)類型為KeyValuePair<string, object>挚歧。
  3. 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.DiagnosticSourceNuget包痴鳄,同時(shí)在構(gòu)建通用Web主機(jī)時(shí)瘟斜,就注入了名為Microsoft.AspNetCoreDiagnosticListener

//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í)璧函,修改DiagnosticObserverOnNext方法如下:

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)用于以下場景 :

  1. 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。
  2. APM:SkyAPM-dotnet實(shí)現(xiàn)就是通過消費(fèi)診斷日志替饿,進(jìn)行鏈路跟蹤谷暮。
  3. 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主要以下核心屬性:

  1. 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)簽列林。

  2. 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鲫懒。

  3. OperationName(操作名稱)
    string OperationName { get; } - 活動(dòng)名稱嫩实,必須在構(gòu)造函數(shù)中指定。

  4. StartTimeUtc
    DateTime StartTimeUtc { get; private set; } - UTC格式的啟動(dòng)時(shí)間窥岩,如果不指定甲献,則在啟動(dòng)時(shí)默認(rèn)指定為DateTime.UtcNow∷桃恚可通過Activity SetStartTime(DateTime startTimeUtc)指定晃洒。

  5. Duration
    TimeSpan Duration { get; private set; } - 如果活動(dòng)已停止,則代表活動(dòng)持續(xù)時(shí)間朦乏,否則為0球及。

  6. 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]刽锤。

  7. 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)求鹅心。

  8. RootId
    string RootId { get; private set; } - 代表根Id

  9. Current
    static Activity Current { get; } - 返回在異步調(diào)用之間流動(dòng)的當(dāng)前Activity。

  10. Parent
    Activity Parent { get; private set; } - 如果活動(dòng)是在同一過程中從另一個(gè)活動(dòng)創(chuàng)建的纺荧,則可以使用Partent獲得該活動(dòng)旭愧。但是颅筋,如果“活動(dòng)”是根活動(dòng)或父項(xiàng)來自流程外部,則此字段可能為null输枯。

  11. Start()
    Activity Start() - 啟動(dòng)活動(dòng):設(shè)置活動(dòng)的Activity.Current和Parent议泵,生成唯一的ID并設(shè)置StartTimeUtc(如果尚未設(shè)置)。

  12. Stop()
    void Stop() - 停止活動(dòng):設(shè)置活動(dòng)的Activity.Current桃熄,并使用Activity SetEndTime(DateTime endTimeUtc)或DateTime.UtcNow中提供的時(shí)間戳計(jì)算Duration先口。

另外DiagnosticSource中也定義了兩個(gè)相關(guān)方法:

  1. StartActivity
    Activity StartActivity(Activity activity, object args) - 啟動(dòng)給定的Activity,并將DiagnosticSource事件消息寫入OperationName.Start格式的命名事件中瞳收。
  2. 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)求之前:

  1. 首先,創(chuàng)建了名為Microsoft.AspNetCore.Hosting.HttpRequestIn的Activity垢箕,該Activity首先嘗試從HTTP請(qǐng)求頭中獲取TraceParent/euqstId作為當(dāng)前Activity的ParentId划栓,這個(gè)很顯然,是用來鏈路跟蹤的条获。
  2. 其次忠荞,嘗試從CorrelationContext中獲取關(guān)聯(lián)上下文信息,然后將其添加到創(chuàng)建的Activity的Baggage中帅掘,進(jìn)行關(guān)聯(lián)上下文的繼續(xù)傳遞钻洒。
  3. 然后,啟動(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. 參考資料

  1. 在 .NET Core 中使用 Diagnostics (Diagnostic Source) 記錄跟蹤信息
  2. Logging using DiagnosticSource in ASP.NET Core
  3. .Net Core中的診斷日志DiagnosticSource講解
  4. Observer Design Pattern
  5. DiagnosticSource User Guide
  6. Activity User Guide
  7. DiagnosticSourcery 101 - Mark Rendle
  8. Improvements in .NET Core 3.0 for troubleshooting and monitoring distributed apps
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末票罐,一起剝皮案震驚了整個(gè)濱河市叉趣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌该押,老刑警劉巖疗杉,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蚕礼,居然都是意外死亡烟具,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門奠蹬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來朝聋,“玉大人,你說我怎么就攤上這事囤躁〖胶郏” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵狸演,是天一觀的道長言蛇。 經(jīng)常有香客問我,道長宵距,這世上最難降的妖魔是什么腊尚? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮满哪,結(jié)果婚禮上婿斥,老公的妹妹穿的比我還像新娘劝篷。我一直安慰自己,他們只是感情好受扳,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布携龟。 她就那樣靜靜地躺著,像睡著了一般勘高。 火紅的嫁衣襯著肌膚如雪峡蟋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天华望,我揣著相機(jī)與錄音蕊蝗,去河邊找鬼。 笑死赖舟,一個(gè)胖子當(dāng)著我的面吹牛蓬戚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宾抓,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼子漩,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了石洗?” 一聲冷哼從身側(cè)響起幢泼,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讲衫,沒想到半個(gè)月后缕棵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涉兽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年招驴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枷畏。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡别厘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拥诡,到底是詐尸還是另有隱情丹允,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布袋倔,位于F島的核電站,受9級(jí)特大地震影響折柠,放射性物質(zhì)發(fā)生泄漏宾娜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一扇售、第九天 我趴在偏房一處隱蔽的房頂上張望前塔。 院中可真熱鬧嚣艇,春花似錦、人聲如沸华弓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寂屏。三九已至贰谣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間迁霎,已是汗流浹背吱抚。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留考廉,地道東北人秘豹。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像昌粤,于是被迫代替她去往敵國和親既绕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360