Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Api Laravel Package

open-telemetry/api

OpenTelemetry API for PHP provides vendor-neutral interfaces for traces, metrics, logs, and context propagation, letting you instrument applications without binding to a specific backend.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to understand is that open-telemetry/api is only the contract layer — interfaces, no-op implementations, context storage, baggage, and propagation. Installing it alone wires everything to no-ops: your code compiles, runs, and exports nothing. In a Laravel app you almost always get it transitively via the SDK:

composer require open-telemetry/sdk \
                 open-telemetry/exporter-otlp \
                 open-telemetry/opentelemetry-auto-laravel

Verify it landed:

composer show open-telemetry/api
# open-telemetry/api  1.x.x  API for OpenTelemetry PHP.

Where to look first (all under OpenTelemetry\API\):

Namespace / Class Why you care
Globals The single entry point: Globals::tracerProvider(), meterProvider(), loggerProvider(), propagator(), registerTracerProvider()
Trace\Span Span::getCurrent() — grab the active span anywhere, no DI
Trace\TracerInterface / SpanBuilderInterface Building spans manually
Context\Context / Context\Scope Activation/detachment of the current span & baggage
Baggage\Baggage Cross-service key/value propagation
Common\Time\Clock The PSR-20 clock used for span timestamps (moved into API in 1.1)

First use case — a span around a slow service call inside a Laravel service class:

use OpenTelemetry\API\Globals;

$tracer = Globals::tracerProvider()->getTracer('app', '1.0.0');

$span = $tracer->spanBuilder('pdf.render')
    ->setSpanKind(\OpenTelemetry\API\Trace\SpanKind::KIND_INTERNAL)
    ->setAttribute('pdf.template', $template)
    ->startSpan();

$scope = $span->activate();
try {
    $result = $this->renderer->render($template);
    return $result;
} catch (\Throwable $e) {
    $span->recordException($e);
    $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR, $e->getMessage());
    throw $e;
} finally {
    $scope->detach();
    $span->end();
}

That is the whole day-to-day loop: start → activate → work → detach → end. Everything else is attributes, status, and propagation.


Implementation Patterns

Don't hand-roll HTTP middleware — decorate the auto-instrumented span

open-telemetry/opentelemetry-auto-laravel already creates a server span for each request. Rather than starting a second one, enrich the current one in a middleware:

public function handle($request, Closure $next)
{
    $span = \OpenTelemetry\API\Trace\Span::getCurrent();

    if ($span->isRecording()) {          // cheap guard, see Gotchas
        $span->setAttribute('app.tenant', tenant()->id);
        $span->setAttribute('app.locale', app()->getLocale());
    }

    return $next($request);
}

Register it late in the web group so the route (and therefore route.action) is resolved when it runs.

Service-layer instrumentation with cheap guards

Wrap the domain calls that can be slow or fail — PDF rendering, third-party HTTP, report generation — not controllers. Guard attribute construction, since building attributes isn't free:

$span = Span::getCurrent();
if ($span->isRecording()) {
    $span->setAttribute('order.items', $order->items->count());
    $span->setAttribute('order.total', $order->total);
}

Queue jobs: link back to the dispatch site

Auto-instrumentation handles the producer/consumer handshake, but a Link makes the causal chain explicit when a job is dispatched from a request:

$span = Globals::tracerProvider()->getTracer('app')
    ->spanBuilder('invoice.email')
    ->addLink(Span::getCurrent()->getContext())
    ->startSpan();

Better still: use the WithSpan attribute from OpenTelemetry\API\Instrumentation on the job's handle() method if auto-instrumentation is loaded — you get span creation, activation, and error recording for free.

Propagation on outbound HTTP

Guzzle middleware via the SDK's open-telemetry/opentelemetry-guzzle (contrib) covers this, but if you're using raw Http::withHeaders() the API lets you inject traceparent yourself:

$carrier = [];
Globals::propagator()->inject($carrier);

Http::withHeaders($carrier)->post('https://billing.internal/charge', $payload);

And on the way in, extract from a PSR-7 request before starting your own span:

$parent = Globals::propagator()->extract(
    $request->headers->all()  // flatten to scalar values first
);
$span = Globals::tracerProvider()
    ->getTracer('app')
    ->spanBuilder('webhook.handle')
    ->setParent($parent)
    ->startSpan();

Baggage for tenant/user correlation

Set baggage once per request (in a middleware), then read it in any service without threading parameters through:

$baggage = \OpenTelemetry\API\Baggage\Baggage::getBuilder()
    ->set('tenant.id', (string) tenant()->id)
    ->set('user.id', (string) auth()->id())
    ->build();

$ctx = \OpenTelemetry\API\Context\Context::storage()->get()
    ->withContextValue($baggage);

