建立計量

本文適用於:✔️.NET Core 6 和更新版本 ✔️ .NET Framework 4.6.1 和更新版本

您可以使用 System.Diagnostics.Metrics API 來檢測 .NET 應用程式,以追蹤重要的計量。 某些計量包含在標準 .NET 程式庫中,但您可能會想要新增與應用程式和程式庫相關的自訂計量。 在本教學課程中,您將新增計量,並了解可用的計量類型。

注意

.NET 有一些較舊的計量 API,也就是 EventCountersSystem.Diagnostics.PerformanceCounter,此處未涵蓋這些 API。 若要深入了解這些替代方案,請參閱比較計量 API

建立自訂計量

必要條件.NET Core 6 SDK 或更新版本

建立參考 System.Diagnostics.DiagnosticSource NuGet 套件第 8 版或更高版本的新主控台應用程式。 目標為 .NET 8+ 的應用程式預設包含此參考。 然後,更新 Program.cs 中的程式碼以符合:

> dotnet new console
> dotnet add package System.Diagnostics.DiagnosticSource
using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each second that sells 4 hats
            Thread.Sleep(1000);
            s_hatsSold.Add(4);
        }
    }
}

System.Diagnostics.Metrics.Meter 類型是程式庫的進入點,用來建立具名的檢測群組。 檢測會記錄計算計量所需的數值測量。 我們在這裡用 CreateCounter 建立名為「hatco.store.hats_sold」的計數器檢測。 在每個假設的交易期間,程式碼會呼叫 Add 來記錄已銷售的帽子數量,在此案例中為 4。 「hatco.store.hats_sold」檢測會針對可從這些測量計算隱含定義一些計量,例如銷售的帽子總數或每秒銷售的帽子。最後,計量收集工具決定要計算的計量,以及如何執行這些計算,但每個檢測都有一些傳達開發人員意圖的預設慣例。 針對計數器檢測,慣例是收集工具會顯示總計數和/或計數增加的速率。

Counter<int>CreateCounter<int>(...) 上的泛型參數 int 會定義此計數器必須能夠將值儲存到 Int32.MaxValue。 您可以使用 byteshortintlongfloatdoubledecimal 之間任何一項,這視您需要儲存的資料大小以及是否需要小數值而定。

執行應用程式並任其執行。 接下來,我們將檢視計量。

> dotnet run
Press any key to exit

最佳作法

  • 對於不是為在相依性插入 (DI) 容器中使用而設計的程式碼,建立一次 Meter 並將其儲存在靜態變數中。 對於在 DI 感知程式庫中的使用方式,靜態變數被認為是反向模式,下方的 DI 防範顯示了一種更慣用的方法。 每個程式庫或程式庫子元件都能夠 (而且通常最好) 建立自己的 Meter。 如果您預期應用程式開發人員希望能夠簡單地個別啟用和停用計量群組,請考慮建立新的 Meter,而不是重複使用現有的計量。

  • 傳遞給 Meter 建構函式的名稱應該是唯一的,以將其與其他 Meter 區分開來。 我們建議使用 OpenTelemetry 命名方針,其使用點狀階層名稱。 要檢測的程式碼之組件名稱或命名空間名稱通常是不錯的選擇。 如果組件是在第二個獨立組件中新增程式碼的檢測,則名稱應該以定義計量的組件為基礎,而不是要檢測其程式碼的組件。

  • .NET 沒有為檢測強制執行任何命名配置,但我們建議遵循 OpenTelemetry 命名方針,其使用小寫點狀階層名稱和底線 ('_') 作為同一元素中多個字組之間的分隔符號。 並非所有計量工具都將 Meter 名稱保留為最終計量名稱的一部分,因此,使檢測名稱本身具有全域唯一性是有益的。

    範例檢測名稱:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • 用來建立檢測和記錄測量的 API 對執行緒是安全的。 在 .NET 程式庫中,在從多個執行緒對相同物件上叫用時,大部分的執行個體方法都需要進行同步處理,但在此情況下則不需要。

  • 記錄測量的檢測 API (此範例中為 Add) 通常會在未收集任何資料時,或是當高效能收集程式庫或工具收集測量時,於 <10 奈秒內執行。 這可讓這些 API 在大部分情況下自由地使用,但請小心處理非常敏感的程式碼。

檢視新的計量

有許多選項可用來儲存和檢視計量。 本教學課程使用 dotnet-counters 工具,這適用於臨機操作分析。 您也可以查看其他替代方案的計量集合教學課程。 如果尚未安裝 dotnet-counters 工具,請使用 SDK 進行安裝:

> dotnet tool update -g dotnet-counters
You can invoke the tool using the following command: dotnet-counters
Tool 'dotnet-counters' (version '7.0.430602') was successfully installed.

當範例應用程序仍在執行時,請使用 dotnet-counters 監視新計數器:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)                          4

與預期相符,您可以看到 HatCo 商店每秒穩定銷售 4 頂帽子。

透過相依性插入取得 Meter

在前面的範例f中,Meter 是透過使用 new 建構它並將其指派給靜態欄位取得的。 當使用相依性插入 (DI) 時,以這種方式使用靜態變數不是良好的方法。 在使用 DI 的程式碼中,例如 ASP.NET Core 或具有泛型主機的應用程式,使用 IMeterFactory 建立 Meter 物件。 從 .NET 8 開始,主機將自動在服務容器中註冊 IMeterFactory,也可以透過呼叫 AddMetrics 手動在任意 IServiceCollection 中註冊類型。 計量中心將計量與 DI 整合,使不同服務集合中的 Meter 相互隔離,即使它們使用相同的名稱。 這對於測試特別有用,這樣平行執行的多個測試只能觀察同一測試案例中產生的測量。

若要取得為 DI 設計的類型中的 Meter,請向建構函式新增 IMeterFactory 參數,然後呼叫 Create。 此範例顯示在 ASP.NET Core 應用程式中使用 IMeterFactory。

定義保留檢測的類型:

public class HatCoMetrics
{
    private readonly Counter<int> _hatsSold;

    public HatCoMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("HatCo.Store");
        _hatsSold = meter.CreateCounter<int>("hatco.store.hats_sold");
    }

    public void HatsSold(int quantity)
    {
        _hatsSold.Add(quantity);
    }
}

Program.cs 中用 DI 容器註冊類型。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<HatCoMetrics>();

視需要插入計量型別和記錄值。 因為計量型別是在 DI 中註冊,所以可以搭配 MVC 控制器、基本 API 或 DI 所建立的任何其他型別使用:

app.MapPost("/complete-sale", ([FromBody] SaleModel model, HatCoMetrics metrics) =>
{
    // ... business logic such as saving the sale to a database ...

    metrics.HatsSold(model.QuantitySold);
});

最佳作法

  • System.Diagnostics.Metrics.Meter 實作 IDisposable,但 IMeterFactory 會自動管理其建立的任何 Meter 物件之存留期,並在 DI 容器被處置時對其進行處置。 沒有必要新增額外程式碼來叫用 Meter 上的 Dispose(),也不會有任何效果。

檢測類型

目前爲止,我們只示範了檢測 Counter<T>,但有更多可用的檢測類型。 檢測有兩種方式:

  • 預設計量計算 - 收集和分析檢測測量的工具會根據檢測來計算不同的預設計量。
  • 彙總資料的儲存 - 最實用的計量需要從許多測量彙總資料。 其中一個選項是呼叫端會在任意時間提供個別測量,而收集工具會管理彙總。 或者,呼叫端可以管理彙總測量,並在回呼中視需要提供。

