Questo tutorial mostra il valore pratico del distributed tracing attraverso tre scenari di debug reali. Per chi non ha familiarità con i concetti base di OTel (spans, traces, context propagation), si consiglia prima la lettura di Introduzione a OpenTelemetry.

Struttura dell’articolo:

  1. Quick Start - avvia la demo
  2. Tre scenari di debug - silent failure, latency spike, fan-out
  3. Quando NON usare OTel - limiti e alternative
  4. Appendice - setup dettagliato (opzionale, per chi vuole replicare)

Architettura Demo

Useremo MockMart, un e-commerce con architettura a microservizi:

Gateway (nginx:80)
├── Shop UI (:3000)
├── Shop API (:3001)
│   ├── Inventory Service (:3011)
│   ├── Payment Service (:3010)
│   └── Notification Service (:3009)
├── Keycloak (:8080)
└── Grafana LGTM (:3005)

Flusso checkout (fan-out pattern):

POST /api/checkout
├─ Inventory Service  → verifica disponibilità
├─ Payment Service    → processa pagamento
├─ Inventory Service  → riserva prodotti
├─ PostgreSQL         → salva ordine
└─ Notification       → invia conferma

Stack tecnico:

  • Shop API: Node.js, Express, pg driver
  • Payment Service: Node.js, simulazione gateway pagamento
  • Inventory Service: Node.js, gestione stock in-memory
  • Notification Service: Node.js con template rendering
  • Database: PostgreSQL
  • Autenticazione: Keycloak (OAuth2/OIDC)

Quick Start

# Clone e avvia
git clone https://github.com/monte97/MockMart
cd MockMart
make up

# Verifica
docker ps  # tutti i container healthy
open http://localhost:3005  # Grafana

# Scenari demo
./scripts/scenario-1-silent-failure.sh   # Silent failure
./scripts/scenario-2-latency-spike.sh    # Latency spike
./scripts/scenario-3-fanout-debug.sh     # Fan-out debug (complex)

# Esplora in Grafana
# Explore → Tempo → Query TraceQL

Nota: La demo include già OpenTelemetry configurato. Per i dettagli di setup, vedi l’Appendice.


Scenario 1: Silent Failure

Il Problema

L’utente completa il checkout, riceve conferma (“Ordine completato!”), ma non riceve mai la notifica email.

Dalla prospettiva API: 200 OK restituito al client. Dalla prospettiva utente: Nessuna email.

Questo è un silent failure: il sistema sembra funzionare ma ha un problema non visibile.

Riproduzione

# Clona la demo
git clone https://github.com/monte97/MockMart
cd MockMart

# Avvia lo stack
make up

# Esegui lo scenario (simula email validation failure)
./scripts/scenario-1-silent-failure.sh

Lo script:

  1. Autentica un utente via Keycloak
  2. Configura il notification service per simulare un errore di validazione email
  3. Esegue un checkout
  4. Mostra che l’ordine viene salvato ma la notifica fallisce

Debug con OpenTelemetry

Step 1: Apri Grafana → Explore → Tempo

Query:

{ resource.service.name = "shop-api" }

Con l’order ID (mostrato dallo script):

{ resource.service.name = "shop-api" && span.order_id = <ID> }
Query TraceQL per trovare la trace dell'ordine

Step 2: Esamina la trace

POST /api/checkout (320ms) [STATUS: OK]
├─ pg.query SELECT user (45ms) [OK]
├─ pg.query INSERT INTO orders (180ms) [OK]
└─ HTTP POST /api/notifications/order (45ms) [ERROR: 400]
Waterfall della trace - silent failure visibile sullo span di notification

Lo span della chiamata a notification service mostra:

  • http.status_code: 400
  • error: true
Attributi di errore sullo span del notification service

Step 3: Click “View Logs” sullo span con errore

{"msg":"Received order notification request","orderId":8472,"userId":"abc-123","userEmail":"mario.rossi@example.com","traceId":"...","spanId":"..."}
{"msg":"Validating email address...","orderId":8472,"userEmail":"mario.rossi@example.com","traceId":"...","spanId":"..."}
{"msg":"Cannot send notification - invalid email address","orderId":8472,"userEmail":"mario.rossi@example.com","reason":"Email validation failed: invalid domain or format","traceId":"...","spanId":"..."}
Log correlati del notification service visibili dalla trace

Root cause identificato: Il notification service ha ricevuto la richiesta, ha tentato di validare l’email, e ha risposto 400 perché la validazione è fallita (scenario simulato).

Cosa Ci Mostra Questo Scenario

  1. Visibilità cross-service: Vediamo che la chiamata è stata effettuata e ha ricevuto risposta
  2. Error attribution: Sappiamo quale servizio ha causato l’errore
  3. Correlazione automatica: Log del notification service visibili dal contesto della trace
  4. Senza OTel: Avremmo dovuto grep su più file log, correlare timestamp manualmente, e sperare di trovare la causa

Scenario 2: Latency Spike

Il Problema

Report utenti:

  • “Il checkout è lentissimo, 3-4 secondi!” (Luigi)
  • “A me va veloce, 200ms” (Mario)

Problema intermittente, non immediatamente chiaro come riprodurre.

Riproduzione

./scripts/scenario-2-latency-spike.sh

Lo script:

  1. Autentica due utenti (Mario e Luigi)
  2. Configura il notification service per usare un template “premium” lento per Luigi
  3. Esegue checkout per entrambi gli utenti
  4. Mostra la differenza di timing

Nota sulla simulazione: Il template “premium” simula un’operazione lunga con un delay artificiale. In un sistema reale potrebbe essere: rendering PDF complesso, chiamate a servizi esterni, query database non ottimizzate, etc. La simulazione serve a creare uno scenario riproducibile per imparare il flusso di debug.

Debug con OpenTelemetry

Step 1: Query trace lente

{ resource.service.name = "shop-api" && duration > 2s }
Query TraceQL per trace lente

Step 2: Confronta trace

Trace Luigi (~3100ms):

POST /api/checkout (3100ms)
├─ pg-pool.connect (3ms)
├─ pg.query:INSERT orders (2ms)
└─ POST /api/notifications/order (3050ms) ← bottleneck
Trace di Luigi - bottleneck nel notification service

Trace Mario (~150ms):

POST /api/checkout (150ms)
├─ pg-pool.connect (3ms)
├─ pg.query:INSERT orders (2ms)
└─ POST /api/notifications/order (50ms)
Trace di Mario - checkout veloce

Pattern: Database sempre veloce. La differenza è nel notification service.

Step 3: Drill-down nei log

Click “View Logs” sulla trace di Luigi:

{"msg":"Received order notification request","orderId":20,"userId":"luigi-uuid","userEmail":"luigi.verdi@example.com","traceId":"..."}
{"msg":"Processing order notification","orderId":20,"template":"order_confirmation_premium","traceId":"..."}
{"msg":"Rendering premium template...","orderId":20,"template":"order_confirmation_premium","traceId":"..."}
{"msg":"Template rendering took long","orderId":20,"renderTimeMs":3000,"traceId":"..."}
{"msg":"Email notification sent","orderId":20,"userEmail":"luigi.verdi@example.com","traceId":"..."}
Log di Luigi con template premium lento

Click “View Logs” sulla trace di Mario:

{"msg":"Received order notification request","orderId":17,"userId":"mario-uuid","userEmail":"mario.rossi@example.com","traceId":"..."}
{"msg":"Processing order notification","orderId":17,"template":"order_confirmation_basic","traceId":"..."}
{"msg":"Email notification sent","orderId":17,"userEmail":"mario.rossi@example.com","traceId":"..."}
Log di Mario con template basic

Root cause: Il template order_confirmation_premium richiede ~3 secondi di rendering vs ~50ms del order_confirmation_basic.

Metriche Derivate (Service Graph)

Per analisi aggregate, LGTM genera metriche dalle trace. In Grafana → Explore → Prometheus (modalità Code):

histogram_quantile(0.95,
  rate(traces_service_graph_request_server_seconds_bucket{server="shop-api"}[5m])
)

Nota: Queste metriche sono derivate dal service graph di Tempo, non metriche SDK esplicite.


Scenario 3: Fan-out Debug

Il Problema

Questo scenario dimostra il vero valore del distributed tracing: debugging di un sistema con pattern fan-out dove una singola richiesta attraversa più servizi in parallelo.

Ticket di supporto:

“Il checkout è lentissimo, ci mette più di 3 secondi!”

La sfida: Il checkout chiama 4 servizi diversi. Quale causa il rallentamento?

POST /api/checkout (3200ms totali)
├─ Inventory check    (??ms)
├─ Payment process    (??ms)
├─ Inventory reserve  (??ms)
├─ DB save            (??ms)
└─ Notification       (??ms)

Riproduzione

# Esegui lo scenario che testa ogni servizio singolarmente
./scripts/scenario-3-fanout-debug.sh

Output dello script:

3️⃣  Baseline checkout (all services normal)...
   ⏱️  Baseline: 206ms
   🔗 Trace: 3d9e2441545eb8c0c1050b51daa4489f

4️⃣  Checkout with SLOW PAYMENT SERVICE (2s delay)...
   ⏱️  With slow payment: 2117ms
   🔗 Trace: ae1799f3ed5836602288d4c39ea156a2

5️⃣  Checkout with SLOW INVENTORY SERVICE (1.5s delay)...
   ⏱️  With slow inventory: 1678ms
   🔗 Trace: 6325d443f6aa4c6f1ab1d6f7c31c0c5e

6️⃣  Checkout with SLOW NOTIFICATION SERVICE (3s delay)...
   ⏱️  With slow notification: 3138ms
   🔗 Trace: e43750f867a144eec9396095c39bb6da

📊 Results Summary:
   Scenario           | Duration | Trace ID
   -------------------|----------|----------------------------------
   Baseline (normal)  | 206ms    | 3d9e2441545eb8c0c1050b51daa4489f
   + Slow Payment     | 2117ms   | ae1799f3ed5836602288d4c39ea156a2
   + Slow Inventory   | 1678ms   | 6325d443f6aa4c6f1ab1d6f7c31c0c5e
   + Slow Notification| 3138ms   | e43750f867a144eec9396095c39bb6da

Lo script fornisce il Trace ID per ogni scenario, permettendo di aprire direttamente la traccia in Grafana.

Approccio Tradizionale

# Log dell'API
docker logs shop-api | grep "checkout"
# [10:45:23] INFO Processing checkout
# [10:45:26] INFO Order saved  ← 3 secondi dopo, ma DOVE?

# Log di ogni servizio
docker logs payment-service | grep "order"
# [10:45:23] INFO Payment processed  ← sembra veloce

docker logs inventory-service | grep "order"
# [10:45:23] INFO Stock checked
# [10:45:24] INFO Stock reserved  ← 1 secondo?

docker logs notification-service | grep "order"
# [10:45:24] INFO Notification sent  ← sembra veloce

Problema: I timestamp sono su macchine diverse, non sincronizzati. Non è possibile sommare i tempi né determinare quali chiamate sono sequenziali e quali parallele.

Debug con Distributed Tracing

Grafana → Explore → Tempo

Usare il Trace ID dall’output dello script per cercare la traccia direttamente:

ae1799f3ed5836602288d4c39ea156a2

Oppure cercare tutte le tracce lente:

{ resource.service.name = "shop-api" && duration > 2s }

Waterfall della trace “Slow Payment” (2117ms):

POST /api/checkout (2117ms) [shop-api]
├─ POST /api/inventory/check (4ms) [inventory-service]
├─ POST /api/payments/process (2020ms) [payment-service] ← BOTTLENECK!
├─ POST /api/inventory/reserve (4ms) [inventory-service]
├─ pg.query INSERT orders (12ms) [shop-api]
└─ POST /api/notifications/order (55ms) [notification-service]
Waterfall trace Slow Payment - bottleneck nel payment service

Confronto con trace Baseline (~200ms):

POST /api/checkout (206ms) [shop-api]
├─ POST /api/inventory/check (5ms) [inventory-service]
├─ POST /api/payments/process (85ms) [payment-service]
├─ POST /api/inventory/reserve (4ms) [inventory-service]
├─ pg.query INSERT orders (15ms) [shop-api]
└─ POST /api/notifications/order (54ms) [notification-service]
Waterfall trace baseline - tutti i servizi veloci

Root Cause Identification

Confrontando i waterfall dei 4 scenari, il bottleneck è immediato:

ScenarioServizio lentoDurationBaseline
Slow PaymentPOST /api/payments/process2020ms85ms
Slow InventoryPOST /api/inventory/check~1500ms5ms
Slow NotificationPOST /api/notifications/order~3000ms54ms

Ogni test isola un singolo servizio. Il waterfall mostra esattamente quale span contribuisce alla latenza totale.

Click sugli span lenti → View Logs:

// Payment service (scenario slow payment)
{"msg":"Processing payment","orderId":18,"traceId":"..."}
{"msg":"Payment gateway slow response","orderId":18,"responseTimeMs":2000,"traceId":"..."}

// Notification service (scenario slow notification)
{"msg":"Processing order notification","orderId":24,"template":"order_confirmation_premium","traceId":"..."}
{"msg":"Template rendering took long","orderId":24,"renderTimeMs":3000,"traceId":"..."}
Log dei servizi lenti correlati alla trace

Cosa Mostra Questo Scenario

Senza OTelCon OTel
“Il checkout è lento”Waterfall mostra ESATTAMENTE quale servizio
Timestamp non correlabiliTimeline unificata con parent-child
“Forse è il database?”DB query visibile: 12ms (non è lui)
Ipotesi su 4 serviziCertezza: inventory check + notification
Debug per esclusione su 4 serviziRoot cause visibile nel waterfall

Il Vero Valore del Distributed Tracing

Questo scenario dimostra dove il distributed tracing offre il vantaggio maggiore:

  1. Fan-out visibility: Vedi tutte le chiamate parallele e sequenziali
  2. Proportional blame: Il waterfall mostra quanto ogni servizio contribuisce
  3. Cross-service correlation: I log di 4 servizi correlati automaticamente
  4. No clock sync needed: La timeline è basata su span parent-child, non su timestamp

Senza tracing distribuito, con 4 servizi, ci sono 4! = 24 possibili combinazioni da investigare. Con il tracing, il bottleneck è visibile nel waterfall in pochi minuti.


Quando NON Usare Distributed Tracing

Il distributed tracing non è sempre la soluzione giusta. Considera alternative quando:

1. Architettura Monolitica

Con un singolo servizio, il tracing distribuito aggiunge overhead senza benefici. Alternative:

  • Profiler applicativo (Node.js inspector, py-spy)
  • APM tradizionale
  • Log strutturati con request ID

2. Problemi di Performance Locali

Se il bottleneck è dentro una singola funzione (CPU-bound, memory leak), il tracing non aiuta. Usa:

  • Flame graphs
  • Memory profiler
  • Load testing con profiling

3. Debug di Errori Noti

Se conosci già dove cercare, grep sui log è più veloce di una query TraceQL.

4. Ambienti con Risorse Limitate

L’auto-instrumentation ha overhead:

  • CPU: ~2-5% per servizio
  • Memoria: ~50-100MB per SDK
  • Network: dipende dal volume di trace
  • Storage: può crescere rapidamente senza sampling

Se le risorse sono critiche, valuta attentamente il trade-off.

