Skip to main content
Distributed tracing lets you follow a request as it moves through your application and across services. Each unit of work is captured as a span, and a collection of related spans forms a trace. Spans record timing, attributes, and status, giving you a detailed picture of where time is spent and where errors occur. This package automatically instruments common Laravel operations (HTTP requests, database queries, queue jobs, and more). You can also create spans manually for any code you want to trace.

Creating spans

Use the Tracer facade to create custom spans. newSpan returns a SpanBuilder you can configure before starting the span.

Using measure

The simplest approach wraps a callback. The span is automatically activated (so it becomes the parent for any nested spans), and it ends when the callback returns — even if an exception is thrown.
use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;

Tracer::newSpan('my custom trace')->measure(function () {
    // do something
});
The SpanInterface instance is passed to the callback, so you can add attributes or record events:
use OpenTelemetry\API\Trace\SpanInterface;

Tracer::newSpan('process-order')->measure(function (SpanInterface $span) {
    $span->setAttribute('order.id', $orderId);
    // process the order
});

Manual span management

For finer control, start and end the span yourself:
use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;

$span = Tracer::newSpan('my custom trace')->start();

// do something

$span->end();
With start, the span is not automatically set as the active span. Child spans created inside the same block will not see it as their parent unless you activate it (see below).

Activating a span

To make a manually started span the active span (so nested spans inherit it as parent), activate it with a scope:
use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;

$span = Tracer::newSpan('my custom trace')->start();
$scope = $span->activate();

// do something — spans created here will be children of $span

$scope->detach();
$span->end();
Always detach the scope before ending the span, and always end the span after detaching.

Configuring spans

The SpanBuilder returned by newSpan exposes several builder methods before you call start or measure:
MethodDescription
setAttribute(string $key, mixed $value)Set a single attribute on the span
setAttributes(iterable $attributes)Set multiple attributes at once
setParent(?ContextInterface $context)Override the parent context
setSpanKind(int $spanKind)Set the span kind (e.g. SpanKind::KIND_CLIENT)
setStartTimestamp(CarbonInterface|int $timestamp)Set a custom start timestamp
addLink(SpanContextInterface $context, iterable $attributes)Link to another span

Tracer facade reference

The Tracer facade provides utility methods for working with the active trace context:
use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;

// Check whether a trace is currently active
Tracer::traceStarted();

// Get the active trace ID (returns null when no trace is active)
Tracer::traceId();

// Get the currently active span
Tracer::activeSpan();

// Get the currently active scope
Tracer::activeScope();

// Get the current trace context (useful for advanced use cases)
Tracer::currentContext();

// Get propagation headers to pass to outgoing requests
Tracer::propagationHeaders();

// Extract a context from incoming propagation headers
Tracer::extractContextFromPropagationHeaders($headers);

// Inject the current trace ID into the Laravel log context
Tracer::updateLogContext();

// End all active spans up to (and not including) an optional root span
Tracer::terminateActiveSpansUpToRoot();

Distributed tracing across services

When calling an external service, inject the current trace context into the outgoing request headers so the downstream service can continue the same trace.
use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
use Illuminate\Support\Facades\Http;

$response = Http::withHeaders(Tracer::propagationHeaders())
    ->get('https://api.example.com/endpoint');
When receiving a request from another service, extract the trace context from the incoming headers and use it as the parent for your spans:
use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;

$context = Tracer::extractContextFromPropagationHeaders($request->headers->all());

Tracer::newSpan('handle-downstream-request')
    ->setParent($context)
    ->measure(function () {
        // handle the request
    });
The HTTP server and HTTP client instrumentations propagate trace context automatically. Manual propagation is only needed when using custom transports or non-Laravel HTTP clients.

Log context

When a trace is active, the current trace ID can be automatically injected into log records so you can correlate logs with traces in your observability backend.
  • Automatic: When using the built-in instrumentations (e.g. HTTP server), the trace ID is injected into the log context for the duration of the request.
  • Manual: When you start a root span manually, call Tracer::updateLogContext() after activating the span to inject the trace ID.
$span = Tracer::newSpan('my-job')->start();
$scope = $span->activate();

Tracer::updateLogContext();

// logs written here will include the trace_id field
Log::info('Processing started');

$scope->detach();
$span->end();
This behaviour is controlled by two config options in config/opentelemetry.php:
OptionDefaultDescription
logs.inject_trace_idtrueEnable trace ID injection into the log context
logs.trace_id_fieldtrace_idThe field name used in the log context
When you use the otlp log channel, the trace ID is always included in exported log records automatically — you do not need to call updateLogContext().

Build docs developers (and LLMs) love