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

Opentelemetry Propagation Server Timing Laravel Package

open-telemetry/opentelemetry-propagation-server-timing

Propagates OpenTelemetry trace context through the Server-Timing HTTP header, helping PHP applications correlate backend traces with browser and frontend requests for simpler debugging and performance analysis.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal install

composer require open-telemetry/opentelemetry-propagation-server-timing

This is a propagator, not an exporter — on its own it produces nothing. You need open-telemetry/sdk, a tracer provider, and an exporter already wired up (OTLP, console, whatever you use). The propagator's whole job is to write trace context into the HTTP response's Server-Timing header so that a browser-side consumer (frontend OTel SDK, RUM agent, custom JS) can extract it and stitch frontend spans onto the backend trace.

Enable it

Contrib packages in this family ship a _register.php in the Composer files autoload that registers the propagator factory with the SDK Registry under a short name (typically server-timing). Once installed, opt in via config rather than code:

OTEL_PROPAGATORS=tracecontext,baggage,server-timing

If your bootstrap builds the provider manually, register it explicitly instead:

use OpenTelemetry\SDK\Registry;
use OpenTelemetry\Contrib\Propagation\ServerTiming\ServerTimingPropagatorFactory;

Registry::registerTextMapPropagatorFactory('server-timing', new ServerTimingPropagatorFactory());

Where to look first

  1. src/ — the propagator class itself. Read fields() and inject(); the header name(s) it emits and the exact Server-Timing syntax it uses (entry name, desc, whether it also writes dur) are the contract you'll be parsing on the frontend and appending to on the backend.
  2. _register.php — confirms the registry key you must put in OTEL_PROPAGATORS.
  3. The README's usage example — specifically which PropagationSetterInterface it expects for a PSR-7/PSR-15 response, since Laravel does not use PSR-7.
  4. The upstream OTel "Server-Timing" propagation discussion — this is a non-core, proposal-level convention, so the wire format is worth verifying against your frontend before you build on it.

First use case: open DevTools → Network → any request → Timing tab, confirm an entry named after the propagator appears, then curl -sI https://your.app/ | grep -i server-timing and check that the value round-trips into a traceparent on the frontend. That five-minute loop validates install, registration, and injection point before you touch application code.

Implementation Patterns

The Laravel injection point

Propagation must happen after the response object exists but before it is sent. A middleware wrapping $next($request) is the correct seam:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\Context\Propagation\ArrayAccessGetterSetter;
use Symfony\Component\HttpFoundation\Response;

class ServerTimingTrace
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $context = Span::getCurrent()->getContext();
        if (!$context->isValid()) {
            return $response;
        }

        // Array carrier + array setter: avoids depending on a PSR-7 setter
        // from a package that has never seen a Symfony Response.
        $carrier = [];
        Globals::propagator()->inject($carrier, new ArrayAccessGetterSetter(), $context);

        if (!empty($carrier['Server-Timing'])) {
            // third arg = false => append, do not clobber existing metrics
            $response->headers->set('Server-Timing', $carrier['Server-Timing'], false);
        }

        return $response;
    }
}

The array-carrier trick is the single most useful pattern here: it decouples you from whatever setter the package documents, and works identically for Symfony responses, JsonResponse, and streamed responses.

Append, never replace

Server-Timing is a list header and your app (or a profiler bundle) probably already writes app;dur=12.3. Symfony's HeaderBag::set($key, $values, $replace = true) — pass false. If you're on raw PHP or another framework, join with , . Keep the OTel entry last so your own metrics stay human-readable in DevTools.

Which context to inject

Inject the local root span's context, not the currently active child span, if you want the browser's traceparent to point at the trace as a whole:

$rootSpan = Span::getCurrent(); // inside middleware this is usually the server span
$context  = $rootSpan->getContext();

If you have custom request-scoped spans, resolve the intended one explicitly and store it via Context::storage() in an earlier middleware rather than relying on ambient state — especially with Octane, where "ambient" is a moving target.

Scope the propagator deliberately

Globals::propagator() returns the composite propagator from OTEL_PROPAGATORS. If you list baggage alongside this package, baggage entries land in Server-Timing too — a public header. Either:

  • inject only the trace-context propagator:
