Search by

rasuvaeff / yii3-metrics-prometheus

rasuvaeff

Prometheus metrics backend for rasuvaeff/yii3-metrics (Yii3)

Package info

github.com/rasuvaeff/yii3-metrics-prometheus

pkg:composer/rasuvaeff/yii3-metrics-prometheus

Statistics

Installs: 43

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 1

v2.3.0 2026-09-25 18:13 UTC

README

Stable Version Total Downloads Build Static Analysis Psalm Level PHP License Русская версия

Prometheus backend for rasuvaeff/yii3-metrics. It records the core MetricRegistry metrics into promphp/prometheus_client_php and exposes them at a /metrics endpoint.

Using an AI coding assistant? llms.txt has a compact API reference you can pass as context.

Requirements

  • PHP 8.3+
  • rasuvaeff/yii3-metrics ^1.0
  • promphp/prometheus_client_php ^2.0
  • A shared storage backend for php-fpm: ext-apcu, ext-redis, predis/predis, or a PDO DSN

Installation

composer require rasuvaeff/yii3-metrics-prometheus

Installing this package binds the swappable MeterProviderInterface — the core MetricRegistry now records into Prometheus. Do not also bind the provider yourself (a deliberate yiisoft/config Duplicate key).

Usage

Multiprocess storage (required for php-fpm)

promphp's default InMemory storage is per process, so under php-fpm /metrics would only reflect the worker that served the scrape. Use a shared adapter via env:

Runtime PROMETHEUS_STORAGE Needs
php-fpm (multiple workers) apcng (recommended), apcu, redis, predis, or pdo ext-apcu / ext-redis / predis/predis / a PDO DSN
RoadRunner / Swoole (one long-running process) in_memory —
CLI / tests in_memory —

apcng is promphp's newer APCu adapter with much cheaper scrape-time collection on large registries — prefer it over apcu for new deployments. predis uses the pure-PHP client (no ext-redis).

use Rasuvaeff\Yii3MetricsPrometheus\StorageFactory;

$adapter = (new StorageFactory())->create('apcng');
// pure-PHP Redis client (no ext-redis):
$adapter = (new StorageFactory())->create('predis', ['host' => 'redis', 'port' => 6379]);
$adapter = (new StorageFactory())->create('redis', [
    'host' => 'redis', 'persistent_connections' => true,
    'timeout' => 0.2, 'read_timeout' => 0.5,
    'database' => 2, 'prefix' => 'checkout:PROMETHEUS_',
]);
// opt-in EVALSHA: writes address Lua scripts by SHA-1, plain EVAL after NOSCRIPT
$adapter = (new StorageFactory())->create('predis', [
    'host' => 'redis', 'evalsha' => true,
]);
// or, without apcu/redis (MySQL, PostgreSQL, SQLite):
$adapter = (new StorageFactory())->create('pdo', [
    'dsn' => 'mysql:host=db;dbname=app',
    'username' => 'app',
    'password' => getenv('DB_PASSWORD'),
]);

What evalsha: true saves. Each Redis write normally carries the full Lua script body (several hundred bytes). With evalsha the write addresses the script by its SHA-1, so only the JSON metadata and label values stay on the wire — they are still sent on every write, and the number of round trips is unchanged (one per write): the win is payload size, not latency. The script is looked up optimistically — no SCRIPT LOAD round trip and no client-side state — so the saving applies from the first write after Redis's script cache is warm, in php-fpm workers and long-running processes alike, and the mode keeps working where the SCRIPT command is disabled by ACL. A NOSCRIPT reply (cold cache, Redis restart, SCRIPT FLUSH) transparently falls back to plain EVAL for that write, and a failing write throws on either path (RedisClientException) — on phpredis the fallback runs under the same error check as the EVALSHA probe. Latency wins come from buffered writes (#25, not implemented yet). evalsha accepts the usual boolean spellings (true/false, 1/0, "yes"/"no", "on"/"off").

An unknown adapter name throws (no silent fallback), and selecting in_memory under php-fpm is reported — a scrape that silently shows one worker's counters is worse than a visible note. The report goes to the application's PSR-3 logger (the package DI passes it in), or to error_log() when there is none.

It is deliberately not a PHP warning. yiisoft/error-handler converts warnings into ErrorException, so a trigger_error() here would make the shipped default configuration (in_memory + php-fpm) throw out of the DI factory and 500 every request that touches metrics.

The /metrics endpoint

MetricsEndpoint (PSR-15) renders the registry as Prometheus text exposition (text/plain; version=0.0.4). Route your /metrics path to it — it needs a PSR-17 ResponseFactoryInterface. The handler itself has no access control: it serves the full exposition to any request that reaches it, so restrict the path at your edge (see Security). A sample whose labels no longer match its metric (possible with a Redis storage) is rendered as a comment instead of failing the whole scrape.

use Rasuvaeff\Yii3MetricsPrometheus\MetricsEndpoint;

$endpoint = new MetricsEndpoint($collectorRegistry, $responseFactory);

If storage cannot be read, the endpoint returns 503 with the plain text body metrics storage unavailable. Protect the endpoint at the edge.

Safe labels (cardinality)

SanitizingRouteResolver collapses id-like path segments (/users/123 → /users/:id, UUIDs → :uuid) for the RED middleware's route label.

The core's shipped default is ConstantRouteResolver — the route label is the constant (unset) until the application picks a resolver, because a raw path is attacker-controlled. This resolver is one of the opt-ins, and it narrows the id case only: arbitrary scanner paths and non-UUID tokens stay unique, so it neither bounds cardinality nor hides secrets in a path. Prefer the core's CurrentRouteResolver (matched router pattern) when you have a router, or wrap this one in BoundedRouteResolver to cap the series count.

Rebind it in your app config (an app-layer override):

// config/common/di.php
use Rasuvaeff\Yii3Metrics\RouteResolverInterface;
use Rasuvaeff\Yii3MetricsPrometheus\SanitizingRouteResolver;

return [
    RouteResolverInterface::class => SanitizingRouteResolver::class,
];

Keep label values low-cardinality — one time series is created per unique label combination.

Recording with a label name that was not declared at registration throws InvalidArgumentException (a typo'd label would otherwise silently record under an empty value); so does a declared label missing from the recording's set — silently filing it as "" merged every such observation into one empty-valued series, the same corruption the undeclared case is rejected for.

Histogram defaults

A histogram registered without explicit buckets gets the core's Buckets::PROMETHEUS_DEFAULTS (11 bounds, 0.005 s … 10 s) — the same list on every backend, so bucket assertions in in-memory tests describe the production schema. promphp's own default layout (14 bounds) is never used. Metric names and bucket layouts are validated by the same core Validation the in-memory and no-op meters apply.

Numeric input contract

Recorded amounts must be finite: inc/observe/add and gauge inc/dec throw on NAN/±INF; gauge set() allows ±INF but throws on NAN (promphp has no renderable NaN token). Mirrors the core contract, so the same code cannot behave differently per backend — and nothing non-finite reaches the shared, durable storage adapters, where a single unguarded NAN would poison the series total until the storage is flushed.

Metric namespace

Set PROMETHEUS_NAMESPACE (params namespace) to prefix every metric: checkout_http_server_requests_total. Empty by default.

For Redis and Predis, pass storage_options.prefix to set promphp's process-global metric prefix before adapter creation. Use a distinct prefix per application sharing Redis; short timeouts and persistent connections are recommended for metrics.

Strict naming

Set params strict_naming to true (or pass new PrometheusMeterProvider($registry, strictNaming: true)) to apply the core's registration checks (yii3-metrics 2.3+). Registration then throws InvalidArgumentException when:

  • a counter does not end with _total, or a gauge, up-down counter or histogram does;
  • a histogram ends with _bucket, _sum or _count;
  • a name is re-registered with another kind, label names, buckets or a different non-empty help (an empty help matches any);
  • two metrics expose the same series (histogram latency + gauge latency_count).

Names are checked without the namespace prefix. One guard is shared by every scoped meter of the provider. The checks are per process: two workers that register the same name differently are not compared. Off by default: without it the first registration's help, label names and buckets win silently.

Classes

Class Purpose
PrometheusMeterProvider core MeterProviderInterface over a promphp CollectorRegistry
PrometheusMeter / PrometheusCounter / PrometheusGauge / PrometheusUpDownCounter / PrometheusHistogram adapters
PrometheusRenderer render a registry as text exposition
MetricsEndpoint PSR-15 /metrics handler
StorageFactory build the storage adapter (in_memory/apcu/apcng/redis/predis/pdo)
SanitizingRouteResolver opt-in low-cardinality route label

Security

  • Label values are arbitrary — keep ids/tokens out of them; use SanitizingRouteResolver for the route label.
  • php_info and other promphp default metrics are disabled (registerDefaultMetrics: false).
  • Protect /metrics: the exposition reveals routes, traffic, and error rates. Restrict it to your scrape network (firewall / ingress allowlist) or put basic auth in front — do not expose it publicly.

Examples

Runnable, server-independent scripts in examples/. See examples/README.md.

Dependency analysers

This leaf package is selected by the root application through config-plugin and may legitimately have no class reference in an autoloaded source directory. Keep the direct dependency: the application, not a core package, selects the backend or bridge. Scope the Composer Dependency Analyser exception to this package:

use ShipMonk\ComposerDependencyAnalyser\Config\Configuration;
use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType;

return (new Configuration())->ignoreErrorsOnPackage(
    'rasuvaeff/yii3-metrics-prometheus',
    [ErrorType::UNUSED_DEPENDENCY],
);

composer-require-checker detects used but undeclared symbols, not unused packages, so this config-only dependency needs no require-checker suppression.

Development

The core is resolved via a path repository, so run Docker with the monorepo root mounted as /repo:

docker run --rm -v /path/to/monorepo:/repo -w /repo/yii3-metrics-prometheus \
  composer:2 composer build

See AGENTS.md.

License

BSD-3-Clause. See LICENSE.md.