Mert Özen Her Satırda Daha İleri
DevOps Observability — #12

.NET 8'den .NET 11'e Observability'de Gelenler: IMeterFactory, Gauge, Yeni Built-in Metric'ler ve ASP.NET Core'da Yerleşik Tracing

Mert Özen 15 Kas 2026 9 dk 13 görüntülenme
.NET 8'den .NET 11'e Observability'de Gelenler: IMeterFactory, Gauge, Yeni Built-in Metric'ler ve ASP.NET Core'da Yerleşik Tracing

.NET 11'de ASP.NET Core, HTTP server span'ının OpenTelemetry attribute'larını kendisi yazıyor ve MemoryCache dotnet.cache.* metric'lerini yayınlıyor; instrumentation paketi gerekmiyor. .NET 8'in IMeterFactory'sinden .NET 9'un Gauge'una, .NET 10'un Identity metric'leri ve schema URL'ine kadar sürüm haritasını çıkarıyor, .NET 11 maddelerini GA SDK'sıyla çalıştırıp eski kodu okumanın ipuçlarını veriyoruz.

Sürüm notu: Bu yazıdaki örnekler .NET 11 (SDK 11.0.100) ve OpenTelemetry .NET 1.21 ile denenmiştir. Örnek proje repo'da samples/Net11Observability altında ve kendi global.json'ıyla .NET 11 SDK'sını istiyor; Bookstore'un kendisi .NET 10 (LTS) üzerinde kalıyor.

Önce Tahmin Et

Serinin başından beri Bookstore'da (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) HTTP span'larını OpenTelemetry.Instrumentation.AspNetCore paketi üretiyor: AddAspNetCoreInstrumentation(). .NET 11'de küçük bir uygulama yazdık ve bu paketi hiç eklemedik; tracing'e yalnızca AddSource("Microsoft.AspNetCore") yazdık. Console exporter'ın çıktısında GET /api/books/{isbn} span'ı var mı? Varsa http.route ve http.response.status_code gibi attribute'ları taşıyor mu? Ve paketi kaldırmazsan, span ikiye mi katlanır?

Neden Sürüm Sürüm Bakmak?

.NET'in observability API'leri son dört sürümde çok değişti ve production'da kod hep aynı sürümde değil: bir servis .NET 8'de, diğeri .NET 10'da olabiliyor. Bir dashboard'un neden bir serviste bir metric'i gösterip diğerinde göstermediğini, ya da bir instrumentation paketinin neden artık gereksiz olduğunu anlamak için hangi özelliğin hangi sürümde geldiğini bilmek gerekiyor. Bu yazı bir sürüm haritası: .NET 8, 9 ve 10'u kısaca, .NET 11'i ise çalıştırarak geçiyoruz. .NET 11'in maddeleri 2026-11-10'daki genel sürümün (GA) resmi "What's new" sayfalarından; yalnızca orada yazanları aldık ve bu yazıda gösterdiğimiz her çıktı .NET 11 SDK'sıyla çalıştırıldı.

.NET 8 ve .NET 9: Metric'ler Platforma Giriyor

.NET 8, metric tarafındaki temeli attı. IMeterFactory ile meter'lar DI (dependency injection; bağımlılıkların dışarıdan verilmesi) üzerinden oluşturulabildi; böylece test edilebilir ve her host'a ayrı oldular (Bookstore'un OrderingMetrics'i bunu kullanıyor). ASP.NET Core, Kestrel ve HttpClient kendi metric'lerini yazmaya başladı: http.server.request.duration, kestrel.active_connections, http.client.request.duration gibi. OpenTelemetry semantic conventions'a uygun isimler ve birimlerle geldiler; serinin metric yazısında kullandığımız histogram'ların çoğu buradan. Test tarafında da Microsoft.Extensions.Diagnostics.Testing paketindeki MetricCollector<T> geldi; metric'i unit test etmenin yolu o.

