Accéder au contenu principal

Introducing beberlei/metrics v3.0.0

Since 2012, beberlei/metrics has been doing one small job well: giving PHP applications a single, consistent API to send metrics (counters, timings, gauges…) without tying the calling code to a specific backend. Swap StatsD for Prometheus, or send to both at once, and the instrumented code never changes.

Version 3.0 is the biggest release in the project’s history. It drops everything older than PHP 8.4, rewrites the core API around strict types, and – the part I’m most excited about – ships four brand-new collectors: Chain, OpenTelemetry, InfluxDB v2 and AWS CloudWatch.

Info

Every breaking change is documented in detail, with before/after examples, in the UPGRADE.md guide. The full list of changes lives in CHANGELOG.md.

Section intitulée what-is-beberlei-metrics-againWhat is beberlei/metrics, again?

It’s a thin abstraction layer: you ask a Factory for a collector, and you call the same four methods no matter what’s behind it.

$collector = \Beberlei\Metrics\Factory::create('statsd');

$collector->increment('foo.bar');
$collector->decrement('foo.bar');

$start = hrtime(true);
// ... do work ...
$milliseconds = (hrtime(true) - $start) / 1_000_000;
$collector->timing('foo.bar', $milliseconds);

$collector->flush();

Collectors that buffer their calls (most of them do) send everything on flush(), which the Symfony bundle wires automatically to kernel.terminate / console.terminate.

As of 3.0, the supported backends are: Chain, CloudWatch, Doctrine DBAL, DogStatsD, Graphite, InfluxDB v1, InfluxDB v2, Logger, Null, OpenTelemetry, Prometheus, StatsD, and Telegraf.

Section intitulée what-s-new-in-3–0What’s new in 3.0

Section intitulée four-new-collectorsFour new collectors

This is the headline change: four new backends, covering the tools most PHP shops actually run in production today.

OpenTelemetry: records measurements on a MeterProviderInterface, mapping each call to the instrument that matches its semantics: UpDownCounter for measure()/increment()/decrement(), Histogram for timing(), Gauge for gauge().

$collector = \Beberlei\Metrics\Factory::create('opentelemetry', [
    'meter_provider' => $meterProvider,
    'tags' => ['dc' => 'west'], // optional
]);

$collector->increment('foo.bar');
$collector->flush(); // forwards to $meterProvider->forceFlush()

InfluxDbV2: writes points to an InfluxDB 2.x/3.x bucket through the official influxdata/influxdb-client-php client (v3 servers accept the same write API in compatibility mode):

$collector = \Beberlei\Metrics\Factory::create('influxdb_v2', [
    'write_api' => $client->createWriteApi(),
    'tags' => ['dc' => 'west'],
]);

CloudWatch: publishes data points through the PutMetricData API via the official AWS SDK:

$collector = \Beberlei\Metrics\Factory::create('cloudwatch', [
    'client' => $client, // Aws\CloudWatch\CloudWatchClient
    'namespace' => 'my_app',
    'tags' => ['dc' => 'west'], // turned into CloudWatch dimensions
]);

Chain: dispatches every call to a list of other collectors, so the same metric can go to StatsD and a logger without touching the calling code:

$collector = new \Beberlei\Metrics\Collector\Chain(
    \Beberlei\Metrics\Factory::create('statsd'),
    \Beberlei\Metrics\Factory::create('logger', ['logger' => $logger]),
);

$collector->increment('foo.bar');
$collector->flush();

Chain also implements GaugeableCollectorInterface: gauge() calls are silently skipped on the collectors that don’t support gauges instead of erroring out.

Section intitulée a-stricter-tag-native-apiA stricter, tag-native API

CollectorInterface moves to native type declarations across the board: every method now returns void, and every metric method accepts an array $tags = [] argument directly:

interface CollectorInterface
{
    public function measure(string $variable, int $value, array $tags = []): void;
    public function increment(string $variable, array $tags = []): void;
    public function decrement(string $variable, array $tags = []): void;
    public function timing(string $variable, int|float $time, array $tags = []): void;
    public function flush(): void;
}