$scope = $ctx->activate();
// ... later, anywhere:
// Baggage::fromContext(Context::storage()->get())->getEntry('tenant.id')->getValue()

Baggage travels automatically to downstream services via the propagator.

Metrics for business counters

The API is identical across the SDK, so metric code written now survives exporter changes:

$meter = Globals::meterProvider()->getMeter('app');
$ordersCreated = $meter->createCounter('orders.created', 'orders', 'Orders created');

$ordersCreated->add(1, ['plan' => $tenant->plan]);

For anything derived from existing state (queue depth, active subscriptions), use an observable/callback instrument rather than incrementing on every code path.

Structured log correlation

Build log records through Logger::logRecordBuilder() so trace_id/span_id attach automatically, then bridge Monolog via open-telemetry/opentelemetry-logger-monolog. Do not hand-format [trace_id=...] into the message — the exporter's attributes are what your backend queries.

Config via env (Twelve-Factor friendly)

OTEL_SERVICE_NAME=my-laravel-app
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
OTEL_PHP_DISABLED_INSTRUMENTATIONS=laravel           # local dev escape hatch
OTEL_LOG_LEVEL=debug

Testing

Register a test provider with an in-memory exporter, assert on finished spans, and never rely on a real collector in CI. Globals::registerTracerProvider() is the hook; keep a TestCase helper that swaps in a fresh provider per test so span state doesn't leak between tests.


Gotchas and Tips

No-op by default is the #1 time sink. If Globals::tracerProvider() returns a NoopTracerProvider, every call succeeds, every span is a no-op, and nothing appears in your backend. Diagnose with OTEL_LOG_LEVEL=debug, and confirm the SDK's Sdk::builder() actually ran (typically via the composer autoloader file the SDK installs). A missing composer dump-autoload --optimize after installing the auto-instrumentation packages silently skips bootstrap.

Detach before end, always in finally. Forgetting $scope->detach() leaves the span active, so every subsequent span in the request becomes its child. Forgetting $span->end() means the span is never exported — no error, just silence.

Octane and long-running workers. A worker process keeps its provider and context storage alive across requests. Any span you activate must be detached in finally, and any baggage you set must be scoped to the request. If you see span trees growing across requests under Octane, you have a leaked scope — check that custom middleware isn't returning early before detach().

isRecording() guards, not just isEnabled(). Sampling decisions are made at span start. Checking Span::getCurrent()->isRecording() before computing expensive attributes avoids costing CPU on the 90% of requests your sampler drops. The same applies to metric callbacks — they're always weakened for async observers, so don't assume the callback runs on the thread that registered it.

Attribute value rules are strict. Only scalars and homogeneous arrays of scalars are accepted. Nested arrays, null, and mixed arrays are silently dropped (see the 1.0.0beta10 change "drop attributes with non-homogenous array values"). For anything structured, json_encode() it and send a string, or flatten with dotted keys (order.item.0.sku).

Baggage is size-capped. W3C limits (8192 bytes / 180 entries) were enforced in 1.10.0. Never put payloads, JWTs, or user profiles in baggage — put an ID in baggage and look up the rest.

Deprecations to steer around. InstrumentationInterface and ConfigurationResolver are deprecated as of 1.9.0 — use Globals plus env/file config. The event logger was deprecated in 1.2.0 in favor of Logger::logRecordBuilder(). Writing new code against the deprecated surface means a rewrite in the next minor.

Status semantics matter to your backend. Set STATUS_ERROR with a description on failure; leave status Unset on success. Marking spans STATUS_OK explicitly breaks many backends' error-rate calculations and conflicts with the spec's guidance.

The SpanAttribute attribute needs hooks. #[SpanAttribute] on a method parameter is only interpreted by the auto-instrumentation extension (opentelemetry-php-instrumentation) plus a registered hook. In plain PHP without the extension it is inert metadata — don't expect attributes to appear from a normal method call.

Extension vs. no extension. Auto-instrumentation of third-party libraries requires the native ext-opentelemetry. Laravel's own auto-instrumentation works without it, but if a package's spans never appear, check php -m | grep opentelemetry before debugging config.

Pin API and SDK together. They're separate Packagist packages released from one monorepo. Mixing a fresh open-telemetry/sdk with a stale open-telemetry/api (or a beta contrib instrumentation package with stable core) produces subtle interface-mismatch errors. Constrain both to the same minor and let Composer resolve.

Context::storage() is process-global static state. That's what makes Span::getCurrent() work without DI — and also what makes tests order-dependent. Reset context between tests; don't let one test's activated span bleed into the next.

Extension points for custom needs: implement TextMapPropagatorInterface for a bespoke header format, or a SamplerInterface for per-tenant sampling (sample 100% for your enterprise tenants, 1% for free tier). Both plug in via the SDK builder, and your application code never changes.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator