Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tracewayapp/opentelemetry-symfony-bundle/llms.txt

Use this file to discover all available pages before exploring further.

Automatic instrumentation covers framework-level operations, but business logic — order processing, third-party API calls, custom queue handling — often deserves its own spans. TracingInterface gives you a single trace() method that wraps any callable in a properly lifecycle-managed span, handles exceptions, and propagates context, all without the ceremony of building a tracer, starting a span, and managing scopes manually.

Interface Signature

<?php

namespace Traceway\OpenTelemetryBundle;

use OpenTelemetry\API\Trace\SpanKind;
use Throwable;

interface TracingInterface
{
    /**
     * Execute a callable inside a new span.
     *
     * @template T
     *
     * @param non-empty-string   $name       Span name (e.g. "db.query", "http.client", "cache.get")
     * @param callable(): T      $callback   The work to trace
     * @param array<string, mixed> $attributes Span attributes set before the callback runs
     * @param SpanKind::KIND_*   $kind       Span kind (defaults to INTERNAL)
     *
     * @return T The callback's return value
     *
     * @throws Throwable
     */
    public function trace(
        string $name,
        callable $callback,
        array $attributes = [],
        int $kind = SpanKind::KIND_INTERNAL,
    ): mixed;
}
The concrete Tracing implementation also implements Symfony’s ResetInterface, so span state is cleared automatically between Messenger worker cycles, FrankenPHP loops, and RoadRunner requests — no stale context leaks between jobs.

Injecting and Using TracingInterface

Use constructor injection to receive the service. Because you typehint against the interface rather than the concrete class, swapping it out in tests requires no container override.
<?php

use OpenTelemetry\API\Trace\SpanKind;
use Traceway\OpenTelemetryBundle\TracingInterface;

class OrderService
{
    public function __construct(private readonly TracingInterface $tracing) {}

    public function process(int $orderId): void
    {
        $this->tracing->trace('order.validate', fn () => $this->validate($orderId));

        $this->tracing->trace('order.fulfill', function () {
            $this->tracing->trace('inventory.reserve', fn () => $this->reserve());
            $this->tracing->trace('payment.charge', fn () => $this->charge());
        });
    }
}
The order.fulfill span automatically becomes the parent of inventory.reserve and payment.charge because trace() activates the span as the current context for the duration of the callback. No manual context threading is required.

Parameters Reference

ParameterTypeDefaultDescription
$namenon-empty-string(required)Span name shown in your backend — use dot-namespaced names like db.query, payment.charge, inventory.reserve for readability
$callbackcallable(): T(required)The callable to execute inside the span; its return value is returned from trace() unchanged
$attributesarray<string, mixed>[]Key-value span attributes applied before the callback runs — useful for IDs, types, and context that are known upfront
$kindSpanKind::KIND_*KIND_INTERNALOTel span kind: KIND_INTERNAL (default), KIND_CLIENT (outgoing RPC/DB calls), KIND_SERVER (inbound calls), KIND_PRODUCER (message publish), KIND_CONSUMER (message processing)

Setting Span Attributes

Attributes are set before the callback executes, so they appear on the span even if the callback throws:
$this->tracing->trace(
    'payment.charge',
    fn () => $this->chargeCard($amount, $currency),
    attributes: [
        'payment.amount'   => $amount,
        'payment.currency' => $currency,
        'payment.provider' => 'stripe',
    ],
    kind: SpanKind::KIND_CLIENT,
);

Exception Handling

If the callback throws, trace() calls recordException() and sets STATUS_ERROR on the span before re-throwing the original exception. Your error budgets and alerting rules on the backend will see the failure without any extra code.

Testing

Because all service code typehints against TracingInterface, testing is straightforward with PHPUnit stubs:
<?php

use Traceway\OpenTelemetryBundle\TracingInterface;

class OrderServiceTest extends TestCase
{
    public function testProcessCallsValidateAndFulfill(): void
    {
        $tracing = $this->createStub(TracingInterface::class);

        // Make trace() invoke the callback directly so business logic runs normally
        $tracing->method('trace')
            ->willReturnCallback(fn (string $name, callable $cb) => $cb());

        $service = new OrderService($tracing);
        $service->process(42);

        // ... assert on side effects
    }
}
No real OTel SDK is needed in unit tests. The stub invokes the callback directly, so your business logic executes normally and you can assert on its effects without any span overhead or export infrastructure.
For integration tests that should assert on span output, configure an in-memory exporter in your test environment. The stub approach is recommended for unit tests where you care about logic, not observability.

Behavior Without an Active SDK

When OTEL_PHP_AUTOLOAD_ENABLED is not set to true (or when no SDK is configured), TracingInterface::trace() checks whether the tracer is enabled and, if not, invokes the callback directly without creating any span. There is no performance overhead and no errors — the service degrades gracefully to a plain function call.
// In a local dev environment with no OTEL_PHP_AUTOLOAD_ENABLED=true,
// this is equivalent to calling $this->validate($orderId) directly.
$this->tracing->trace('order.validate', fn () => $this->validate($orderId));
ResetInterface and long-running processesTracing implements Symfony’s ResetInterface. The Messenger worker, FrankenPHP, and RoadRunner all call reset() between requests/messages, which clears the cached tracer reference and enabled flag. This guarantees that a stale “enabled=false” decision from one cycle does not carry over to the next after the SDK is reloaded.

Build docs developers (and LLMs) love