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-otlpWire OpenTelemetryTracer
Replace NoOpTracer with OpenTelemetryTracer in 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
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.3Supported Backends
Any OTLP-compatible backend works — point OTEL_EXPORTER_OTLP_ENDPOINT at the collector and spans flow automatically.
| Backend | OTLP Endpoint | Guide |
|---|---|---|
| Grafana Tempo | http://tempo:4318/v1/traces | Tempo setup |
| Jaeger | http://jaeger:4318/v1/traces | Jaeger setup |
| Datadog Agent | http://datadog-agent:4318/v1/traces | Datadog setup |
| AWS X-Ray | Via OpenTelemetry Collector | OTel Collector docs |
| Google Cloud Trace | Via OpenTelemetry Collector | OTel 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.