5. Team Piccoli con Architettura Semplice

Con 2-3 servizi in catena lineare (A→B→C) e un team che conosce bene il sistema, l’investimento nel distributed tracing potrebbe non ripagare. Il beneficio cresce con:

  • Pattern fan-out (come il checkout di MockMart: 4 servizi chiamati da una request)
  • Numero di servizi (5+)
  • Dimensione del team
  • Turnover del personale
  • Frequenza di incidenti

Regola Pratica

Usa il distributed tracing quando: Il tempo medio di debug di un incidente cross-service supera le ore, specialmente con pattern fan-out dove una request attraversa più servizi (come il checkout demo).

Evitalo quando: Gli incidenti sono rari, localizzati, l’architettura è lineare (A→B→C), o il team ha già strumenti efficaci.


Limiti di Questo Tutorial

Cosa Non È Coperto

  1. Production deployment: LGTM all-in-one non scala. Servono deployment separati con:

    • Storage persistente (S3, GCS per Tempo/Loki)
    • Retention policies
    • High availability
    • Autenticazione e autorizzazione
  2. Sampling: In production, non è possibile mantenere il 100% delle trace. Servono strategie di:

    • Head sampling (percentuale fissa)
    • Tail sampling (100% errori, sample del resto)
    • Rate limiting
  3. Costi e storage: Il volume di dati può crescere rapidamente. Calcola:

    • ~1KB per span (media)
    • 100 req/s × 5 span/req × 1KB × 86400s = ~43GB/giorno
    • Con sampling 10%: ~4.3GB/giorno
  4. Security: Attenzione a non loggare:

    • Token e credenziali
    • PII (email, nomi, indirizzi)
    • Dati sensibili nei span attributes

Simulazioni vs Realtà

Gli scenari demo usano simulazioni controllate:

  • Il “template premium lento” è un setTimeout(3000)
  • L’“email invalida” è un flag di configurazione
  • I “servizi lenti” nel fan-out sono endpoint /config/simulate-slow che aggiungono delay

In produzione, i problemi reali sono:

  • Più difficili da riprodurre
  • Spesso intermittenti
  • Causati da combinazioni di fattori

OTel aiuta proprio perché cattura questi scenari quando accadono in produzione, senza doverli riprodurre in dev.


Appendice: Setup OpenTelemetry

Questa sezione è opzionale. La demo MockMart include già tutto configurato. Utile per chi vuole replicare il setup in un progetto proprio.

Cosa Serve Configurare (Realisticamente)

Prima di iniziare, chiariamo cosa significa “auto-instrumentation” e che richiede comunque una fase di setup.

Documentazione ufficiale: Per approfondire, consulta la guida Automatic Instrumentation for Node.js e la lista delle librerie supportate.

Lato Applicazione

1. Dipendenze da installare:

npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-grpc \
  @opentelemetry/resources \
  @opentelemetry/api \
  pino
PackageScopo
sdk-nodeSDK principale per Node.js, orchestra tutti i componenti
auto-instrumentations-nodeBundle di instrumentazioni automatiche (Express, HTTP, pg, etc.)
exporter-trace-otlp-grpcEsporta trace via protocollo OTLP su gRPC
resourcesDefinizione delle risorse (service name, attributi)
apiAPI per interagire con trace nel codice applicativo
pinoLogger ad alte prestazioni per Node.js

2. File instrumentation.js da creare (e caricare PRIMA di tutto):

Importante: Questo file deve essere caricato prima di qualsiasi altro import. L’auto-instrumentation funziona tramite monkey-patching dei moduli Node.js (Express, pg, Pino, etc.) al momento del loro caricamento. Se i moduli vengono importati prima dell’inizializzazione OTel, non saranno instrumentati.

// instrumentation.js - Inizializza OpenTelemetry PRIMA di qualsiasi altro import
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { resourceFromAttributes } = require('@opentelemetry/resources');

const serviceName = process.env.OTEL_SERVICE_NAME || 'my-service';
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317';

