OpenTelemetry
Laravel
Spans

Spans

The package creates spans for HTTP requests, DB queries, queue jobs, cache calls, view renders, and outbound HTTP automatically. To measure sub-operations like business logic, third-party API calls, or batched work, create spans using the Tracer facade.

Using the Tracer Facade

keepsuit/laravel-opentelemetry ships a Tracer facade. The standard pattern is to build a span with newSpan('name') and then call measure(...) (auto-activates the span, runs the callback, ends the span):

use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
 
class OrderService
{
    public function processOrder(int $orderId): void
    {
        Tracer::newSpan('order.validate')->measure(function () use ($orderId) {
            $this->validateOrder($orderId);
        });
 
        Tracer::newSpan('order.charge')->measure(function () use ($orderId) {
            $this->chargePayment($orderId);
        });
    }
}

measure returns the callback's return value, so you can capture results:

$total = Tracer::newSpan('order.total')->measure(fn () => $this->calculateTotal());

Exception handling inside measure: if the callback throws, measure calls $span->recordException($e) and rethrows. It does not call setStatus(STATUS_ERROR). The exception event is attached to the span, but the span's status code stays UNSET. TracePath's Issues feed still picks up the exception event, but if you want the span to be marked as failed in the trace view, set the status yourself:

use OpenTelemetry\API\Trace\StatusCode;
 
Tracer::newSpan('charge')->measure(function ($span) {
    try {
        $this->stripe->charge();
    } catch (\Throwable $e) {
        $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
        throw $e;
    }
});

Or if the exception goes through Laravel's exception handler (i.e., bubbles up out of measure), the package's auto-registered reportable() callback sets the status on the still-active span before measure's finally ends it, so you usually don't need to do this manually.

With Attributes

Set attributes on the active span inside your callback:

use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
use OpenTelemetry\API\Trace\Span;
 
$result = Tracer::newSpan('stripe.charge')->measure(function () use ($amount) {
    Span::getCurrent()->setAttribute('payment.amount', $amount);
    Span::getCurrent()->setAttribute('payment.currency', 'usd');
 
    return $this->stripe->charge($amount);
});

Nested Spans

Child spans created with measure are automatically activated, so any further spans nest under them:

Tracer::newSpan('order.fulfill')->measure(function () {
    Tracer::newSpan('inventory.reserve')->measure(fn () => $this->reserve());
    Tracer::newSpan('payment.charge')->measure(fn () => $this->charge());
    Tracer::newSpan('email.send')->measure(fn () => $this->notify());
});

Manual Span Management

If you need finer control (e.g., spans that cross function boundaries), start and end the span yourself. You must call activate() to make it the parent for new spans, then detach() the scope when done:

use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
 
$span = Tracer::newSpan('long-running-job')->start();
$scope = $span->activate();
 
try {
    $this->doWork();
} finally {
    $scope->detach();
    $span->end();
}

Always end spans in a finally block so they are closed even if an exception is thrown.

If the span is the root of its trace, set the span kind. The snippet above is safe inside an HTTP request, a queue job or an instrumented Artisan command, where the span is a child of something TracePath already records. Started on its own (a cron closure, a hand-rolled background loop, a non-instrumented command) it is a root INTERNAL span, and TracePath discards root INTERNAL spans that carry no HTTP attributes and no console.command. The span, every child under it, and the trace linkage for any logs emitted inside are dropped with no error.

use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
use OpenTelemetry\API\Trace\SpanKind;
 
$span = Tracer::newSpan('long-running-job')
    ->setSpanKind(SpanKind::KIND_CONSUMER)
    ->start();

setSpanKind() takes any SpanKind::KIND_* constant and is available on the builder returned by Tracer::newSpan(). A CONSUMER root lands in Tasks. See Tasks.

Other Tracer Utilities

The Tracer facade exposes a few helpers that come in handy when working with custom spans:

use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
 
Tracer::traceId();              // the active trace id (string)
Tracer::activeSpan();           // the currently-active SpanInterface
Tracer::activeScope();          // the currently-active ScopeInterface
Tracer::currentContext();       // the OTel Context object, for advanced use
Tracer::propagationHeaders();   // headers to inject into outbound requests so downstream services join the trace
Tracer::extractContextFromPropagationHeaders($headers);  // build a Context from inbound headers
Tracer::updateLogContext();     // inject the active trace id into Laravel's log context (only needed if you start the root span manually)

The builder returned by Tracer::newSpan() also has setSpanKind(), setParent(), setAttribute(), setAttributes() and setStartTimestamp(), each of which returns the builder so calls chain before start() or measure().

Adding Attributes

Attach metadata to spans for filtering and debugging:

use OpenTelemetry\API\Trace\Span;
 
$span = Tracer::newSpan('db-query')->start();
$scope = $span->activate();
try {
    $span->setAttribute('db.system', 'mysql');
    $span->setAttribute('db.statement', 'SELECT * FROM users WHERE id = ?');
    $span->setAttribute('db.row_count', $rowCount);
} finally {
    $scope->detach();
    $span->end();
}

Recording Errors on Spans

When a span's operation fails, record the exception and set the status:

use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
use OpenTelemetry\API\Trace\StatusCode;
 
$span = Tracer::newSpan('external-api-call')->start();
$scope = $span->activate();
try {
    $response = \Http::get('https://api.example.com/data');
} catch (\Throwable $e) {
    $span->recordException($e);
    $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
    throw $e;
} finally {
    $scope->detach();
    $span->end();
}

Span Naming Conventions

Use descriptive names that indicate the operation type:

GoodBad
db.users.findquery
cache.sessions.getcache
stripe.chargeapi
s3.upload-imageupload
email.send-welcomesend