Adapters
NoOp, Prometheus, StatsD, and OpenTelemetry OTLP — choosing the right metrics backend, pull vs push, and worker-safe runtime behavior.
Metrics Adapters
vortos-metrics ships four adapters. You switch between them in config/metrics.php with a single call — all auto-instrumentation works the same regardless of which adapter is active.
Pull vs push
| Model | Adapter | How data leaves the process | Best for |
|---|---|---|---|
| Pull | Prometheus | Prometheus scrapes GET /metrics | Self-hosted Prometheus and Grafana |
| Push | StatsD | UDP packets to a local agent | DogStatsD, Telegraf, Graphite |
| Push | OpenTelemetry OTLP | HTTP/protobuf to a collector or vendor endpoint | Vendor-neutral metrics, New Relic, Datadog, OTel Collector |
Prometheus pull is excellent when your monitoring system can reach every app instance. OTLP push is better when your app should send to a local sidecar, collector, or managed vendor endpoint. StatsD is the simplest low-latency push path when a UDP agent is already part of your platform.
NoOp (default)
The default adapter. Every call to MetricsInterface is a silent no-op — zero syscalls, zero allocations, zero I/O. The singleton instrument pattern means even repeated calls for the same metric don't allocate objects.
Use this in:
- Development, when you don't have a metrics backend running
- CI/CD environments where metrics infra is not needed
- Any environment where you want zero overhead
No configuration required.
Prometheus
Prometheus is a pull-based metrics system. Your application exposes a /metrics endpoint that Prometheus scrapes every 15 seconds. Each scrape returns all current metric values in the Prometheus text exposition format.
Why Prometheus?
- Industry standard — Grafana, Alertmanager, and every major observability platform speak Prometheus natively
- Counters and histograms survive process restarts (with Redis or APC storage)
- Exemplar support — histogram samples can link to the trace that produced them (Grafana/Tempo integration)
- Time series are stored in Prometheus itself — your app holds only current values
Enable Prometheus
composer require promphp/prometheus_client_php$config->adapter(MetricsAdapter::Prometheus)
->prometheusEndpointToken($_ENV['METRICS_TOKEN'] ?? '');Storage backends
Prometheus counters must persist across PHP requests. The right storage backend depends on your runtime.
In-memory (default — dev only)
$config->prometheusStorageInMemory(); // this is the defaultStores counters in the current PHP process's memory. Each request starts from zero — counters accumulate only within a single process lifecycle. Not suitable for multi-process environments. Use in development only.
APC shared memory
$config->prometheusStorageApc();Stores counters in APC shared memory, which is shared across all PHP-FPM workers on the same machine. Counters persist across requests. Requires the apcu PHP extension.
Use this with PHP-FPM (traditional process-per-request model).
Redis
$config->prometheusStorageRedis(
prefix: 'metrics:',
host: $_ENV['REDIS_HOST'] ?? '127.0.0.1',
port: 6379,
password: $_ENV['REDIS_PASSWORD'] ?? '',
);Stores counters in Redis. All PHP workers (across machines, across processes) share the same metric values. Counters survive process restarts.
Use this with FrankenPHP worker mode. FrankenPHP runs multiple long-lived PHP workers — in-memory storage is process-local and cannot aggregate across workers. Redis is the only safe option.
In-memory storage in FrankenPHP gives wrong numbers
If you use prometheusStorageInMemory() with FrankenPHP, each worker maintains its own counter independently. The /metrics scrape hits one worker at random — you see only a fraction of actual traffic. Use Redis storage in production with FrankenPHP.
Prometheus /metrics endpoint
When the Prometheus adapter is active, vortos-metrics registers a GET /metrics controller. Prometheus scrapes this endpoint to collect current metric values.
See Prometheus Endpoint for security configuration and scrape setup.
StatsD
StatsD (and compatible agents like Telegraf, Datadog DogStatsD) is a push-based metrics system. Your application sends metric updates over UDP to a local agent. The agent aggregates and forwards to your metrics backend.
Why StatsD?
- Works with Datadog, Graphite, InfluxDB, and many others
- UDP is fire-and-forget — metrics never block your application, even if the agent is down
- No
/metricsendpoint to secure - DogStatsD tag extension provides the same label filtering as Prometheus
Enable StatsD
No extra packages needed — StatsD uses raw UDP sockets.
$config->adapter(MetricsAdapter::StatsD)
->statsDHost($_ENV['STATSD_HOST'] ?? '127.0.0.1')
->statsDPort(8125)
->statsDSampleRate(1.0); // 1.0 = send all metricsUDP and batching
StatsD sends metric updates via UDP — a connectionless protocol with no acknowledgement, no retry, no blocking. If the StatsD agent is unreachable, packets are silently dropped. This is intentional: metrics should never affect application latency.
StatsDMetrics batches metric updates into a single UDP packet per request, capped at the UDP MTU (1400 bytes). Instead of one syscall per metric, you get one syscall per request. StatsDFlushListener triggers the flush on kernel.terminate (after the response has been sent), ensuring metrics are dispatched without adding to response latency.
Sample rate
$config->statsDSampleRate(0.1); // send 10% of metric updatesThe sample rate is a fraction between 0 and 1. At 0.1, only 1 in 10 metric updates is sent via UDP. The StatsD agent automatically scales the received values back up — a counter that receives 10 increments at 0.1 sample rate is reported as 100.
Use sampling to reduce UDP traffic volume for very high-throughput metrics. For most applications 1.0 (send everything) is correct.
Tag format
Vortos uses the Datadog DogStatsD tag extension: |#key:value,key:value. This format is supported by Datadog DogStatsD, Telegraf (statsd_input plugin), and most modern StatsD agents.
vortos.http_requests_total:1|c|#method:POST,route:orders.place,status:201OpenTelemetry OTLP
OpenTelemetry OTLP is the vendor-neutral push adapter. Vortos records metrics through MetricsInterface, bridges them to the official OpenTelemetry PHP SDK, then exports them as OTLP HTTP/protobuf.
Vortos does not implement custom protobuf serialization. The adapter requires:
composer require open-telemetry/sdk open-telemetry/exporter-otlp guzzlehttp/guzzleEnable OTLP
use Vortos\Metrics\Config\MetricsAdapter;
use Vortos\Metrics\DependencyInjection\VortosMetricsConfig;
return static function (VortosMetricsConfig $config): void {
$config
->adapter(MetricsAdapter::OpenTelemetry)
->namespace('myapp')
->service('checkout-api', version: $_ENV['APP_VERSION'] ?? '', environment: $_ENV['APP_ENV'] ?? 'prod')
->otlp(
endpoint: $_ENV['OTEL_EXPORTER_OTLP_METRICS_ENDPOINT'] ?? 'http://otel-collector:4318/v1/metrics',
headers: [],
timeoutMs: 200,
);
};Collector helper
$config->otlpCollector(
endpoint: 'http://otel-collector:4318/v1/metrics',
timeoutMs: 200,
);New Relic helper
$config->newRelicOtlp(
licenseKey: $_ENV['NEW_RELIC_LICENSE_KEY'] ?? '',
timeoutMs: 200,
);Datadog helper
$config->datadogOtlp(
apiKey: $_ENV['DD_API_KEY'] ?? '',
site: $_ENV['DD_SITE'] ?? 'datadoghq.com',
timeoutMs: 200,
);Use the vendor helper only when your app pushes directly to the vendor. In larger deployments, prefer pushing to a local OpenTelemetry Collector and let the collector handle vendor credentials, retries, batching, and routing.
Lifecycle safety
The OTLP adapter flushes at lifecycle boundaries:
HTTP request terminate -> forceFlush()
console command terminate -> forceFlush()
console command error -> forceFlush()
process shutdown -> shutdown()forceFlush() is per request or command. shutdown() is only registered for actual process exit. This distinction matters in FrankenPHP worker mode: calling shutdown() after request 1 would close the meter provider and break request 2.
There is no intervalMs timer. PHP-FPM and FrankenPHP worker mode both use lifecycle-driven flushing instead of fake background timers.
Timeout and retry policy
The OTLP transport uses a strict timeout. The default is 200ms; configured values are clamped between 50ms and 1000ms.
$config->otlpCollector(timeoutMs: 200);Retries are disabled in the application process. If the collector or vendor endpoint is slow, Vortos drops that flush, writes a local warning if logging is available, and returns the worker to the pool. Monitoring must not degrade application availability.
Memory behavior
The adapter caches OpenTelemetry instruments by metric name, not by label value. Label values are attached at record time. That keeps instrument caches bounded by your declared metric set.
Gauges keep the last value per label set, so do not use unbounded labels such as user ID, email, IP address, request ID, order ID, or raw URL path.
Choosing an adapter
| Scenario | Recommended adapter |
|---|---|
| Local development | NoOp (default) |
| Production with Grafana / Prometheus | Prometheus + Redis storage |
| Production with an OpenTelemetry Collector | OpenTelemetry OTLP |
| Production with New Relic | OpenTelemetry OTLP |
| Production with Datadog | OpenTelemetry OTLP or StatsD (DogStatsD) |
| Production with Graphite | StatsD |
| CI / testing | NoOp (default) |
| FrankenPHP worker mode | Prometheus + Redis, StatsD, or OpenTelemetry OTLP |
For FrankenPHP specifically: Prometheus requires Redis storage for correct cross-worker counters. StatsD works because UDP packets leave the worker immediately. OTLP works because Vortos flushes with forceFlush() at lifecycle boundaries and reserves shutdown() for actual process exit.
Metrics
Counters, gauges, and histograms across HTTP, CQRS, messaging, cache, persistence, policy limits, and security — NoOp by default, Prometheus pull, StatsD push, or OpenTelemetry OTLP push when you need real numbers.
OpenTelemetry OTLP Metrics
Push Vortos metrics to an OpenTelemetry Collector, New Relic, Datadog, or any OTLP-compatible backend without vendor SDK lock-in.