arkaitzgarro/elastic-apm-laravel
Integrates Elastic APM with Laravel to monitor application performance. Automatically traces HTTP requests, database queries, and background jobs, sending transaction and span data to the Elastic APM server for analysis and debugging.
Requirements for v8.0.0: PHP 8.3+ and Laravel 12 or 13. If you are still on Laravel 11 or PHP 8.2 you cannot upgrade — this is a hard gate, not a soft deprecation.
Install the package:
composer require arkaitzgarro/elastic-apm-laravel
Publish the config:
php artisan vendor:publish --tag=elastic-apm-laravel-config
This creates config/elastic-apm-laravel.php.
Add the minimum set of environment variables to .env:
ELASTIC_APM_SERVICE_NAME=my-laravel-app
ELASTIC_APM_SERVER_URL=http://localhost:8200
ELASTIC_APM_SECRET_TOKEN=
ELASTIC_APM_ENVIRONMENT=local
ELASTIC_APM_ENABLED=true
Add the same keys to .env.example, but leave ELASTIC_APM_SECRET_TOKEN and ELASTIC_APM_API_KEY empty.
Register the package middleware. In Laravel 12/13 (bootstrap/app.php):
->withMiddleware(function (Middleware $middleware) {
$middleware->prepend(\Arkaitzgarro\ElasticApmLaravel\Middleware\RecordTransaction::class);
})
Place it early in the stack so the transaction starts before routing, auth, and DB work you want measured.
First use case — validate on one known endpoint. Hit GET / through the app, then open Kibana → Observability → APM → Services and confirm a transaction appears with a sane name, duration, and no PHP-agent warnings. Once you see that single trace, enable it in staging.
Where to look first: config/elastic-apm-laravel.php (the whole integration is driven from here), then the package README for the exact middleware class names and tags it ships with — class names have shifted across majors.
Global transaction capture via middleware. The standard pattern is one global middleware that opens a transaction per request and names it. Keep it in the global stack, not the web group, so API routes and health checks are covered too.
Keep transaction names low-cardinality. Prefer the route pattern (GET /users/{id}) over the concrete URI (GET /users/42). High-cardinality names blow up Kibana’s aggregation and make the APM UI useless. If the package does not normalise this for you, override it after the transaction is created.
Custom spans around outbound I/O. Wrap third-party HTTP calls, S3 uploads, or slow internal RPC in explicit spans so they show up as children of the request rather than as an unexplained gap:
$transaction = \Elastic\Apm\ElasticApm::getCurrentTransaction();
$span = $transaction->beginCurrentSpan('stripe.charge', 'external.http');
try {
$response = $this->stripe->charges->create([...]);
} finally {
$span->end();
}
The finally block matters — an un-ended span corrupts the parent/child timing.
Use labels for slicing, not for messages. Attach low-cardinality tags (tenant_tier, api_version, feature_flag) to the transaction. Never put user IDs, emails, or request bodies in labels; they are indexed and retained.
Pair the middleware with exception handling. Middleware catches the request lifecycle, but exceptions rendered by the handler can escape the transaction. Verify that a thrown ModelNotFoundException or a 500 still produces an error event with the stack trace attached to the current transaction — if not, capture it in App\Exceptions\Handler / bootstrap/app.php and record it explicitly.
Disable aggressively outside production-like environments. In phpunit.xml and local .env:
ELASTIC_APM_ENABLED=false
This avoids test-suite slowdowns and phantom transactions from feature tests polluting your service metrics.
Tune sampling per environment. Start at ELASTIC_APM_TRANSACTION_SAMPLE_RATE=1.0 in staging, drop to 0.1–0.2 in production once the service is validated. Sampling applies to transactions, not errors — error events still come through.
Deploy as a config-published package, not a black box. Commit config/elastic-apm-laravel.php to the repo rather than relying on vendor defaults, so that upgrades in v8.x don’t silently change behaviour under you.
Version-stamp your deployments. Set ELASTIC_APM_SERVICE_VERSION from your release tag (e.g. git describe --tags) so APM can correlate error-rate spikes to a deploy.
Integrate with CI as a gate. Run the app against a throwaway APM Server container in an integration job and assert that one request produces one transaction — this catches middleware-ordering regressions that unit tests never see.
composer.json platform config, CI matrix (shivammathur/setup-php versions), Dockerbase images, and any laravel/framework constraint that still permits ^11. If any downstream package pins Laravel 11, this is a blocker, not a warning.elastic_apm PHP extension. The package is a Laravel wrapper; whether it drives the extension or talks to the APM Server directly determines your Dockerfile (docker-php-ext-install / PECL) and your php.ini. Get this wrong and you get a silently no-op integration that never errors.php artisan config:cache freezes env vars. Deploy pipelines that cache config at build time will bake in whatever ELASTIC_APM_* values existed then. Cache after env injection, or read these values via env() in a non-cached bootstrap path.StartSession or after the auth middleware, unauthenticated-request traces and early exceptions are invisible. Prepend it.SERVER_URL or a missing token produces zero data and zero logs. When debugging, set ELASTIC_APM_LOG_LEVEL=debug and watch storage/logs/laravel.log — this is the first thing to check when "nothing shows up in Kibana".php artisan serve keep a single process alive; per-request memory and transaction boundaries need explicit attention. Test under Octane if you deploy there — behaviour can differ meaningfully from FPM./health, /up, and /_horizon/* (middleware exclusion or route-level opt-out) or they will swamp real traffic in the service view and skew p95 latency.ELASTIC_APM_SECRET_TOKEN / ELASTIC_APM_API_KEY in a committed .env or a config file is a real incident. Keep them in your secret manager and confirm config:cache output isn’t written to a public artefact.handle() manually with a transaction."arkaitzgarro/elastic-apm-laravel": "^8.0" only after your Laravel 12/13 and PHP 8.3+ baselines are confirmed across all environments; the package has no graceful fallback for the dropped platform versions — Composer will simply refuse to install.How can I help you explore Laravel packages today?