.NET 9 üç eksik parçayı tamamladı. Gauge<T>, dışarıdan set edilen anlık bir değeri senkron olarak kaydetmeyi sağladı; daha önce bunun için yalnızca callback'li ObservableGauge vardı. Bookstore'un stoğu azalan kitap sayısı bu instrument'la yazılıyor. Activity.AddLink ile bir span'a, oluşturulduktan sonra başka bir trace context'ine link eklenebildi; daha önce link'ler yalnızca span oluşturulurken verilebiliyordu. Bu, bir mesajın hangi işlem içinde işleneceği sonradan belli olduğunda işe yarıyor. Üçüncüsü process dışından dinleme: dotnet-counters gibi araçlar meter adını tam yazmadan, * ya da Bookstore* gibi bir önekle bütün meter'ları dinleyebildi. Runtime'ın kendi metric'leri de bu sürümde System.Runtime meter'ına taşındı; metric üretme yazısında runtime instrumentation paketinin .NET 9 ve sonrasında yalnızca bu meter'ı açtığını görmüştük.

.NET 10: Güvenlik Metric'leri ve Schema URL

ASP.NET Core 10 güvenlik tarafına metric ekledi. Microsoft.AspNetCore.Identity meter'ı kullanıcı oluşturma ve güncelleme süreleri, şifre kontrol denemeleri, üretilen ve doğrulanan token'lar, sign-in ve sign-out sayıları gibi metric'ler yazıyor (aspnetcore.identity.sign_in.sign_ins, aspnetcore.identity.user.check_password_attempts gibi). Authentication ve authorization middleware'leri de challenge, forbid ve yetkilendirme gerektiren istek sayılarını tutuyor; Kestrel, IIS (Windows'un web sunucusu) ve HTTP.sys'in kullandığı memory pool için de metric'ler geldi. Bunların güvenlik açısından nasıl okunacağı ASP.NET Core Güvenliği serisinin konusu.

Kütüphane tarafında ActivitySource ve Meter, ürettikleri telemetry'nin hangi semantic conventions sürümüne uyduğunu söyleyebiliyor:

using System.Diagnostics;

// .NET 10: bir ActivitySource (ve bir Meter), telemetry'sinin hangi semantic conventions sürümüne uyduğunu söyleyebiliyor.
public static class BookstoreTelemetry
{
    public static readonly ActivitySource Catalog = new(new ActivitySourceOptions("Bookstore.Catalog")
    {
        Version = "1.0.0",
        TelemetrySchemaUrl = "https://opentelemetry.io/schemas/1.37.0"
    });
}

OpenTelemetry SDK'sı bu URL'i instrumentation scope'una koyuyor; backend, attribute adları değiştiğinde iki sürümü birbirine çevirmek için onu kullanabiliyor. Process dışına aktarılan trace verisi de .NET 10'da Activity event'lerini ve link'lerini taşımaya başladı ve saniyedeki root activity sayısını sınırlayan bir sampling seçeneği kazandı. Bir not: OpenTelemetry .NET'in son sürümleri hedef framework listesinden net8.0 ve net9.0'ı çıkardı (netstandard2.0 üzerinden çalışmaya devam ediyorlar); bu yüzden kullandığın SDK sürümünü uygulamanın .NET sürümüyle birlikte düşün.

.NET 11: ASP.NET Core'un Kendi Span'ı

.NET 11'in observability'deki en görünür değişikliği ASP.NET Core'da: server activity'si artık OpenTelemetry'nin HTTP server span'ı için tanımladığı attribute'ları kendisi yazıyor ve bunun için ayrı bir instrumentation paketi gerekmiyor. Örnek projenin tamamı bu:

using System.Diagnostics;
using Microsoft.Extensions.Caching.Memory;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

// .NET 11: ASP.NET Core ve MemoryCache'in instrumentation paketi olmadan, kendiliğinden ürettikleri.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// İstatistikler açılınca MemoryCache dotnet.cache.* metric'lerini yayınlıyor.
builder.Services.AddMemoryCache(options => options.TrackStatistics = true);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        // OpenTelemetry.Instrumentation.AspNetCore yok: server activity'si HTTP attribute'larını zaten taşıyor.
        .AddSource("Microsoft.AspNetCore", "Bookstore.*")
        .AddConsoleExporter())
    .WithMetrics(metrics => metrics
        .AddMeter("Microsoft.Extensions.Caching.Memory.MemoryCache")
        .AddConsoleExporter());

WebApplication app = builder.Build();

