.NET'te Metric Üretmek: Counter, Histogram, Gauge, Built-in Metric'ler ve Cardinality Bütçesi
.NET'te metric, IMeterFactory'den alınan bir Meter'ın instrument'larıyla üretilir: sayaç için Counter, artıp azalan toplam için UpDownCounter, süre için Histogram, dışarıdan set edilen anlık değer için Gauge. Bookstore'un checkout metric'lerini yazıyor, built-in metric'leri ve RED/USE yöntemlerini tanıyor, MetricCollector ile test ediyor ve SDK'nın 2000'lik cardinality sınırında ne yaptığını ölçüyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10, OpenTelemetry .NET 1.21, Microsoft.Extensions.Diagnostics.Testing 10.11, xUnit v3 4.0 ve dotnet-counters 10.0 ile denenmiştir.
Önce Tahmin Et
Bookstore'a (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) "şu anda kaç checkout işleniyor" bilgisini veren bir metric ekledik: checkout başlarken bir artıyor, bitince bir azalıyor. Trafik sürerken dotnet-counters'ı uygulamaya bağlayıp bu metric'e baktık ve şu değeri okuduk:
bookstore.checkouts.active ({checkout}) = -3
Aynı anda işlenen checkout sayısı nasıl eksi olabilir? Kodda bir hata mı var, yoksa ölçen tarafta mı? Cevap, instrument türlerinin nasıl çalıştığıyla ilgili; yazının ortasında.
Instrument Türleri: Hangisi Ne Zaman?
.NET'in System.Diagnostics.Metrics API'sinde bir metric'i bir Meter'dan oluşturulan instrument ile kaydediyorsun. Türü seçmek, değerin backend'de nasıl toplanacağını seçmek demek:
| Instrument | Ne kaydeder | Bookstore örneği |
|---|---|---|
Counter<T> | Yalnız artan sayaç; backend'de hız (rate) olarak okunur | Verilen sipariş sayısı |
UpDownCounter<T> | Artıp azalabilen toplam; değişimleri (delta) kaydeder | İşlenmekte olan checkout sayısı |
Histogram<T> | Değerlerin dağılımı; bucket'lar, toplam ve adet | Checkout süresi, sipariş tutarı |
Gauge<T> (.NET 9+) | Dışarıdan set edilen anlık değer; son değer geçerli | Stoğu azalan kitap sayısı |
Observable* | Değeri sen kaydetmezsin; toplama anında callback'in döner | Bellek, queue uzunluğu gibi hazır tutulan değerler |
İki ayrım önemli. Birincisi, kaydedilen şey değişim mi yoksa durum mu: Counter ve UpDownCounter değişimi (+1, -1) kaydeder, Gauge durumu (şu anki değer 2). İkincisi, değeri kim üretiyor: senkron instrument'larda kod olay anında Add ya da Record çağırıyor; ObservableCounter, ObservableUpDownCounter ve ObservableGauge'da ise SDK toplama zamanı geldiğinde senin verdiğin callback'i çağırıyor. Süre için hemen her zaman histogram kullanılır. Ortalama süreyi bir gauge'a yazmak cazip görünür ama bir önceki yazıda gördüğümüz gibi ortalama kuyruğu saklar ve sonradan yüzdeliğe çevrilemez.
IMeterFactory ile Ordering Metric'leri
Meter'ı new Meter(...) ile static oluşturmak yerine DI (dependency injection; bağımlılıkların dışarıdan verilmesi) container'ındaki IMeterFactory'den alıyoruz. Factory, meter'ı DI container'ına bağlıyor: container dispose olunca meter da kapanıyor ve testte uygulamanın kendi meter'ı izole şekilde dinlenebiliyor (aşağıda kullanacağız).
public sealed class OrderingMetrics
{
public const string MeterName = "Bookstore.Ordering";
private readonly Counter<long> ordersPlaced;
private readonly Histogram<double> checkoutDuration;
private readonly UpDownCounter<long> activeCheckouts;
public OrderingMetrics(IMeterFactory meterFactory)
{
Meter meter = meterFactory.Create(MeterName);
ordersPlaced = meter.CreateCounter<long>(
"bookstore.orders.placed", unit: "{order}", description: "Orders that were placed and paid.");
checkoutDuration = meter.CreateHistogram(
"bookstore.checkout.duration", unit: "s", description: "Duration of POST /api/orders, payment included.",
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 7.5, 10] });
activeCheckouts = meter.CreateUpDownCounter<long>(
"bookstore.checkouts.active", unit: "{checkout}", description: "Checkouts in progress.");
}
public void CheckoutStarted() => activeCheckouts.Add(1);
// outcome: placed | out_of_stock | payment_failed | cart_empty | cart_not_found | cart_changed
public void CheckoutFinished(string outcome, string orderType, TimeSpan elapsed)
{
activeCheckouts.Add(-1);
KeyValuePair<string, object?>[] tags =
[
new("bookstore.checkout.outcome", outcome),
new("bookstore.order.type", orderType)
];
checkoutDuration.Record(elapsed.TotalSeconds, tags);
if (outcome == "placed")
{
ordersPlaced.Add(1, new KeyValuePair<string, object?>("bookstore.order.type", orderType));
}
}
}
İsimlendirme OpenTelemetry'nin kurallarını izliyor: küçük harf, noktayla ayrılmış ad alanları, kendi metric'lerin için kendi önekin (bookstore.). Birim adın içinde değil ayrı bir alanda ve UCUM (Unified Code for Units of Measure; birimleri yazmak için kullanılan standart kodlar) biçiminde: süre için s, sayılan şeyler için süslü parantezli {order}. Süreyi milisaniye yerine saniye ile ve double olarak kaydediyoruz; .NET'in built-in metric'leri ve semantic convention'lar da böyle. Prometheus tarafında bu ad bookstore_checkout_duration_seconds olacak; çeviriyi altıncı yazıda göreceğiz.
advice parametresi (.NET 9 ile geldi) histogram'ın bucket sınırları için bir öneri. OpenTelemetry SDK'sının varsayılan sınırları (0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500…) milisaniye düşünülerek seçilmiş; saniye cinsinden bir süre için neredeyse bütün değerler ilk bucket'a düşerdi. Biz sınırları checkout'un gerçek dağılımına göre seçtik: normal siparişler 0,05–0,25 arasında, retry basamakları 2,5, 5 ve 7,5 civarında. Advice yalnızca bir öneri; uygulamayı kuran kişi SDK'da bir view ile onu ezebilir.
Tag'ler bilinçli olarak az ve düşük cardinality'li. bookstore.checkout.outcome altı değerden birini, bookstore.order.type iki değerden birini alıyor (10 ve üstü satırlı sepet kurumsal sipariş sayılıyor). Sipariş numarası, müşteri ya da kitap ISBN'i (kitapları tanımlayan uluslararası standart numara) tag değil. Meter'ı OpenTelemetry'ye tanıtmak için kurulumda tek satır yeterli; joker karakter, ileride eklenecek Bookstore.Inventory gibi meter'ları da kapsıyor. Aynı satıra Polly'nin meter'ını da ekledik, çünkü retry'ların metric'i oradan geliyor:
.WithMetrics(metrics => metrics
// Bookstore.Ordering, Bookstore.Inventory… ve Polly'nin resilience metric'leri (retry, timeout)
.AddMeter("Bookstore.*", "Polly")
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation())
Trafik sırasında dotnet-counters collect ile 10 saniyelik bir pencerede şunları okuduk (Polly tag'lerini kısalttık, yüzdelik değerlerin bir kısmını çıkardık):
bookstore.orders.placed ({order} / 10 sec)[bookstore.order.type=retail] = 3
bookstore.orders.placed ({order} / 10 sec)[bookstore.order.type=institutional] = 1
bookstore.checkout.duration (s)[bookstore.checkout.outcome=placed;bookstore.order.type=retail;Percentile=95] = 5.078125
bookstore.checkout.duration (s)[bookstore.checkout.outcome=placed;bookstore.order.type=institutional;Percentile=95] = 2.63671875
bookstore.checkouts.active ({checkout}) = -3
bookstore.inventory.low_stock_books ({book}) = 2
resilience.polly.strategy.events (Count / 10 sec)[event.name=OnRetry;...;strategy.name=Retry] = 4
resilience.polly.strategy.events (Count / 10 sec)[event.name=OnTimeout;...;strategy.name=Timeout] = 4
resilience.polly.strategy.attempt.duration (ms)[attempt.handled=True;attempt.number=0;...;Percentile=95] = 2004
Önce Tahmin Et'in cevabı bu çıktıda. UpDownCounter durumu değil değişimleri kaydediyor; değeri, dinleyenin dinlemeye başladığı andan beri gelen +1 ve -1'lerin toplamı. dotnet-counters bağlandığında dört checkout zaten işleniyordu; onların +1'lerini hiç görmedi, bitişlerindeki -1'leri gördü. Kodda hata yok. OpenTelemetry SDK'sı uygulamayla birlikte başladığı için onun gördüğü değer doğru; ama her geç bağlanan dinleyici bu kaymayı yaşar. Polly satırları da değerli: 10 saniyede 4 retry ve 4 timeout. Bir önceki yazıda log'lardan elle çıkarmaya çalıştığımız bilgi burada hazır bir sayı.
Gauge ve Observable Callback'ler
"Stoğu 5'in altına düşen kaç kitap var?" sorusunun cevabı bir veritabanı sorgusu. İlk akla gelen ObservableGauge: SDK her toplamada callback'i çağırır, callback de sorguyu çalıştırır. Sorun şu ki callback senkron, SDK'nın toplama thread'inde ve her export'ta çalışıyor; Prometheus her 15 saniyede bir scrape ederse her seferinde, birden çok okuyucu varsa her biri için. Veritabanına giden bir callback, telemetry'nin uygulamayı yavaşlatmasının en kısa yolu. .NET 9 ile gelen Gauge<T> burada daha uygun: değeri bir background service kendi takvimiyle ölçüp set ediyor.
public sealed class InventoryMetrics(IMeterFactory meterFactory, IServiceScopeFactory scopes, ILogger<InventoryMetrics> logger)
: BackgroundService
{
public const string MeterName = "Bookstore.Inventory";
private const int LowStockThreshold = 5;
private readonly Gauge<int> lowStockBooks = meterFactory.Create(MeterName).CreateGauge<int>(
"bookstore.inventory.low_stock_books", unit: "{book}", description: $"Books with fewer than {LowStockThreshold} available copies.");
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));
do
{
try
{
await using AsyncServiceScope scope = scopes.CreateAsyncScope();
BookstoreContext db = scope.ServiceProvider.GetRequiredService<BookstoreContext>();
int count = await db.StockItems.CountAsync(s => s.QuantityOnHand - s.QuantityReserved < LowStockThreshold, stoppingToken);
lowStockBooks.Record(count);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogWarning(ex, "Could not measure low stock");
}
}
while (await timer.WaitForNextTickAsync(stoppingToken));
}
}
Yukarıdaki çıktıdaki low_stock_books = 2 bu service'ten geliyor: seed'de stoğu 0 ve 3 olan iki kitap. Observable instrument'lar ise değeri zaten bellekte tutulan şeyler için doğru seçim: bir Channel'ın içindeki eleman sayısı, bir connection pool'un boyutu gibi, okuması nanosaniyeler süren değerler.
Built-in Metric'ler: Kod Yazmadan Gelenler
Bir önceki yazıda dashboard'un listelediği meter'ların ne anlattığına kısaca bakalım. http.server.request.duration (ASP.NET Core) sunucunun gördüğü istek süresi; route, method ve status code tag'leriyle. Aşağıda anlatacağımız istek hızı, hata ve süre dashboard'unun neredeyse tamamı bu tek histogram'dan çıkıyor. http.server.active_requests o an işlenen istek sayısı. Kestrel'in kestrel.active_connections, kestrel.queued_connections ve kestrel.connection.duration metric'leri bağlantı katmanını, System.Net.Http'nin http.client.request.duration'ı dışarıya yapılan çağrıları gösteriyor; ödeme sağlayıcısının süresi buradan okunuyor. System.Runtime meter'ı (.NET 9+) GC (garbage collector; kullanılmayan belleği toplayan mekanizma) sayısı ve süresi, heap boyutu, thread pool kuyruğu ve thread sayısı, exception sayısı, lock contention gibi process sağlığını anlatıyor. .NET 10'da ASP.NET Core'a aspnetcore.memory_pool.* metric'leri ile authentication, authorization ve Identity için metric'ler eklendi; Bookstore.Api'de henüz kimlik doğrulama (authentication) olmadığı için o meter'lar boş. Güvenlik metric'lerinin kendisi ASP.NET Core Güvenliği serisinin konusu.
Built-in metric'lerin değeri, isimlerinin semantic convention'larla aynı olması. Grafana'daki hazır bir ASP.NET Core dashboard'u da, başka bir dilde yazılmış bir servisin dashboard'u da http.server.request.duration'ı arıyor.
RED ve USE: Neyi Ölçeceğine Karar Vermek
Metric'leri rastgele eklemek yerine iki bilinen yöntemden biriyle başlamak işi kolaylaştırıyor. RED (Rate, Errors, Duration; istek hızı, hata oranı ve süre) yöntemi, Tom Wilkie'nin istek alan servisler için önerdiği yaklaşım: her endpoint için saniyedeki istek, bunların ne kadarının hata ile bittiği ve süre dağılımı. Bookstore'da üçü de http.server.request.duration'dan ve iş tarafında bookstore.checkout.duration'ın outcome tag'inden geliyor. USE (Utilization, Saturation, Errors; kullanım, doygunluk ve hata) yöntemi, Brendan Gregg'in kaynaklar için önerdiği yaklaşım: CPU, bellek, thread pool, connection pool gibi her kaynağın ne kadar kullanıldığı, ne kadar beklettiği ve hata verip vermediği. .NET'te thread pool kuyruğu (dotnet.thread_pool.queue.length) ve Kestrel'in kuyruktaki bağlantıları doygunluğun tipik göstergeleri.
İkisi birbirini tamamlıyor: RED kullanıcının ne yaşadığını, USE bunun sebebinin kaynak tükenmesi olup olmadığını söylüyor. Bizim vakada RED alarm verirdi (checkout'un p95'i 5 saniye), USE ise sakin kalırdı; çünkü sorun bir kaynakta değil, dışarıdaki sağlayıcıda. Bu yüzden iş metric'lerini ve dış çağrıların metric'lerini (Polly'nin retry sayısı gibi) RED'in yanına eklemek gerekiyor.
Metric'i Test Etmek: MetricCollector
Metric'ler de kod; refactoring sırasında bir tag'in adı değişirse dashboard ve alarm sessizce bozulur. Microsoft.Extensions.Diagnostics.Testing paketindeki MetricCollector<T> bir instrument'ın ölçümlerini toplayıp test etmeyi sağlıyor. Bookstore'un integration testleri WebApplicationFactory ile gerçek API'yi kaldırıyor; collector'ı uygulamanın kendi IMeterFactory'sine bağladık:
[Fact]
public async Task A_placed_order_is_counted_and_its_checkout_duration_is_recorded()
{
// Collector, uygulamanın kendi IMeterFactory'sinin oluşturduğu meter'ı dinler.
IMeterFactory meterFactory = factory.Services.GetRequiredService<IMeterFactory>();
using MetricCollector<long> placed = new(meterFactory, OrderingMetrics.MeterName, "bookstore.orders.placed");
using MetricCollector<double> duration = new(meterFactory, OrderingMetrics.MeterName, "bookstore.checkout.duration");
HttpResponseMessage cart = await client.PostAsync("/api/carts", null, ct);
Guid cartId = (await cart.Content.ReadFromJsonAsync<CartView>(ct))!.Id;
await client.PutAsJsonAsync($"/api/carts/{cartId}/items/{Emma}", new SetQuantityRequest(1), ct);
HttpResponseMessage response = await client.PostAsJsonAsync("/api/orders",
new PlaceOrderRequest(cartId, "Ada Lovelace", "ada@example.com"), ct);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
CollectedMeasurement<long> order = Assert.Single(placed.GetMeasurementSnapshot());
Assert.Equal(1, order.Value);
Assert.Equal("retail", order.Tags["bookstore.order.type"]);
CollectedMeasurement<double> checkout = Assert.Single(duration.GetMeasurementSnapshot());
Assert.Equal("placed", checkout.Tags["bookstore.checkout.outcome"]);
Assert.InRange(checkout.Value, 0, 5);
}
İlk çalıştırmada test kırmızıydı:
failed Bookstore.Api.Tests.OrderingMetricsTests.A_placed_order_is_counted_and_its_checkout_duration_is_recorded (389ms)
Assert.Single() Failure: The collection contained 2 items
Collection: [CollectedMeasurement`1 { Tags = [["bookstore.order.type"] = "retail"], ..., Value = 1 }, CollectedMeasurement`1 { Tags = [["bookstore.order.type"] = "retail"], ..., Value = 1 }]
Bu test sınıfı, diğer test sınıflarıyla aynı API host'unu paylaşıyor ve xUnit sınıfları paralel çalıştırıyor. Collector, aynı anda OrderingTests'in verdiği siparişi de saydı. Collector'ı uygulamanın IMeterFactory'sine bağlamak başka host'ların meter'larını dışarıda bırakıyor, ama aynı host içindeki diğer isteklerin ölçümlerini değil. Telemetry testlerini paralel çalışmayan bir collection'a aldık:
// API host'u bütün testlerde ortak; meter'ları paralel testlerin verdiği her siparişi görür.
// Telemetry testleri tek başına çalışır, yoksa başka bir testin ölçümü snapshot'a düşer.
[CollectionDefinition(DisableParallelization = true)]
public sealed class MetricsCollection;
[Collection(typeof(MetricsCollection))]
public sealed class OrderingMetricsTests(BookstoreApiFactory factory)
Bu değişiklikten sonra test, üç ardışık dotnet test çalıştırmasında da yeşil kaldı.
Cardinality Bütçesi
İlk yazıda metric'in maliyetinin zaman serisi sayısıyla büyüdüğünü söyledik. OpenTelemetry .NET SDK'sı bunu bir sınırla koruyor: bir metric en fazla 2000 farklı tag kombinasyonu tutuyor (paketin XML dokümanına göre tag'siz nokta ve taşma noktası bu sayıya dahil değil). Ne olduğunu görmek için bir öğrenme testi yazdık; bilerek yanlış olan şeyi yapıp sipariş numarasını tag yaptık:
[Fact]
public void An_order_id_tag_hits_the_cardinality_limit()
{
List<Metric> exported = [];
using Meter meter = new("Bookstore.CardinalityDemo");
using MeterProvider provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddInMemoryExporter(exported)
.Build();
Counter<long> placed = meter.CreateCounter<long>("bookstore.orders.placed");
for (int orderId = 1; orderId <= 2500; orderId++)
{
// Bilerek yanlış: her sipariş kendi zaman serisi olur.
placed.Add(1, new KeyValuePair<string, object?>("bookstore.order.id", orderId));
}
provider.ForceFlush();
List<MetricPoint> points = [];
foreach (MetricPoint point in exported.Single().GetMetricPoints())
{
points.Add(point);
}
MetricPoint overflow = points.Single(p => HasTag(p, "otel.metric.overflow"));
// 2000 sipariş id'si kendi serisini aldı; kalan 500'ü taşma serisinde birleşti.
Assert.Equal(2001, points.Count);
Assert.Equal(500, overflow.GetSumLong());
}
Test bu iki assert ile geçiyor. 2000 sipariş kendi serisini aldı; 2001'inci siparişten itibaren gelen 500 ölçüm tek bir seride, otel.metric.overflow=true tag'iyle birleşti. Veri kaybolmadı, toplam doğru; ama 2001'inci siparişten sonra hangi siparişin ne kadar olduğunu artık bilmiyorsun ve SDK bunun için hata ya da uyarı log'u yazmıyor. Sınırı view ile instrument bazında değiştirmek mümkün (MetricStreamConfiguration.CardinalityLimit), ama çözüm sınırı büyütmek değil, tag'i doğru seçmek. Sınır SDK'daki bellek kullanımını koruyor; backend'deki maliyet ise her serinin saklanmasıyla, sınırdan çok önce büyüyor.
Pratik bir bütçe yöntemi: her metric için tag'lerin alabileceği değer sayılarını çarp. bookstore.checkout.duration'da 6 outcome, 2 sipariş tipi ve 10 bucket sınırı var; bu, uygulama başına en fazla 120 bucket serisi demek. Kubernetes'te 20 pod çalışıyorsa her pod kendi serisini ürettiği için 2.400. Tag eklemeden önce bu çarpımı yapmak, kararı somutlaştırıyor.
Üretimde Dikkat
Histogram'ın bucket'larını gerçek dağılıma göre seç. Varsayılan sınırlar milisaniye için; saniye kaydediyorsan advice ya da view ile sınır vermezsen bütün değerler tek bir bucket'ta toplanır ve yüzdelikler anlamsız çıkar.
Tag değerlerini sınırlı bir kümeden seç. Exception mesajı, kullanıcı girdisi, URL'nin tamamı ya da bir id, cardinality sınırına kaçınılmaz olarak çarpar. Exception için mesaj yerine tipi (error.type), URL için route şablonunu kullan.
Observable callback'lerde I/O yapma. Callback her toplamada ve her okuyucu için çalışır; veritabanı ya da HTTP çağrısı gerekiyorsa değeri bir background service ile ölçüp Gauge<T>'a yaz.
Meter adlarını kurulumla tutarlı tut. AddMeter("Bookstore.*") joker karakteri yeni module'lerin meter'larını otomatik kapsıyor; Ordering.Metrics gibi farklı bir önek seçen biri ise hiçbir hata almadan metric'siz kalır.
Sık Düşülen Tuzaklar
Tag'e userId ya da orderId koymak. Yukarıdaki test bunun sonucunu gösteriyor: 2000'den sonrası taşma serisinde kaybolur, backend faturası ise ondan çok önce büyür. Bu bilgi trace'e ve log'a ait.
Ortalama süreyi gauge ile raporlamak. Gauge son değeri tutar; birden çok pod'un ortalamalarının ortalaması da, bir zaman aralığındaki yüzdelik de ondan hesaplanamaz.
Aynı metric'i iki farklı birimle yayınlamak. Bir yerde milisaniye, bir yerde saniye kaydedilen checkout.duration, backend'de birbirine karışan seriler demek; birimi addan değil unit alanından ver ve tek bir birimde kal.
UpDownCounter'ın değerini mutlak sanmak. Geç bağlanan bir dinleyici, Önce Tahmin Et'teki gibi eksi değer görebilir. Kesin "şu anki" değer gerekiyorsa ve kaynak hazırsa ObservableUpDownCounter daha doğru.
Metric testlerini paralel çalışan testlerle aynı host'ta koşturmak. Ölçümler karışır ve test zaman zaman kırmızıya döner; telemetry testlerini ayrı bir collection'a al.
Küçük Bir Deneme
★☆☆ bookstore.checkout.duration'dan advice parametresini çıkar ve dashboard'da (ya da dotnet-counters'ta) p95'e bak. Varsayılan bucket'larla değer neden anlamsız çıkıyor?
★★☆ Sepetteki satır sayısını kaydeden bir Histogram<int> ekle (bookstore.cart.lines, birim {line}) ve ona bir MetricCollector testi yaz. Bucket sınırlarını kurumsal siparişleri ayırt edecek şekilde seç.
★★★ ObservableGauge ile "o anda OrderConfirmationQueue'da bekleyen mesaj sayısı" gibi bellekte tutulan bir değeri yayınla (bir sonraki yazıda bu queue'yu ekleyeceğiz). Callback'in kaç kez çağrıldığını sayarak OTLP (OpenTelemetry Protocol; telemetry taşıma protokolü) export aralığı ile çağrı sayısı arasındaki ilişkiyi ölç.
Metric'ler "ne sıklıkla, ne kadar kötü" sorusuna cevap veriyor. "Bu yavaş sipariş zamanını nerede harcadı" sorusu ise trace'in işi. Bir sonraki yazıda distributed tracing'e geçiyoruz: span, parent/child ilişkisi, W3C (World Wide Web Consortium; web standartlarını yayınlayan kuruluş) traceparent header'ı, ActivitySource ile checkout'a kendi span'larımızı eklemek ve trace context'i bir queue üzerinden bir background worker'a taşımak.