Instrumentazione basica passo-passo in .NET con OpenTelemetry

Alessandro Mengoli

In questo articolo vedremo come aggiungere il supporto a OpenTelemetry in un’applicazione .NET per implementare tracciamento distribuito, metriche e logging. Partiremo da zero fino ad avere un’applicazione con telemetria di base configurata.

Per approfondire cos’è OpenTelemetry, i suoi componenti e perché utilizzarlo, puoi leggere l’articolo introduttivo su OpenTelemetry.

Il codice sorgente con un’esempio completo lo potete trovare nel repository GitHub.

Creazione del Progetto

Come esempio andremo a fare alcune semplici API utilizzando le minimal API in dotnet 8. Per iniziare, creiamo un nuovo progetto web:

dotnet new web 

Setup Iniziale

Prima di tutto, installiamo il pacchetto necessario per OpenTelemetry:

dotnet add package OpenTelemetry.Extensions.Hosting

Apriamo il Program.cs e aggiungiamo OpenTelemetry ai servizi:

builder.Services.AddOpenTelemetry();

Componenti Fondamentali di OpenTelemetry

Prima di procedere con l’implementazione, è importante comprendere due componenti chiave di OpenTelemetry: Resource e SDK Configuration.

Resource

Una Resource in OpenTelemetry è un set di attributi che definisce l’ambiente l’ambiente nel quale viene raccolta la telemetria. Nel nostro caso, è la nostra applicazione .NET.

SDK Configuration

L’SDK di OpenTelemetry è il componente centrale che gestisce la raccolta dei dati telemetrici…

👉 Vuoi approfondire l'architettura di OpenTelemetry e capire cos'è l'SDK? Leggi l'articolo OpenTelemetry dall'interno: Componenti e Funzionamento.

Implementazione del Tracciamento

Per iniziare a tracciare le operazioni della nostra applicazione, installiamo l’exporter di base:

dotnet add package OpenTelemetry.Exporter.Console

Configuriamo il tracciamento con l’output su console, utile in fase demo per vedere i primi dati:

builder.Services.AddOpenTelemetry()
   .WithTracing(trace => trace
      .AddConsoleExporter());

Nel corso dell’articolo andremo a cambiare l’output per essere pronti ad utilizzarlo sia per sviluppo che per produzione.

Aggiunta dell’Instrumentazione ASP.NET Core

Per ottenere dati significativi, aggiungiamo l’instrumentazione di ASP.NET Core:

