Mert Özen Her Satırda Daha İleri
Backend Aspire ile Dağıtık Uygulamalar — #03

Service Discovery ve ServiceDefaults: "https+http://catalog" Nasıl Çözülür, Varsayılanlar Ne Yapar?

Mert Özen 14 Kas 2026 11 dk 10 görüntülenme
Service Discovery ve ServiceDefaults: "https+http://catalog" Nasıl Çözülür, Varsayılanlar Ne Yapar?

"https+http://catalog" adresi, Aspire'ın verdiği services__catalog__http__0 değişkeninden okunan Services:catalog configuration'ıyla çözülür; kayıt yoksa ad olduğu gibi DNS'e bırakılır, bu yüzden aynı kod production'da da çalışır. Yazı çözümleme log'larını, template'teki AllowedSchemes yorumunun AllowAllSchemes kapatılmadan işe yaramadığını, ServiceDefaults'un içini, durmuş bir service için 30 saniyelik beklemeyi 5 saniyeye indirmeyi ve YARP gateway'i gerçek çıktılarla gösteriyor.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Aspire 13.6, Microsoft.Extensions.ServiceDiscovery 10.11 ve Yarp.ReverseProxy 2.3 ile derlenip denenmiştir.

Önce Tahmin Et

ServiceDefaults template'inin Extensions.cs dosyasında yorum satırı hâlinde bir örnek var: "service discovery'de izin verilen scheme'leri kısıtlamak için şunu aç" diyor ve altında options.AllowedSchemes = ["https"]; satırı duruyor. Bu satırı açtın. Ordering, Catalog'u https+http://catalog adresiyle çağırıyor ve Catalog'un yalnız HTTP endpoint'i var. Çağrı ne olur: HTTP'ye düşüp çalışmaya devam mı eder, yoksa HTTPS bulamadığı için hata mı verir? Yazının ortasında iki varyasyonu da çalıştırıyoruz.

Ordering, Catalog'u Adıyla Çağırıyor

Sitedeki serilerin ortak örnek uygulaması Bookstore'a (bir online kitapçı) ikinci bir service ekliyoruz: Bookstore.Ordering.Api. Sipariş alırken her kalemin başlığını ve fiyatını Catalog'dan okuyor. Service'lerin nasıl bölüneceği ve sipariş akışının tasarımı .NET ile Mikroservis Uygulaması serisinin konusu; burada Ordering bilerek basit (siparişler bellekte duruyor), çünkü odak iki service'in birbirini nasıl bulduğu. Catalog'a giden typed client:

namespace Bookstore.Ordering.Api.Orders;

public sealed record CatalogBook(string Isbn, string Title, decimal Price, string Currency);

// Typed client; base address, service discovery'nin çözdüğü mantıksal bir ad
public sealed class CatalogClient(HttpClient http)
{
    public async Task<CatalogBook?> GetBookAsync(string isbn, CancellationToken ct)
    {
        using HttpResponseMessage response = await http.GetAsync($"/api/books/{isbn}", ct);
        if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return null;
        }

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<CatalogBook>(ct);
    }
}

Ve kaydı:

builder.AddServiceDefaults();

builder.Services.AddHttpClient<CatalogClient>(client => client.BaseAddress = new Uri("https+http://catalog"));

Kodda port yok, host yok. catalog, AppHost'taki resource adı. AppHost tarafında Ordering'e Catalog'u referans olarak veriyoruz ve önüne bir gateway koyuyoruz:

IResourceBuilder<ProjectResource> ordering = builder.AddProject<Projects.Bookstore_Ordering_Api>("ordering")
    .WithReference(catalog)
    .WaitFor(catalog)
    .WithHttpHealthCheck("/health");

IResourceBuilder<ProjectResource> gateway = builder.AddProject<Projects.Bookstore_Gateway>("gateway")
    .WithReference(catalog)
    .WithReference(ordering)
    .WithHttpHealthCheck("/health");

builder.AddJavaScriptApp("web", "../../web/bookstore-web", "start")
    .WithHttpEndpoint(env: "PORT")
    .WithExternalHttpEndpoints()
    .WithReference(gateway)
    .WaitFor(gateway);

