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.

Metrics are disabled by default to keep the bundle zero-config for tracing-only setups. Enable them with a single config key and the bundle begins emitting OpenTelemetry metrics for Messenger, Doctrine DBAL, HTTP server and client, and Mailer — all following OTel semantic conventions. For custom business metrics, inject MeterRegistryInterface and record counters, histograms, and up/down counters without touching MeterProvider directly.

Enabling Metrics

# config/packages/open_telemetry.yaml
open_telemetry:
    metrics:
        enabled: true
Set the exporter via environment variable:
OTEL_METRICS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
Each sub-integration can be individually toggled:
open_telemetry:
    metrics:
        enabled: true
        meter_name: 'opentelemetry-symfony'

        messenger:
            enabled: true
            excluded_queues: []

        doctrine:
            enabled: true

        http_server:
            enabled: true
            excluded_paths: []

        http_client:
            enabled: true
            excluded_hosts: []

        mailer:
            enabled: true

Built-in Instruments

Messenger

These instruments are emitted on both the dispatch and consume sides of the Messenger bus.
InstrumentKindUnitSourceAttributes
messaging.process.durationHistogramsMessenger consumemessaging.system, messaging.operation.name, messaging.operation.type, messaging.destination.name, error.type on failure
messaging.client.consumed.messagesCounter{message}Messenger consumeSame as messaging.process.duration
messaging.client.operation.durationHistogramsMessenger dispatchmessaging.system, messaging.operation.name=send, messaging.operation.type=send, messaging.destination.name (from SentStamp::getSenderAlias(), falls back to sender FQCN)
messaging.client.sent.messagesCounter{message}Messenger dispatchSame as messaging.client.operation.duration
excluded_queues matches the transport name on both sides: ReceivedStamp::getTransportName() on consume and SentStamp::getSenderAlias() on dispatch. An envelope dispatched to multiple transports emits one metric point per non-excluded transport.

Doctrine DBAL

InstrumentKindUnitSourceAttributes
db.client.operation.durationHistogramsDBAL connectiondb.system.name, db.namespace, server.address, server.port, db.operation.name, db.collection.name (primary table when extractable), error.type on failure
SQL text is never recorded in metrics. Only the leading SQL keyword is stored as db.operation.name (e.g. SELECT, INSERT) and the primary table name as db.collection.name when it can be extracted. This prevents unbounded label cardinality and avoids leaking query parameters into your metrics backend.

HTTP Server

InstrumentKindUnitSourceAttributes
http.server.request.durationHistogramsHTTP serverhttp.request.method, url.scheme, http.route (when matched), http.response.status_code, server.address, server.port, error.type on failure
http.server.active_requestsUpDownCounter{request}HTTP serverhttp.request.method, url.scheme, server.address, server.port
http.server.request.body.sizeHistogramByHTTP serverSame as http.server.request.duration (emitted only when Content-Length is present)
http.server.response.body.sizeHistogramByHTTP serverSame as http.server.request.duration (emitted only when Content-Length is present)
Only main requests are measured. Sub-requests are already covered by the main request’s duration, so they are excluded to avoid double-counting. Service identity comes from the OTel resource (OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES), not from metric name prefixing.
http.server.request.duration and error.type carry Stable OTel stability; the body size instruments are Development.

HTTP Client

InstrumentKindUnitStabilityAttributes
http.client.request.durationHistogramsStablehttp.request.method, server.address, server.port, url.scheme, http.response.status_code on response, error.type on transport failure
http.client.request.body.sizeHistogramByDevelopmentSame as duration (emitted when Content-Length header or a string body is present)
http.client.response.body.sizeHistogramByDevelopmentSame as duration (emitted when response Content-Length is set or the body is fully read)
The OTLP endpoint (OTEL_EXPORTER_OTLP_ENDPOINT) is always auto-excluded from HTTP client metrics to prevent the exporter itself from generating metric data. Use excluded_hosts to skip additional hostnames.

Mailer

InstrumentKindUnitStabilityAttributes
messaging.client.operation.durationHistogramsDevelopmentmessaging.system=symfony_mailer, messaging.operation.name=send, messaging.operation.type=send, messaging.destination.name (from X-Transport header when present), error.type on failure
messaging.client.sent.messagesCounter{message}DevelopmentSame as duration
Mailer metric points are recorded while the trace span is still active. Backends that support exemplars can link directly from a metric data point to the corresponding trace, enabling one-click drill-down from a latency spike in a dashboard to the exact span that caused it. The same exemplar linkage applies to HTTP client metrics.

Manual Metrics

Inject MeterRegistryInterface to record your own counters, histograms, and up/down counters without dealing with MeterProvider or MeterInterface boilerplate.

MeterRegistryInterface Signature

<?php

namespace Traceway\OpenTelemetryBundle\Metrics;

use OpenTelemetry\API\Metrics\CounterInterface;
use OpenTelemetry\API\Metrics\HistogramInterface;
use OpenTelemetry\API\Metrics\UpDownCounterInterface;

interface MeterRegistryInterface
{
    public function counter(
        string $name,
        ?string $unit = null,
        ?string $description = null,
    ): CounterInterface;

    public function histogram(
        string $name,
        ?string $unit = null,
        ?string $description = null,
    ): HistogramInterface;

    public function upDownCounter(
        string $name,
        ?string $unit = null,
        ?string $description = null,
    ): UpDownCounterInterface;
}

Example: Custom Download Counter

<?php

use OpenTelemetry\API\Metrics\CounterInterface;
use Traceway\OpenTelemetryBundle\Metrics\MeterRegistryInterface;

final class MediaDownloader
{
    private readonly CounterInterface $downloads;

    public function __construct(MeterRegistryInterface $metrics)
    {
        $this->downloads = $metrics->counter(
            'media.download.count',
            description: 'Media downloads by outcome',
        );
    }

    public function download(string $url): void
    {
        try {
            // ... download logic
            $this->downloads->add(1, ['outcome' => 'success']);
        } catch (\Throwable $e) {
            $type = $e::class;
            if (str_contains($type, '@anonymous')) {
                $type = get_parent_class($e) ?: \Throwable::class;
            }
            $this->downloads->add(1, ['outcome' => 'error', 'error.type' => $type]);
            throw $e;
        }
    }
}
The @anonymous guard in the example normalises PHP anonymous-class names (which embed a filesystem path) to their parent class. Without this, each unique instantiation site creates a distinct label value, causing unbounded cardinality in your metrics backend.

Instrument Caching

The registry caches instruments by name: calling ->counter('media.download.count') multiple times returns the same CounterInterface instance. This matches the OTel specification requirement that instruments with the same name share a single underlying instrument.

Behavior Without an Active SDK

When the OTel SDK is not configured (no OTEL_PHP_AUTOLOAD_ENABLED=true or no OTEL_METRICS_EXPORTER), the MeterRegistry lazily resolves the meter from the global NoOp provider. All ->add() / ->record() calls become no-ops — no exceptions, no overhead. It is safe to inject MeterRegistryInterface unconditionally in production services. MeterRegistry also implements ResetInterface, so its cached meter and instrument maps are cleared between Messenger worker cycles, FrankenPHP loops, and RoadRunner requests.
For the full list of metrics.* configuration keys, see the Configuration Reference.

Build docs developers (and LLMs) love