Skip to content

Metrics

Metrics

Counters

Monotonic, cumulative, shared across processes:

Telemetry::counter('orders.created', 'Orders created')->inc();
Telemetry::counter('orders.created')->inc(5, ['tenant' => 'acme']);

Negative increments are silently ignored (counters never decrease). Prometheus output gets the conventional _total suffix: orders_created_total{tenant="acme"} 5.

Gauges

// Pull: evaluated at scrape time. Preferred when the value is queryable.
Telemetry::gauge('queue.depth', fn () => Queue::size(), unit: '{jobs}');

// Push: stored at event time. For values only known when they happen.
Telemetry::gauge('deploy.timestamp')->set(now()->timestamp);

// Push gauges also adjust atomically — for values that go up AND down:
Telemetry::gauge('jobs.in_flight')->increment(labels: ['queue' => 'mail']);
Telemetry::gauge('jobs.in_flight')->decrement(labels: ['queue' => 'mail']);

Observable callbacks may return a single number or multiple series as [value, labels] pairs. Failing callbacks are dropped for that scrape and reported — the endpoint never 500s because one source is down.

Keep scrape-time callbacks cheap; they run on every scrape and every telemetry:flush.

Histograms

Telemetry::histogram('http.client.duration', unit: 'ms')->record($ms);

Telemetry::histogram('import.duration', buckets: [100, 500, 1000, 5000])
    ->time(fn () => $importer->run());

Defaults come from telemetry.default_buckets (1 ms – 10 s). Buckets are stored non-cumulatively (OTLP form) and accumulated into cumulative le buckets when rendered for Prometheus.

Cardinality: every labelset costs buckets + 2 store fields. Keep label values bounded — route patterns, not URLs; status codes, not user ids.

The store

Driver Write mechanics Scope
redis one HASH per metric family + index SETs; steady-state writes are a single atomic command (Redis Cluster-safe); SMEMBERS+HGETALL scrape — never KEYS/SCAN cluster-wide
apcu CAS loops with explicit key indexes — never iterates the full APCu keyspace single node
array plain arrays per process (tests)

Metadata (description, unit, buckets) is written idempotently with the first sample, so scrapes are self-describing.

Cache visibility is two independent switches: instrument.cache (aggregate counters, no keys) and instrument.cache_spans — the Nightwatch-style timeline where every hit/miss/write/forget appears as a span with its key, store and measured duration. Keys are safe on spans (per-occurrence); they are never metric labels.

Changing histogram buckets requires a store wipe (php artisan telemetry:flush --wipe) — old bucket counts cannot be re-binned into new boundaries. Metadata itself refreshes automatically on deploy. On Redis Cluster every write is a single command, so the store works without cross-slot transaction concerns.

There is deliberately no summary instrument — quantiles come from histograms in your backend (histogram_quantile() in PromQL).