Guía para implementar trazado distribuido con OpenTelemetry en aplicaciones PHP monolito

Por · Publicado el · Actualizado el · 10 min de lectura

patrones monocromáticos con bloques cuadrados formando una línea sinuosa sobre fondo claro
Representación abstracta de flujo y conexión, adecuada para trazado distribuido en arquitectura Foto: Google DeepMind / Pexels

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 contexto

Ver también