PHPUnit Telemetry Bridge
PHPUnit extension allowing to collect test suite telemetry and export it to any OTLP-compatible backend (OpenTelemetry Collector, Grafana Alloy, Honeycomb, Datadog, Jaeger, etc.).
Installation
For detailed installation instructions, see the installation page.
Configuration
Add the extension to your phpunit.xml.dist:
<extensions>
<bootstrap class="Flow\Bridge\PHPUnit\Telemetry\TelemetryExtension">
<parameter name="service_name" value="my-test-suite"/>
<parameter name="endpoint" value="http://localhost:4318"/>
<parameter name="emit_traces" value="true"/>
<parameter name="emit_metrics" value="true"/>
<parameter name="emit_test_spans" value="true"/>
<parameter name="emit_test_case_spans" value="true"/>
</bootstrap>
</extensions>
Configuration Parameters
Shared
| Parameter | Environment variable | Default | Description |
|---|---|---|---|
service_name |
FLOW_PHPUNIT_OTEL_SERVICE_NAME |
phpunit |
Service name reported in telemetry data |
transport |
FLOW_PHPUNIT_OTEL_TRANSPORT |
curl |
Transport type: curl, grpc, or stream |
endpoint |
FLOW_PHPUNIT_OTEL_ENDPOINT |
http://localhost:4318 |
OTLP endpoint URL (curl/grpc) or destination path (stream) |
headers |
FLOW_PHPUNIT_OTEL_HEADERS |
— | Additional headers (see Authentication) |
emit_traces |
FLOW_PHPUNIT_OTEL_EMIT_TRACES |
true |
Enable/disable trace emission |
emit_metrics |
FLOW_PHPUNIT_OTEL_EMIT_METRICS |
true |
Enable/disable metric emission |
emit_test_spans |
FLOW_PHPUNIT_OTEL_EMIT_TEST_SPANS |
true |
Create individual spans for each test |
emit_test_case_spans |
FLOW_PHPUNIT_OTEL_EMIT_TEST_CASE_SPANS |
true |
Create spans for test case classes |
batch_size |
FLOW_PHPUNIT_OTEL_BATCH_SIZE |
512 |
Items per batch for span/metric/log batching processors |
shutdown_timeout_ms |
FLOW_PHPUNIT_OTEL_SHUTDOWN_TIMEOUT_MS |
5000 |
Wall-clock budget in ms for draining pending requests at shutdown (curl/grpc) |
error_handler |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER |
error_log |
How telemetry errors are surfaced (see Error Handlers) |
Curl transport (transport=curl)
Timeouts are in milliseconds (defaults assume a local collector on loopback / sidecar). timeout_ms is the
per-request deadline; shutdown_timeout_ms (shared, see above) is a wall-clock budget enforced at shutdown only —
it lets you keep timeout_ms tight for steady-state without freezing graceful exit.
| Parameter | Environment variable | Default | Description |
|---|---|---|---|
curl_timeout_ms |
FLOW_PHPUNIT_OTEL_CURL_TIMEOUT_MS |
250 |
Total request deadline in milliseconds |
curl_connect_timeout_ms |
FLOW_PHPUNIT_OTEL_CURL_CONNECT_TIMEOUT_MS |
250 |
TCP/TLS connect deadline in milliseconds |
curl_compression |
FLOW_PHPUNIT_OTEL_CURL_COMPRESSION |
false |
Enable automatic response decompression |
curl_follow_redirects |
FLOW_PHPUNIT_OTEL_CURL_FOLLOW_REDIRECTS |
true |
Follow HTTP redirects |
curl_max_redirects |
FLOW_PHPUNIT_OTEL_CURL_MAX_REDIRECTS |
3 |
Maximum number of redirects to follow |
curl_proxy |
FLOW_PHPUNIT_OTEL_CURL_PROXY |
— | Proxy server URL (e.g. http://proxy:8080) |
curl_ssl_verify_peer |
FLOW_PHPUNIT_OTEL_CURL_SSL_VERIFY_PEER |
true |
Verify SSL peer certificate |
curl_ssl_verify_host |
FLOW_PHPUNIT_OTEL_CURL_SSL_VERIFY_HOST |
true |
Verify SSL host name |
curl_ssl_cert_path |
FLOW_PHPUNIT_OTEL_CURL_SSL_CERT_PATH |
— | Path to client SSL certificate |
curl_ssl_key_path |
FLOW_PHPUNIT_OTEL_CURL_SSL_KEY_PATH |
— | Path to client SSL private key |
curl_ca_info_path |
FLOW_PHPUNIT_OTEL_CURL_CA_INFO_PATH |
— | Path to CA certificate bundle |
curl_serializer |
FLOW_PHPUNIT_OTEL_CURL_SERIALIZER |
json |
Payload serializer: json or protobuf |
gRPC transport (transport=grpc)
Requires the grpc PHP extension and the google/protobuf package. Payload is always protobuf (per OTLP/gRPC spec).
gRPC has no separate connect timeout — grpc_timeout_ms is the per-call deadline that bounds DNS, connect, send and
receive together.
| Parameter | Environment variable | Default | Description |
|---|---|---|---|
grpc_timeout_ms |
FLOW_PHPUNIT_OTEL_GRPC_TIMEOUT_MS |
250 |
Per-call deadline in milliseconds |
grpc_insecure |
FLOW_PHPUNIT_OTEL_GRPC_INSECURE |
true |
Use insecure channel credentials |
Stream transport (transport=stream)
Writes JSONL telemetry to a file or php:// stream wrapper instead of an OTLP collector
(OTLP File Exporter spec). The endpoint
parameter holds the destination path. Useful for local debugging or pipelines that scrape JSONL from disk / stdout.
| Parameter | Environment variable | Default | Description |
|---|---|---|---|
stream_file_permissions |
FLOW_PHPUNIT_OTEL_STREAM_FILE_PERMISSIONS |
0644 |
Permissions for newly created files (ignored for php://) |
stream_create_directories |
FLOW_PHPUNIT_OTEL_STREAM_CREATE_DIRECTORIES |
true |
Create parent directories of the destination if missing |
Error Handlers
Telemetry-internal errors (exporter failures, transport timeouts) are routed to a configurable error handler instead of
being thrown into the test runner. Pick one with the error_handler parameter; all options follow the same
error_handler_* parameter family.
| Type | Description |
|---|---|
error_log |
Default. Writes via PHP's error_log() (stderr in CLI). |
noop |
Silently discards every Throwable. |
stream |
Appends to a file path or php:// stream wrapper. |
syslog |
Local syslog via openlog / syslog / closelog. |
udp_syslog |
RFC 5424 syslog frames over UDP to a remote collector. |
Mixing parameters across handler types throws InvalidArgumentException at boot.
error_log (default)
| Parameter | Environment variable | Default | Description |
|---|---|---|---|
error_handler_message_type |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_MESSAGE_TYPE |
operating_system |
operating_system (0), email (1), file (3), sapi (4) |
error_handler_expand_newlines |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_EXPAND_NEWLINES |
false |
Emit one error_log() call per line of the formatted message |
error_handler_message_prefix |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_MESSAGE_PREFIX |
[flow-telemetry] |
Prefix prepended to every message |
noop
No parameters. Use when you want telemetry-internal failures to stay silent (e.g. CI runs where the collector is intentionally absent).
stream
Appends formatted Throwables (one per line) to a destination. The handle is opened lazily on first error and reused.
| Parameter | Environment variable | Default | Description |
|---|---|---|---|
error_handler_destination |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_DESTINATION |
— | File path or php://stdout/php://stderr/etc. (required) |
error_handler_file_permissions |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_FILE_PERMISSIONS |
0644 |
Permissions for newly created files (ignored for php://) |
error_handler_create_directories |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_CREATE_DIRECTORIES |
true |
Create parent directories of the destination if missing |
error_handler_message_prefix |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_MESSAGE_PREFIX |
[flow-telemetry] |
Prefix prepended to every line |
syslog
| Parameter | Environment variable | Default | Description |
|---|---|---|---|
error_handler_ident |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_IDENT |
flow-telemetry |
Syslog identity tag |
error_handler_facility |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_FACILITY |
user |
RFC 5424 facility (e.g. user, local0..local7, mail) |
error_handler_log_opts |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_LOG_OPTS |
1 (LOG_PID) |
Bitmask of LOG_* flags passed to openlog() |
error_handler_severity |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_SEVERITY |
error |
RFC 5424 severity (e.g. error, warning, notice, info) |
udp_syslog
Sends RFC 5424 syslog frames over UDP. Useful when test runs ship to a centralized log aggregator.
| Parameter | Environment variable | Default | Description |
|---|---|---|---|
error_handler_host |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_HOST |
— | Remote syslog host (required) |
error_handler_port |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_PORT |
514 |
Remote syslog port |
error_handler_ident |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_IDENT |
flow-telemetry |
Syslog identity tag |
error_handler_facility |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_FACILITY |
user |
RFC 5424 facility |
error_handler_severity |
FLOW_PHPUNIT_OTEL_ERROR_HANDLER_SEVERITY |
error |
RFC 5424 severity |
Authentication
OTLP endpoints that require authentication (Grafana Alloy with Bearer auth, Honeycomb with x-honeycomb-team, vendor
tenant headers, etc.) are configured through the headers parameter.
Headers use the OpenTelemetry spec format: comma-separated name=value pairs, with values URL-encoded (so commas,
spaces and equal signs inside values don't collide with the delimiter):
<parameter name="endpoint" value="https://alloy.example.com:4318"/>
<parameter name="headers" value="Authorization=Bearer%20xxx,X-Scope-OrgID=tenant-1"/>
Invalid header format (missing =, empty name) will throw InvalidArgumentException when PHPUnit boots the extension.
gRPC Transport
<extensions>
<bootstrap class="Flow\Bridge\PHPUnit\Telemetry\TelemetryExtension">
<parameter name="transport" value="grpc"/>
<parameter name="endpoint" value="otel.example.com:4317"/>
<parameter name="headers" value="api-key=xxx"/>
<parameter name="grpc_insecure" value="false"/>
<parameter name="grpc_timeout_ms" value="2000"/>
</bootstrap>
</extensions>
Stream Transport
<extensions>
<bootstrap class="Flow\Bridge\PHPUnit\Telemetry\TelemetryExtension">
<parameter name="transport" value="stream"/>
<parameter name="endpoint" value="var/otel/test-suite.jsonl"/>
<parameter name="error_handler" value="stream"/>
<parameter name="error_handler_destination" value="var/otel/test-suite-errors.log"/>
</bootstrap>
</extensions>
Environment Variables
Every parameter has an env var counterpart (see the tables above).
Precedence: environment variable > <parameter> in phpunit.xml > default. Empty-string env vars are treated as unset.
Lookup order: $_ENV → $_SERVER → getenv(). Values loaded by Symfony DotEnv or vlucas/phpdotenv
(which populate $_ENV / $_SERVER but don't always call putenv()) are picked up the same as shell env vars.
[!IMPORTANT] When setting boolean env vars through PHPUnit's
<env>directive inphpunit.xml.dist, addverbatim="true"— otherwise PHPUnit casts the bare strings"true"/"false"to PHP booleans, whichputenv()then stringifies to"1"/"", and the empty string is treated as unset by the resolver (falling back to the default).<env name="FLOW_PHPUNIT_OTEL_EMIT_TEST_SPANS" value="false" verbatim="true"/>This only applies to
<env>in PHPUnit XML; real shell env vars and.enventries are already plain strings.
Typical usage — credentials stay out of version control:
export FLOW_PHPUNIT_OTEL_ENDPOINT="https://alloy.example.com:4318"
export FLOW_PHPUNIT_OTEL_HEADERS="Authorization=Bearer%20${ALLOY_TOKEN}"
./vendor/bin/phpunit
Deprecation: otel_collector_url
The otel_collector_url parameter and its FLOW_PHPUNIT_OTEL_COLLECTOR_URL environment variable are deprecated and
trigger E_USER_DEPRECATED. They remain functional as an alias for endpoint with transport=curl.
Mixing the deprecated parameter with any of the new-shape parameters throws InvalidArgumentException — migrate
fully when you switch.
Features
Traces
When enabled, the extension creates spans for:
- Test suite runs (root span)
- Individual test suites
- Test case classes (optional)
- Individual tests (optional)
Each span includes attributes like:
test.suite- Test suite nametest.id- Test identifiertest.name- Test nametest.class- Test class nametest.method- Test method nametest.status- Test result status (passed, failed, errored, skipped, incomplete)
Metrics
When enabled, the extension records:
phpunit.suite.duration- Histogram of suite execution timephpunit.suite.test_count- Counter of tests per suitephpunit.test.duration- Histogram of individual test execution timephpunit.test.count- Counter of tests by status
Running with Docker Compose
To visualize test telemetry, run an OTEL collector with a backend like Jaeger:
services:
otel-collector:
image: otel/opentelemetry-collector:latest
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
volumes:
- ./otel-collector-config.yaml:/etc/otelcol/config.yaml
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
Then run your tests:
./vendor/bin/phpunit
View the traces in Jaeger at http://localhost:16686.
Found a typo or an outdated section? Edit this page on GitHub