timing() now accepts integer or floating-point milliseconds, and every collector preserves the fractional part instead of truncating it on the way to the backend.

TaggableCollector::setTags() is gone: it required collectors to be mutable, which no longer fits a final, strictly-typed API. Tags are now either passed per call, or fixed once in the constructor:

// Before (2.x)
$collector = \Beberlei\Metrics\Factory::create('null_inlinetaggable');
$collector->setTags(['dc' => 'west']);
$collector->increment('foo.bar');

// After (3.0)
$collector = \Beberlei\Metrics\Factory::create('null');
$collector->increment('foo.bar', ['dc' => 'west']);

On top of that, every collector is now guaranteed to never let an error or exception from the underlying client reach the instrumented application: a metrics call should never be the thing that crashes your request.

Section intitulée the-symfony-bundle-grew-upThe Symfony bundle grew up

Every configured collector now gets an autowiring alias, so you inject a specific one without touching the container configuration:

use Beberlei\Metrics\Collector\CollectorInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

final readonly class MyService
{
    public function __construct(
        private CollectorInterface $collector,              // the default collector
        #[Target('prom')] private CollectorInterface $prom, // a named one
    ) {
    }
}

The beberlei_metrics.collector service alias is gone in favour of injecting CollectorInterface directly, every collector is now tagged kernel.reset (so long-running workers get a clean state on container reset), and the bundle no longer loads an XML services file: collector prototypes are registered programmatically instead.

Section intitulée a-full-demo-applicationA full demo application

The examples/ folder now ships a Symfony application wired to every collector, backed by a Docker stack (jolicode/docker-starter + Castor) that provisions Grafana dashboards out of the box for Prometheus, Graphite/StatsD/DogStatsD, InfluxDB v1 and v2, PostgreSQL, and CloudWatch (via LocalStack). One castor start, and every backend already has a dashboard waiting for it.

Homepage InfluxDB v1

Section intitulée housekeepingHousekeeping

The dependencies moved on too: the abandoned corley/influxdb-sdk is replaced by InfluxData’s own v1 client, influxdb/influxdb-php, and jimdo/prometheus_client_php by its actively maintained fork, promphp/prometheus_client_php. Every collector class, plus Factory itself, is now final. The test suite runs on native PHPUnit instead of the Symfony bridge, PHPStan and PHP-CS-Fixer are part of CI, and Travis has been replaced by GitHub Actions.

Section intitulée breaking-changes-at-a-glanceBreaking changes at a glance

Requirement 2.x 3.0
PHP >= 5.6 >= 8.4
Symfony (bundle) any (best effort) >= 6.4
psr/log ^1.0 || ^2.0 || ^3.0 ^3.0

The rest, in short:

  • The Zabbix and Librato collectors are removed with no replacement: relay through Telegraf or DogStatsD if you still need those backends.
  • Collector\CollectorCollector\CollectorInterface, Collector\GaugeableCollectorCollector\GaugeableCollectorInterface.
  • InfluxDB is renamed InfluxDbV1 (it only ever spoke the v1 API), with a new inner client.
  • DoctrineDBAL now stores a full datetime instead of a date-only column.

This list is not exhaustive: see UPGRADE.md for every change with a before/after snippet.

Section intitulée migrating-from-2-xMigrating from 2.x

For most projects, migration is a matter of renaming a handful of interfaces, moving setTags() calls into constructor arguments or per-call $tags, updating the type of any influxdb entry in your bundle configuration, and removing the zabbix/librato ones. The UPGRADE.md guide walks through each case with real code.

beberlei/metrics 3.0 is on GitHub and Packagist. Try the demo application to see every collector reporting to a live Grafana dashboard in a few minutes.

Special thanks to Benjamin Eberlei for his initial work on this library and for trusting me as a core maintainer. Anyone who knows me knows I’m obsessed with metrics, dashboards, and performance, so working on 3.0 was a real privilege. Fun fact: I actually started this refactoring back in 2024… time flies!

Commentaires et discussions

Ces clients ont profité de notre expertise