Vortos
Metrics

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

ModelAdapterHow data leaves the processBest for
PullPrometheusPrometheus scrapes GET /metricsSelf-hosted Prometheus and Grafana
PushStatsDUDP packets to a local agentDogStatsD, Telegraf, Graphite
PushOpenTelemetry OTLPHTTP/protobuf to a collector or vendor endpointVendor-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/metrics.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 default

Stores 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 /metrics endpoint to secure
  • DogStatsD tag extension provides the same label filtering as Prometheus

Enable StatsD

No extra packages needed — StatsD uses raw UDP sockets.

config/metrics.php
$config->adapter(MetricsAdapter::StatsD)
       ->statsDHost($_ENV['STATSD_HOST'] ?? '127.0.0.1')
       ->statsDPort(8125)
       ->statsDSampleRate(1.0);  // 1.0 = send all metrics

UDP 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 updates

The 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:201

OpenTelemetry 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/guzzle

Enable OTLP

config/metrics.php
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

ScenarioRecommended adapter
Local developmentNoOp (default)
Production with Grafana / PrometheusPrometheus + Redis storage
Production with an OpenTelemetry CollectorOpenTelemetry OTLP
Production with New RelicOpenTelemetry OTLP
Production with DatadogOpenTelemetry OTLP or StatsD (DogStatsD)
Production with GraphiteStatsD
CI / testingNoOp (default)
FrankenPHP worker modePrometheus + 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.

On this page