Symfony
The traceway/opentelemetry-symfony bundle automatically instruments your Symfony application and exports traces, metrics, and logs to Traceway's OTLP endpoints. No manual instrumentation is needed.
Prerequisites
- PHP 8.1+ (no PECL extension needed, the bundle is pure PHP)
- Symfony 6.4 LTS, 7.x, or 8.x
- Composer
- A Traceway project (create one in the dashboard)
Note: the upstream
open-telemetry/opentelemetry-auto-*instrumentations require theext-opentelemetryPECL extension. This bundle does not, so it installs on managed hosts where you cannot build extensions.ext-protobufis only worth adding if you switch the exporter tohttp/protobuf.
Step 1: Install Packages
composer require traceway/opentelemetry-symfony open-telemetry/exporter-otlp php-http/guzzle7-adapterThe bundle pulls in open-telemetry/sdk for you. open-telemetry/exporter-otlp and php-http/guzzle7-adapter are the OTLP/HTTP exporter and its HTTP client, and both are needed at runtime to actually ship data to Traceway.
Step 2: Register the Bundle
Flex does not do this for you on a stock Symfony app. The bundle's recipe lives in symfony/recipes-contrib, and new apps ship with extra.symfony.allow-contrib set to false, so composer require prints IGNORING traceway/opentelemetry-symfony and registers nothing. Add it to config/bundles.php yourself:
// config/bundles.php
return [
// ...
Traceway\OpenTelemetryBundle\OpenTelemetryBundle::class => ['all' => true],
];The bundle is what produces the spans. Without it registered, the SDK starts but nothing instruments your app, and Traceway stays empty.
Step 3: Configure Environment Variables
Add the following to your .env file, replacing the endpoint and token with your project values:
OTEL_SERVICE_NAME=my-symfony-app
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_ENDPOINT=https://your-traceway-instance.com/api/otel
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-project-token"Your Traceway dashboard includes a Connection page with a ready-made config snippet and your project token. Go to Connection in the sidebar to grab it.
Do not put
OTEL_PHP_AUTOLOAD_ENABLEDin.env. OpenTelemetry PHP decides whether to start during Composer autoload, which happens before Symfony's Dotenv reads.env. The variable is always read too late there. Worse, once Dotenv has loaded it the bundle's own fallback (Step 4) sees it as already set and skips starting the SDK, so every signal silently becomes a no-op. Step 4 shows the two places the flag actually works.
Note:
http/jsonneeds no extra extensions and is the easiest starting point.http/protobufalso works, but the pure-PHP protobuf encoder is much slower. Installext-protobuf(pecl install protobuf) if you switch to it in production.grpcis not included out of the box and additionally needsext-grpcplusopen-telemetry/transport-grpc.
Step 4: Turn the SDK On
The instrumentation bundle and the OpenTelemetry SDK are two different things. Step 2 wired the bundle. This step starts the SDK that actually exports data. Pick one of the two options below.
Option A (recommended): let the bundle start it
Create config/packages/open_telemetry.yaml:
# config/packages/open_telemetry.yaml
open_telemetry:
sdk:
autoload_enabled: trueThe bundle sets OTEL_PHP_AUTOLOAD_ENABLED during its boot() and loads the SDK's autoload file itself. This works everywhere, including symfony server:start and php -S, and it works with Symfony Secrets. It is the only option that does not need access to the server config.
Option B: set a real process environment variable
If you control the process environment, set the flag there instead and skip the YAML key:
; php-fpm pool config
env[OTEL_PHP_AUTOLOAD_ENABLED] = trueENV OTEL_PHP_AUTOLOAD_ENABLED=trueSetEnv OTEL_PHP_AUTOLOAD_ENABLED truePick one option, not both.
Leave public/index.php alone
You never call SdkAutoloader::autoload() by hand, and you never edit the front controller. open-telemetry/sdk registers its _autoload.php as a Composer files autoload entry, so it runs on every request already. The stock Symfony front controller is correct as generated:
<?php
// public/index.php (leave this file exactly as Symfony generated it)
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};Replacing vendor/autoload_runtime.php with vendor/autoload.php drops symfony/runtime, which is what boots Dotenv. Your .env then never loads, so both the OTEL variables and every %env(...)% in your config break. A typical app starts throwing Environment variable not found: "DATABASE_URL".
Step 5: Verify
bin/console cache:clear
bin/console traceway:doctorA healthy setup looks like this:
Traceway Doctor
═══════════════
Runtime
○ protocol is http/json; ext-protobuf not required
✓ ext-opentelemetry not loaded (no conflict risk)
○ protocol is http/json; gRPC transport not required
SDK configuration
✓ OTEL_SERVICE_NAME = "my-symfony-app"
✓ OTEL_TRACES_EXPORTER = otlp
✓ OTEL_EXPORTER_OTLP_ENDPOINT = https://your-traceway-instance.com/api/otel
✓ OTEL_EXPORTER_OTLP_PROTOCOL = http/json
✓ OTEL_TRACES_SAMPLER unset (defaults to parentbased_always_on)
✓ TracerProvider is TracerProvider
Bundle configuration
○ propagator=w3c, id_generator=default; X-Ray not configured
✓ Messenger tracing enabled and symfony/messenger is installed
✓ Log export wired (LoggerProvider: LoggerProvider)
Connectivity
✓ OTLP endpoint reachable (HTTP 404, 6ms)
Results: 10 ok, 0 warning, 0 error, 3 skipped, 0 infoYour counts will differ. The Messenger and Connectivity lines above need symfony/messenger and symfony/http-client to be installed. On an app without them you get ⚠ traces.messenger.enabled is true but symfony/messenger is not installed and a skipped reachability probe. Neither affects HTTP tracing.
The line that matters most is TracerProvider. If you see this instead:
✗ TracerProvider is NoopTracerProvider — spans are silently droppedthe SDK never started. Nothing in your code will be exported, and there will be no error anywhere to tell you. Go back to Step 4 and make sure exactly one of the two options is in place, and that OTEL_PHP_AUTOLOAD_ENABLED is not in .env.
bin/console traceway:doctor --format=json --skip-network prints a stable JSON envelope, so you can gate CI on it.
Exclude cache:clear from console tracing
In the dev environment bin/console cache:clear prints a red block like this:
In DebugScope.php line 79:
User Notice: Scope: missing call to Scope::detach() for scope #608, created
at OpenTelemetry.Context.Context.activate(Context.php:82)The cache is still cleared and the command still exits 0. The notice comes from OpenTelemetry's debug scope tracking, which only runs when kernel.debug is on, and cache:clear rebuilds the kernel underneath the console span. Add cache:clear to the excluded command list and it goes away:
# config/packages/open_telemetry.yaml
open_telemetry:
traces:
console:
excluded_commands: ['messenger:consume', 'messenger:consume-messages', 'cache:clear']You lose nothing by excluding it. cache:clear is a build step, not a job worth tracking on the Tasks page.
Complete Working Configuration
Everything above in one place. This is the full set of files needed for endpoints, issues, spans, tasks, logs, and metrics.
composer require traceway/opentelemetry-symfony open-telemetry/exporter-otlp php-http/guzzle7-adapter
composer require symfony/monolog-bundle # only if you want logs// config/bundles.php (add this yourself, Flex skips the contrib recipe)
return [
// ...
Traceway\OpenTelemetryBundle\OpenTelemetryBundle::class => ['all' => true],
];# .env (note that OTEL_PHP_AUTOLOAD_ENABLED is deliberately not here)
OTEL_SERVICE_NAME=my-symfony-app
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_ENDPOINT=https://your-traceway-instance.com/api/otel
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-project-token"# config/packages/open_telemetry.yaml
open_telemetry:
sdk:
autoload_enabled: true # starts the SDK; Dotenv runs too late to do it
traces:
excluded_paths: ['/_profiler', '/_wdt', '/health']
console:
excluded_commands: ['messenger:consume', 'messenger:consume-messages', 'cache:clear']
messenger:
root_spans: true # each consumed message becomes its own Task
metrics:
enabled: true
http_server:
enabled: true
logs:
export:
enabled: true
level: debug # raise to info or warning in productionLeave public/index.php exactly as Symfony generated it. Then run bin/console cache:clear and bin/console traceway:doctor.
What Gets Captured
Once configured, the bundle automatically captures:
- Endpoints: every HTTP request, grouped by route template (
GET /users/{id}, notGET /users/42) - Status codes: the real response code, including 500s from unhandled exceptions
- Exceptions: unhandled errors become Issues with a full PHP stack trace (see Exceptions)
- Client IP, body size, user agent: standard HTTP attributes
- Doctrine queries: child spans per query if
doctrine/dbalis installed (see Spans) - Twig renders, cache operations, outgoing HttpClient calls, Mailer sends: child spans, each individually toggleable
- Console commands: every
bin/consolerun lands on the Tasks page (see Tasks) - Messenger jobs: dispatched and consumed messages (see Tasks)
- Monolog records: trace id and span id injected into every log record, plus opt-in forwarding to Traceway (see Logs)
Unrouted requests such as 404s group under UNMATCHED rather than leaking the raw URL into your endpoint list.
Configuration Reference
Every option is optional. The keys below are the v2.0+ nested schema. The old flat keys (excluded_paths, messenger_root_spans, log_export_enabled, and friends) still load but emit a deprecation and are removed in v4.0.
# config/packages/open_telemetry.yaml
open_telemetry:
sdk:
autoload_enabled: true # start the SDK from bundle config
resource_attributes:
service.version: '%env(APP_VERSION)%' # becomes the App Version column
traces:
excluded_paths: # matched as path prefixes
- /_profiler
- /_wdt
- /health
record_client_ip: true # default true, set false for GDPR
error_status_threshold: 500 # minimum 500, a value of 400-499 fails at boot
console:
enabled: true
excluded_commands: ['messenger:consume', 'messenger:consume-messages']
messenger:
root_spans: false # true = each consumed message is its own trace
doctrine:
record_statements: false # true records the SQL text on the span
http_client:
enabled: true # OTLP endpoint is auto-excluded
cache:
enabled: true
twig:
enabled: true
mailer:
enabled: true
record_subject: false # subjects can contain personal data
metrics:
enabled: false # master switch, off by default
http_server:
enabled: false
logs:
correlation:
enabled: true # trace_id / span_id in every log record
export:
enabled: false # forward Monolog records to Traceway
level: debugSetting service.version is worth doing early. It fills Traceway's App Version column, which is how you tell one release apart from the next when an Issue starts spiking.
The on/off switches above are read at compile time to decide which services get wired, so they must be plain true or false values. Use separate config/packages/dev/ and config/packages/prod/ files to vary them per environment. %env(...)% placeholders do work inside sdk.resource_attributes and sdk.exporter_otlp_headers, because those are only read at boot.
Test Your Integration
Add a test route to verify data is flowing:
// src/Controller/TestController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class TestController
{
#[Route('/testing', name: 'testing')]
public function index(): Response
{
throw new \RuntimeException("Test error from Traceway integration");
}
}Visit /testing in your browser. Within a few seconds you should see two things in the dashboard:
- Endpoints shows
GET /testingwith status code 500 - Issues shows
RuntimeException: Test error from Traceway integrationwith a PHP stack trace pointing atTestController.php
Both come from the same request. Symfony's error listener turns the throwable into a 500 response, and the bundle records the exception on the request span, so the endpoint row and the Issue agree.
Next Steps
- Exceptions: manually capture caught exceptions with context
- Spans: create custom spans to measure sub-operations
- Tasks: trace Symfony Messenger jobs and console commands
- Metrics: track custom counters, histograms, and gauges
- Logs: forward Monolog / PSR-3 logs to Traceway
- OpenTelemetry docs: how OTel traces, metrics, and logs map to Traceway