builder.Services.AddOpenTelemetry()
   .WithTracing(trace => trace
   .AddAspNetCoreInstrumentation()
      .AddConsoleExporter();

Configurazione delle Risorse

È importante identificare correttamente il servizio e la sua versione:

var serviceName = builder.Configuration["ServiceName"] ?? "MyService";
var serviceVersion = builder.Configuration["ServiceVersion"] ?? "1.0.0"; // only for demo purposes

builder.Services.AddOpenTelemetry()
   .ConfigureResource(resource => resource
        .AddService(serviceName, serviceVersion)
        .AddAttributes(new Dictionary<string, object>
        {
            ["deployment.environment"] = builder.Environment.EnvironmentName,
            ["host.name"] = Environment.MachineName,
            ["service.instance.id"] = Guid.NewGuid().ToString()
        }))
   .WithTracing(/* configurazione precedente */);

Implementazione delle Metriche

Aggiungiamo il supporto per le metriche utilizzando la stessa instrumentazione ASP.NET Core:

builder.Services.AddOpenTelemetry()
   .ConfigureResource(/* configurazione precedente */)
   .WithTracing(/* configurazione precedente */)
   .WithMetrics(metrics => metrics
      .AddAspNetCoreInstrumentation()
      .AddConsoleExporter());

Comprendere le Metriche

L’instrumentazione ASP.NET Core fornisce automaticamente diverse metriche utili. Analizziamo le più importanti:

http.server.request.duration

Questa metrica fornisce statistiche dettagliate sulla durata delle richieste HTTP:

Metric Name: http.server.request.duration
Unit: s
Value: Sum: 0,23764910 Count: 8 Min: 0,001442 Max: 0,183147

I bucket dell’istogramma mostrano la distribuzione delle richieste per durata:

  • (-Infinity,0,005]: 3 richieste molto veloci (< 5ms)
  • (0,005,0,01]: 3 richieste veloci (5-10ms)
  • (0,01,0,025]: 1 richiesta media (10-25ms)
  • (0,1,0,25]: 1 richiesta lenta (100-250ms)

Questa distribuzione aiuta a identificare outlier e problemi di performance.

aspnetcore.routing.match_attempts

Questa metrica traccia l’efficienza del routing:

Metric Name: aspnetcore.routing.match_attempts
Unit: {match_attempt}
Value: 8

Gli attributi chiave includono:

  • aspnetcore.routing.is_fallback: indica se è stato usato il fallback routing
  • aspnetcore.routing.match_status: risultato del matching (“success” o “failure”)

Un numero elevato di tentativi o fallimenti potrebbe indicare problemi nella configurazione delle route.

Implementazione dei Log

OpenTelemetry si integra perfettamente con il sistema di logging di .NET. Per ottenere il massimo beneficio, è importante utilizzare log strutturati che permettono di aggiungere contesto e metadati ai log in modo standardizzato.

var builder = WebApplication.CreateBuilder(args);

var serviceName = builder.Configuration["ServiceName"] ?? "MyService"; 
var serviceVersion = builder.Configuration["ServiceVersion"] ?? "1.0.0"; // only for demo purposes

builder.Services.AddOpenTelemetry()
   .ConfigureResource(/* configurazione precedente */)
   .WithTracing(/* configurazione precedente */)
   .WithMetrics(/* configurazione precedente */)
   .WithLogging(logger => logger
      .AddConsoleExporter());

var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.Run();

Esempio completo

Gli exporter sono i componenti che inviano i dati telemetrici a sistemi esterni. In questo esempio utilizziamo sia il Console Exporter (utile in fase di sviluppo) che l’OTLP Exporter (per ambienti di produzione). L’OTLP Exporter permette di inviare i dati a qualsiasi sistema compatibile con il protocollo OpenTelemetry, come Azure Monitor, Datadog, New Relic o un OpenTelemetry Collector. Utilizzando l’OTLP Exporter, il codice rimane indipendente dal vendor di destinazione, permettendo di cambiare il backend di monitoring senza modificare l’applicazione. L’OTLP Exporter supporta due protocolli di trasporto: gRPC (più efficiente e consigliato per la produzione) e HTTP/Protobuf (utile quando gRPC non è disponibile).

Installiamo il pacchetto necessario:

dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

Vediamo ora un esempio completo di configurazione che include tracciamento, metriche, logging e l’OTLP exporter:

var builder = WebApplication.CreateBuilder(args);

var serviceName = builder.Configuration["ServiceName"] ?? "MyService"; 
var serviceVersion = builder.Configuration["ServiceVersion"] ?? "1.0.0"; // only for demo purposes
builder.Services.AddOpenTelemetry()
   .ConfigureResource(resource => resource
        .AddService(serviceName, serviceVersion)
        .AddAttributes(new Dictionary<string, object>
        {
           ["deployment.environment"] = builder.Environment.EnvironmentName,
           ["host.name"] = Environment.MachineName,
           ["service.instance.id"] = Guid.NewGuid().ToString()
        }))
   .WithTracing(trace => trace
      .AddAspNetCoreInstrumentation()
      .AddConsoleExporter()
      .AddOtlpExporter(exporter =>
      {
         exporter.Endpoint = new Uri("http://localhost:4317"); // only for demo purposes
         exporter.Protocol = OtlpExportProtocol.Grpc;
      }))
   .WithMetrics(metrics => metrics
      .AddAspNetCoreInstrumentation()
      .AddConsoleExporter()
      .AddOtlpExporter(exporter =>
      {
         exporter.Endpoint = new Uri("http://localhost:4317"); // only for demo purposes
         exporter.Protocol = OtlpExportProtocol.Grpc;
      }))
   .WithLogging(logger => logger
      .AddConsoleExporter()
      .AddOtlpExporter(exporter =>
      {
         exporter.Endpoint = new Uri("http://localhost:4317"); // only for demo purposes
         exporter.Protocol = OtlpExportProtocol.Grpc;
      }));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/orders/{orderId}", async (string orderId, ILogger<Program> logger) =>
{
   try
   {
      logger.LogInformation("Processing order request for {OrderId}", orderId);
      await Task.Delay(100); // simulate processing
      return Results.Ok(new { OrderId = orderId, Status = "Processed" });
   }
   catch (Exception ex)
   {
      logger.LogError(ex, "Failed to process order {OrderId}", orderId);
      return Results.StatusCode(500);
   }
});
app.Run();

Conclusioni

Con la configurazione implementata, la nostra applicazione ora genera telemetria completa che include:

  • Tracce: informazioni dettagliate su ogni richiesta HTTP, inclusi tempi di risposta e metadati
  • Metriche: statistiche sulle prestazioni dell’applicazione, come durata delle richieste e tentativi di routing
  • Log: log strutturati con context tracking e correlazione automatica con le tracce

OpenTelemetry in .NET richiede pochi passaggi e offre un’infrastruttura robusta per il monitoraggio delle applicazioni. L’utilizzo dell’OTLP Exporter ci permette di essere indipendenti dal vendor di monitoring scelto, facilitando future migrazioni o integrazioni con nuovi strumenti.

Questa serie continua con altri articoli che approfondiscono aspetti avanzati come il tracciamento distribuito, baggage, log strutturati e metriche personalizzate. Potete trovare il codice completo con tutti gli esempi sul repository GitHub.

#tutorial
#opentelemetry
#dotnet
#observability