Angular artık yalnız gateway'i tanıyor; proxy.conf.js GATEWAY_HTTPS/GATEWAY_HTTP'yi okuyor. Gateway'den bir sipariş:

curl -i -X POST http://localhost:5070/api/orders -H "Content-Type: application/json" \
  -d '{"customerEmail":"ada@example.com","items":[{"isbn":"9780441172719","quantity":1},{"isbn":"9780547928227","quantity":2}]}'

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /api/orders/01a1dab9-91ac-707a-9e65-24fd61280668

{"id":"01a1dab9-91ac-707a-9e65-24fd61280668","customerEmail":"ada@example.com","lines":[{"isbn":"9780441172719","title":"Dune","unitPrice":420.00,"quantity":1,"lineTotal":420.00},{"isbn":"9780547928227","title":"The Hobbit","unitPrice":340.00,"quantity":2,"lineTotal":680.00}],"total":1100.00,"currency":"TRY","placedAtUtc":"2026-11-14T15:42:36.972609+00:00"}

"https+http://catalog" Nasıl Çözülüyor?

Service discovery'nin kavramı (client-side ve server-side discovery, registry) Microservices serisinin Service Discovery yazısında; burada .NET'teki uygulamasına bakıyoruz. AddServiceDefaults() içindeki AddServiceDiscovery(), Microsoft.Extensions.ServiceDiscovery library'sini kaydediyor ve ConfigureHttpClientDefaults onu her HttpClient'ın pipeline'ına bir handler olarak ekliyor. İstek çıkarken handler, URI'deki (bir kaynağın adresini tanımlayan metin) host'u (catalog) gerçek bir adrese çeviriyor. Adresler nereden geliyor? WithReference(catalog)'un Ordering'e verdiği değişkenlerden:

services__catalog__http__0 = http://localhost:5080
CATALOG_HTTP = http://localhost:5080

.NET configuration'da çift alt çizgi : demek; yani bu değişken Services:catalog:http:0 anahtarı. Library'nin configuration provider'ı tam olarak bu yolu okuyor. Debug log'u açınca (Logging__LogLevel__Microsoft.Extensions.ServiceDiscovery=Debug) ilk istekte olanlar görünüyor:

Creating endpoint resolver for service 'https+http://catalog' with 2 providers: Configuration, Pass-through.
Resolving endpoints for service 'https+http://catalog'.
Using configuration from path 'Services:catalog:http' to resolve endpoint 'http' for service 'catalog'.
Endpoints configured for service 'catalog' from path 'Services:catalog:http': http://localhost:5080/.
Resolved 1 endpoints for service 'https+http://catalog': http://localhost:5080/ (Configuration).

İki provider var ve sırayla deneniyorlar. Configuration provider Services:catalog:<endpoint> altında adres arıyor. Bulamazsa pass-through provider devreye giriyor: adı olduğu gibi (catalog) DNS adı olarak bırakıyor. Scheme listesi https+http "önce HTTPS, yoksa HTTP" demek. Catalog'u http launch profile'ıyla çalıştırdığımız için yalnız Services:catalog:http var ve çözüm oraya düştü. https profilinde Aspire bir de services__catalog__https__0 verecek ve aynı kod, hiçbir değişiklik olmadan HTTPS'i seçecekti. Library'nin kuralı da net: bir HTTPS endpoint'i bulunursa HTTP endpoint'leri listeye hiç eklenmiyor.

Bir service'in birden fazla endpoint'i varsa adı alt çizgiyle veriyorsun: https+http://_admin.catalog, Catalog'un admin adlı endpoint'ine gidiyor. Alt çizgi, bunun bir subdomain değil endpoint adı olduğunu söylüyor.

Aynı Kod, Aspire Olmadan

Bu mekanizmanın Aspire'a bağımlı olmadığını görmek için Ordering'i AppHost dışında, elle verdiğimiz bir değişkenle çalıştırdık:

services__catalog__http__0=http://localhost:5080 dotnet run --project src/Bookstore.Ordering.Api --no-launch-profile

Sipariş yine 201 döndü, log'da yine (Configuration) yazıyordu. Aynı değeri appsettings.json'a da yazabilirsin; birden fazla adres verirsen her çözümlemede biri seçiliyor:

{
  "Services": {
    "catalog": {
      "http": [ "localhost:5080" ]
    }
  }
}

Production'da genellikle bu da gerekmiyor. Kubernetes'te catalog adında bir Service, Azure Container Apps'te catalog adında bir uygulama varsa platformun DNS'i adı zaten çözüyor; configuration'da kayıt olmadığında pass-through provider adı olduğu gibi bırakıyor ve istek catalog host'una gidiyor. Kodda "lokalde şu adres, production'da bu adres" diye bir dal yok.

AllowedSchemes Deneyi

Önce Tahmin Et'teki soruya dönelim. Ordering'i yine AppHost dışında çalıştırıp iki varyasyonu denedik. Birincisi template'teki yorumun aynısı:

builder.Services.Configure<ServiceDiscoveryOptions>(options =>
{
    options.AllowedSchemes = ["https"];
});
Resolved 1 endpoints for service 'https+http://catalog': http://localhost:5080/ (Configuration).

Hiçbir şey değişmedi; çağrı HTTP ile gitti, sipariş 201 döndü. Sebep ServiceDiscoveryOptions'taki ikinci property: AllowAllSchemes'ın varsayılan değeri true ve true olduğu sürece AllowedSchemes listesine bakılmıyor. İkinci varyasyonda onu da kapattık:

builder.Services.Configure<ServiceDiscoveryOptions>(options =>
{
    options.AllowAllSchemes = false;
    options.AllowedSchemes = ["https"];
});
Resolved 1 endpoints for service 'https+http://catalog': catalog (Pass-through).
System.Net.Http.HttpRequestException: ... (catalog:443)

Bu kez configuration provider HTTP adresini kullanmadı, HTTPS bulamayınca çözüm pass-through'a düştü ve istek catalog:443'e gitmeye çalışıp DNS hatası verdi. Yani kısıtlama gerçekten iki satırla çalışıyor. Template'teki yorum ise tek başına yanıltıcı; repo'daki ServiceDefaults'ta yorumu düzelttik ki bir gün açan kişi doğru kodu açsın. Production'da yalnız HTTPS'e izin vermek, internal trafiğin de TLS (trafiği şifreleyen protokol) ile gittiği ortamlarda anlamlı; Kubernetes'te internal trafik çoğu zaman düz HTTP olduğu için bu kararı ortamına göre ver.

.NET Olmayan Resource'lar ve Container Ağı