const resource = resourceFromAttributes({
  'service.name': serviceName,
});

const sdk = new NodeSDK({
  resource,
  traceExporter: new OTLPTraceExporter({ url: otlpEndpoint }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-fs': { enabled: false },
    }),
  ],
});

sdk.start();
console.log(`OpenTelemetry initialized for ${serviceName}`);

process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((err) => console.error('Error terminating', err))
    .finally(() => process.exit(0));
});

3. Avvio applicazione modificato:

# --require carica instrumentation.js PRIMA di server.js
node --require ./instrumentation.js server.js

In package.json:

{
  "scripts": {
    "start": "node --require ./instrumentation.js server.js"
  }
}

4. Logger con Pino (trace context iniettato automaticamente):

// lib/logger.js
const pino = require('pino');

// PinoInstrumentation inietta automaticamente trace_id e span_id
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
});

module.exports = logger;

Esempio di utilizzo nel codice applicativo:

const logger = require('./lib/logger');
const { trace } = require('@opentelemetry/api');

app.post('/api/checkout', requireAuth, async (req, res) => {
  const user = req.user;
  const cart = carts[req.session.id];

  // Log con attributi strutturati - trace_id/span_id iniettati da PinoInstrumentation
  logger.info({ userId: user.id, itemCount: cart.length }, 'Processing checkout');

  try {
    // ... inserimento ordine nel database ...
    const orderId = orderResult.rows[0].id;

    // Aggiungi order_id allo span per query TraceQL: { span.order_id = 123 }
    trace.getActiveSpan()?.setAttribute('order_id', orderId);

    logger.info({ orderId, userId: user.id, total }, 'Order saved to database');

    // Chiama notification service
    logger.info({ orderId, url: NOTIFICATION_SERVICE_URL }, 'Calling notification service');

    await axios.post(`${NOTIFICATION_SERVICE_URL}/api/notifications/order`, {...});

    logger.info({ orderId }, 'Notification sent successfully');

    res.json({ success: true, order });
  } catch (err) {
    // Gli errori includono automaticamente il trace context per correlazione
    logger.error({ error: err.message }, 'Checkout failed');
    res.status(500).json({ error: 'Checkout failed' });
  }
});

Output nei log (Pino con trace context):

{
  "level": 30,
  "time": 1770291707877,
  "pid": 1,
  "hostname": "shop-api",
  "trace_id": "b0b197c5337c4b07f80e2ef7b130f2f4",
  "span_id": "e8815f03044501df",
  "trace_flags": "01",
  "orderId": 8472,
  "total": 99.99,
  "msg": "Order saved to database"
}

trace_id e span_id sono iniettati automaticamente da PinoInstrumentation.

Lato Infrastruttura

LGTM all-in-one (per sviluppo/staging):

grafana:
  image: grafana/otel-lgtm:latest
  ports:
    - "3005:3000"   # Grafana UI
    - "4317:4317"   # OTLP gRPC
    - "4318:4318"   # OTLP HTTP

Variabili ambiente e comando nei servizi (da docker-compose.yml):

shop-api:
  environment:
    - OTEL_SERVICE_NAME=shop-api
    - OTEL_EXPORTER_OTLP_ENDPOINT=http://grafana:4317
  # Carica instrumentation.js PRIMA di server.js
  command: ["node", "--require", "./instrumentation.js", "server.js"]

notification-service:
  environment:
    - OTEL_SERVICE_NAME=notification-service
    - OTEL_EXPORTER_OTLP_ENDPOINT=http://grafana:4317
  command: ["node", "--require", "./instrumentation.js", "server.js"]

Nota: Per production, LGTM all-in-one non è adeguato. Servono deploy separati di Tempo, Loki, Mimir con storage persistente, retention policies, e sampling. Questi aspetti saranno trattati in un articolo dedicato.

Personalizzare l’Instrumentazione

Attributi Custom

L’auto-instrumentation cattura attributi standard (HTTP method, URL, status code, query SQL). Per facilitare le ricerche, aggiungi attributi business-specific.

Approfondimento: La guida Manual Instrumentation spiega come creare span custom, aggiungere attributi, e gestire il context manualmente.

const { trace } = require('@opentelemetry/api');

app.post('/api/checkout', async (req, res) => {
  // ... logica checkout ...

  const orderId = result.rows[0].id;

  // --- Aggiungi attributo business allo span corrente ---
  // trace.getActiveSpan() restituisce lo span creato dall'auto-instrumentation
  // per questa richiesta HTTP (span "POST /api/checkout")
  // L'optional chaining (?.) gestisce il caso (raro) in cui non ci sia span attivo
  trace.getActiveSpan()?.setAttribute('order_id', orderId);

  // È possibile aggiungere più attributi - utili per filtering e grouping
  trace.getActiveSpan()?.setAttributes({
    'order_id': orderId,
    'user_tier': req.user.tier,        // es. "premium", "basic"
    'cart_item_count': cart.length,
    'order_total': order.total,
  });

  // ... resto del codice ...
});

Query TraceQL con attributi custom:

# Trova tutte le trace per un ordine specifico
{ span.order_id = 8472 }

# Trova checkout lenti di utenti premium
{ span.user_tier = "premium" && duration > 1s }

# Combina con attributi standard
{ resource.service.name = "shop-api" && span.order_total > 100 }

Span Manuali (per operazioni non instrumentate)

Per tracciare un’operazione specifica non coperta dall’auto-instrumentation:

const { trace, SpanStatusCode } = require('@opentelemetry/api');

async function processPayment(order) {
  // Ottieni il tracer (usa lo stesso nome del servizio per consistenza)
  const tracer = trace.getTracer('shop-api');

  // Crea uno span manuale - sarà child dello span HTTP corrente
  return tracer.startActiveSpan('process-payment', async (span) => {
    try {
      // Aggiungi attributi specifici di questa operazione
      span.setAttributes({
        'payment.method': order.paymentMethod,
        'payment.amount': order.total,
        'payment.currency': 'EUR',
      });

      // ... logica pagamento ...
      const result = await paymentGateway.charge(order);

      // Aggiungi risultato
      span.setAttribute('payment.transaction_id', result.transactionId);

      return result;
    } catch (error) {
      // Marca lo span come errore e registra l'eccezione
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    } finally {
      // IMPORTANTE: chiudi sempre lo span
      span.end();
    }
  });
}

Risultato nella trace:

POST /api/checkout (850ms)
├─ pg.query:INSERT orders (15ms)
├─ process-payment (500ms)          ← span manuale
│   └─ payment.method: "credit-card"
│   └─ payment.amount: 99.99
│   └─ payment.transaction_id: "txn_123"
└─ HTTP POST /api/notifications (320ms)

Cosa NON Viene Catturato Automaticamente

L’auto-instrumentation cattura:

CatturatoEsempi
HTTP request metadatamethod, url, status_code, user_agent
Database queriesquery SQL (sanitizzata), operation name, db.system
Timingduration di ogni operazione
Erroristatus code, exception type

Non catturato (richiede intervento manuale):

Non catturatoSoluzione
Contenuto body HTTPsetAttribute() (attenzione a PII)
Valori business (orderId, userId)setAttribute()
Operazioni custom (es. calcoli)Span manuali con startActiveSpan()
Log applicativiLogger OTel (sezione precedente)

Risorse

Repository demo:

Documentazione OpenTelemetry (Node.js):

LGTM Stack:


Prossimi Articoli

  • Sampling strategies: Head vs tail sampling, quando usare quale
  • Production deployment: LGTM in Kubernetes con storage persistente
  • Cost optimization: Calcolare e controllare i costi di observability
  • Security: Filtrare PII e dati sensibili dalle trace

Per domande o feedback: francesco@montelli.dev | LinkedIn | GitHub