Guía para implementar trazado distribuido con OpenTelemetry en aplicaciones PHP monolito
Por Kevin Marcondes · Publicado el · Actualizado el · 10 min de lectura
Esta guía muestra, paso a paso, cómo implementar trazado distribuido con OpenTelemetry en aplicaciones PHP monolito. Instrumentarás peticiones HTTP, consultas a la base de datos, llamadas a APIs externas y jobs, configurarás exporters OTLP y definirás estrategias de muestreo seguras para producción.
Por qué hacer trazado en un monolito PHP
Aunque sea un monolito, existen muchos puntos ciegos: controladores, servicios internos, colas, cron jobs y llamadas a APIs. El trazado distribuido encadena todo en una única línea temporal.
Acelera el diagnóstico de lentitudes, N+1 en SQL, cuellos de botella de I/O y fallos intermitentes. Mide cada etapa y visualiza cómo la petición recorre el sistema.
Si aún evalúas herramientas, compara las opciones para PHP y cuándo conviene usar OpenTelemetry frente a agentes propietarios.
Para completar la observabilidad, correlaciona trazas con logs estructurados y métricas. La correlación reduce el tiempo de investigación de incidentes.
- Identifica rutas y endpoints más lentos.
- Visualiza dependencias externas con mayor latencia.
- Aísla consultas SQL y cache misses.
- Rastrea jobs y cron jobs que generan picos de carga.
Arquitectura mínima y elección del exportador
La arquitectura básica incluye: el SDK de OpenTelemetry en PHP, propagadores para contexto (traceparent), un sampler y un exporter. En producción es común enviar spans a un OpenTelemetry Collector, que a su vez los reenvía al backend de trazas.
OTLP es el formato estándar del ecosistema. Puedes usar HTTP/protobuf (puerto 4318) o gRPC (puerto 4317), según la red y la política de seguridad.
Si ya tienes un backend que acepta Zipkin o Jaeger directamente, el Collector realiza la traducción y buffering. Esto evita acoplar tu monolito al proveedor final.
| Exportador | Protocolo | Cuándo usar | Observaciones |
|---|---|---|---|
| OTLP/HTTP | HTTP 4318 | Entornos con proxies e inspección de tráfico más estrictos | Estándar, sencillo de depurar con herramientas HTTP. |
| OTLP/gRPC | gRPC 4317 | Baja latencia y conexiones de larga duración | Puede requerir reglas específicas en el balanceador. |
| Zipkin | HTTP 9411 | Compatibilidad con coletores legados | Úsalo vía Collector para no acoplar la aplicación. |
| Jaeger | UDP/HTTP | Entornos con Jaeger ya existente | Prefiere OTLP vía Collector cuando sea posible. |
Prequisitos y planificación del rollout
Planifica antes de tocar código. Define convenciones de nombre de servicio y versiones. Decide el muestreo inicial y los endpoints del exporter por ambiente.
Protege la privacidad: no captures datos sensibles como contraseñas o tarjetas. Prefiere IDs técnicos y hashes. Revisa atributos y eventos antes del go‑live.
Asegura que el front controller del monolito sea el punto único para iniciar y cerrar el span raíz. En frameworks, usa middleware para envolver la petición.
- PHP 8.x con Composer.
- Acceso para editar el bootstrap de la app o middleware.
- Endpoint del OTLP/Collector por ambiente.
- Estrategia de muestreo inicial (ej.: 5–10%).
- Convenciones de atributos (http., db., net*).
Instalación y bootstrap de OpenTelemetry en el monolito
Instala los paquetes del SDK y del exporter OTLP vía Composer. A continuación, crea un bootstrap de trazado para registrar el TracerProvider, sampler, processor y exporter.
Los nombres de clases pueden variar según la versión del SDK. Verifica el namespace instalado por Composer y ajusta si es necesario. El ejemplo usa OTLP/HTTP.
Carga este bootstrap en el front controller (p. ej. public/index.php) para garantizar que cada petición tenga un span raíz y que el cierre haga flush de los spans.
composer require open-telemetry/sdk open-telemetry/exporter-otlp
Ejemplo de bootstrap: provider, sampler y exporter OTLP/HTTP
Este ejemplo ilustra una configuración típica: servicio nombrado, sampler parent-based con ratio, procesamiento por lotes y exportación OTLP/HTTP a un Collector. Ajusta variables de entorno según tu entorno.
Usa variables de entorno para endpoints y muestreo, facilitando cambios sin redeploy. Cerrar el provider al final de la petición garantiza el envío de los spans.
<?php
// bootstrap/tracing.php
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\Context\Propagation\ArrayAccessGetterSetter;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\Sampler\TraceIdRatioBased;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\Exporter as OtlpExporter;
$serviceName = getenv('OTEL_SERVICE_NAME') ?: 'php-monolith';
$endpoint = getenv('OTEL_EXPORTER_OTLP_ENDPOINT') ?: 'http://localhost:4318';
$sampleRatio = (float) (getenv('OTEL_TRACES_SAMPLER_ARG') ?: '0.1');
$transport = (new OtlpHttpTransportFactory())
->create(rtrim($endpoint, '/') . '/v1/traces', 'application/x-protobuf');
$exporter = new OtlpExporter($transport);
$processor = new BatchSpanProcessor($exporter);
$sampler = new ParentBased(new TraceIdRatioBased($sampleRatio));
$tracerProvider = new TracerProvider($processor, $sampler);
Globals::setTracerProvider($tracerProvider);
// Extração do contexto de entrada (W3C traceparent)
$headers = function_exists('getallheaders') ? getallheaders() : [];
$propagator = TraceContextPropagator::getInstance();
$carrier = $headers;
$getterSetter = new ArrayAccessGetterSetter();
$context = $propagator->extract($carrier, $getterSetter);
// Tracer para a aplicação
$tracer = Globals::tracerProvider()->getTracer($serviceName);
// Span raiz da requisição
$span = $tracer->spanBuilder('http.request')->setParent($context)->startSpan();
$span->setAttribute('service.name', $serviceName);
$span->setAttribute('http.method', $_SERVER['REQUEST_METHOD'] ?? 'CLI');
$span->setAttribute('http.target', $_SERVER['REQUEST_URI'] ?? '');
$span->activate();
register_shutdown_function(function () use ($span, $tracerProvider) {
$span->end();
$tracerProvider->shutdown();
});
Instrumenta peticiones, SQL y jobs con spans hijos
Con el tracer activo, crea spans hijos para operaciones críticas. Nombra los spans por propósito y añade atributos clave. Registra excepciones en el span para facilitar el triage.
En el ejemplo creamos spans para el controlador, una consulta PDO y un job de cola. En frameworks, adapta esto a middleware o handlers específicos.
<?php
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
$tracer = Globals::tracerProvider()->getTracer('php-monolith');
// Controlador/ação
$controllerSpan = $tracer->spanBuilder('controller:OrdersController@index')
->setSpanKind(SpanKind::KIND_INTERNAL)
->startSpan();
try {
// ... lógica do controlador
// SQL com PDO
$dbSpan = $tracer->spanBuilder('db.query:orders_by_customer')->startSpan();
try {
$dbSpan->setAttribute('db.system', 'mysql');
$dbSpan->setAttribute('db.statement', 'SELECT * FROM orders WHERE customer_id = ?');
// $stmt = $pdo->prepare('SELECT * FROM orders WHERE customer_id = ?');
// $stmt->execute([$customerId]);
} finally {
$dbSpan->end();
}
// Job de fila
$jobSpan = $tracer->spanBuilder('queue:send_invoice_email')->startSpan();
try {
$jobSpan->setAttribute('messaging.system', 'redis');
$jobSpan->setAttribute('messaging.destination', 'emails');
// enqueue(...)
} finally {
$jobSpan->end();
}
} catch (Throwable $e) {
$controllerSpan->recordException($e);
$controllerSpan->setAttribute('exception.escaped', true);
throw $e;
} finally {
$controllerSpan->end();
}
Propagación de contexto en llamadas externas (traceparent)
Propaga el contexto al llamar APIs. Esto encadena spans entre servicios. Extrae en la entrada e inyecta en la salida. El estándar es W3C traceparent.
En el ejemplo creamos un span CLIENT y inyectamos el header traceparent en una llamada HTTP. Adáptalo a tu librería HTTP. Lo mismo aplica para mensajería.
<?php
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\Propagation\ArrayAccessGetterSetter;
$tracer = Globals::tracerProvider()->getTracer('php-monolith');
$clientSpan = $tracer->spanBuilder('http.client:payment/charge')
->setSpanKind(SpanKind::KIND_CLIENT)
->startSpan();
$scope = $clientSpan->activate();
try {
// Injeta traceparent nos headers
$headers = [];
$propagator = TraceContextPropagator::getInstance();
$propagator->inject($headers, new ArrayAccessGetterSetter(), Context::getCurrent());
// Exemplo com cURL
$ch = curl_init('https://payments.example.com/charge');
$curlHeaders = [];
foreach ($headers as $k => $v) {
$curlHeaders[] = $k . ': ' . $v;
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $curlHeaders,
]);
$resp = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE) ?: 0;
$clientSpan->setAttribute('http.status_code', $status);
curl_close($ch);
} finally {
$scope->detach();
$clientSpan->end();
}
Muestreo en producción: estrategias y ajustes seguros
El muestreo controla coste y volumen. Usa parent-based para mantener la coherencia: si la raíz fue muestreada, los hijos siguen la misma decisión. En producción, combina ratio con reglas que siempre recojan errores.
Evita cambios bruscos. Ajusta la razón por ambiente y por franjas horarias. Para detectar incidentes raros, incrementa temporalmente el muestreo vía variable de entorno.
| Estrategia | Tipo | Cuándo usar | Config (ejemplo) |
|---|---|---|---|
| always_on | head-based | Diagnóstico en staging o pruebas | OTEL_TRACES_SAMPLER=always_on |
| always_off | head-based | Desactivar temporalmente | OTEL_TRACES_SAMPLER=always_off |
| traceidratio | head-based | Control global simple | OTEL_TRACES_SAMPLER=traceidratio; OTEL_TRACES_SAMPLER_ARG=0.1 |
| parentbased_traceidratio | head-based | Mantener coherencia en los hijos | OTEL_TRACES_SAMPLER=parentbased_traceidratio |
# Variáveis úteis por ambiente
export OTEL_SERVICE_NAME=php-monolith
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.3.0"
Correlacionando trace_id con logs estructurados
Incluye trace_id y span_id en los logs. Esto permite saltar de un error en el log a la traza correspondiente. Prefiere logs JSON con campos estables.
Si ya usas logs estructurados, añade los IDs de contexto al logger. Revisa el formato para estandarizarlo en producción.
<?php
use OpenTelemetry\API\Trace\Span;
$span = Span::getCurrent();
$ctx = $span->getContext();
$traceId = method_exists($ctx, 'getTraceId') ? $ctx->getTraceId() : null;
$spanId = method_exists($ctx, 'getSpanId') ? $ctx->getSpanId() : null;
$log = [
'level' => 'error',
'message' => 'Pedido não encontrado',
'trace_id' => $traceId,
'span_id' => $spanId,
'order_id' => $orderId ?? null,
];
error_log(json_encode($log));
Cómo validar la instrumentación sin backend de trazas
Para validar localmente, comienza con muestreo always_on en un entorno de pruebas. Genera peticiones y verifica la propagación del header traceparent en llamadas externas.
También puedes exponer temporalmente el trace_id en la respuesta HTTP de entornos de prueba. Elimina esto antes del go‑live. El objetivo es confirmar encadenamiento y atributos.
- Activa always_on solo en staging.
- Genera carga realista con scripts sencillos.
- Confirma la presencia de traceparent en las salidas HTTP.
- Revisa atributos para evitar PII en spans.
<?php
// Em ambiente de teste, retorne o trace_id no header para depuração
use OpenTelemetry\API\Trace\Span;
$ctx = Span::getCurrent()->getContext();
if (method_exists($ctx, 'getTraceId')) {
header('x-trace-id: ' . $ctx->getTraceId());
}
Preguntas frecuentes
¿Los monolitos realmente se benefician del trazado distribuido?
Sí. Incluso sin microservicios, los monolitos tienen capas, llamadas a APIs, jobs y bases de datos. El trazado mapea el recorrido completo de la petición, revelando cuellos de botella y fallos que los logs aislados no muestran.
¿Qué exportador debo usar en PHP para OpenTelemetry?
Prefiere OTLP. Usa HTTP/protobuf cuando la red tenga proxies o inspección, y gRPC para conexiones de larga duración y menor overhead. En ambos casos, envía primero a un OpenTelemetry Collector cuando sea posible.
¿Cuál es el impacto en el rendimiento del trazado en PHP?
Depende del muestreo, volumen y procesador de spans. Batch con parent-based ratio suele tener bajo impacto. Evita recolectar atributos sensibles y spans innecesarios. Mide siempre en staging antes del go‑live.
¿Cómo evitar capturar datos sensibles en los spans?
Define convenciones de atributos. No registres PII. Enmascara o haz hash de identificadores cuando sea necesario. Revisa eventos y atributos antes de producción y en auditorías periódicas.
¿Cómo correlacionar logs con trazas en PHP?
Incluye trace_id y span_id en los logs. En logs JSON, usa campos estables. Así saltas del log a la traza y viceversa. Un patrón de logs consistente acelera mucho el análisis de incidentes.
¿Puedo empezar con muestreo al 100%?
Sí, en entornos de pruebas. En producción, usa parentbased_traceidratio con una razón controlada. Aumenta temporalmente el muestreo cuando investigues incidentes y vuelve al valor seguro después.
¿Necesitas ayuda para aplicarlo en tu monolito?
Si quieres acelerar el rollout de trazado con OpenTelemetry en tu PHP, revisar la instrumentación y estandarizar atributos, hablemos sobre tu contexto.
Hablar sobre tu contextoVer también
- Guía: checklist de pre‑deploy y plan de rollback rápido para aplicaciones PHP en producción
- Checklist de seguridad para pipelines CI/CD con runners self‑hosted y gestión de secretos
- Guía: centralización de logs con Fluentd y Elasticsearch para aplicaciones PHP en VPS
- Guía: organizar migraciones y rollbacks en PostgreSQL para equipos pequeños