Benchmark four PHP runtime/instrumentation variants under the same load profile and compare their telemetry and response time behavior.
This stack runs:
- A Slim PHP app in 4 variants:
uninstrumented(uninstrumented)apm-php-9-alpha(solarwinds/apm + solarwinds/apm_ext + swotel collector)otel(OpenTelemetry PHP auto-instrumentation)apm-8(Current GA Solarwinds APM PHP library)
- A Locust load generator that continuously hits all 4 variants.
- Three collectors:
otel-collector-locustfor Locust OTLP metrics exportotel-collector-apm-php-9-alphafor solarwinds/apm telemetry exportotel-collector-otelfor OpenTelemetry PHP telemetry export
The default benchmark target route is /complex, which intentionally creates spans and emits app-side metrics/logs to stress instrumentation overhead.
| Component | Service name | Purpose | Host port |
|---|---|---|---|
| Slim baseline | nginx-uninstrumented -> php-fpm-uninstrumented |
No APM instrumentation | 8000 |
| Slim + apm-php-9-alpha | nginx-apm-php-9-alpha -> php-fpm-apm-php-9-alpha |
solarwinds/apm + solarwinds/apm_ext + swotel collector | 8001 |
| Slim + otel | nginx-otel -> php-fpm-otel |
OpenTelemetry PHP auto-instrumentation exporting OTLP telemetry | 8002 |
| Slim + apm-8 | nginx-apm-8 -> php-fpm-apm-8 |
Current GA Solarwinds APM PHP library | 8003 |
| Load generator | apm-php-bench-locust |
Headless Locust workload + custom OTLP metric | n/a |
| Collector (Locust) | otel-collector-locust |
Forwards telemetry from Locust to backend | n/a |
| Collector (alpha) | otel-collector-apm-php-9-alpha |
Forwards telemetry from APM PHP alpha to backend | n/a |
| Collector (otel) | otel-collector-otel |
Forwards telemetry from OpenTelemetry PHP app to backend | n/a |
- Docker Desktop (or Docker Engine + Compose plugin)
- Valid collector/token values for your backend
A template exists at .env.dev:
OTEL_COLLECTOR=<otel-collector>
OTEL_TOKEN=<token>
SW_APM_COLLECTOR=<apm-collector>
SW_APM_TOKEN=<token>Use either of these options:
- Run Compose directly with the template file
- Copy
.env.devto.envand edit values
cp .env.dev .envdocker compose --env-file .env.dev up --buildIf you use .env, you can omit --env-file:
docker compose up --buildStop and clean up:
docker compose downAfter startup, verify health endpoints:
curl -fsS http://localhost:8000/healthcheck
curl -fsS http://localhost:8001/healthcheck
curl -fsS http://localhost:8002/healthcheck
curl -fsS http://localhost:8003/healthcheckExpected response:
Yay healthy
Locust starts in headless mode with:
- 8 users
- spawn rate 5
- host
http://0.0.0.0:8000(individual tasks call service DNS names directly)
Tasks in locust-app/locustfile.py call:
http://nginx-uninstrumented/complexasuninstrumentedhttp://nginx-apm-php-9-alpha/complexas9.0.0-alphahttp://nginx-otel/complexasotelhttp://nginx-apm-8/complexas8.13.0
Locust also publishes a custom histogram metric:
- metric name:
apm.php.benchmark.response.time - attribute key:
benchmark.app.kind
Available routes in slim-app/index.php:
/healthcheck- liveness check/request- outbound call toexample.com/metrics- app counters + histogram, includessleep(1)for stable response-time profile/logs- emits logs via Monolog OTel handler/sdk- manual SDK span + outbound call/complex- benchmark-heavy path (creates many manual spans and records metrics)
.
├── docker-compose.yaml
├── otel-collector-config.yaml
├── swotel-collector-config.yaml
├── locust-app/
│ ├── Dockerfile
│ ├── locustfile.py
│ └── requirements.txt
└── slim-app/
├── Dockerfile-uninstrumented
├── Dockerfile-apm-php-9-alpha
├── Dockerfile-otel
├── Dockerfile-apm-8
├── composer-uninstrumented.json
├── composer-apm-php-9-alpha.json
├── composer-otel.json
├── index.php
└── nginx-*.conf
- Containers exit early: inspect logs with
docker compose logs --tail=200 <service>. - Health check fails: ensure
php-fpm-*services built successfully and Nginx depends_on conditions are met. - No telemetry in backend: verify
.env(.dev)token/collector values and collector service connectivity.
Licensed under Apache 2.0. See LICENSE.