app.MapGet("/api/books/{isbn}", (string isbn, IMemoryCache cache) =>
    cache.GetOrCreate(isbn, _ =>
    {
        // Kitabı yalnızca cache miss yüklüyor (ve span alıyor).
        using Activity? activity = BookstoreTelemetry.Catalog.StartActivity("load book");
        activity?.SetTag("bookstore.book.isbn", isbn);
        return new BookView(isbn, "Dune");
    }));

app.Run();

internal sealed record BookView(string Isbn, string Title);

Uygulamayı çalıştırıp aynı kitabı üç kere ve var olmayan bir adresi bir kere istedik (launchSettings.json adresi 127.0.0.1:5096 ve metric export aralığını 5 saniye yapıyor):

cd samples/Net11Observability
dotnet run
curl http://127.0.0.1:5096/api/books/9780441172719
curl http://127.0.0.1:5096/api/books/9780441172719
curl http://127.0.0.1:5096/api/books/9780441172719
curl http://127.0.0.1:5096/api/nothing

Kitap isteklerinden birinin span'ı:

Activity.DisplayName:        GET /api/books/{isbn}
Activity.Kind:               Server
Activity.Tags:
    client.address: 127.0.0.1
    network.peer.address: 127.0.0.1
    network.peer.port: 54457
    server.address: 127.0.0.1
    server.port: 5096
    http.request.method: GET
    user_agent.original: curl/8.7.1
    url.scheme: http
    url.path: /api/books/9780441172719
    http.response.status_code: 200
    network.protocol.version: 1.1
    http.route: /api/books/{isbn}
Instrumentation scope (ActivitySource):
    Name: Microsoft.AspNetCore

Önce Tahmin Et'in ilk iki sorusunun cevabı evet: span var, adı route şablonundan geliyor ve semantic conventions'ın istediği attribute'ların hepsi orada. 404 dönen istekte span adı yalnızca GET, çünkü eşleşen bir route yok; http.route da yok. Bu, high cardinality'yi önleyen doğru davranış: bilinmeyen her URL ayrı bir span adı olsaydı backend'de sınırsız sayıda operation oluşurdu. Bu davranışı istemeyen uygulamalar için bir AppContext switch'i var: Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData.

Üçüncü soru için aynı örneğe OpenTelemetry.Instrumentation.AspNetCore 1.21.0'ı ekleyip AddAspNetCoreInstrumentation()'ı da çağırdık. Bir istek yine tek bir server span'ı üretti: instrumentation paketi kendi span'ını açmıyor, ASP.NET Core'un activity'sini dinleyip zenginleştiriyor. Yani .NET 11'e geçerken paketi hemen kaldırmak zorunda değilsin; ama exception kaydı, filtreleme ve enrich callback'leri gibi paketin ek özelliklerini kullanmıyorsan bağımlılık listesinden bir paket eksilir.

Cache miss olduğunda açtığımız kendi span'ımız ise .NET 10'un schema URL'ini taşıyor:

Activity.DisplayName:        load book
Activity.Kind:               Internal
Activity.Tags:
    bookstore.book.isbn: 9780441172719
Instrumentation scope (ActivitySource):
    Name: Bookstore.Catalog
    Version: 1.0.0
    Schema URL: https://opentelemetry.io/schemas/1.37.0

.NET 11: MemoryCache Metric'leri

İkinci yenilik MemoryCache'te: TrackStatistics açıkken Microsoft.Extensions.Caching.Memory.MemoryCache meter'ı dört observable instrument yayınlıyor: dotnet.cache.requests, dotnet.cache.evictions, dotnet.cache.entries ve dotnet.cache.estimated_size. Üç istekten sonraki ilk export:

Metric Name: dotnet.cache.requests, Description: Total cache requests., Unit: {request}, Metric Type: LongSum
Instrumentation scope (Meter):
	Name: Microsoft.Extensions.Caching.Memory.MemoryCache
	Tags:
		dotnet.cache.name: Default
(2026-11-14T20:29:40.1109490Z, 2026-11-14T20:29:42.9744830Z] dotnet.cache.name: Default dotnet.cache.request.result: hit
Value: 2
(2026-11-14T20:29:40.1109490Z, 2026-11-14T20:29:42.9744830Z] dotnet.cache.name: Default dotnet.cache.request.result: miss
Value: 1

Metric Name: dotnet.cache.entries, Description: Current number of cache entries., Unit: {entry}, Metric Type: LongSumNonMonotonic
(2026-11-14T20:29:40.1119160Z, 2026-11-14T20:29:42.9745160Z] dotnet.cache.name: Default
Value: 1

Bir miss, iki hit; hit oranı artık ek bir adapter ya da kendi sayaçlarımız olmadan PromQL ile hesaplanabiliyor. dotnet.cache.estimated_size çıktıda yok, çünkü cache'e bir boyut sınırı vermedik; değer uygulamanın tanımladığı boyut birimlerinden hesaplanıyor. Meter'ın dotnet.cache.name tag'i ve DI üzerinden gelen IMeterFactory sayesinde her cache instance'ı ayrı ölçülüyor. Cache'in neyi, ne kadar tutacağı ve bu metric'lerin nasıl okunacağı Caching serisinin konusu.

.NET 11: Kural Tabanlı Tracing ve Diğerleri

Microsoft.Extensions.Diagnostics'e kural tabanlı bir tracing configuration API'si geldi: AddTracing ile hangi ActivitySource'ların ve operation'ların dinleneceği, kurallarla ve configuration'dan açılıp kapatılabiliyor. Yanında ActivitySourceFactory geldi ve ActivitySource'un sealed olma özelliği kaldırıldı. Bunun OpenTelemetry ile nasıl birlikte çalıştığını denedik:

using Microsoft.Extensions.Diagnostics.Tracing;

builder.Services.AddTracing(tracing => tracing.DisableTracing(sourceName: "Microsoft.AspNetCore"));

Bu satırla örneği yeniden çalıştırıp bir istek gönderdik; console exporter span'ı yine yazdı. OpenTelemetry .NET 1.21'in TracerProvider'ı kendi ActivityListener'ını kullanıyor ve bu kurallar onun neyi dinleyeceğini değiştirmedi. Kurallar bu yeni altyapıyla kaydedilen listener'lar için anlamlı; OpenTelemetry SDK'sının ne zaman ve nasıl destekleyeceğini kendi sürüm notlarından takip etmek gerekiyor. OpenTelemetry'de neyin toplanacağını bugün yine AddSource ve sampler belirliyor.

İki küçük madde daha var. HttpClient'ın high cardinality open_connections ve active_requests metric'leri observable instrument'a dönüştü; MeterListener'ı doğrudan kullanan kod artık RecordObservableInstruments() çağırmazsa bu değerleri görmüyor. OpenTelemetry SDK'sı ve dotnet-counters bunu zaten yapıyor. Logging tarafında [LoggerMessage] source generator'ı generic method'ları destekliyor; boxing'den kaçınmak için kullanışlı.

Eski Kodu Okumak

Production'daki codebase'lerde bu yeniliklerden önce yazılmış observability kodu da var. Üç kalıntıyı tanımak işe yarıyor. Birincisi Application Insights'ın klasik SDK'sı: TelemetryClient.TrackEvent, TrackDependency, TrackMetric çağrıları ve ITelemetryInitializer'lar. Application Insights .NET SDK'sının 3.x sürümleri OpenTelemetry üzerine yeniden yazıldı; Microsoft'un önerisi 2.x'te olanların mevcut kodu en az değişiklikle taşımak için 3.x'e geçmesi, yeni uygulamaların ise doğrudan OpenTelemetry tabanlı distro'yla başlaması (serinin production yazısında). İkincisi EventCounters: EventCounter ve PollingCounter ile yazılmış sayaçlar. dotnet-counters ikisini de okuyor, ama OpenTelemetry'ye taşınan, etiket (tag) destekleyen ve histogram yazabilen API System.Diagnostics.Metrics; yeni kodda EventCounters kullanma. Üçüncüsü OpenTracing ve OpenCensus: OpenTelemetry bu iki projenin birleşmesinden doğdu ve ikisi de arşivlendi. Kodda OpenTracing.ITracer ya da bir OpenCensus exporter'ı görüyorsan, o kod artık bakım görmeyen bir library'ye bağlı. Backend'ler de desteği bırakıyor: trace backend'i yazısında gördüğümüz Tempo 3.0, OpenCensus receiver'ını kaldırdı.

Üretimde Dikkat

Bir servisi .NET sürümü yükselttiğinde telemetry'sini de karşılaştır. .NET 11'e geçen bir servisin span'ları attribute bakımından eski instrumentation paketinin ürettiklerine çok yakın, ama birebir aynı olduğunu varsayma: dashboard'larda ve alarmlarda kullandığın attribute'ların yükseltmeden sonra hâlâ geldiğini bir staging ortamında kontrol et.

Built-in metric'lerin hepsini toplama. Her yeni sürüm yeni meter'lar getiriyor (Identity, memory pool, cache) ve AddMeter("*") gibi geniş bir seçim, cardinality bütçesini fark ettirmeden büyütür. İhtiyacın olan meter'ları adıyla ekle.

Örnek projemiz Bookstore'dan ayrı duruyor, çünkü Bookstore .NET 10 (LTS) üzerinde ve LTS bir sürümden STS (Standard Term Support; LTS'ten kısa destek süreli sürüm) bir sürüme geçmek bir ürün kararı. .NET 11'in observability yenilikleri tek başına bir yükseltme sebebi değil; ama .NET 12 LTS'e geçerken bu yazıdaki maddeler yapılacak işler listesinin parçası olacak.

Sık Düşülen Tuzaklar

.NET 11'e geçince instrumentation paketini kaldırıp AddSource("Microsoft.AspNetCore")'u eklemeyi unutmak. Paket kendi listener kaydını yapıyordu; o gidince ASP.NET Core'un activity'sini kimse dinlemez ve HTTP span'ları sessizce kaybolur.

Yeni API'leri sürümü kontrol etmeden kullanmak. Gauge<T> .NET 9, ActivitySourceOptions.TelemetrySchemaUrl .NET 10, MemoryCache metric'leri .NET 11 ile geliyor. Ortak bir library yazıyorsan hedef framework'lerine göre #if ya da ayrı paket gerekir.

MeterListener ile kendi metric toplayıcını yazmak. Observable instrument'lara geçiş gibi değişiklikler bu tür kodu sessizce bozar. Test için MetricCollector<T>, production için OpenTelemetry SDK'sı yeterli.

İki Azure distro'sunu aynı uygulamada birlikte başlatmak. Microsoft'un dokümanı yeni Microsoft OpenTelemetry Distro ile eski Azure Monitor OpenTelemetry Distro'nun birlikte başlatılmamasını açıkça söylüyor; geçişte birini kaldırmadan diğerini ekleme.

Küçük Bir Deneme

★☆☆ Örnek projeye OpenTelemetry.Instrumentation.AspNetCore'u ekleyip AddAspNetCoreInstrumentation()'ı çağır ve 404 dönen isteğin span'ını iki durumda karşılaştır. Attribute listesinde bir fark var mı?

★★☆ AddMemoryCache'e SizeLimit ver, her girişe Size = 1 ata ve 20 farklı ISBN (kitabın uluslararası standart numarası) iste. dotnet.cache.estimated_size ve dotnet.cache.evictions'ın nasıl değiştiğine bak; sonra bunu Bookstore'un catalog sorgularına uygulamanın bir planını yaz.

★★★ AddTracing ile EnableTracing("Bookstore.Catalog") kuralı yaz ve kendi ActivityListener'ını bu altyapı üzerinden kaydetmeye çalış. Kuralları appsettings.json'dan okuyup uygulama çalışırken değiştirdiğinde "load book" span'larının nasıl açılıp kapandığını gözle.

Serinin son yazısı, Application Insights ve Log Analytics'teki telemetry'yi KQL (Azure'un log sorgulama dili) ile sorgulamayı uygulamalı olarak anlatıyor. Buraya kadarki yol şöyleydi: İlk yazıda Gül'ün "bazen 8 saniye" şikâyetiyle başladık; log, metric ve trace'i ayrı ayrı ve birbirine bağlayarak okuduk, Collector'la telemetry'yi tek yerden yönettik, Prometheus, Grafana, Tempo ve Loki ile sorunu dashboard'dan trace'e, trace'ten log'a izledik, retry fırtınasını ve N+1'i düzelttik, alarmları ve production maliyetini konuştuk. Bundan sonra her yeni özellik için sorulacak soru aynı: bu bozulduğunda bunu ilk kim, hangi sinyalle görecek?