services__catalog__http__0 biçimi .NET configuration için tasarlanmış. Node.js, Python ya da Go tarafında okunması zahmetli olduğu için Aspire aynı adresi CATALOG_HTTP (ve HTTPS endpoint'i varsa CATALOG_HTTPS) olarak da veriyor: resource adı ve endpoint adı, büyük harfle. Angular'ın proxy.conf.js'i 01'den beri bunu kullanıyor; şimdi GATEWAY_HTTP'yi okuyor.

Adresin içeriği, onu kimin kullanacağına göre değişiyor. Host üzerinde çalışan process'ler için Catalog localhost:5080. Bir container'ın içinden ise localhost container'ın kendisi demek. Bunu görmek için AppHost'a geçici olarak Aspire'ın YARP (Yet Another Reverse Proxy; Microsoft'un .NET ile yazılmış reverse proxy library'si) container'ını ekleyip (aşağıda anlatıyoruz) environment variable'larına baktık:

CATALOG_HTTP = http://aspire.dev.internal:5080
services__catalog__http__0 = http://aspire.dev.internal:5080
OTEL_EXPORTER_OTLP_ENDPOINT = http://aspire.dev.internal:19010

Aspire, container'lara host'taki service'leri aspire.dev.internal adıyla gösteriyor; bu ad, Aspire'ın container ağına kurduğu tünel üzerinden host'a çıkıyor. Ters yön için de aynı mantık geçerli: bir container'ın endpoint'i host'taki process'e localhost:<port>, başka bir container'a container ağındaki adıyla veriliyor. AppHost API'sinde bu ayrımı NetworkIdentifier taşıyor (KnownNetworkIdentifiers.LocalhostNetwork, KnownNetworkIdentifiers.DefaultAspireContainerNetwork); GetEndpoint("http", networkIdentifier) ile bir endpoint'in belirli bir ağdan görünen adresini alabiliyorsun. WithReference bunu senin yerine yapıyor; elle bir adres birleştirmen gerektiğinde bu ayrımı hatırlaman yeterli.

ServiceDefaults'un İçi ve Kendi Varsayılanlarımız

01'de ServiceDefaults'u template'ten ekleyip içine bakmadan geçtik. AddServiceDefaults() dört iş yapıyor. ConfigureOpenTelemetry() log, metric ve trace'i açıyor: ASP.NET Core, HttpClient ve runtime instrumentation'ı, uygulamanın adıyla bir ActivitySource ve OTEL_EXPORTER_OTLP_ENDPOINT tanımlıysa OTLP (OpenTelemetry Protocol; telemetry verisini taşıyan standart protokol) exporter'ı. /health ve /alive istekleri trace'lere girmiyor, filtreyle dışarıda tutuluyor. AddDefaultHealthChecks() live etiketli bir "self" check'i ekliyor. AddServiceDiscovery() yukarıda gördüğümüz çözümlemeyi kaydediyor. Son olarak her HttpClient'a standard resilience handler'ı ekleniyor: rate limiter, toplam timeout, retry, circuit breaker ve deneme başına timeout. Pipeline'ın ayrıntısı External Service Integration ve Resilience serisinin Standard Pipeline yazısında.

MapDefaultEndpoints() iki endpoint map'liyor: /health (readiness: bütün check'ler geçmeli, trafik alabilir mi?) ve /alive (liveness: yalnız live etiketliler, process yeniden başlatılmalı mı?). İkisi de yalnız Development ortamında map'leniyor. Production'da açmak isteyeceksin, çünkü Kubernetes probe'ları ve load balancer'lar bunlara bakıyor, ama Aspire dokümanının uyarısı yerinde: dışarıya açık bir health endpoint'i, her istekte veritabanına ve Redis'e giden check'leri herkesin tetikleyebileceği bir kapı. Önerilen önlemler host filtreleme ya da authorization, kısa bir request timeout ve cevabın birkaç saniye output cache'te tutulması. Bunu yapmadan IsDevelopment() koşulunu silmek en sık yapılan hata.

Catalog Durunca Ordering Ne Kadar Bekliyor?

Template'in resilience ayarlarını değiştirmeden Catalog'u aspire resource catalog stop ile durdurup gateway üzerinden sipariş verdik:

curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" -X POST http://localhost:5070/api/orders ...
500 30.031173s

Otuz saniye. Ordering'in log'unda sebebi var: her deneme The operation didn't complete within the allowed timeout of '00:00:10' ile bitiyor, toplam süre de standard pipeline'ın 30 saniyelik üst sınırına takılıyor. Neden bağlantı hemen reddedilmedi? 01'de gördüğümüz DCP (Developer Control Plane; Aspire'ın lokal orchestration motoru) proxy'si yüzünden: Catalog durmuş olsa da 5080'i proxy dinliyor, bağlantıyı kabul ediyor ve cevap vermiyor. Doğrudan curl -m 5 http://localhost:5080/api/books da 5 saniye sonra cevapsız (000) kaldı. Production'da çöken bir pod genellikle bağlantıyı reddeder, ama ağır yük altında yavaşlayan bir service'te aynı tablo oluşur: çağıran taraf varsayılan sürelerle yarım dakika bekler, kullanıcı ise çoktan sayfayı kapatmıştır.

Bookstore'un service'leri birbirini çağırırken bu bütçe fazla. ServiceDefaults'u kendi varsayılanlarımızla güncelledik:

builder.Services.ConfigureHttpClientDefaults(http =>
{
    // Resilience varsayılan olarak açık; Bookstore service'leri arasındaki çağrılar çabuk vazgeçer
    http.AddStandardResilienceHandler(options =>
    {
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(5);
    });

    // Service discovery varsayılan olarak açık
    http.AddServiceDiscovery();
});

// Yalnız HTTPS'e izin vermek için AllowAllSchemes da kapatılmalı;
// AllowedSchemes'i tek başına ayarlamak bir şey değiştirmez.
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
// {
//     options.AllowAllSchemes = false;
//     options.AllowedSchemes = ["https"];
// });

