OpenTelemetry per PHP: distributed tracing end-to-end senza vendor lock-in
A gennaio 2025 gestivo un'architettura a microservizi per un cliente del settore logistico: otto servizi PHP (cinque su Symfony, tre su Laravel) orchestrati su un cluster Kubernetes Hetzner, con un volume di 200.000 richieste al giorno distribuite su flussi di business che attraversavano 3-5 servizi ciascuno. Il monitoring era affidato a Datadog - un servizio eccellente dal punto di vista tecnico, con distributed tracing, metriche infrastrutturali e log centralizzati in un'unica piattaforma. Il problema era il costo: 30 euro per host al mese, per 11 host (8 servizi + 3 nodi Kubernetes + 1 database), più il costo per volume di log ingeriti, portava la fattura mensile a circa 520 euro. Per una PMI che fattura 3 milioni l'anno, dedicare 6.240 euro all'anno al monitoring quando l'intera infrastruttura Kubernetes costa 560 euro all'anno era un rapporto sproporzionato. Il titolare mi ha chiesto se esistesse un'alternativa open source con la stessa visibilità sulle trace distribuite. La risposta è OpenTelemetry - lo standard aperto per la telemetria delle applicazioni, adottato da CNCF, supportato da tutti i major vendor di observability, e completamente gratuito quando combinato con backend open source come Jaeger e Prometheus.
In tre settimane ho migrato l'intero stack di observability da Datadog a OpenTelemetry con collector auto-hosted e Jaeger come backend di tracing. Il costo infrastrutturale aggiuntivo: un singolo VPS Hetzner CPX21 (8 euro al mese) che ospita il collector OpenTelemetry, Jaeger e Prometheus. La visibilità sulle trace distribuite è identica - posso seguire una richiesta HTTP dal primo servizio all'ultimo, vedere le latenze di ogni span, identificare i colli di bottiglia e correlare le trace con i log applicativi. Il risparmio annuo: circa 5.600 euro, che il cliente ha reinvestito in ore di sviluppo per migliorare l'applicazione stessa.
Come funziona il distributed tracing con OpenTelemetry e perché è diverso dal logging tradizionale?
Il logging tradizionale registra eventi isolati: "richiesta ricevuta", "query eseguita in 45 ms", "risposta inviata con status 200." Quando hai 8 servizi e una richiesta attraversa 4 di essi, hai 4 set di log separati senza nessun collegamento tra loro. Se la richiesta è lenta, non sai quale dei 4 servizi è il collo di bottiglia senza correlare manualmente i timestamp - un processo che richiede minuti o ore e che spesso non è possibile perché i clock dei server non sono perfettamente sincronizzati.
Il distributed tracing risolve questo problema assegnando un trace ID unico alla richiesta iniziale e propagandolo a ogni servizio che la richiesta attraversa. Ogni operazione significativa all'interno di un servizio (una query SQL, una chiamata HTTP a un altro servizio, un'elaborazione CPU-bound) viene registrata come span - un segmento temporale con inizio, fine, metadati e parent span. Il risultato è un albero gerarchico di span che mostra esattamente dove la richiesta ha speso il suo tempo, servizio per servizio, operazione per operazione. Se una richiesta impiega 3 secondi e il tracing mostra che 2.500 ms sono stati spesi in una query SQL sul servizio di magazzino, sai immediatamente dove intervenire - senza aprire un singolo file di log.
OpenTelemetry è lo standard che definisce come le applicazioni generano, raccolgono e esportano questi dati di telemetria. Non è un prodotto - è un protocollo e un insieme di SDK per ogni linguaggio di programmazione. L'SDK PHP di OpenTelemetry instrumenta l'applicazione (crea span per le richieste HTTP, le query SQL, le chiamate a servizi esterni), il collector raccoglie gli span da tutti i servizi e li esporta verso un backend di visualizzazione (Jaeger, Zipkin, Tempo, o un servizio commerciale come Datadog). Il punto cruciale è la portabilità: se oggi usi Jaeger come backend e domani vuoi migrare a Grafana Tempo o tornare a Datadog, cambi solo la configurazione dell'exporter - il codice applicativo non cambia. Nel mio profilo professionale trovi il dettaglio dell'esperienza che porto nella progettazione di stack di observability - un'area dove la scelta degli strumenti giusti ha un impatto diretto sia sulla capacità diagnostica del team sia sul costo operativo mensile.
Strumentazione PHP: SDK OpenTelemetry per Laravel e Symfony
L'SDK PHP di OpenTelemetry si installa come estensione PECL (opentelemetry) per l'auto-instrumentation e come pacchetto Composer per la strumentazione manuale. L'auto-instrumentation cattura automaticamente le richieste HTTP in ingresso, le query PDO, le chiamate HTTP in uscita (via Guzzle o cURL) e i log Monolog - senza modificare il codice applicativo. La strumentazione manuale permette di aggiungere span custom per operazioni di business specifiche.
L'installazione su un servizio Laravel è minimale. L'estensione PECL fornisce l'auto-instrumentation per PDO e cURL, mentre il pacchetto Composer configura l'exporter verso il collector:
# Installazione dell'estensione OpenTelemetry per PHP
pecl install opentelemetry
echo "extension=opentelemetry.so" > /etc/php/8.2/mods-available/opentelemetry.ini
phpenmod opentelemetry
# Pacchetti Composer per la configurazione
composer require open-telemetry/sdk \
open-telemetry/exporter-otlp \
open-telemetry/transport-grpcLa configurazione avviene tramite variabili d'ambiente - un approccio che permette di cambiare exporter e collector senza modificare il codice:
# Configurazione OpenTelemetry nel .env di Laravel
OTEL_SERVICE_NAME=ordini-service
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_PHP_AUTOLOAD_ENABLED=trueCon questa configurazione, ogni richiesta HTTP ricevuta dal servizio genera automaticamente una trace con span per la richiesta principale, per ogni query PDO eseguita, e per ogni chiamata HTTP in uscita. Il trace ID viene propagato agli altri servizi tramite l'header traceparent (standard W3C Trace Context) - se il servizio ordini chiama il servizio magazzino via HTTP, il trace ID è lo stesso e le trace dei due servizi sono automaticamente collegate nell'interfaccia di Jaeger.
Per operazioni di business che non rientrano nell'auto-instrumentation, aggiungo span manuali:
// Span manuale per un'operazione di business
use OpenTelemetry\API\Globals;
$tracer = Globals::tracerProvider()->getTracer('ordini-service');
$span = $tracer->spanBuilder('calcolo_sconto_cliente')
->setAttribute('cliente.id', $cliente->id)
->setAttribute('ordine.importo', $ordine->importo_totale)
->startSpan();
try {
$sconto = $this->calcolaSconto($cliente, $ordine);
$span->setAttribute('sconto.percentuale', $sconto->percentuale);
$span->setAttribute('sconto.importo', $sconto->importo);
} finally {
$span->end();
}Campionamento: non tracciare tutto, tracciare il necessario
Un aspetto critico che i tutorial trascurano è il campionamento delle trace. In un'architettura con 200.000 richieste al giorno, tracciare il 100% delle richieste produce un volume di dati che satura rapidamente lo storage di Jaeger e rende l'interfaccia inutilizzabile per l'analisi. La best practice è il campionamento adattivo: tracciare il 100% delle richieste con errori (status 4xx e 5xx), il 100% delle richieste con latenza superiore a una soglia (ad esempio 2 secondi), e un campione casuale del 5-10% delle richieste normali. Questo approccio garantisce che tutti gli incidenti siano tracciati per intero, che i problemi di performance siano catturati, e che le richieste normali siano sufficientemente rappresentate per il monitoring delle baseline.
Il collector OpenTelemetry supporta il campionamento configurabile tramite il file di configurazione YAML. Il pattern che uso è il tail-based sampling: il collector riceve tutte le trace dai servizi e decide dopo aver visto tutti gli span se conservare la trace o scartarla. Questo è superiore al campionamento head-based (dove la decisione viene presa dal primo servizio prima di sapere come la richiesta andrà a finire), perché permette di conservare tutte le trace con errori o latenza anomala indipendentemente dalla percentuale di campionamento casuale. Il costo è che il collector deve bufferizzare tutte le trace in memoria prima di decidere - il che richiede RAM proporzionale al throughput. Per 200.000 richieste al giorno, 2 GB di RAM nel collector sono sufficienti con un buffer di 30 secondi.
Un errore comune che ho visto in due dei tre progetti che gestivo prima della migrazione è il campionamento inconsistente tra servizi: il servizio A campiona al 10%, il servizio B al 50%, e il risultato sono trace incomplete dove gli span del servizio A sono presenti ma quelli del servizio B mancano. La regola è che la decisione di campionamento deve essere presa una sola volta dal primo servizio (o dal collector centralizzato) e propagata a tutti i servizi successivi tramite il flag di campionamento nel header traceparent. OpenTelemetry gestisce questo nativamente - ma solo se tutti i servizi usano lo stesso SDK con la stessa configurazione di campionamento, o se il campionamento è delegato al collector.
Il collector e il backend: Jaeger su VPS a 8 euro al mese
Il collector OpenTelemetry è un componente intermedio che riceve le trace dai servizi, le processa (filtra, campiona, arricchisce con metadati) e le esporta verso il backend di storage e visualizzazione. Lo installo come container Docker sullo stesso VPS che ospita Jaeger e Prometheus:
Il deployment dell'intero stack di observability - collector OpenTelemetry, Jaeger per le trace e Prometheus per le metriche - gira su un singolo VPS Hetzner CPX21 con 4 vCPU e 8 GB di RAM. Jaeger è configurato con storage su Badger (embedded, nessun database esterno) e retention di 7 giorni - sufficiente per diagnosticare problemi operativi senza accumulare terabyte di dati. Il consumo di risorse a regime: circa 1,2 GB di RAM per Jaeger, 800 MB per il collector, 500 MB per Prometheus - il 30% delle risorse del VPS, con margine ampio per i picchi.
L'impatto sulla diagnostica: da ore a minuti
Il valore del distributed tracing si manifesta nella velocità di diagnostica degli incidenti. Prima della migrazione a OpenTelemetry, un bug che causava risposte lente intermittenti sul portale clienti richiedeva 2-4 ore di analisi - il team doveva controllare i log di 8 servizi separati, correlare i timestamp manualmente, e formulare ipotesi su quale servizio fosse il collo di bottiglia. Dopo l'implementazione, la stessa analisi richiede 5 minuti: apri Jaeger, cerchi le trace con latenza superiore alla soglia, e il waterfall chart mostra immediatamente quale span è responsabile del rallentamento. Nel caso specifico, il 90% delle richieste lente era causato da una query sul servizio di pricing che faceva un full table scan su una tabella da 2 milioni di righe - un problema invisibile nei log perché la query in sé non era lenta (200 ms), ma veniva eseguita 15 volte per richiesta (una per ogni riga dell'ordine). La trace mostrava chiaramente il pattern: 15 span consecutivi da 200 ms ciascuno, per un totale di 3 secondi su un singolo servizio. L'aggiunta di un eager loading ha risolto il problema in 10 minuti di lavoro.
Il ritorno dell'investimento del progetto è stato immediato e quantificabile: il costo del monitoring è sceso da 520 euro al mese a 8 euro al mese (il VPS del collector), il tempo medio di diagnostica degli incidenti è sceso da 2-4 ore a 5-15 minuti, e il team ha identificato e risolto tre problemi di performance latenti che con il solo logging sarebbero rimasti invisibili per mesi. Il vantaggio strategico più importante è l'assenza di vendor lock-in: i dati di telemetria sono in formato OpenTelemetry Protocol (OTLP), uno standard aperto che qualsiasi backend compatibile può consumare. Se domani il cliente decide di provare Grafana Tempo invece di Jaeger, la migrazione è una modifica di configurazione nel collector - non una riscrittura dell'instrumentazione nei servizi.
Ho documentato un approccio complementare al monitoring applicativo con Laravel Pulse, che è lo strumento ideale per applicazioni monolitiche dove il distributed tracing non è necessario. Pulse e OpenTelemetry non sono alternativi - sono complementari: Pulse per il monitoring quotidiano dell'applicazione singola, OpenTelemetry per la diagnostica cross-service in architetture a microservizi. Se gestisci un'architettura con più servizi PHP e stai pagando un servizio di observability commerciale il cui costo è sproporzionato rispetto all'infrastruttura, o se non hai nessun sistema di tracing e diagnostichi i problemi leggendo log a mano, contattami per implementare OpenTelemetry: in due giornate di lavoro instrumentiamo i servizi, configuriamo il collector e il backend, e facciamo un primo ciclo di analisi delle trace per identificare i colli di bottiglia che il team non sapeva di avere.