RO EN

Observability (3) — distributed tracing cu OpenTelemetry

Observability (3) — distributed tracing cu OpenTelemetry ✨ Imagine generată cu AI
Doru Bulubașa
07 august 2026
27 vizualizări

A treia parte din seria despre observabilitate. Log-urile din partea a doua au context, dar rămân insulare per serviciu. Distributed tracing le leagă: același trace id, de la request-ul HTTP până în worker-ul care consumă mesajul.


De ce OpenTelemetry și nu SDK-ul clasic

Application Insights are un SDK clasic de instrumentare, dar direcția oficială Microsoft e OpenTelemetry: standardul deschis pentru trace-uri, metrici și log-uri. Motivele practice:

  • Instrumentezi o singură dată — schimbi backend-ul (Application Insights azi, altceva mâine) schimbând exporter-ul, nu codul
  • Instrumentări automate mature — ASP.NET Core, HttpClient, SDK-urile Azure (Cosmos, Service Bus, Blob) emit span-uri fără cod din partea ta
  • Baza .NET e deja OTelActivity și ActivitySource din runtime sunt primitivele OpenTelemetry; standardul e nativ, nu adăugat
dotnet add package Azure.Monitor.OpenTelemetry.AspNetCore

ASP.NET Core: configurarea completă

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .UseAzureMonitor(options =>
    {
        // Connection string din configurare (App Settings / Key Vault)
        options.ConnectionString =
            builder.Configuration["ApplicationInsights:ConnectionString"];
    })
    .WithTracing(tracing => tracing
        .AddSource("ChatApi.*")          // sursele custom (mai jos)
        .SetSampler(new ParentBasedSampler(
            new TraceIdRatioBasedSampler(0.25))))  // 25% la volum mare
    .WithMetrics(metrics => metrics
        .AddMeter("ChatApi.*"));

UseAzureMonitor activează automat instrumentarea pentru ASP.NET Core (fiecare request devine span), HttpClient (fiecare apel extern devine span de dependency) și SDK-urile Azure. Fără nicio linie suplimentară, apelurile către Cosmos DB și Service Bus apar deja în trace-uri, cu durata lor.

Sampler-ul merită atenție: ParentBasedSampler respectă decizia părintelui — dacă request-ul inițial a fost eșantionat, toate span-urile lui din toate serviciile sunt păstrate. Trace-uri complete sau deloc, niciodată găurite.


Azure Functions Isolated Worker

Isolated Worker cere pachetul dedicat și configurarea în HostBuilder:

dotnet add package Microsoft.Azure.Functions.Worker.OpenTelemetry
dotnet add package Azure.Monitor.OpenTelemetry.Exporter
// Program.cs (Functions Isolated Worker)
var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =>
    {
        services.AddOpenTelemetry()
            .UseFunctionsWorkerDefaults()   // instrumentarea runtime-ului Functions
            .UseAzureMonitorExporter();
    })
    .Build();

await host.RunAsync();

Un detaliu de configurare: în Application Settings al Function App-ului, dezactivează colectarea duplicată a host-ului clasic dacă migrezi de pe SDK-ul vechi — altfel vezi fiecare invocație de două ori.


Propagarea prin Service Bus: firul care nu se rupe

Partea cu adevărat valoroasă: trace-ul traversează mesajele. SDK-ul Service Bus propagă automat contextul W3C (traceparent) în proprietățile mesajului, iar consumatorul îl reia. API-ul care publică și worker-ul care consumă apar în același trace, deși rulează în procese diferite, minute mai târziu.

Condiția: consumatorul să pornească span-ul ca link sau copil al contextului din mesaj. Cu ServiceBusProcessor și instrumentarea automată activă, asta se întâmplă implicit. Pentru procesare manuală:

private async Task OnMessageAsync(ProcessMessageEventArgs args)
{
    // Contextul de trace vine din proprietatile mesajului (Diagnostic-Id)
    // Cu instrumentarea Azure SDK activa, span-ul de procesare
    // e legat automat de trace-ul publicarii.

    using var activity = ChatApiActivitySource.Instance
        .StartActivity("ProcessOrderMessage", ActivityKind.Consumer);

    activity?.SetTag("messaging.message_id", args.Message.MessageId);
    activity?.SetTag("tenant.id",
        args.Message.ApplicationProperties["TenantId"]?.ToString());

    await handler.HandleAsync(/* ... */);
}

Rezultatul în Application Insights (Transaction search / End-to-end view): un singur arbore — request HTTP → publicare mesaj → (gap-ul de așteptare în coadă, vizibil!) → procesare în worker → apeluri Cosmos din worker. Gap-ul din coadă e diagnosticul gratuit: latența „misterioasă” e adesea timp de așteptare, nu procesare.


Span-uri custom cu ActivitySource

Instrumentarea automată acoperă infrastructura. Pentru operațiile de business (căutarea în cache-ul semantic, apelul de embeddings), definești surse proprii:

public static class ChatApiActivitySource
{
    // Numele trebuie sa corespunda cu AddSource("ChatApi.*") din config
    public static readonly ActivitySource Instance = new("ChatApi.Core");
}

public class SemanticCacheService
{
    public async Task<CachedAnswer?> TryGetAsync(string query, string tenantId)
    {
        using var activity = ChatApiActivitySource.Instance
            .StartActivity("SemanticCache.Lookup");

        activity?.SetTag("tenant.id", tenantId);

        var embedding = await _embeddings.GetEmbeddingAsync(query);
        var match = await _cacheRepository.FindSimilarAsync(embedding, tenantId);

        // Rezultatul devine atribut -- interogabil in KQL
        activity?.SetTag("cache.hit", match is not null);
        activity?.SetTag("cache.similarity", match?.Similarity ?? 0);

        return match;
    }
}

Acum fiecare trace de chat arată explicit: lookup-ul în cache (hit sau miss, cu similaritatea), apoi — doar la miss — apelul scump către OpenAI. Rata de cache hit devine vizibilă direct din trace-uri, per tenant.

Două reguli de igienă: numele span-ului e operația („SemanticCache.Lookup”), nu valorile („Lookup query X” ar exploda cardinalitatea); valorile merg în tag-uri. Și excepțiile: activity?.SetStatus(ActivityStatusCode.Error) înainte de re-throw, ca span-ul să apară roșu în vizualizare.


Ce urmează

Trace-urile arată drumul; în partea a patra măsurăm sistematic: custom metrics cu Meter (rata de cache hit, mesaje per tenant, RU consumate), dashboards în Azure și alertele care contează.

Întrebări? Scrie-mi la contact@ludoprogramming.com.