Aynı denemede sonuç 500 5.065845s oldu: iki deneme 2'şer saniyede kesildi, toplam timeout 5 saniyede isteği bitirdi. ServiceDefaults bu tür "Bookstore'daki her service'te aynı olsun" kararlarının yeri. Bir service farklı bir bütçe istiyorsa kendi AddHttpClient kaydında üzerine yazabilir.

YARP ile Gateway: Proje mi, Resource mu?

Angular'ın her service'i ayrı ayrı bilmemesi için önüne bir gateway koyduk. YARP ile iki yol var. Biz Bookstore'da gateway'i küçük bir ASP.NET Core projesi olarak yazdık:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
    .AddServiceDiscoveryDestinationResolver();

WebApplication app = builder.Build();

app.MapDefaultEndpoints();
app.MapReverseProxy();

app.Run();
"ReverseProxy": {
  "Routes": {
    "books": { "ClusterId": "catalog", "Match": { "Path": "/api/books/{**rest}" } },
    "orders": { "ClusterId": "ordering", "Match": { "Path": "/api/orders/{**rest}" } }
  },
  "Clusters": {
    "catalog": { "Destinations": { "catalog": { "Address": "https+http://catalog" } } },
    "ordering": { "Destinations": { "ordering": { "Address": "https+http://ordering" } } }
  }
}

Destination adreslerinde de aynı https+http:// biçimi var. YARP kendi HttpClient'ını kullandığı için yukarıdaki handler oraya girmiyor; adresleri Microsoft.Extensions.ServiceDiscovery.Yarp paketindeki AddServiceDiscoveryDestinationResolver() çözüyor. Gateway bir proje olduğu için ileride authentication, rate limiting ya da response birleştirme gibi kodlar buraya ekleniyor; YARP'ın kendisi API Gateway ve YARP serisinin konusu.

İkinci yol Aspire'ın YARP hosting integration'ı (Aspire.Hosting.Yarp). Gateway bir container olarak çalışıyor ve route'lar AppHost'ta kodla yazılıyor. Bir kopyada denedik:

builder.AddYarp("edge")
    .WithConfiguration(yarp =>
    {
        yarp.AddRoute("/api/books/{**catch-all}", catalog);
        yarp.AddRoute("/api/orders/{**catch-all}", ordering);
    });

Container mcr.microsoft.com/dotnet/nightly/yarp:2.3-preview image'ıyla açıldı ve /api/books/9780441172719 isteğini 200 ile Catalog'a iletti. AppHost'taki route'lar container'a environment variable olarak gidiyor:

REVERSEPROXY__ROUTES__route0__MATCH__PATH = /api/books/{**catch-all}
REVERSEPROXY__ROUTES__route0__CLUSTERID = cluster_catalog
REVERSEPROXY__CLUSTERS__cluster_catalog__DESTINATIONS__destination1__ADDRESS = http://catalog
services__catalog__http__0 = http://aspire.dev.internal:5080

