Production'da Diagnostics: dotnet-monitor, Collection Rule'lar, Kubernetes Sidecar ve Continuous Profiling
dotnet-monitor, trace, gcdump ve dump'ı kimsenin container'a girmesine gerek bırakmadan bir HTTP API ve collection rule'larla toplar; rule'lar için Listen modu, güvenlik için localhost'a bağlı API, API key ve sınırlı egress gerekir. Bookstore'u kind'de sidecar'la çalıştırıyor, yavaş rapor isteklerinde ve kendi meter'ımızda tetiklenen kuralları, Limits'in throttling'ini ve suspend/nosuspend farkını ölçüyor, OpenTelemetry Profiles'ın alpha durumuna bakıyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (container'da runtime 10.0.15) ile derlenip denenmiştir; dotnet-monitor 10.0.5, kind 0.33 üzerinde Kubernetes 1.37, kubectl 1.37, Microsoft.Diagnostics.Tracing.TraceEvent 4.0.0 ve yük için k6 2.4.0 kullanılmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) production'da Kubernetes'te çalışıyor ve pod'una dotnet-monitor'ı sidecar olarak eklemeye karar verdin. Uygulama container'ına tek bir ortam değişkeni ekliyorsun: DOTNET_DiagnosticPorts=/diag/dotnet-monitor.sock. İki soru. Bir gün monitor container'ı başlayamazsa uygulama ne yapar: normal açılır mı, hata verip kapanır mı, yoksa başka bir şey mi olur? İkincisi: rapor endpoint'i yavaşladığında otomatik CPU trace'i alan bir kural yazdın ve yük yarım saat boyunca yüksek kaldı. Kaç trace dosyası birikir? Cevaplar sidecar ve collection rule bölümlerinde.
dotnet-monitor: Diagnostics İçin Bir HTTP API
Şimdiye kadar kullandığımız araçların hepsi birinin container'a girip komut çalıştırmasını istiyordu. dotnet-monitor aynı işleri (trace, gcdump, dump, log, metric) bir HTTP API'si üzerinden yapan, uzun süre çalışan bir süreç: GET /processes, /trace, /gcdump, /dump, /logs, /livemetrics, Prometheus formatında /metrics. Asıl farkı collection rule'lar: belirli bir koşul oluşunca toplama işini insan beklemeden kendisi başlatıyor ve sonucu bir egress hedefine (dosya sistemi, Azure Blob Storage, S3 (Amazon'un nesne depolama API'si) uyumlu depolama) yazıyor.
dotnet-monitor process'lerle 01'de anlattığımız diagnostic port üzerinden konuşuyor ve bunun iki yolu var. Connect modunda (varsayılan) monitor, uygulamanın açtığı socket'e bağlanıyor; dokümana göre bu modda trace, gcdump, dump ve metric çalışıyor ama collection rule'lar ve trigger'lar çalışmıyor. Listen modunda tersine monitor bir socket açıp bekliyor, uygulamanın runtime'ı açılışta ona bağlanıyor. Collection rule istiyorsan Listen modu gerekiyor; bu yazıdaki kurulum o.
Kubernetes'te Sidecar

Sidecar'ın iskeleti dotnet-monitor deposundaki Kubernetes örneğinden geliyor: iki container'ın ortak bir emptyDir volume'ü, monitor tarafında Listen modu ve paylaşılan yol, uygulama tarafında socket'i gösteren DOTNET_DiagnosticPorts. 01'de sidecar'ın görmesi gereken iki şeyi (PID namespace'i ve /tmp) elle bağlamıştık; Listen modunda PID namespace'ini paylaşmak gerekmiyor, çünkü bağlantıyı uygulama kuruyor.
apiVersion: apps/v1
kind: Deployment
metadata:
name: bookstore-api
spec:
replicas: 1
selector:
matchLabels:
app: bookstore-api
template:
metadata:
labels:
app: bookstore-api
spec:
containers:
- name: app
image: profseries-api:lab08
imagePullPolicy: IfNotPresent
env:
# runtime, dotnet-monitor'ın socket'ine bağlanıyor; suspend = açılışta onu bekle
- name: DOTNET_DiagnosticPorts
value: /diag/dotnet-monitor.sock
ports:
- containerPort: 8080
volumeMounts:
- name: diag
mountPath: /diag
resources:
requests:
cpu: "1"
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
- name: monitor
image: mcr.microsoft.com/dotnet/monitor:10.0.5
args: [ "collect" ]
env:
- name: DOTNETMONITOR_DiagnosticPort__ConnectionMode
value: Listen
- name: DOTNETMONITOR_Storage__DefaultSharedPath
value: /diag
# API yalnızca pod içinde: kubectl port-forward ile eriş, asla Service ile açma
- name: DOTNETMONITOR_Urls
value: http://localhost:52323
- name: DOTNETMONITOR_Metrics__Endpoints
value: http://+:52325
volumeMounts:
- name: diag
mountPath: /diag
- name: monitor-config
mountPath: /etc/dotnet-monitor
readOnly: true
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 250m
memory: 256Mi
volumes:
- name: diag
emptyDir:
sizeLimit: 2Gi
- name: monitor-config
projected:
sources:
- secret:
name: bookstore-monitor-auth
- configMap:
name: bookstore-monitor-rules
Monitor'ın kaynak değerleri dokümandaki önerilen alt ve üst sınırlar. DefaultSharedPath verilince monitor socket'i /diag/dotnet-monitor.sock adıyla açıyor, dump'ları da aynı yerde ürettiriyor. Ayarların geri kalanı /etc/dotnet-monitor'dan geliyor: dotnet-monitor bu klasördeki settings.json'ı ve dosya adı ayar anahtarı olan dosyaları (key-per-file) okuyor. Projected volume ile bir Secret'ı (API key) ve bir ConfigMap'i (collection rule'lar) aynı klasöre koydum. Lab'ı kind ile kurduğum yerel bir Kubernetes 1.37 cluster'ında çalıştırdım:
kind load docker-image profseries-api:lab08 --name profseries
kubectl create configmap bookstore-monitor-rules --from-file=settings.json=monitor-settings.json
kubectl apply -f bookstore-api.yaml
kubectl get pods
NAME READY STATUS RESTARTS AGE
bookstore-api-558bd5cc7c-4644d 2/2 Running 0 2m38s
Tahmin sorusunun ilk cevabı DOTNET_DiagnosticPorts'un varsayılanında saklı: suspend. Runtime, socket'e bağlanıp monitor'dan devam komutu alana kadar açılışı durduruyor. Monitor olmadan ne olduğunu bir container'da denedim; 10 saniye sonra /health hâlâ cevap vermiyordu ve uygulama logu şunu diyordu:
The runtime has been configured to pause during startup and is awaiting a Diagnostics IPC ResumeStartup command from a Diagnostic Port.
DOTNET_DiagnosticPorts="/tmp/dotnet-monitor.sock"
DOTNET_DefaultDiagnosticPortSuspend=0
Değeri /diag/dotnet-monitor.sock,nosuspend yapınca uygulama monitor'ı beklemeden açıldı ve /health 200 döndü. Seçim bir takas: suspend ile açılıştaki event'leri (assembly yükleme, startup trigger'ları) kaçırmazsın ama monitor'ın sağlığı uygulamanın açılmasına bağlanır; nosuspend ile monitor çökse de servis ayağa kalkar, açılışın ilk anları kaydedilmeyebilir. Production'da ben nosuspend'i seçerim; açılış analizini staging'de suspend ile yaparım. Pod'un CPU ve memory limit'lerinin .NET GC'ye (garbage collector; kullanılmayan object'leri temizleyen runtime bileşeni) etkisi Kubernetes serisinin "Resource Request ve Limit'ler: QoS, OOMKilled, CPU Throttling ve Container'da .NET GC" yazısında.
Authentication: API Key, HTTPS ve Erişim
dotnet-monitor API'si process'in bütün belleğini dump olarak verebilen bir kapı, bu yüzden varsayılan olarak kimlik doğrulama istiyor. Dokümanın önerdiği yol Microsoft Entra ID (Microsoft'un kimlik hizmeti, eski adıyla Azure Active Directory), en basit yolu ise API key. Aracın kendi komutu bir key çifti üretiyor: yapılandırmaya gidecek Subject ve PublicKey, isteklerde kullanılacak bir Authorization: Bearer başlığı:
docker run --rm mcr.microsoft.com/dotnet/monitor:10.0.5 generatekey -o MachineJson
kubectl create secret generic bookstore-monitor-auth \
--from-literal=Authentication__MonitorApiKey__Subject=<subject> \
--from-literal=Authentication__MonitorApiKey__PublicKey=<public-key>
Secret'taki anahtar adları key-per-file kuralına göre dosya adı oluyor, dotnet-monitor da onları Authentication:MonitorApiKey ayarı olarak okuyor. Monitor açılırken logunda iki satır dikkatimi çekti:
Connection mode: Listen at '/diag/dotnet-monitor.sock'
WARNING: Authentication is enabled over insecure http transport. This can pose a security risk and is not intended for production environments.
Uyarı yerinde: API key HTTP üzerinden düz metin gidiyor. Bu lab'da API'yi yalnızca localhost:52323'te dinlettim; pod'un dışından ya da başka bir pod'dan erişilemiyor, ulaşmanın tek yolu kubectl port-forward ve bunun için de cluster'da yetki gerekiyor. Production'da bunun üstüne HTTPS ekle. Metric endpoint'i (52325) ise dokümana göre hiç kimlik doğrulama yapmıyor; orada yalnızca sayılar var, ama yine de ağ politikasıyla sadece Prometheus'a aç.
kubectl port-forward deploy/bookstore-api 5483:8080 52323:52323 52325:52325
curl -s -o /dev/null -w '%{http_code}\n' localhost:52323/processes
curl -s -H "Authorization: Bearer <token>" localhost:52323/processes
401
[{"pid":1,"uid":"4e04af7e-ab87-4aa8-af00-3a69b20fb22e","name":"dotnet","isDefault":true}]
İsteğe Bağlı Toplama: Trace ve gcdump
En basit kullanım, olay anında API'den bir şey istemek. Pod'da tek .NET process'i olduğu için o varsayılan process; pid vermek yine de iyi alışkanlık. Profile adları dotnet-trace'tekilerden farklı: Cpu, Http, Logs, Metrics, GcCollect.
curl -s -H "Authorization: Bearer <token>" "localhost:52323/gcdump?pid=1" -o bookstore.gcdump
curl -s -H "Authorization: Bearer <token>" "localhost:52323/trace?pid=1&durationSeconds=5&profile=Cpu,Http" -o bookstore.nettrace
gcdump 2,1 saniyede 6,6 MB, 5 saniyelik trace 5,1 MB geldi; dosyalar doğrudan HTTP cevabıyla akıyor. Aynı isteğe egressProvider=artifacts eklersen dosya senin makinene değil, yapılandırılmış egress hedefine gidiyor ve API hemen 202 Accepted ile bir Location: /operations/{id} başlığı dönüyor; işin durumunu o adresten soruyorsun. Büyük dump'lar için doğru yol bu, çünkü port-forward üzerinden yüzlerce MB taşımak hem yavaş hem kırılgan.
Metric endpoint'i varsayılan olarak System.Runtime ve ASP.NET Core Hosting'in EventCounter'larını Prometheus formatında veriyor (systemruntime_cpu_usage_ratio, microsoftaspnetcorehosting_requests_per_second gibi). 02'de kullandığımız yeni System.Runtime Meter'ı ya da Bookstore'un kendi meter'ını görmek için Metrics:Meters altında meter adlarını eklemek gerekiyor. Bu metric'leri Prometheus ve dashboard'a taşımak DevOps tarafındaki Observability serisinin konusu.
Collection Rule'lar: Trigger, Action, Egress, Limits
Bir collection rule dört parçadan oluşuyor: ne zaman (trigger), ne yapılacak (action listesi), sonuç nereye yazılacak (egress) ve ne kadar sık (limits). ConfigMap'teki settings.json iki kural tanımlıyor:
{
"Egress": {
"FileSystem": {
"artifacts": {
"DirectoryPath": "/diag/artifacts"
}
}
},
"CollectionRules": {
"SlowReports": {
"Trigger": {
"Type": "AspNetRequestDuration",
"Settings": {
"RequestCount": 10,
"RequestDuration": "00:00:00.500",
"SlidingWindowDuration": "00:00:30",
"IncludePaths": [ "/api/reports/**" ]
}
},
"Actions": [
{
"Name": "CpuTrace",
"Type": "CollectTrace",
"Settings": {
"Profile": "Cpu",
"Duration": "00:00:10",
"Egress": "artifacts"
}
}
],
"Limits": {
"ActionCount": 1,
"ActionCountSlidingWindowDuration": "01:00:00"
}
},
"SlowPayments": {
"Trigger": {
"Type": "EventMeter",
"Settings": {
"MeterName": "Bookstore.Ordering",
"InstrumentName": "bookstore.payment.duration",
"HistogramPercentile": 95,
"GreaterThan": 0.2,
"SlidingWindowDuration": "00:00:15"
}
},
"Actions": [
{
"Name": "HttpTrace",
"Type": "CollectTrace",
"Settings": {
"Profile": "Http",
"Duration": "00:00:10",
"Egress": "artifacts"
}
}
],
"Limits": {
"ActionCount": 1,
"ActionCountSlidingWindowDuration": "01:00:00"
}
}
}
}
SlowReports, rapor yollarında 30 saniye içinde 500 ms'den uzun süren 10 istek görürse 10 saniyelik bir CPU trace'i alıyor. SlowPayments ise 02'de yazdığımız Bookstore.Ordering meter'ındaki ödeme süresi histogram'ının p95'i 15 saniye boyunca 200 ms'nin üstünde kalırsa HTTP trace'i alıyor; EventMeter trigger'ı dotnet-monitor 8'den beri var ve kendi metric'lerinle tetikleme yapabilmenin yolu bu. Kuralların durumunu API'den sorabiliyorsun:
curl -s -H "Authorization: Bearer <token>" "localhost:52323/collectionrules?pid=1"
{"SlowPayments":{"state":"Running","stateReason":"This collection rule is active and waiting for its triggering conditions to be satisfied."},"SlowReports":{"state":"Running","stateReason":"This collection rule is active and waiting for its triggering conditions to be satisfied."}}
Port-forward üzerinden 04'teki yavaş CSV (virgülle ayrılmış düz metin) export'una saniyede 20 istekle 25 saniye yük verdim. 2 CPU limit'li pod'da istekler ortalama 2 saniye sürdü (p95 3,05 sn). Monitor'ın JSON logundan zaman ve mesaj alanlarını süzünce:
01:08:39.008 Information Collection rule 'SlowReports' trigger 'AspNetRequestDuration' completed.
01:08:39.114 Information Collection rule 'SlowReports' action 'CollectTrace' started.
01:08:49.315 Information Egressed artifact to /diag/artifacts/20261114_010839_125_1.nettrace
01:08:49.319 Information Collection rule 'SlowReports' action 'CollectTrace' completed.
01:08:49.320 Information Collection rule 'SlowReports' trigger 'AspNetRequestDuration' started.
01:08:50.939 Information Collection rule 'SlowReports' trigger 'AspNetRequestDuration' completed.
01:08:51.041 Warning Collection rule 'SlowReports' actions throttled due to action limits.
Tahmin sorusunun ikinci cevabı burada: bir. Trigger, yük sürdüğü için iki saniyede bir yeniden tetiklendi, ama ActionCount: 1 yüzünden her seferinde "throttled" uyarısı verip hiçbir şey toplamadı. Limits olmasaydı yarım saatlik yükte yüzlerce 12 MB'lık trace volume'ü doldururdu; emptyDir'e verdiğim 2 GiB'lık sizeLimit aşıldığında kubelet pod'u evict eder. Kuralın durumu da bunu söylüyor:
{"lifetimeOccurrences":1,"slidingWindowOccurrences":1,"actionCountLimit":1,"actionCountSlidingWindowDurationLimit":"01:00:00","slidingWindowDurationCountdown":"00:59:24","ruleFinishedCountdown":null,"state":"Throttled","stateReason":"This collection rule is temporarily throttled because the ActionCountLimit has been reached within the ActionCountSlidingWindowDuration."}
Ödeme kuralını kart ödemesi yapan 20 sanal kullanıcıyla test ettim; istemcinin gördüğü p95 214 ms'ydi; trigger yük sürerken tetiklendi ve 10 saniyelik HTTP trace'ini aynı klasöre yazdı. Toplanan dosyayı uygulama container'ından çekip 06'daki Bookstore.TraceStats ile açtım; monitor image'ında tar ve shell olmadığı için (denedim) kubectl cp monitor container'ında çalışmıyor, ortak volume sayesinde dosya uygulama container'ından alınabiliyor:
kubectl cp -c app bookstore-api-558bd5cc7c-4644d:/diag/artifacts/20261114_010839_125_1.nettrace ./slowreports.nettrace
dotnet run -c Release -- cpu slowreports.nettrace \
--inc "module Microsoft.AspNetCore.Server.Kestrel.Core" --exc UNMANAGED_CODE_TIME \
--foldpats CPU_TIME --group "{%}!->module $1"
Exc % Inc % Name (total 5,135 ms)
98.1 99.9 module System.Private.CoreLib
1.0 99.9 module Microsoft.AspNetCore.Server.Kestrel.Core
0.8 96.1 module Bookstore.Api
İstek zamanının yüzde 96'sı Bookstore kodunun altında ve neredeyse tamamı CoreLib'de: 04'te bulduğumuz StringBuilder ve LOH (Large Object Heap; büyük object'lerin tutulduğu heap bölgesi) tablosunun aynısı, bu kez kimse container'a girmeden toplandı.
Continuous Profiling
Collection rule'lar "bir şey olunca kaydet" modeli. Continuous profiling ise düşük frekansla sürekli örnek alıp zaman serisi gibi saklıyor; bir sorunun başladığı ana geri dönüp o dakikanın flame graph'ına bakabiliyorsun. OpenTelemetry bu sinyali standartlaştırmak için Profiles adlı yeni bir sinyal ve OTLP (OpenTelemetry Protocol; telemetri verisini taşıyan standart protokol) formatı geliştiriyor. 2026'da duyurulan public alpha'ya göre format pprof ile kayıpsız dönüştürülebiliyor, profiller trace'lere bağlanabiliyor ve Linux'taki eBPF tabanlı profiler .NET 9 ve 10'u destekliyor. Aynı duyuru alpha'nın kritik production iş yükleri için önerilmediğini ve production'a hazır backend'lerin henüz olmadığını açıkça söylüyor. Bugün kullanılabilecek açık kaynak bir seçenek Grafana Pyroscope: dokümanına göre .NET profiler'ı Linux'ta x64 ve Arm64 üzerinde .NET 8, 9 ve 10'u destekliyor. Bulut tarafında Azure Application Insights Profiler gibi sağlayıcıların kendi profiler'ları ve ticari APM (Application Performance Monitoring; uygulama performansı izleme) ürünleri de aynı işi yönetilen hizmet olarak yapıyor; seçerken veri nereye gidiyor, ne kadar saklanıyor ve maliyeti ne sorularını soruyorsun. Benim önerim: önce collection rule'larla olay anını yakala, sürekli profiling'i ancak aralıklı ve tekrar üretilemeyen sorunlar için ekle.
Üretimde Dikkat
Dump'lar ve trace'ler secret ve kişisel veri taşır. Full dump'ta connection string, token ve müşteri verisi bellekte nasıl duruyorsa öyle duruyor; 07'de gördüğümüz database profile'ı her komutla birlikte connection string'i taşıyordu. Egress hedefini buna göre seç: erişimi kısıtlı bir depolama hesabı ya da bucket, bir yaşam döngüsü kuralıyla otomatik silme (örneğin 7 gün), şifreleme ve kimin indirdiğinin kaydı. FileSystem egress'i bir emptyDir'e yazıyorsa dosyalar pod silinince gider; olay sonrası analize lazımsa kalıcı bir hedefe yaz.
dotnet-monitor API'sini Service, Ingress ya da LoadBalancer ile asla dışarı açma. Bu lab'daki gibi localhost'ta dinlet, pods/portforward yetkisini RBAC (rol tabanlı yetkilendirme) ile yalnızca nöbetçi ekibe ver, API key'i Secret'ta tut ve düzenli değiştir, HTTPS kullan. Monitor container'ının kaynak limitleri küçük; ama trace sırasında event tamponu uygulama process'inde büyüdüğü için uygulamanın memory limit'inde pay bırak. Limits her kuralda zorunlu kabul et: ActionCount olmadan yazılmış bir kural, sorunun en kötü anında disk doldurup pod'u düşürebilir.
Sık Düşülen Tuzaklar
- Monitor'ı Connect modunda bırakıp collection rule'ların neden hiç çalışmadığını aramak; trigger'lar Listen modu istiyor.
DOTNET_DiagnosticPorts'unosuspendolmadan verip monitor'ın başlayamadığı günü düşünmemek: uygulama açılışta bekler.--no-authile denemek ve o ayarı production manifest'ine taşımak; örnek deployment'taki uyarı tam olarak bunun için.- Kurala Limits koymamak ve sürekli tetiklenen bir trigger'ın volume'ü doldurmasına izin vermek.
- Egress olarak
emptyDirkullanıp pod yeniden başlayınca kanıtın gittiğini olaydan sonra fark etmek. - Metric endpoint'inde yeni
System.RuntimeMeter'ını beklemek; varsayılan olarak eski EventCounter adları geliyor, meter'ları yapılandırmadan eklemek gerekiyor.
Küçük Bir Deneme
★☆☆ Lab'ı kind'de kur, DOTNET_DiagnosticPorts'u önce varsayılanla, sonra nosuspend ile dene; monitor container'ının image adını bilerek bozup iki durumda uygulamanın açılıp açılmadığına bak.
★★☆ 05'teki sızan /ship endpoint'i için bir kural yaz: System.Runtime Meter'ındaki bir bellek metric'i eşiği aşınca CollectGCDump çalışsın. Yarım saat arayla iki gcdump toplanacak şekilde Limits'i ayarla ve iki dosyayı 05'teki diff script'iyle karşılaştır.
★★★ FileSystem egress'ini S3 uyumlu bir depolamaya (yerelde bir MinIO container'ı) çevir; dosyaların yaşam döngüsü kuralıyla 1 gün sonra silindiğini ve API key'i olmayan bir istemcinin /operations sonuçlarını göremediğini doğrula.
Elimizde artık her şey var: metric, trace, gcdump, IDE araçları ve production'da otomatik toplama. Bir sonraki yazıda bunları tek bir vakada sırasıyla kullanıyoruz: checkout'un p99'u SLO'yu (Service Level Objective; servisin hedeflenen güvenilirlik seviyesi) yakarken dotnet-counters'tan başlayıp kök nedeni bulacak, BenchmarkDotNet ve k6 ile düzeltmeyi kanıtlayacağız.