Vortos
Tracing

OpenTelemetry

Plug in OpenTelemetryTracer to replace NoOpTracer and send spans to a collector — Jaeger, Tempo, Datadog, or any OTLP-compatible backend.

OpenTelemetry

By default, TracingInterface is backed by NoOpTracer — zero overhead, no dependencies. To enable distributed tracing, replace the inner tracer with OpenTelemetryTracer.

Current Status

OpenTelemetryTracer is included in vortos-tracing (src/Tracing/OpenTelemetry/). It wraps the open-telemetry/sdk PHP package. The wiring to replace NoOpTracer is done in config/services.php — not automatic yet.

OpenTelemetryTracer implements Symfony's ResetInterface. In FrankenPHP worker mode, Vortos calls Runner::cleanUp() after each request, and cleanup detaches any OpenTelemetry scopes that are still active.

Install OpenTelemetry SDK

composer require open-telemetry/sdk open-telemetry/exporter-otlp

Wire OpenTelemetryTracer

Replace NoOpTracer with OpenTelemetryTracer in config/services.php:

config/services.php
use Vortos\Tracing\OpenTelemetry\OpenTelemetryTracer;
use Vortos\Tracing\NoOpTracer;

// Replace the inner tracer in the decorator chain
$services->set(NoOpTracer::class, OpenTelemetryTracer::class)
    ->arg('$tracer', service('otel.tracer'))   // inject OTel SDK tracer
    ->public(false);

Scope cleanup

Every startSpan() activates an OpenTelemetry scope. Ending the span detaches that scope. If a request exits with scopes still active, OpenTelemetryTracer::reset() detaches them during framework cleanup.

$span = $this->tracer->startSpan('billing.charge');

try {
    // work
    $span->setStatus('ok');
} catch (\Throwable $e) {
    $span->recordException($e);
    $span->setStatus('error');
    throw $e;
} finally {
    $span->end();
}

Always end spans yourself. Reset exists to keep long-running workers safe when something goes wrong.

The decorator chain wires itself around whatever is registered as NoOpTracer::class. Replacing this registration swaps the inner tracer without touching SamplingTracer or ModuleAwareTracer.

Configure the OTel SDK

config/services.php
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\Contrib\Otlp\OtlpHttpSpanExporter;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;

$services->set(OtlpHttpSpanExporter::class)
    ->arg('$endpoint', $_ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? 'http://collector:4318/v1/traces');

$services->set(BatchSpanProcessor::class)
    ->arg('$exporter', service(OtlpHttpSpanExporter::class));

$services->set(TracerProvider::class)
    ->call('addSpanProcessor', [service(BatchSpanProcessor::class)]);

$services->set('otel.tracer')
    ->factory([service(TracerProvider::class), 'getTracer'])
    ->arg('$name', 'vortos');

Environment Variables

# OTLP endpoint (Jaeger, Tempo, Datadog Agent, etc.)
OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces

# Service name shown in the tracing UI
OTEL_SERVICE_NAME=myapp

# Resource attributes
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=prod,service.version=1.2.3

Supported Backends

Any OTLP-compatible backend works — point OTEL_EXPORTER_OTLP_ENDPOINT at the collector and spans flow automatically.

BackendOTLP EndpointGuide
Grafana Tempohttp://tempo:4318/v1/tracesTempo setup
Jaegerhttp://jaeger:4318/v1/tracesJaeger setup
Datadog Agenthttp://datadog-agent:4318/v1/tracesDatadog setup
AWS X-RayVia OpenTelemetry CollectorOTel Collector docs
Google Cloud TraceVia OpenTelemetry CollectorOTel Collector docs

For Docker Compose configuration, backend-specific setup steps, and verification, see the integration guides linked above.

NoOp is always safe

If OTEL_EXPORTER_OTLP_ENDPOINT is unreachable or OpenTelemetry is not configured, NoOpTracer ensures the application continues normally with zero overhead. Tracing failures never affect application availability.

On this page