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

Instrumentation Bundle Laravel Package

worldia/instrumentation-bundle

Symfony bundle that adds OpenTelemetry instrumentation to your app, with automatic tracing for HTTP requests, console commands, Doctrine, Messenger, and HTTP clients. Collect and export traces and metrics to your observability backend.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and enable. Add the bundle with Composer and register it in config/bundles.php. This bundle is instrumentation only — it does not ship an exporter. You still need the OpenTelemetry SDK plus an exporter (OTLP is the usual choice) and a TracerProviderInterface / MeterProviderInterface in the container. If you already run the Symfony OpenTelemetry SDK bundle or wire the providers by hand, this bundle just decorates your services and subscribes to your events.

composer require worldia/instrumentation-bundle
// config/bundles.php
Worldia\Bundle\InstrumentationBundle\InstrumentationBundle::class => ['all' => true],

Where to look first. Two files answer 90% of "can it do X?" questions:

  1. src/DependencyInjection/Configuration.php — the authoritative config tree. Everything under tracing.* and metrics.* is declared here, including which features default to true vs false and which have blacklist / methods nodes.
  2. src/DependencyInjection/Extension.php (loadTracing() / loadMetrics()) — the feature loop. This tells you exactly which <feature>.php file is loaded under what condition, and whether a feature is gated on an optional dependency (interface_exists()).

Then read the matching config/tracing/<feature>.php to see the concrete service ids being decorated or the events being subscribed to.

First use case — request + Doctrine traces into a local collector. Run otel/opentelemetry-collector (or Jaeger all-in-one) locally, enable request and doctrine tracing, hit a page, and confirm a root server span with a child DB span appears. That single round-trip validates: exporter wiring, provider wiring, bundle boot, and span propagation. Do this before touching Messenger or metrics — those have more moving parts (async boundaries, separate toggles).

Then add metrics. Metrics are opt-in per feature (booleanNode('enabled')->defaultFalse()), so you can adopt them incrementally after tracing is proven. The core metrics.php (MeterProvider plumbing) always loads; only the per-feature subscribers are gated.

Implementation Patterns

Tracing and metrics are independent subsystems — never bolt one onto the other. Even when both observe the same event, the bundle keeps them as separate subscribers under separate config toggles (compare src/Tracing/Messenger/... with src/Metrics/EventSubscriber/MessageEventSubscriber.php). If you need both, write both, don't decorate.

From application code, use the static facade. Instrumentation\Tracing\Tracing::getTracer() exists precisely for app code where DI is inconvenient. Use it to wrap domain operations the bundle can't know about:

$span = Tracing::getTracer()->spanBuilder('order.recalculate')->startSpan();
try {
    // ...
} finally {
    $span->end();
}

From bundle/library code, inject the provider. TracerProviderInterface via the constructor, then consume it through TracerAwareTrait::getTracer(). Never the facade. Same rule for metrics: inject MeterProviderInterface and call $meterProvider->getMeter('instrumentation') — there is no Metrics::getMeter() equivalent.

Wrapping services: pick decorator vs subscriber deliberately.

  • Decorator when you can wrap an interface and the span must outlive the call (deferred/async results). Model: TracingHttpClient. For deferred results, end the span at consumption and add a __destruct() backstop (see TracedResponse).
  • EventSubscriber when Symfony gives you start/end events. Models: Request, Command, Messenger.

Overriding semantics instead of forking. Every attribute provider and span-name resolver is an interface bound to a default impl in config/semconv/semconv.php. To change span names or add attributes for your domain, redefine the service in config/services.yaml — you get the bundle's wiring and your own semantics without touching vendor code.

Filtering with the blacklist voter. Each feature has an optional blacklist node copied into tracing.<feature>.blacklist as a param, consumed by a Voter extending AbstractVoter (regex matching → VOTE_DROP / VOTE_ABSTAIN). Use it to drop noisy health-check routes, cache:clear commands, or fire-and-forget bus messages. Note it is a drop mechanism, not head sampling — it prevents the span from being created at all.

Adding a new instrumentation follows a five-part shape (enforced by convention, and reviewers will reject anything else): swappable AttributeProvider (+ semconv constants, no inline attribute strings), swappable OperationNameResolver, both registered in semconv.php, a blacklist Voter, and DI wiring across config/tracing/<feature>.php → Configuration.php → Extension::loadTracing(). For features with multiple tagged services you must discover them at compile time via a dedicated *CompilerPass — register an ->abstract() template service and invoke the pass from Extension::process() gated on $container->hasDefinition(Template::class).

Adding a metric follows a flatter shape. New src/Metrics/EventSubscriber/<Feature>EventSubscriber.php implementing EventSubscriberInterface, MeterProviderInterface injected, meter cached in the constructor, instruments created at the record site (creation is idempotent by name). Naming: snake_case, unit suffix (_seconds, _bytes), _total for monotonic counters. Attributes are plain strings here — metrics historically do not use semconv constants, though you should prefer a published semconv metric name when one exists (e.g. gen_ai.client.token.usage).

Gotchas and Tips

Deferred spans leak silently. A span that only ends in a downstream callback (Guzzle promise resolution, an async response body) will never end if the caller discards the result. The bundle's TracedResponse handles this with a destructor backstop — replicate that pattern in anything you write, or you'll get a pile of unterminated spans and a confused trace view.

Don't call the static facade from bundle code. Tracing::getTracer() reads a global; it works, but it breaks testability and diverges from every existing listener. The rule is: facade for application code, injected TracerProviderInterface for everything inside the bundle. Same divergence applies to metrics — there is deliberately no facade, so don't add one.

Attribute-name literals are the classic review rejection. If a semconv constant exists for a key, use it — OpenTelemetry\SemConv\TraceAttributes (flat) and Attributes\* / Incubating\Attributes\* (split). Grep the installed open-telemetry/sem-conv package before inventing a name; names move between incubating and stable across versions. Only genuinely bundle-specific keys (e.g. messenger.bus) may be inline.

Blacklist params must be set before the feature file loads. Extension::loadTracing() copies blacklist/methods into tracing.<feature>.* params before $loader->load('<feature>.php'). If you hand-edit a feature config file expecting the param to exist, it won't. Configure through the bundle config tree, not by editing vendor config.

Optional-dependency features fail loudly at compile time if not gated. Features like doctrine or messenger are wrapped in interface_exists() / class_exists() checks. If you build a custom instrumentation around an optional package, gate it the same way or the container will fail to compile in apps that don't install it.

Cardinality is your problem, not the bundle's. Auto-instrumentation emits SQL statements, messenger class names, and HTTP routes as attributes. Before shipping, check what your attribute providers actually emit and use the blacklist / a custom provider to collapse high-cardinality values (raw URLs with IDs, SQL with bound params). A trace backend bill is a much better teacher than a code review.

Use a local collector for debugging, and check env precedence. If spans don't appear, verify in this order: (1) a TracerProviderInterface is actually registered in the container, (2) the feature is enabled and not blacklisted, (3) the exporter endpoint env var (OTEL_EXPORTER_OTLP_ENDPOINT) matches the collector, (4) the span is being ended. Most "missing span" reports die at step 4.

Sampling is split across two mechanisms. The blacklist voter is a pre-creation drop filter; head sampling belongs to the SDK's sampler. If you want "10% of requests", configure the sampler — don't try to emulate it with regex blacklists, which are deterministic per-route and will bias your data badly.

The bundle is small (28 stars, zero known dependents) — pin it and read the source. That's not a red flag for an instrumentation bundle (the surface is narrow and the conventions are strict), but it does mean you are the integration test. Pin to a tagged release, and when something looks wrong, src/Tracing/<Feature>/ is short enough to read end-to-end. The skill docs shipped in the repo (add-tracing-instrumentation, add-metric) are the most reliable specification of intent — treat them as the contract when opening a PR upstream.

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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment