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).