rasuvaeff / yii3-metrics
Vendor-neutral metrics facade (counters, gauges, histograms) for Yii3
Requires
- php: 8.3 - 8.5
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.33
- maglnet/composer-require-checker: ^4.17
- nyholm/psr7: ^1.8
- rasuvaeff/property-testing: ^2.3
- rector/rector: ^2.4
- roave/backward-compatibility-check: ^8.0
- testo/bridge-infection: ^0.1.6
- testo/testo: ^0.10.25
- vimeo/psalm: ^6.16
- yiisoft/dummy-provider: ^1.1
- yiisoft/router: ^4.0.2
Suggests
- rasuvaeff/yii3-metrics-prometheus: Prometheus exposition backend (binds MeterProviderInterface)
- yiisoft/router: Router-aware low-cardinality route label via CurrentRouteResolver
This package is auto-updated.
Last update: 2026-07-10 18:31:12 UTC
README
Vendor-neutral metrics for Yii3: a MetricRegistry facade over counters, gauges,
and histograms, plus a PSR-15 RED middleware. The exporter is a swappable backend
(Prometheus today; the swappable provider key leaves room for others).
Using an AI coding assistant? llms.txt has a compact API reference you can pass as context.
Requirements
- PHP 8.3+
- PSR-7/PSR-15 interfaces (for the RED middleware)
Installation
composer require rasuvaeff/yii3-metrics
For real export, add the backend: rasuvaeff/yii3-metrics-prometheus.
Without one, bind
MeterProviderInterface => NullMeterProvider (see Wiring).
Usage
Record metrics
use Rasuvaeff\Yii3Metrics\LabelSet; use Rasuvaeff\Yii3Metrics\MetricRegistry; /** @var MetricRegistry $registry (injected) */ $orders = $registry->counter('orders_total', 'Orders placed', ['channel']); $orders->inc(1.0, new LabelSet(['channel' => 'web'])); $inflight = $registry->upDownCounter('inflight_jobs', 'Jobs in flight'); $inflight->add(1.0); // job started $inflight->add(-1.0); // job finished $temperature = $registry->gauge('room_temperature', 'Measured value'); $temperature->set(21.5); $latency = $registry->histogram('db_query_seconds', 'Query time', ['op'], [0.001, 0.01, 0.1]); $latency->observe(0.023, new LabelSet(['op' => 'select']));
Instruments record into per-name accumulating state — asking for
counter('orders_total') again returns an instrument over the same series. A
counter rejects a negative increment.
Gauge vs up-down counter. A gauge is for a measured absolute value
(set() — temperature, disk usage); an up-down counter is for counted ups and
downs (add(±δ) — in-flight requests, pool size). Prefer the up-down counter
for counted values: each process contributes deltas, so it aggregates correctly
across short-lived php-fpm workers, where a gauge's inc()/dec() (kept for
single-process convenience) would restart from the process-local value.
Naming & labels
- Metric names follow the Prometheus grammar
^[a-zA-Z_:][a-zA-Z0-9_:]*$(underscores, no dots) — the lowest common denominator both backends render. LabelSetvalidates label names (^[a-zA-Z_]\w*$) and stores them in canonical order, so equality is order-independent.
RED middleware
RedMetricsMiddleware (PSR-15) records, for every request, a
http_server_requests_total counter and a http_server_request_duration_seconds
histogram, labelled by method, route, and status (500 when the handler
throws).
use Rasuvaeff\Yii3Metrics\RedMetricsMiddleware; $middleware = new RedMetricsMiddleware($registry); // add to your PSR-15 stack // Latency profile doesn't fit the Prometheus defaults (0.005s…10s)? // Override the histogram bounds (seconds, strictly increasing; +Inf appended): $middleware = new RedMetricsMiddleware($registry, durationBuckets: [0.1, 1.0, 10.0, 60.0]); // Skip scrape/probe endpoints (exact paths) — their self-traffic is noise: $middleware = new RedMetricsMiddleware($registry, excludedPaths: ['/metrics', '/health']);
With yiisoft/config wiring, both come from the package params instead:
// config/common/params.php (app override) 'rasuvaeff/yii3-metrics' => [ 'red' => [ 'duration_buckets' => [0.1, 1.0, 10.0], 'excluded_paths' => ['/metrics', '/health'], ], ],
Cardinality: the
routelabel defaults to the raw path — a new time series per/users/123. In production inject the router-aware resolver (below) or a sanitizing one from the Prometheus backend.
With yiisoft/router installed, CurrentRouteResolver resolves the label to the
matched route pattern (/users/{id}) — low-cardinality by construction.
Unmatched requests (404, scanners) collapse to (unmatched) unless you pass a
fallback resolver:
// config/common/di.php — app-side rebind (the core binds PathRouteResolver) use Rasuvaeff\Yii3Metrics\CurrentRouteResolver; use Rasuvaeff\Yii3Metrics\RouteResolverInterface; return [ RouteResolverInterface::class => CurrentRouteResolver::class, ];
Place RedMetricsMiddleware before the router middleware — the label is
resolved after the handler ran, when CurrentRoute is populated.
Inspecting metrics in tests
use Rasuvaeff\Yii3Metrics\InMemoryMeterProvider; use Rasuvaeff\Yii3Metrics\MetricRegistry; $provider = new InMemoryMeterProvider(); $registry = new MetricRegistry($provider); $registry->counter('c')->inc(); $snapshots = $provider->snapshots(); // list<MetricSnapshot>, no timestamp
API surface
| Type | Role |
|---|---|
MetricRegistry |
facade: counter/gauge/upDownCounter/histogram(name, help, labelNames, buckets) |
MeterProviderInterface / MeterInterface |
swappable backend entry point; a meter creates and memoizes instruments |
CounterInterface / GaugeInterface / UpDownCounterInterface / HistogramInterface |
instrument contracts |
LabelSet / MetricKind |
validated label pairs / instrument kind enum (Counter, Gauge, UpDownCounter, Histogram) |
MetricSnapshot / MetricSample |
collected state: a metric (name, kind, help) and its per-label-set samples |
NullMeterProvider, NullMeter, NullCounter, NullGauge, NullUpDownCounter, NullHistogram |
no-op backend (config-only default; still validates structure) |
InMemoryMeterProvider, InMemoryMeter, InMemoryCounter, InMemoryGauge, InMemoryUpDownCounter, InMemoryHistogram |
single-process dev/test backend with snapshots() |
RedMetricsMiddleware, RouteResolverInterface, PathRouteResolver |
PSR-15 RED instrumentation |
CurrentRouteResolver |
route label from the matched yiisoft/router pattern (optional dep) |
Wiring (yiisoft/config)
The core config/di.php binds the facade (MetricRegistry) and the default
RouteResolverInterface. It never binds MeterProviderInterface — that swappable
key is owned by exactly one source:
// config/common/di.php — with no backend installed use Rasuvaeff\Yii3Metrics\MeterProviderInterface; use Rasuvaeff\Yii3Metrics\NullMeterProvider; return [ MeterProviderInterface::class => NullMeterProvider::class, ];
Installing a backend provides the real binding — binding it in two vendor packages
is a deliberate yiisoft/config Duplicate key error.
Security
- Label names are validated; label values are arbitrary — keep high-cardinality or sensitive values (ids, tokens) out of labels.
- The RED
routelabel defaults to the path; sanitize it in production.
Examples
Runnable, server-independent scripts in examples/. See
examples/README.md.
Development
docker run --rm -v "$PWD":/app -w /app composer:2 composer build
Runs validate → normalize → require-checker → cs → psalm → tests (incl. property tests). See AGENTS.md.
License
BSD-3-Clause. See LICENSE.md.