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

Symfony Sdk Bundle Laravel Package

open-telemetry/symfony-sdk-bundle

Symfony bundle for integrating the OpenTelemetry PHP SDK into Symfony applications. It provides bundle configuration and container wiring for tracing, metrics, and logs, making it easier to instrument services and export telemetry through configured OpenTelemetry exporters such as OTLP.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the bundle and its transitive SDK dependencies:

composer require open-telemetry/symfony-sdk-bundle

Note the package name on Packagist and the GitHub repo name (contrib-sdk-bundle) do not match — read the repo README's install snippet before you assume the vendor/package string, and pin the minor version you land on.

Register it in config/bundles.php (it is not auto-registered by Flex in most setups):

return [
    // ...
    OpenTelemetry\Contrib\Symfony\OtelSdkBundle\OtelSdkBundle::class => ['all' => true],
];

Then dump the reference config — this is the single most important first step, because the bundle's config tree is the source of truth for key names and it is not fully documented in the README:

bin/console config:dump-reference opentelemetry

Start with a config that exports to the console so you get feedback with zero infrastructure:

# config/packages/opentelemetry.yaml
opentelemetry:
    resource:
        attributes:
            service.name: '%env(OTEL_SERVICE_NAME)%'
            deployment.environment: '%kernel.environment%'
    trace:
        sampler: always_on          # confirm exact key via config:dump-reference
        exporters:
            console: ~              # swap for otlp once you have a collector

First use case: run bin/console cache:clear or hit one route and confirm you see span output (or a trace in Jaeger/Tempo/otel-tidy). If you see nothing, stop and fix that before adding any manual instrumentation — the failure modes are almost always exporter/sampler wiring, not your code.

Then move to a real backend:

opentelemetry:
    trace:
        sampler:
            type: parent_based
            root:
                type: trace_id_ratio
                ratio: 0.1
        exporters:
            otlp:
                endpoint: '%env(OTEL_EXPORTER_OTLP_ENDPOINT)%'
                protocol: http/protobuf

The bundle mostly auto-instruments: it wires the Symfony HttpKernel (one root span per request with route/controller/status attributes), Console commands, the Symfony HttpClient, and (depending on installed packages) Doctrine DBAL, Twig, and Messenger. Your mental model should be "the bundle gives me a request span and an outbound-call span tree for free; I add the business-meaningful spans in the middle."

Where to look when something's off, in order:

  1. bin/console debug:container --tag=opentelemetry (adjust the tag) to see what was actually registered.
  2. config:dump-reference opentelemetry for valid keys.
  3. The bundle's DependencyInjection/Configuration.php on GitHub — it is the real documentation.
  4. Your var/log/dev.log for the SDK's own logger output (missing PSR-18 client and bad endpoints surface here, not as exceptions).

Implementation Patterns

Let auto-instrumentation own the edges. Don't hand-roll a span in a controller when the bundle already emits the request span. Read the parent/child shape in your trace UI first, then add children where they add information. Typical controller-level cleanup: strip noisy spans by disabling individual instrumentations (Twig rendering usually generates more spans than it earns), and keep the DB/HTTP client spans because they're where latency actually is.

Manual spans via injected TracerProviderInterface. This is the day-to-day API you'll write:

use OpenTelemetry\API\Trace\TracerProviderInterface;
use OpenTelemetry\API\Trace\SpanKind;

final class ImportOrders
{
    public function __construct(private TracerProviderInterface $tracerProvider) {}

    public function __invoke(string $feedId): void
    {
        $span = $this->tracerProvider
            ->getTracer('app.import')
            ->spanBuilder('import.orders')
            ->setSpanKind(SpanKind::KIND_INTERNAL)
            ->setAttribute('feed.id', $feedId)
            ->setAttribute('feed.rows', 0)
            ->startSpan();

        $scope = $span->activate();   // required: makes it "current" for children
        try {
            // work; children created here attach to $span
            $span->setAttribute('feed.rows', 1234);
        } catch (\Throwable $e) {
            $span->recordException($e);
            $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR, $e->getMessage());
            throw $e;
        } finally {
            $scope->detach();
            $span->end();
        }
    }
}

The activate()/detach() pair is the part people forget; without it, child spans and auto-instrumented spans inside the block attach to the request span instead, and your trace becomes a flat list.

Add spans at the use-case layer, not the framework layer. Service classes (command handlers, message handlers, importers) are the right place: they're reachable from HTTP, CLI, and Messenger, so one span definition covers all three entry points.