目前可用的檢測類型:

  • Counter (CreateCounter) - 這項檢測會追蹤隨著時間增加的值,而呼叫端會使用 Add 報告增量。 大部分工具都會計算總數和總數變更率。 對於只顯示一個項目的工具,建議使用變更率。 例如,假設呼叫端會以連續值 1、2、4、5、4、4、3 每秒叫用 Add() 一次。 如果收集工具每隔三秒更新一次,則三秒後的總數為 1+2+4=7,而六秒後的總數為 1+2+4+5+4+3=19。 變更率是 (current_total - previous_total),因此工具會在三秒回報 7-0=7,並在六秒後回報 19-7=12。

  • UpDownCounter (CreateUpDownCounter) - 此檢測會追蹤可能會隨著時間增加或減少的值。 呼叫端會使用 Add 報告遞增和遞減。 例如,假設呼叫端會以連續值 1、5、-2、3、-1、-3、-3 每秒叫用 Add() 一次。 如果收集工具每隔三秒更新一次,則三秒後的總數為 1+5-2=4,而六秒後的總數為 1+5-2+3-1-3=3。

  • ObservableCounter (CreateObservableCounter) - 這項檢測與 Counter 類似,不同之處在於呼叫端現在負責維護彙總計數。 呼叫端會在建立 ObservableCounter 時提供回呼委派,並在工具需要觀察目前的總數時叫用回呼。 例如,如果收集工具每隔三秒更新一次,則回呼函式也會每隔三秒叫用一次。 大部分工具在可用總數中都會有變更的總數和比率。 如果只能顯示一個項目,建議使用變更率。 如果回呼在初始呼叫時傳回 0,在三秒後再次呼叫 7,並在六秒後呼叫時傳回 19,則工具會回報這些值未變更為總數。 針對變更率,此工具會在三秒後顯示 7-0=7,並在六秒後顯示 19-7=12。

  • ObservableUpDownCounter (CreateObservableUpDownCounter) - 這項檢測類似於 UpDownCounter,不同之處在於呼叫端現在負責維護彙總計數。 呼叫端會在建立 ObservableUpDownCounter 時提供回呼委派,並在工具需要觀察目前的總數時叫用回呼。 例如,如果收集工具每隔三秒更新一次,則回呼函式也會每隔三秒叫用一次。 回呼所傳回的任何值都會顯示在集合工具中,不會變更為總數。

  • ObservableGauge (CreateObservableGauge) - 此檢測可讓呼叫端提供回呼,其中測量值會直接傳遞為計量。 每次收集工具更新時,都會叫用回呼,而回呼所傳回的任何值都會顯示在工具中。

  • 長條圖 (CreateHistogram) - 此檢測會追蹤測量分佈。 描述一組測量沒有一種單一標準方式,但建議使用長條圖或計算百分位數的工具。 例如,假設呼叫端在收集工具的更新間隔期間叫用 Record 記錄這些測量:1,5,2,3,10,9,7,4,6,8。 收集工具可能會報告這些測量的第 50 個、第 90 個和第 95 個百分位數分別為 5、9 和 9。

選取檢測類型時的最佳做法

  • 若要計算項目,或任何其他只會隨著時間增加的值,請使用 Counter 或 ObservableCounter。 根據哪一項新增至現有程式碼較容易,在 Counter 和 ObservableCounter 之間進行選擇:每個遞增作業的 API 呼叫,或會從程式碼維護的變數讀取目前總數的回呼。 在效能很重要且使用 Add 的極忙碌程式碼路徑中,每個執行緒每秒會建立一百萬個以上的呼叫,使用 ObservableCounter 可能會提供更多最佳化機會。

  • 針對為項目計時,通常會偏好長條圖。 通常這最好了解這些分佈的結尾 (第 90、第 95、第 99 個百分位數),而不是平均值或總數。

  • 其他常見案例,例如快取命中率或快取大小、佇列和檔案,通常非常適合 UpDownCounterObservableUpDownCounter。 根據哪一項新增至現有程式碼較容易其中之間進行選擇:每個遞增和遞減作業的 API 呼叫,或會從程式碼維護的變數讀取目前值的回呼。

注意