Integration'ın Angular için iki yardımcısı daha var: WithStaticFiles(path) run modunda bir klasörü container'ın /wwwroot'una bağlıyor, PublishWithStaticFiles(web) ise publish sırasında Angular'ın build çıktısını gateway image'ına kopyalayan bir Dockerfile üretiyor. Böylece production'da Angular'ı ayrı bir web sunucusu olmadan aynı gateway sunuyor; bunu 07'de publish çıktısıyla göreceğiz. Hangisini seçmeli? Gateway'de yalnız route varsa resource daha az kod. Gateway'e kendi kodunu yazacaksan (ya da image'ın nightly kanalından gelmesi seni rahatsız ediyorsa) proje daha esnek.

Bir Service'in Port'u Değişince

Planın son maddesini de denedik: Catalog'un launch profile'ındaki port'u 5080'den 5090'a çektik ve AppHost'u yeniden başlattık. Başka hiçbir dosyaya dokunmadık.

order via gateway: 201
books via gateway: 200

CATALOG_HTTP = http://localhost:5090
services__catalog__http__0 = http://localhost:5090

Ordering ve gateway yeni adresi environment variable'dan aldı. Kodda, appsettings.json'da ya da Angular'da Catalog'un port'u hiç geçmediği için değişecek bir şey yoktu. Denemeden sonra port'u 5080'e geri aldık.

Üretimde Dikkat

Lokalde service discovery'yi Aspire'ın environment variable'ları besliyor; production'da kimin beslediğine baştan karar ver. Kubernetes ve Azure Container Apps'te platform DNS'i ve pass-through yeterli. Birden fazla instance'ı client tarafında dağıtmak istiyorsan configuration'a birden fazla adres yazabilir ya da Kubernetes için DNS SRV (bir service'in host ve port'unu veren DNS kaydı) provider'ını (AddDnsSrvServiceEndpointProvider) kullanabilirsin.

Health endpoint'lerini production'da bilinçli aç. IsDevelopment() koşulunu kaldırırken host filtreleme ya da authorization, kısa timeout ve kısa süreli output cache ekle; /alive'a dış bağımlılık check'i koyma, yoksa Redis'teki bir sorun bütün pod'ların yeniden başlatılmasına dönüşür.

Resilience bütçesini çağrı zincirine göre ayarla. Gateway → Ordering → Catalog zincirinde her halka 30 saniye beklerse en dıştaki istemci çok daha önce vazgeçmiş olur. İç çağrıların toplam timeout'u, onları çağıran isteğin timeout'undan kısa olmalı.

Sık Düşülen Tuzaklar

WithReference'ı unutmak. Configuration'da kayıt olmayınca pass-through devreye giriyor ve hata "service discovery çalışmıyor" değil, catalog adında bir host bulunamadığını söyleyen bir DNS hatası olarak görünüyor. Böyle bir hata görünce önce AppHost'taki referansa bak.

Template'teki AllowedSchemes yorumuna güvenmek. Yukarıda gördüğümüz gibi AllowAllSchemes = false olmadan kısıtlama çalışmıyor ve hata da vermiyor.

Durdurulmuş bir resource'un port'unu kapalı sanmak. DCP proxy'si port'u dinlemeye devam ediyor; bağlantı kuruluyor ama cevap gelmiyor. Lokal denemelerde "connection refused" yerine timeout görmen bu yüzden.

Session ömürlü bir container'a sabit ad verip AppHost'u hemen yeniden başlatmak. WithContainerName("aspire-redis") ile aspire stop ve hemen ardından aspire start dediğimizde, eski container silinmeden yenisi oluşturulmaya çalışıldı: Conflict. The container name "/aspire-redis" is already in use ve Redis "Exited" durumunda kaldı. Birkaç saniye beklemek ya da aspire resource redis start ile yeniden başlatmak yetiyor; sabit ada ihtiyacın yoksa hiç vermemek daha kolay.

Gateway'de route'u eksik yazmak. /api/books/{**rest} hem /api/books'u hem /api/books/9780441172719'u yakalıyor (ikisi de 200 döndü); /api/books/{isbn} gibi tek segmentlik bir route yazarsan liste isteği gateway'de 404 olur.

Küçük Bir Deneme

★☆☆ Ordering'in log seviyesini Microsoft.Extensions.ServiceDiscovery=Debug yapıp iki sipariş ver. İkinci siparişte "Resolving endpoints" satırı tekrar görünüyor mu? Çözümlenen adresler nerede, ne kadar süre tutuluyor olabilir?

★★☆ AppHost'u https launch profile'ıyla çalıştır (önce aspire certs trust). Ordering'e gelen değişkenlerde services__catalog__https__0 var mı? Debug log'unda çözüm hangi path'ten geliyor?

★★★ Catalog'a WithHttpEndpoint(port: 5081, name: "admin") ile ikinci bir endpoint ekle ve Ordering'de https+http://_admin.catalog adresine giden ikinci bir typed client yaz. AppHost'un Ordering'e verdiği değişkenlerde admin endpoint'i nasıl görünüyor?

Artık iki service, bir gateway, bir veritabanı ve bir cache var; bir isteğin bunların arasında nasıl dolaştığını görmenin zamanı geldi. Bir sonraki yazıda dashboard'a geçiyoruz: structured log'lar, trace waterfall'u, metric'ler, CLI'dan aspire otel sorguları, 13.6 ile gelen kalıcı telemetry ve dashboard'u Aspire olmadan, tek başına kullanmak.