$propagator = new \OpenTelemetry\API\Propagation\MultiTextMapPropagator([
    new \OpenTelemetry\API\Propagation\TraceContextPropagator(),
    new ServerTiming(),
]);
  • or accept it, but audit every baggage key for PII/tenant IDs.

Frontend handoff

Expose the header and parse it in the browser:

$response->headers->set('Access-Control-Expose-Headers', 'Server-Timing', false);
// @opentelemetry/sdk-trace-web
const entry = performance.getEntriesByType('resource')[0]
  ?.serverTiming?.find(e => e.name.includes('trace'));
// build a carrier from the parsed value, then:
propagation.extract(context.active(), { traceparent: value });

performance resource timing is the natural reader — no XHR hook needed — but sampling of resource timing buffers and cross-origin restrictions apply.

Testing

Feature-test the header directly; you don't need a real collector:

$response = $this->get('/');
$response->assertHeader('Server-Timing');
$this->assertStringContainsString('traceparent', $response->headers->get('Server-Timing'));

Bypass the middleware with withoutMiddleware(ServerTimingTrace::class) in the rest of your suite so header assertions elsewhere don't get noisy, and run one test with an in-memory exporter to confirm the trace ID in the header matches the root span.

Integration tips

  • Register the middleware globally (bootstrap/app.php → $middleware->append()), after whatever middleware establishes the trace, and before SubstituteBindings is irrelevant — order only matters relative to the tracing middleware.
  • Add it to the web group only if you don't want machine-to-machine API traffic carrying trace IDs in a public header.
  • Combine with a Vary or explicit CDN cache-key rule if you cache HTML (see Gotchas).
  • For queued jobs and console commands, there is no response — the propagator is inert, so no guard clause needed beyond the isValid() check.

Gotchas and Tips

_register.php and stale autoloaders. After install, run composer dump-autoload. A missing registration fails silently — no exception, no header, no warning. First debugging step is always composer show -a open-telemetry/opentelemetry-propagation-server-timing plus confirming the factory key appears in your effective OTEL_PROPAGATORS.

Injecting in terminate() is too late. Laravel's terminating middleware runs after the response is flushed; the header will never reach the client. Same trap with StreamedResponse — set headers before invoking the callback.

Header overwrite kills your own metrics. The most common integration bug: $response->headers->set('Server-Timing', $value) (default $replace = true) silently destroys whatever your profiler or APM agent put there. Always append.

Full-page caches and CDNs corrupt trace topology. If a cached response ships the original request's traceparent, every subsequent visitor inherits a stale trace ID and your browser spans attach to a months-old trace. Strip Server-Timing in your cache layer, or exclude the header from the cache key and regenerate it on the edge.

Some proxies drop or rewrite Server-Timing. It's frequently treated as infrastructure noise by CDNs and WAFs. Verify end-to-end through your real edge, not just from localhost.

Public header, public data. Server-Timing is visible to any client and to browser tooling. Trace IDs are low-risk, but desc fields carrying span names, tenant IDs, or baggage are a real disclosure. Sanitize or inject only the trace-context propagator.

Sampling asymmetry. A non-sampled trace still yields a valid traceparent (flag 00). Decide whether to emit it at all: emitting lets the frontend create spans that are then dropped at the collector; omitting keeps the header honest but breaks correlation when the backend is sampled and the frontend isn't configured to respect the flag. Make the choice explicit in the middleware.

Octane / Swoole context bleed. Long-lived workers keep PHP process state across requests. Never cache the injected carrier or a propagator instance on a singleton; resolve Globals::propagator() per request, and ensure the tracing middleware properly detaches scopes so request N's context isn't injected into request N+1's response.

Pre-1.0, zero dependents, one star. Treat the API as unstable: pin the exact version in composer.json, and wrap the injection in your own tiny class (ServerTimingInjector) that returns a plain string. When the package churns or is abandoned — a real risk for a READONLY contrib module with no dependents — you swap one 20-line class instead of hunting call sites.

Version/date sanity. The listed last release date is ahead of today, which is either a mirroring artifact or a re-tagged build. Diff the tag against the previous one before upgrading; don't assume semantic versioning discipline.

Custom wire format? If you need a different entry name, a dur value, or a second header, don't fork — implement TextMapPropagatorInterface in ~30 lines and compose it with MultiTextMapPropagator alongside TraceContextPropagator. The interface is stable even when this package isn't; that's your real extension point.

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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk