mmft24 / elastic-apm-symfony-bundle
Integrate Elastic APM into Symfony
Package info
github.com/mmft24/elastic-apm-symfony-bundle
Type:symfony-bundle
pkg:composer/mmft24/elastic-apm-symfony-bundle
Fund package maintenance!
Requires
- php: ^8.3
- psr/log: ^1.0|^2.0|^3.0
- symfony/config: ^6.4|^7.0|^8.0
- symfony/console: ^6.4|^7.0|^8.0
- symfony/dependency-injection: ^6.4|^7.0|^8.0
- symfony/event-dispatcher: ^6.4|^7.0|^8.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
Requires (Dev)
- matthiasnoback/symfony-dependency-injection-test: ^6.2
- monolog/monolog: ^3.0
- nyholm/symfony-bundle-test: ^3.1
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^12.5|^13.0
- rector/rector: ^2.2
- symfony/framework-bundle: ^6.4|^7.0|^8.0
- symfony/messenger: ^6.4|^7.0|^8.0
- symfony/phpunit-bridge: ^8.0.0
- symfony/property-access: ^6.4|^7.0|^8.0
- symfony/property-info: ^6.4|^7.0|^8.0
- symfony/serializer: ^6.4|^7.0|^8.0
- symplify/easy-coding-standard: ^13.0
- vimeo/psalm: ^6.13
Suggests
- ext-elastic_apm: To send data to Elastic APM. Without it the bundle falls back to a no-op interactor.
- monolog/monolog: ^3.0 to add trace.id/transaction.id/span.id to log records (elastic_apm.log_correlation).
- symfony/messenger: To propagate distributed tracing through Symfony Messenger and trace consumed messages as transactions.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-19 14:27:47 UTC
README
This bundle integrates the Elastic APM PHP API into Symfony. For more information about Elastic APM, please visit https://www.elastic.co/apm. This bundle adds a lot more essentials. Here's a quick list:
-
Better transaction naming strategy: Your transaction traces can be named accurately by route names, the controller name, the request URI or you can decide on a custom naming strategy via a seamless interface that uses any naming convention you deem fit. While running console commands, it also sets the transaction name as the command name.
-
Console Commands Enhancements: While running console commands, it sets the options and arguments passed via the CLI as custom parameters to the transaction trace for easier debugging (sensitive values are redacted), and sets the transaction result (
CLI <exit code>) and outcome. -
Exception Listening: It also captures all Symfony exceptions in web requests and console commands and sends them to Elastic APM.
-
Interactor Service: It provides access to most of the Elastic APM API via a Service class
ElasticApmInteractorInterface::class. This can be injected into any class, controller or service to communicate with APM. If the defaultAdaptiveInteractoris used then any APM calls will be ignored when the extension isn't loaded (for example in development environments).$this->apm->addLabel('name', 'john');
-
Logging Support: In development, you are unlikely to have Elastic APM setup. There's a configuration to enable logging which outputs all actions to your Symfony log, hence emulating what it would actually do in production.
-
Warnings and deprecations: PHP warnings and deprecations are reported to APM, deduplicated per request.
-
Symfony Messenger tracing (opt-in): traces flow from the producer to the worker, and each consumed message becomes its own transaction.
-
Log ↔ trace correlation (opt-in): Monolog records carry
trace.id,transaction.idandspan.idso Kibana can link logs and traces.
Requirements
- PHP 8.3+
- Symfony 6.4, 7.x or 8.x
- The
elastic_apmPHP extension to actually send data (without it the bundle falls back to a no-op interactor) - Optional:
symfony/messengerfor Messenger tracing,monolog/monolog^3 for log correlation
Upgrading from 2.x
3.0 contains a few breaking and behaviour changes; see the CHANGELOG for the full list. In short:
ElasticApmInteractorInterfacehas a new method,beginCurrentTransactionFromHeaders(). Custom interactors must implement it.ConfigurationExceptionnow extends\InvalidArgumentExceptioninstead of\ErrorException.- The warning and deprecation listeners now actually report (they never registered their error handlers in 2.0.0).
Set
warnings: false/deprecations: falseto keep them out of APM. - With
transaction_naming: controller,[Controller::class, 'action']controllers are namedCallback controller: App\Controller::action()instead ofUnknown Symfony controller. interactor: autonow resolves toAdaptiveInteractorand detects the agent at runtime.
Installation
Step 0: Install Elastic APM
Follow https://www.elastic.co/guide/en/apm/agent/php/current/intro.html.
Step 1: Add dependency
With Symfony Flex (Recommended)
composer require mmft24/elastic-apm-symfony-bundle
The bundle will be automatically registered in config/bundles.php.
Without Symfony Flex
composer require mmft24/elastic-apm-symfony-bundle
Then manually register the bundle in config/bundles.php:
<?php return [ // ... ElasticApmBundle\ElasticApmBundle::class => ['all' => true], ];
Step 2: Configuring Elastic APM
You should review all the configuration items for the agent extension here, https://www.elastic.co/guide/en/apm/agent/php/current/configuration.html. These must be set either through environment variables or php.ini. These cannot be set during the request and so the bundle does not support setting them.
Step 3: Configure the bundle
Create config/packages/elastic_apm.yaml with the following options:
elastic_apm: enabled: true # Defaults to true logging: false # If true, logs all interactions to a PSR-3 logger (default: false) interactor: ~ # The interactor service that is used. Setting enabled=false will override this value deprecations: true # If true, reports E_USER_DEPRECATED to Elastic APM (default: true) warnings: true # If true, reports E_WARNING/E_USER_WARNING to Elastic APM (default: true) report_silenced_errors: false # If true, also reports warnings silenced with @ or excluded by error_reporting() (default: false); deprecations are always reported track_memory_usage: false # If true, records peak memory usage memory_usage_label: memory_usage # The name of the custom label to write memory usage to custom_labels: # Labels added to every transaction team: payments custom_context: # Custom context added to every transaction deployment: blue exceptions: enabled: true # If true, sends exceptions (default: true) unwrap_exceptions: false # If true, also sends the previous/nested exceptions (default: false) capture_min_status_code: 500 # Min HTTP status an HttpException must carry to be reported; lower to 400 to capture 4xx signal (default: 500). Request exceptions (e.g. SuspiciousOperationException) count as 400; on the main request HttpKernel already converts them to BadRequestHttpException ignored_exceptions: # List of exception classes to ignore (subclasses are ignored too) - An\Ignored\Exception http: enabled: true transaction_naming: route # route, controller, uri or service (see below) transaction_naming_service: ~ # Transaction naming service (see below) commands: enabled: true # If true, enhances CLI commands with options and arguments (default: true) explicitly_collect_exceptions: true # Turn this off if you are experiencing multiple reports of exceptions. sensitive_parameter_names: [] # Extra option/argument names whose values are redacted (see note below) messenger: enabled: false # Opt-in; requires symfony/messenger. Read the rollout notes below first transaction_type: message # Type of the transaction started for each consumed message ignored_retryable_exceptions: [] # Exception classes/interfaces not reported while the message will be retried log_correlation: enabled: false # If true, adds trace.id/transaction.id/span.id to Monolog records (see below)
Note: sensitive command options/arguments (those whose name contains
password,token,secret,api_key,dsn, …) are automatically redacted before being sent to APM. Add your own names (matched as whole words, case-insensitive) withcommands.sensitive_parameter_names.
When commands is enabled, the bundle also sets the result and outcome of the current transaction when a command
terminates, like the agent does for HTTP requests: the result is CLI <exit code> (e.g. CLI 0, CLI 1) and the
outcome is success for exit code 0 and failure otherwise. Exit codes changed by other console.terminate
listeners are respected.
Warnings and deprecations
The warning and deprecation listeners install a PHP error handler at kernel boot and remove it at shutdown.
- Warnings silenced with
@or excluded byerror_reporting()are not reported unlessreport_silenced_errors: trueis set. - Deprecations are always reported, silenced or not, like Symfony's own ErrorHandler does:
trigger_deprecation()and doctrine/deprecations emit every deprecation with@. - Unregistering while another error handler sits on top of the bundle's leaves the bundle's handler in place as a pure pass-through (nothing is reported, the chain is kept) instead of popping the other handler.
- The handler always delegates to the previously registered error handler and returns its result unchanged (or
falsewhen there is none), so PHP's own error logging anderror_get_last()behave as if the bundle were not installed. - Identical errors (same level, message, file and line) are reported once per request / Messenger message.
To filter further, extend WarningListener or DeprecationListener, override
shouldReport(int $type, string $message, string $file, int $line): bool (call the parent to keep the level and
silencing checks) and swap the class on the bundle's service definition from a compiler pass, which keeps its
public visibility, kernel.reset tag and configuration:
$container->getDefinition(\ElasticApmBundle\Listener\WarningListener::class) ->setClass(\App\Apm\FilteredWarningListener::class);
Enhanced RUM instrumentation
This bundle does not integrate RUM (see https://www.elastic.co/guide/en/apm/server/current/overview.html) as there are multiple ways to install and configure the instrumentation.
Transaction naming strategies
The bundle comes with three built-in transaction naming strategies:
route— names the transaction after the matched route (default, recommended).controller— names the transaction after the resolved controller.uri— names the transaction after the request method and path. The query string is dropped to avoid leaking sensitive data (tokens, e-mail addresses, session ids) into transaction names. Note that dynamic path segments (e.g./users/123) still produce distinct names, which can increase APM cardinality; preferroutewhen that matters.
However, the bundle also supports custom transaction naming strategies through the service configuration option. If you have selected the service configuration option, you must pass the name of your own transaction naming service as the transaction_naming_service configuration option.
The transaction naming service class must implement the ElasticApmBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface interface. For more information on creating your own services, see the Symfony documentation on Creating/Configuring Services in the Container.
Interactor services
The config key elastic_apm.interactor will accept a service ID to a service implementing ElasticApmInteractorInterface.
This bundle comes with a few services that may be suitable for you.
| Configuration value | Description |
|---|---|
ElasticApmBundle\Interactor\AdaptiveInteractor |
This is the default interactor. It checks once, when the container instantiates it, whether the agent extension is loaded, and delegates to the real or the blackhole interactor accordingly. |
ElasticApmBundle\Interactor\ElasticApmInteractor |
This interactor communicates with the Elastic APM agent. It is the one decorator that actually does some work. |
ElasticApmBundle\Interactor\BlackholeInteractor |
This interactor silently drops any calls. |
auto |
Same as AdaptiveInteractor: the agent is detected at runtime (extension loaded and agent enabled), not when the container is built, so a container warmed up with ELASTIC_APM_ENABLED=false (e.g. during a Docker build) still reports once the agent is enabled. |
Note that if you set elastic_apm.enabled: false you will always use the BlackholeInteractor no matter what value
used for elastic_apm.interactor.
Monolog
The Elastic APM PHP extension does not directly support sending of log entries as anything other than errors. We recommend adding a new log handler and configuring the elasticsearch (or Elastica) client in your application configuration.
Example:
# app/config/config.yml monolog: handlers: errors_to_elasticsearch: type: buffer level: error handler: elasticsearch elasticsearch: type: service id: 'Monolog\Handler\ElasticsearchHandler'
Symfony Messenger
Opt in (requires symfony/messenger) to trace messages across processes:
elastic_apm: messenger: true
- Producer side: every message sent to a transport gets an
ElasticApmBundle\Messenger\ApmTracingStampcarrying the W3Ctraceparent/tracestateheaders of the active span (or of the current transaction when no span is active), so the consumer's transaction becomes its child. This hooks into Messenger'sSendMessageToTransportsEvent, so it works for every bus without any bus configuration. The stamp survives both the default PHP serializer and Symfony'sSerializertransport serializer. - Worker side:
messenger:consumeno longer reports one endless transaction for the whole worker process. The transaction the agent started for the process is discarded as soon as the worker starts and each consumed message gets its own transaction, namedmessenger:consume <transport> <MessageClass>, that continues the producer's trace, including the sample rate carried intracestate. It ends with resultMSG 2xx/outcomesuccesswhen handled, orMSG 5xx/outcomefailurewhen it failed, with the configured custom labels/context and memory usage label attached. No transaction is carried over from one message to the next, including for skipped or rejected messages. - Errors: the exceptions thrown by the handlers are reported, not Messenger's
HandlerFailedExceptionwrapper (exceptions.unwrap_exceptionsstill decides whether their previous exceptions are reported too). Failures you expect to recover on retry can be kept out of APM while the message will still be retried; the final failed attempt is always reported:
elastic_apm: messenger: ignored_retryable_exceptions: - App\Messenger\ExpectedRetryExceptionInterface # instanceof check: subclasses/implementations match
Buses whose SendMessageMiddleware has no event dispatcher (e.g. buses built by hand rather than by FrameworkBundle)
do not dispatch SendMessageToTransportsEvent. Add the elastic_apm.messenger.middleware middleware to them
instead; it is idempotent, so adding it to a FrameworkBundle bus as well is harmless:
framework: messenger: buses: messenger.bus.default: middleware: - elastic_apm.messenger.middleware
Without the elastic_apm extension nothing is stamped and no transaction is started.
Rolling it out safely
The stamp becomes part of every message's wire format, and a consumer that does not have the
ElasticApmBundle\Messenger\ApmTracingStamp class cannot decode a stamped message (the PHP serializer fails with
"class not found", Symfony's Serializer with "could not decode stamp"), so Messenger rejects it. This affects
pods still running an older release during a rolling deploy and other applications consuming the same transport.
- Enable the feature (or at least deploy a bundle version that has the stamp class) on every consumer before any producer starts stamping.
- Do not roll back to a version without the bundle's stamp class while stamped messages are still queued;
drain the transports first, or disable
elastic_apm.messengerand wait for the queues to empty before downgrading.
Log ↔ trace correlation
Opt in to have every Monolog record carry the ids of the APM transaction it was written in, so Kibana can jump
from a log line to its trace (and back). Requires monolog/monolog ^3 and MonologBundle.
elastic_apm: log_correlation: enabled: true # Default: false channels: [] # Only these Monolog channels (default: all channels) handlers: [] # Only these Monolog handlers (combinable with channels) field_names: # Keys written to the record's "extra" (defaults follow ECS) trace_id: trace.id transaction_id: transaction.id span_id: span.id
The ElasticApmBundle\Monolog\TraceContextProcessor (tagged monolog.processor) then adds to $record->extra:
| Field | When |
|---|---|
trace.id |
a transaction is being recorded |
transaction.id |
a transaction is being recorded |
span.id |
additionally, a span is currently active |
Records written outside a transaction (or with the agent missing or disabled) are left untouched, and the processor
never throws. channels and handlers can be combined; one monolog.processor tag is added per entry.
Notes:
- This is unrelated to the
loggingoption, which logs the bundle's own calls to the agent. - Monolog 2 (array records) is not supported. Enabling
log_correlationwithout Monolog 3 fails at container compile time with an explicit message; leaving it disabled works with any (or no) Monolog version. - The fields land in
extra; make sure your formatter/ingest pipeline maps them to the top-level ECStrace.id/transaction.id/span.idfields Kibana uses for correlation, or rename them viafield_names.
Troubleshooting
Exceptions from commands are being recorded multiple times
PHP APM will automatically collect unhandled exceptions. The bundle will also install a listener for command exceptions. Our listener and the default behaviour can conflict which causes this behaviour.
To fix this you can turn off explicitly_collect_exceptions under the commands configuration node.
Development
Running Unit Tests
This bundle includes comprehensive unit tests. You can run them in several ways:
Using Docker (Recommended)
The easiest way to run tests is using Docker. The image includes the elastic_apm PHP extension with the agent
enabled and ELASTIC_APM_DISABLE_SEND=true, so the tests drive the real agent without ever contacting an APM server:
# Build the Docker image (PHP 8.3 by default; use --build-arg PHP_VERSION=8.4 for 8.4) docker build -t elastic-apm-symfony-bundle-test . # Install dependencies (composer.lock is not committed) and run all tests docker run --rm --volume $(pwd):/app elastic-apm-symfony-bundle-test:latest sh -c 'composer update && ./vendor/bin/phpunit' # Lowest supported dependencies (Symfony 6.4) docker run --rm --volume $(pwd):/app elastic-apm-symfony-bundle-test:latest sh -c 'composer update --prefer-lowest && ./vendor/bin/phpunit' # Extension installed but agent disabled: exercises the fallback to the blackhole interactor docker run --rm --volume $(pwd):/app -e ELASTIC_APM_ENABLED=false elastic-apm-symfony-bundle-test:latest ./vendor/bin/phpunit # Run tests with coverage docker run --rm --volume $(pwd):/app -e XDEBUG_MODE=coverage elastic-apm-symfony-bundle-test:latest ./vendor/bin/phpunit --coverage-html coverage-report # Run specific test file docker run --rm --volume $(pwd):/app elastic-apm-symfony-bundle-test:latest ./vendor/bin/phpunit tests/Interactor/ElasticApmInteractorTest.php # Interactive shell for debugging docker run -ti --rm --volume $(pwd):/app elastic-apm-symfony-bundle-test:latest /bin/bash
Running Tests Locally
If you have PHP 8.3+ and the Elastic APM extension installed locally:
# Install dependencies composer install # Run all tests ./vendor/bin/phpunit # Run tests with coverage ./vendor/bin/phpunit --coverage-html coverage-report # Run specific test file ./vendor/bin/phpunit tests/Interactor/ElasticApmInteractorTest.php
Note: Some tests drive the real agent and need the elastic_apm extension loaded and the agent enabled
(ELASTIC_APM_ENABLED not false; set ELASTIC_APM_DISABLE_SEND=true to keep it from sending anything). Without
that, they are skipped: ElasticApmInteractorTest, RealAgentIntegrationTest and the CommandListenerTest cases that
mock Elastic\Apm\TransactionInterface. Everything else, including the kernel integration tests in
tests/Integration (which use a recording interactor), runs anywhere. See the
official installation guide.
Continuous Integration
This project uses GitHub Actions for continuous integration. The workflow automatically:
- Runs PHPUnit tests on PHP 8.3 and 8.4 against the real agent, failing if any test is skipped
- Runs the lowest supported dependencies (
composer update --prefer-lowest, Symfony 6.4) on PHP 8.3 - Runs with the agent disabled (
ELASTIC_APM_ENABLED=false) to cover the fallback interactors - Generates code coverage reports (PHP 8.3 only)
- Runs static analysis and code style checks: ECS, PHPStan, Psalm and Rector (dry-run)
- Executes on:
- Every push to
master/mainbranches - Every pull request targeting
master/main
- Every push to
The workflow uses Docker to ensure the elastic_apm PHP extension is available during testing. You can view the workflow status and results in the Actions tab of the repository.
Contributing
We welcome contributions! Here's how you can help:
Getting Started
- Fork the repository
- Clone your fork:
git clone https://github.com/YOUR-USERNAME/elastic-apm-symfony-bundle.git - Create a feature branch:
git checkout -b feature/your-feature-name - Make your changes
- Run tests to ensure everything works (see Running Unit Tests)
- Commit your changes with a clear message
- Push to your fork:
git push origin feature/your-feature-name - Open a Pull Request
Contribution Guidelines
- Code Style: Follow the project's ECS configuration (
vendor/bin/ecs check) - Tests: Add tests for new features or bug fixes
- Documentation: Update documentation for any changed functionality
- Compatibility: Ensure compatibility with Symfony 6.4, 7.0, and 8.0 (as declared in
composer.json) and PHP 8.3+ - Commit Messages: Write clear, descriptive commit messages
Pull Request Process
- Ensure all tests pass
- Update the README.md if needed
- Update the CHANGELOG.md with your changes
- The PR will be reviewed by maintainers
- Address any feedback or requested changes
- Once approved, your PR will be merged
Reporting Issues
If you find a bug or have a feature request:
- Check if the issue already exists
- If not, create a new issue with a clear title and description
- Include steps to reproduce for bugs
- Include your environment details (PHP version, Symfony version, etc.)
Sponsor
If this bundle is useful to you, please consider sponsoring its development:
<iframe src="https://github.com/sponsors/mmft24/button" title="Sponsor mmft24" height="32" width="114" style="border: 0; border-radius: 6px;"></iframe>Credits
This bundle's design is based on the work done by:
- ekino/EkinoNewRelicBundle - The foundational New Relic bundle that inspired this bundle's architecture.
Special thanks to all contributors who have helped improve this bundle.