如果您使用較舊版本的 .NET 或不支援 UpDownCounterObservableUpDownCounter (第 7 版之前) 的 DiagnosticSource NuGet 套件,ObservableGauge 通常是很好的替代方案。

不同檢測類型的範例

停止先前啟動的範例程序,並將 Program.cs 中的範例程式碼取代為:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>("hatco.store.order_processing_time");
    static int s_coatsSold;
    static int s_ordersPending;

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_coatsSold);
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", () => s_ordersPending);

        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms that each sell 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);

            // Pretend we also sold 3 coats. For an ObservableCounter we track the value in our variable and report it
            // on demand in the callback
            s_coatsSold += 3;

            // Pretend we have some queue of orders that varies over time. The callback for the orders_pending gauge will report
            // this value on-demand.
            s_ordersPending = s_rand.Next(0, 20);

            // Last we pretend that we measured how long it took to do the transaction (for example we could time it with Stopwatch)
            s_orderProcessingTime.Record(s_rand.Next(0.005, 0.015));
        }
    }
}

執行新的進程,並使用 dotnet-counters,如同之前在第二個殼層一般,來檢視計量:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.coats_sold (Count / 1 sec)                                27
    hatco.store.hats_sold (Count / 1 sec)                                 36
    hatco.store.order_processing_time
        Percentile=50                                                      0.012
        Percentile=95                                                      0.014
        Percentile=99                                                      0.014
    hatco.store.orders_pending                                             5

此範例會使用一些隨機產生的數字,因此您的值會有所不同。 您可以看到 hatco.store.hats_sold (Counter) 和 hatco.store.coats_sold (ObservableCounter) 都顯示為速率。 ObservableGauge,也就是 hatco.store.orders_pending 會顯示為絕對值。 Dotnet-counters 會將長條圖檢測轉譯為三個百分位數統計資料 (第 50、第 95 和第 99),但其他工具可能會以不同方式摘要分佈或提供更多組態選項。

最佳作法

  • 長條圖往往比其他計量類型在記憶體中儲存更多的資料。 但是,確切的記憶體使用方式取決於所使用的收集工具。 如果您要定義多個 (>100) 長條圖計量,您可能需要提供使用者指引,不要同時啟用,或設定其工具以降低精確度來節省記憶體。 某些收集工具可能會有其將監視的並行長條圖數目的硬性限制,以防止使用過多的記憶體。

  • 所有可觀察檢測的回呼都會依序叫用,因此任何花費很長時間的回呼可能會延遲或防止收集所有計量。 偏好快速讀取快取值、不傳回任何測量,或擲回例外狀況,而非執行任何可能長時間執行或封鎖作業。

  • ObservableCounter、ObservableUpDownCounter 和 ObservableGauge 回撥發生在通常與更新值的程式碼不同步的執行緒上。 您有責任同步記憶體存取或接受使用不同步存取可能導致的不一致值。 同步存取的常見方法是使用鎖定或呼叫 Volatile.ReadVolatile.Write

  • CreateObservableGaugeCreateObservableCounter 語言函式會傳回檢測物件,但在大多數情況下,您不需要儲存在變數中,因為這不需要與物件進一步互動。 如同我們對其他檢測所做的一樣,指派給靜態變數是合法但容易出錯,因為 C# 靜態初始化是延遲的,而且通常永遠不會參考變數。 以下是問題範例:

    using System;
    using System.Diagnostics.Metrics;
    
    class Program
    {
        // BEWARE! Static initializers only run when code in a running method refers to a static variable.
        // These statics will never be initialized because none of them were referenced in Main().
        //
        static Meter s_meter = new Meter("HatCo.Store");
        static ObservableCounter<int> s_coatsSold = s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_rand.Next(1,10));
        static Random s_rand = new Random();
    
        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
    

描述和單位

檢測可以指定選擇性的描述和單位。 這些值對所有計量計算不透明,但可以在收集工具 UI 中顯示,以協助工程師了解如何解譯資料。 停止您先前啟動的範例程序,並將 Program.cs 中的範例程式碼取代為:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>(name: "hatco.store.hats_sold",
                                                                unit: "{hats}",
                                                                description: "The number of hats sold in our store");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each 100ms that sells 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);
        }
    }
}

執行新的進程,並使用 dotnet-counters,如同之前在第二個殼層一般,來檢視計量:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold ({hats} / 1 sec)                                40

dotnet-counters 目前不會使用 UI 中的描述文字,但會在提供時顯示單位。 在此情況下,您會看到「{帽子}」已取代先前描述中可見的泛型詞彙「計數」。

最佳作法

  • .NET API 允許使用任何字串作為單位,但我們建議使用 UCUM,這是國際單位名稱標準。 「{帽子}」周圍的大括弧是 UCUM 標準的一部分,指示它是描述性註釋,而不是具有標準化含義的單位名稱,如秒或位元組。

  • 建構函式中指定的單位應該描述適合個別測量的單位。 這有時會與最終計量的單位不同。 在此範例中,每個測量都是數個帽子,因此「{帽子}」是傳入建構函式的適當單位。 收集工具會計算速率,並自行衍生計算計量的適當單位為 {帽子}/秒。

  • 在記錄時間測量時,偏好以浮點數或雙值形式記錄的秒為單位。

多維度計量

測量也可以與稱為標籤的索引鍵/值組相關聯,以便將資料分類以供分析。 例如,HatCo 不只要記錄銷售的帽數,也記錄大小和色彩。 稍後分析資料時,HatCo 工程師可以依大小、色彩或兩者的任何組合來細分總數。

計數器和長條圖標籤可以在 Add 的多載,以及接受一或多個 KeyValuePair 引數的 Record 中指定。 例如:

s_hatsSold.Add(2,
               new KeyValuePair<string, object>("product.color", "red"),
               new KeyValuePair<string, object>("product.size", 12));

取代 Program.cs 的程式碼,並重新執行應用程式和 dotnet-counters,如先前所示:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction, every 100ms, that sells two size 12 red hats, and one size 19 blue hat.
            Thread.Sleep(100);
            s_hatsSold.Add(2,
                           new KeyValuePair<string,object>("product.color", "red"),
                           new KeyValuePair<string,object>("product.size", 12));
            s_hatsSold.Add(1,
                           new KeyValuePair<string,object>("product.color", "blue"),
                           new KeyValuePair<string,object>("product.size", 19));
        }
    }
}

Dotnet-counters 現在會展示基本分類:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)
        product.color=blue,product.size=19                                 9
        product.color=red,product.size=12                                 18

針對 ObservableCounter 和 ObservableGauge,標記的測量可以在傳遞至建構函式的回呼中提供:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");

    static void Main(string[] args)
    {
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", GetOrdersPending);
        Console.WriteLine("Press any key to exit");
        Console.ReadLine();
    }

    static IEnumerable<Measurement<int>> GetOrdersPending()
    {
        return new Measurement<int>[]
        {
            // pretend these measurements were read from a real queue somewhere
            new Measurement<int>(6, new KeyValuePair<string,object>("customer.country", "Italy")),
            new Measurement<int>(3, new KeyValuePair<string,object>("customer.country", "Spain")),
            new Measurement<int>(1, new KeyValuePair<string,object>("customer.country", "Mexico")),
        };
    }
}

使用 dotnet-counters 執行時,結果如下:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.orders_pending
        customer.country=Italy                                             6
        customer.country=Mexico                                            1
        customer.country=Spain                                             3

最佳作法

  • 雖然 API 允許使用任何物件做為標記值,但集合工具會預期數數值型別和字串。 指定的收集工具可能或可能不支援其他類型。

  • 我們建議標籤名稱遵循 OpenTelemetry 命名方針,其使用小寫點狀階層名稱和 '_' 字元來分隔同一元素中的多個字組。 如果標籤名稱在不同的計量或其他遙測記錄中重複使用,那麼無論在哪裡使用,它們都應該具有相同的含義和一組合法值。

    範例標籤名稱:

    • customer.country
    • store.payment_method
    • store.purchase_result
  • 請注意,實際上會記錄非常大型或未繫結的標籤值組合。 雖然 .NET API 實作可以處理它,但收集工具可能會為與每個標記組合相關聯的計量資料配置儲存體,而這可能會變得非常大。 例如,如果 HatCo 有 10 種不同的帽色和 25 種帽子大小,則最多可以追蹤 10*25=250 個銷售總數。不過,如果 HatCo 新增了第三個標籤作為銷售用的 CustomerID,且銷售給全球 1 億個客戶,現在現在可能會記錄數十億個不同的標籤組合。 大部分的計量收集工具都會卸載資料,以保持在技術限制內,或可能會有大量的貨幣成本來涵蓋資料儲存和處理。 每個收集工具的實作都會決定其限制,但一個檢測中可能少於 1000 個組合是安全的。 超過 1000 個組合的任何項目都需要收集工具,以套用篩選或設計為高規模運作。 長條圖實作通常會使用比其他計量更多的記憶體,因此安全限制可能會低於 10-100 倍。 如果您預期大量的唯一標記組合,則記錄、事務資料庫或巨量資料處理系統可能更適合以所需的規模運作。

  • 對於具有非常大量標記組合的檢測,偏好使用較小的儲存類型來協助減少記憶體額外負荷。 例如,儲存 shortCounter<short> 只佔用每個標記組合的 2 個位元組,而 doubleCounter<double> 則佔用每個標記組合的 8 個位元組。

  • 建議使用集合工具針對程式碼最佳化,以相同的順序指定相同標記名稱集,讓每次呼叫在相同檢測上記錄測量。 對於需要呼叫且 Add 經常呼叫 Record 的高效能程式碼,偏好針對每個呼叫使用相同的標記名稱序列。

  • .NET API 已針對 AddRecord 呼叫最佳化,並個別指定三個或更少標記。 若要避免具有大量標籤的配置,請使用 TagList。 一般而言,這些呼叫的效能額外負荷會隨著使用更多標籤而增加。

注意

OpenTelemetry 將標籤稱為「屬性」。 這些是相同功能的兩個不同名稱。

測試自訂計量

可以使用 MetricCollector<T> 測試您新增的任何自訂計量。 此類型可以很容易地記錄特定檢測的測量,並判斷這些值是正確的。

使用相依性插入進行測試

以下程式碼顯示了使用相依性插入和 IMeterFactory 的程式碼元件之範例測試案例。

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var services = CreateServiceProvider();
        var metrics = services.GetRequiredService<HatCoMetrics>();
        var meterFactory = services.GetRequiredService<IMeterFactory>();
        var collector = new MetricCollector<int>(meterFactory, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }

    // Setup a new service provider. This example creates the collection explicitly but you might leverage
    // a host or some other application setup code to do this as well.
    private static IServiceProvider CreateServiceProvider()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddMetrics();
        serviceCollection.AddSingleton<HatCoMetrics>();
        return serviceCollection.BuildServiceProvider();
    }
}

每個 MetricCollector 物件記錄一個檢測的所有測量。 如果需要驗證多個檢測的測量,請為每個檢測建立一個 MetricCollector。

無相依性插入的測試

也可以在靜態欄位中測試使用共用全域 Meter 物件的程式碼,但要確保這些測試設定為不平行執行。 因為 Meter 物件是共用的,所以一個測試中的 MetricCollector 將觀察從平行執行之任何其他測試建立的測量。

class HatCoMetricsWithGlobalMeter
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    public void HatsSold(int quantity)
    {
        s_hatsSold.Add(quantity);
    }
}

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var metrics = new HatCoMetricsWithGlobalMeter();
        // Be careful specifying scope=null. This binds the collector to a global Meter and tests
        // that use global state should not be configured to run in parallel.
        var collector = new MetricCollector<int>(null, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }
}