Propagation across async boundaries. For Messenger and queue consumers, inject context in the producer and extract in the consumer so the trace continues rather than starting fresh — or, if the semantics are fire-and-forget, link spans instead of parenting them. The bundle handles W3C traceparent injection for the Symfony HttpClient; it does not do it for raw curl, Guzzle, or a bespoke AMQP envelope, so you inject headers manually with the propagator service for those.

Long-running consumers need explicit flushing. messenger:consume and cron workers never hit the normal shutdown path. Either flush per message in a handler, or ensure the provider is shut down on SIGTERM (BatchSpanProcessor buffers spans; a killed worker loses them silently).

Sampling strategy by environment:

Env Sampler Exporter
dev/test always_on console or in-memory for assertions
staging always_on, 100% OTLP, short retention
prod parent_based + trace_id_ratio 0.05–0.2 OTLP batch → collector (tail-sample there)

Push real sampling decisions to the collector where you can; app-side trace_id_ratio is a blunt tool that will drop the interesting traces along with the boring ones.

Testing. Assert on spans in functional tests using an in-memory exporter: dispatch a request, then inspect recorded spans for name, attributes, and parent/child relationships. This catches the classic regressions — a span that got moved outside its activate() block, a renamed span that breaks dashboards, a missing error status.

Integration tips:

  • Put service.name in the resource, not in span names. One deployment = one service.
  • Keep span names low-cardinality (http.request, import.orders), put the specifics in attributes.
  • Use semantic convention constants from open-telemetry/semconv rather than string literals for http.*, db.*, messaging.*.
  • If you already have Monolog handlers, the OTel log bridge (if wired) should sit as an additional handler, not replace file logs.

Gotchas and Tips

Version skew between api, sdk, semconv, and the bundle is the number one breakage. The bundle pins ranges that drift from the SDK's fast release cadence; a stray composer update on open-telemetry/* gives you Call to undefined method inside the SDK. Pin exact versions in composer.json and upgrade deliberately, all OTel packages together.

Environment variables vs. YAML precedence will bite you. The SDK reads standard OTEL_* vars (OTEL_SERVICE_NAME, OTEL_TRACES_SAMPLER, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SDK_DISABLED) and in several places they win over bundle config, or vice versa depending on the exporter. If behaviour doesn't match your YAML, env | grep OTEL before anything else. OTEL_SDK_DISABLED=true in a Docker base image is a great way to lose an afternoon.

"No HTTP client found" from the OTLP exporter. The exporter needs PSR-18/PSR-17 implementations and does not pull them in. Fix: composer require symfony/http-client nyholm/psr7. Symptom is usually an exception at kernel boot or silent span loss logged at debug level.

Missing spans: work the checklist.

  1. Sampler ratio is 0 (or parent-based and your upstream isn't sampling).
  2. Batch processor never flushed (short-lived CLI process, killed worker) → use simple in CLI or flush on terminate.
  3. Span created but not activate()d, so children orphaned — trace looks wrong, not empty.
  4. Exporter pointed at a collector that isn't reachable — check the SDK log, not the app log.

Sub-requests, ESI, and fragments generate their own kernel spans and can produce confusing nested trees or duplicate "request" roots. If your trace UI is noisy, filter by path or exclude fragment rendering.

Console is a span factory. Cron-heavy apps that instrument every command drown in console.command spans. Disable console instrumentation globally and enable it only where you care, or sample hard on the CLI path.

Cardinality and PII. Never use raw URLs, user IDs, or SQL-with-parameters as span names. db.statement and request bodies are the usual accidental PII leaks — sanitize before setting attributes, and be aware Doctrine instrumentation may capture statements by default.

Sync exporters are a latency tax. A blocking OTLP export on the request path adds tens of milliseconds and couples your p95 to your observability backend. Batch by default in prod; keep console/simple for dev only.

The package is pre-1.0 and lightly used (12 stars, no dependents). Treat its config surface as unstable: read Configuration.php when docs are thin, expect breaking changes between minors, and keep a fallback plan of constructing the SDK manually in a services.yaml factory if the bundle lags the SDK you need. That fallback is genuinely small — a Resource, a SpanProcessor, an exporter, a TracerProvider, then register it as TracerProviderInterface.

Extension points worth knowing:

  • Custom SpanProcessor — decorate the batch processor to redact attributes or drop spans by name before export.
  • Custom ResourceDetector — add host/pod/version attributes beyond the env-var defaults.
  • Custom exporter — wrap the OTLP exporter to add multi-tenant headers or per-service routing.
  • Decorate TracerProviderInterface — cheap global policy layer (force-disable in tests, add common attributes) without touching call sites.

Jaeger exporter is gone. If you're following an old tutorial that configures a jaeger exporter, it no longer exists — send OTLP to the collector and let it fan out to Jaeger/Tempo.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource