open-telemetry/opentelemetry-auto-laravel
Automatic OpenTelemetry instrumentation for Laravel apps. It hooks into the framework to emit traces, metrics, and logs for HTTP requests, database queries, queue jobs, and more, with no manual code changes. Add it to get distributed tracing and observability out of the box.
Understand what you're actually installing first. This is not a service provider you register in config/app.php. It's a set of _register.php hook definitions discovered at process boot by the native opentelemetry PHP extension, which globs vendor/open-telemetry/opentelemetry-auto-*. No extension → no hooks → no spans → no errors. Verify that before anything else:
pecl install opentelemetry
php -m | grep opentelemetry
php --ri opentelemetry
Minimum viable install:
composer require \
open-telemetry/sdk \
open-telemetry/exporter-otlp \
open-telemetry/opentelemetry-auto-laravel
Then set these in the real process environment (php-fpm pool, systemd unit, Docker env) — not .env, which Laravel reads too late:
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_SERVICE_NAME=my-laravel-app
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
First use case — prove the pipeline before tuning anything. Point at the console exporter locally (OTEL_TRACES_EXPORTER=console, add open-telemetry/exporter-console) and hit one route that runs an Eloquent query. You should see a server span for the route plus child spans for the SQL. That single trace validates extension load, autoload discovery, exporter wiring, and sampler in one shot.
Where to look first:
vendor/open-telemetry/opentelemetry-auto-laravel/_register.php and src/LaravelInstrumentation.php — this is the documentation. It lists exactly which Laravel methods are hooked and what attributes each emits. Read it before assuming a span exists.composer show open-telemetry/* — the resolved SDK/exporter versions are what you'll quote in bug reports.Enrich framework spans at the edges; don't replace them. The package gives you HTTP, DB, queue, and console spans for free. The day-to-day win is adding your own context via a global middleware:
public function handle($request, Closure $next)
{
$span = \OpenTelemetry\API\Trace\Span::getCurrent();
$span->setAttribute('enduser.id', $request->user()?->id ?? 'anonymous');
$span->setAttribute('tenant.id', tenant()->id);
return $next($request);
}
Span::getCurrent() returns whatever the instrumentation activated, so your app never imports the package directly — good for keeping vendor options open.
Custom spans inside business logic. Use Globals::getTracer() so you inherit the SDK the extension bootstrapped, and always scope-activate:
$tracer = \OpenTelemetry\API\Globals::getTracer('app.billing');
$span = $tracer->spanBuilder('invoice.generate')->startSpan();
$scope = $span->activate();
try {
// work
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR);
throw $e;
} finally {
$scope->detach();
$span->end();
}
Wrap this in a small helper early — otherwise the boilerplate gets copy-pasted into every service class.
Queued jobs are the sharp edge. Inline (sync) dispatch stays in the same trace. For Redis/SQS/database drivers the worker is a separate process; verify whether the job span is a child or a new root on your version. If orphaned, inject the trace carrier on dispatch and extract it in a job middleware on the worker side.
Console, scheduler, and workers. Artisan commands get spans from the package's console hooks. Long-running processes (queue:work, horizon, schedule:work) interact with the SDK's auto-shutdown — see Gotchas.
Tune per environment. Local: console exporter for zero-infrastructure verification. Production: OTLP with a batch processor and ratio sampling:
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.05
OTEL_BSP_MAX_EXPORT_BATCH_SIZE=512
OTEL_BSP_SCHEDULE_DELAY=1000
Silence noisy instrumentation without code changes. OTEL_PHP_DISABLED_INSTRUMENTATIONS=laravel disables this package entirely; useful for bisecting whether a span came from here or another auto-* package.
Test against an in-memory exporter, not the collector. Build a Sdk with an in-memory SpanExporter in a test base class, hit a route with $this->get('/orders'), and assert on span names/attributes. This catches the most likely regression: a Laravel upgrade that moves an internal method the hook targets.
.env is too late. The extension and SDK read OTEL_* at process start; Laravel reads .env during bootstrap. Variables in .env silently do nothing. Put them in the php-fpm pool (env[OTEL_...]), Docker, or systemd.OTEL_PHP_AUTOLOAD_ENABLED=true, or a vendor/ path the extension can't glob → zero spans, zero errors. Always confirm with the console exporter before blaming the backend.require-dev. The extension discovers the package in vendor/ at runtime. composer install --no-dev in production removes it and you lose all traces with no failure mode.composer why-not open-telemetry/opentelemetry-auto-laravel and check the hook targets in _register.php still exist.setAutoShutdown(false) and flush explicitly; watch for spans that never end and for state bleeding across requests.OTEL_LOG_LEVEL=debug plus the collector's logs split "not instrumented" from "not exported." Do that before touching code.db.statement is on by default in your version; redact at the collector rather than patching vendor code.vendor/open-telemetry/opentelemetry-auto-* and disable what you don't need./up, /horizon, /telescope) will dominate span volume. Filter them at the collector, not in app code.SpanProcessor for tail-based enrichment, ResourceInfo for deployment metadata, and baggage for propagating tenant/user IDs across services. The package stops at span creation — everything downstream is standard OpenTelemetry PHP, so the SDK docs apply.How can I help you explore Laravel packages today?