Vortos
Metrics

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.

Metrics

vortos-metrics records runtime performance numbers across every major framework module. NoOp by default — zero overhead until you configure an adapter. When you switch to Prometheus, StatsD, or OpenTelemetry OTLP, counters, gauges, and histograms start flowing with no code changes required.

What are metrics and why do they matter?

A metric is a number that changes over time — request count, response latency, cache hit rate, queue depth. Unlike logs (individual events) and traces (single request trees), metrics are aggregated: you don't record the duration of every single request individually, you record a histogram that summarises the distribution of all request durations.

Metrics answer questions that logs can't:

  • What is the 99th percentile response time over the last 5 minutes?
  • How many orders failed this hour compared to last hour?
  • Is the cache hit rate dropping as traffic increases?

These patterns only emerge from data aggregated over time — that is what metrics are for.

Three metric types:

TypeWhat it measuresExample
CounterA value that only goes upTotal HTTP requests, total errors
GaugeA value that goes up and downActive connections, queue depth
HistogramDistribution of values in bucketsResponse time in ms, DB query duration

A histogram is the most powerful type. Instead of just knowing the average response time (which hides outliers), a histogram tells you: 50% of requests completed in under 50ms, 95% in under 200ms, 99% in under 500ms. Those percentile breakpoints (p50, p95, p99) are what SLAs are defined on.


Installation

composer require vortos/vortos-metrics

For the Prometheus adapter, also install the client library:

composer require promphp/prometheus_client_php

For the OpenTelemetry OTLP metrics adapter, install the official OpenTelemetry packages and a PSR-18 HTTP client:

composer require open-telemetry/sdk open-telemetry/exporter-otlp guzzlehttp/guzzle

Package registration

bootstrap/app.php
use Vortos\Metrics\DependencyInjection\MetricsPackage;

$packages = [
    new LoggerPackage(),     // order 10
    new TracingPackage(),    // order 50
    new MetricsPackage(),    // order 55
    new HttpPackage(),
    // ...
];

Register MetricsPackage after TracingPackage — metrics auto-instrumentation can optionally attach exemplar links to traces.


Quick start

Development (NoOp — default)

No configuration needed. All MetricsInterface calls are no-ops. Register the package and move on.

Prometheus

config/metrics.php
use Vortos\Metrics\Config\MetricsAdapter;
use Vortos\Metrics\DependencyInjection\VortosMetricsConfig;

return static function (VortosMetricsConfig $config): void {
    $config->adapter(MetricsAdapter::Prometheus)
           ->prometheusEndpointToken($_ENV['METRICS_TOKEN'] ?? '');
};

Prometheus scrapes your /metrics endpoint every 15 seconds. Add the scrape target in your prometheus.yml:

scrape_configs:
  - job_name: 'my-app'
    static_configs:
      - targets: ['your-app:80']
    bearer_token: 'your-metrics-token'

StatsD / Datadog DogStatsD

config/metrics.php
use Vortos\Metrics\Config\MetricsAdapter;
use Vortos\Metrics\DependencyInjection\VortosMetricsConfig;

return static function (VortosMetricsConfig $config): void {
    $config->adapter(MetricsAdapter::StatsD)
           ->statsDHost($_ENV['STATSD_HOST'] ?? '127.0.0.1')
           ->statsDPort(8125);
};

OpenTelemetry OTLP metrics push

Use OTLP when you want vendor-neutral push metrics to an OpenTelemetry Collector, New Relic, Datadog, Honeycomb, or another OTLP-compatible backend.

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(
            name: 'checkout-api',
            version: $_ENV['APP_VERSION'] ?? '',
            environment: $_ENV['APP_ENV'] ?? 'prod',
        )
        ->otlpCollector(
            endpoint: $_ENV['OTEL_EXPORTER_OTLP_METRICS_ENDPOINT'] ?? 'http://otel-collector:4318/v1/metrics',
            timeoutMs: 200,
        );
};

The OTLP adapter uses the official OpenTelemetry SDK and exporter. It calls forceFlush() at request, command, and error lifecycle boundaries. It does not call shutdown() per request, so long-running FrankenPHP workers keep a live meter provider for the next request.


Configuration reference

Create the config file:

php vortos make:config metrics
config/metrics.php
use Vortos\Metrics\Config\{MetricsAdapter, MetricsModule};
use Vortos\Metrics\DependencyInjection\VortosMetricsConfig;

return static function (VortosMetricsConfig $config): void {

    // Choose adapter (default: NoOp)
    $config->adapter(MetricsAdapter::Prometheus);

    // Set the metric namespace prefix — all names become {namespace}_{name}
    $config->namespace('myapp');   // default: 'vortos'

    // Prometheus storage backend (choose one)
    $config->prometheusStorageInMemory();  // default — single process only
    $config->prometheusStorageApc();       // PHP-FPM multi-process safe
    $config->prometheusStorageRedis(       // FrankenPHP multi-process safe
        prefix: 'metrics:',
        host: $_ENV['REDIS_HOST'] ?? '127.0.0.1',
        port: 6379,
        password: $_ENV['REDIS_PASSWORD'] ?? '',
    );

    // Endpoint security (production requires one of these)
    $config->prometheusEndpointToken($_ENV['METRICS_TOKEN'] ?? '');
    // OR confirm network-level protection is in place:
    $config->prometheusEndpointOpenAccess();

    // StatsD settings
    $config->statsDHost('127.0.0.1')
           ->statsDPort(8125)
           ->statsDSampleRate(0.1);   // send 10% of metrics (reduces UDP volume)

    // OpenTelemetry OTLP metrics settings
    $config
        ->service('checkout-api', version: $_ENV['APP_VERSION'] ?? '', environment: $_ENV['APP_ENV'] ?? 'prod')
        ->otlpCollector('http://otel-collector:4318/v1/metrics', timeoutMs: 200);

    // Direct vendor helpers (choose one instead of otlpCollector when pushing directly)
    // $config->newRelicOtlp($_ENV['NEW_RELIC_LICENSE_KEY'] ?? '');
    // $config->datadogOtlp($_ENV['DD_API_KEY'] ?? '', site: $_ENV['DD_SITE'] ?? 'datadoghq.com');

    // Silence high-cardinality or high-volume modules
    $config->disableModule(MetricsModule::Cache);
    $config->disableModule(MetricsModule::Persistence);
};

Environment-specific overrides go in config/{env}/metrics.php. They are merged on top of the base config.


Inject and use MetricsInterface

For application-level metrics — business events that matter to you specifically:

use Vortos\Metrics\Contract\MetricsInterface;

final class OrderService
{
    public function __construct(private readonly MetricsInterface $metrics) {}

    public function place(Order $order): void
    {
        $start = hrtime(true);

        try {
            // ... place order
            $this->metrics->counter('orders_placed_total', [
                'currency' => $order->getCurrency(),
                'channel'  => $order->getChannel(),
            ])->increment();

        } finally {
            $elapsed = (hrtime(true) - $start) / 1_000_000; // nanoseconds → ms
            $this->metrics->histogram('order_processing_duration_ms', labels: [
                'currency' => $order->getCurrency(),
            ])->observe($elapsed);
        }
    }
}

Labels (the array argument) are the dimensions you can filter and group by in your dashboard. Keep them low-cardinality — use currency (a handful of values), not order_id (unbounded). See the auto-instrumentation docs for a detailed explanation.


Framework metrics — what's recorded automatically

All of these are recorded without any code changes. To see the full label set for each metric, see Auto-instrumentation.

MetricTypeModule
{ns}_http_requests_totalCounterHTTP
{ns}_http_request_duration_msHistogramHTTP
{ns}_http_blocked_totalCounterHTTP / Security
{ns}_cqrs_commands_totalCounterCQRS
{ns}_cqrs_command_duration_msHistogramCQRS
{ns}_cqrs_command_failures_totalCounterCQRS
{ns}_cqrs_queries_totalCounterCQRS
{ns}_cqrs_query_duration_msHistogramCQRS
{ns}_cqrs_query_failures_totalCounterCQRS
{ns}_messaging_events_dispatched_totalCounterMessaging
{ns}_messaging_event_failures_totalCounterMessaging
{ns}_messaging_event_duration_msHistogramMessaging
{ns}_messaging_messages_consumed_totalCounterMessaging
{ns}_messaging_message_retries_totalCounterMessaging
{ns}_messaging_message_duration_msHistogramMessaging
{ns}_outbox_backlog_sizeGaugeMessaging
{ns}_outbox_oldest_pending_age_secondsGaugeMessaging
{ns}_dlq_backlog_sizeGaugeMessaging
{ns}_dlq_oldest_failed_age_secondsGaugeMessaging
{ns}_cache_operations_totalCounterCache
{ns}_db_queries_totalCounterPersistence
{ns}_db_query_duration_msHistogramPersistence
{ns}_security_events_totalCounterSecurity
{ns}_rate_limit_allowed_totalCounterRate Limit
{ns}_rate_limit_blocked_totalCounterRate Limit
{ns}_quota_allowed_totalCounterQuota
{ns}_quota_blocked_totalCounterQuota
{ns}_quota_consumed_totalCounterQuota
{ns}_feature_access_allowed_totalCounterFeature Access
{ns}_feature_access_denied_totalCounterFeature Access
{ns}_feature_flag_evaluations_totalCounterFeature Flags

{ns} is the configured namespace (default: vortos).


Module overview

On this page