Add a new OpenTelemetry metric (counter, up-down counter, histogram, or gauge) to this bundle. Use when asked to "add a metric", "count X", "record a histogram for X", or "publish token/usage metrics" in worldia/instrumentation-bundle. Encodes the metrics conventions, which differ from tracing — DI-only meter (no static facade), hardcoded attribute strings, and a separate feature toggle/subscriber.
php artisan boost:add-skill worldia/instrumentation-bundle
Save this content to: .claude/skills/add-metric/SKILL.md
# Add a metric
Metrics in this bundle are wired separately from tracing and follow different rules. Do not bolt metrics onto a tracing decorator/subscriber — even when both observe the same event, the bundle keeps them as independent subscribers under independent toggles (see Messenger: `src/Tracing/Messenger/...` vs `src/Metrics/EventSubscriber/MessageEventSubscriber.php`).
## Key differences from tracing
- **No static facade.** There is no `Metrics::getMeter()` equivalent to `Tracing::getTracer()`. Inject `OpenTelemetry\API\Metrics\MeterProviderInterface` and call `$meterProvider->getMeter('instrumentation')` in the constructor.
- **Attribute names are plain strings** (e.g. `['bus' => ..., 'class' => ...]`), not `OpenTelemetry\SemConv` constants. (Tracing uses semconv constants; metrics historically don't.) If a published semconv _metric_ exists for your case (e.g. `gen_ai.client.token.usage`), prefer that instrument name and its semconv attributes for ecosystem compatibility, but match the surrounding code's style.
## Instrument types & naming
Create instruments on the meter and act on them:
- `createCounter('<name>_total')->add($n, $attrs)` — monotonic.
- `createUpDownCounter('<name>')->add(±1, $attrs)` — gauge-like running count (e.g. `consumers_active`, `messages_handling`).
- `createHistogram('<name>_<unit>', '<unit>', '<desc>')->record($v, $attrs)` — distributions (e.g. `messages_time_to_consume_seconds`).
- `createGauge('<name>_<unit>', null, '<desc>')->record($v, $attrs)` — point-in-time (e.g. `memory_usage_bytes`).
Naming: snake_case, unit suffix (`_seconds`, `_bytes`), `_total` suffix for monotonic counters. Reference: `src/Metrics/EventSubscriber/MessageEventSubscriber.php`, `ConsumerEventSubscriber.php`, `RequestEventSubscriber.php`.
## The subscriber
Add `src/Metrics/EventSubscriber/<Feature>EventSubscriber.php` implementing `EventSubscriberInterface`, with `MeterProviderInterface` injected. Cache the meter in the constructor; create instruments lazily where recorded (the existing code calls `createCounter(...)` at the record site — that's fine, instruments are idempotent by name).
## DI wiring
- **`src/DependencyInjection/config/metrics/<feature>.php`** — register the subscriber with `->autoconfigure()` and `->args([service(MeterProviderInterface::class)])`. Model: `config/metrics/message.php`.
- **`src/DependencyInjection/Configuration.php`** — add `arrayNode('<feature>')->addDefaultsIfNotSet()` under `metrics`, with `booleanNode('enabled')->defaultFalse()` and any `blacklist` node (see the `metrics` tree, lines 180–205).
- **`src/DependencyInjection/Extension.php` → `loadMetrics()`** — add `if ($this->isConfigEnabled($container, $config['<feature>'])) { ...; $loader->load('<feature>.php'); }`. Set `metrics.<feature>.blacklist` param first if the subscriber filters (model: the `request` branch, lines 230–233). The core `metrics.php` (MeterProvider) is always loaded.
## Verify
- Subscriber gets the meter from injected `MeterProviderInterface`, never a static accessor.
- Instrument name follows snake_case + unit/`_total` convention.
- Feature is opt-in via its own `metrics.<feature>.enabled` node and loaded in `loadMetrics()`.
- Add/extend a test under `tests/Metrics/` using an in-memory metric reader, mirroring existing metric tests.
Metrics in this bundle are wired separately from tracing and follow different rules. Do not bolt metrics onto a tracing decorator/subscriber — even when both observe the same event, the bundle keeps them as independent subscribers under independent toggles (see Messenger: src/Tracing/Messenger/... vs src/Metrics/EventSubscriber/MessageEventSubscriber.php).
Metrics::getMeter() equivalent to Tracing::getTracer(). Inject OpenTelemetry\API\Metrics\MeterProviderInterface and call $meterProvider->getMeter('instrumentation') in the constructor.['bus' => ..., 'class' => ...]), not OpenTelemetry\SemConv constants. (Tracing uses semconv constants; metrics historically don't.) If a published semconv metric exists for your case (e.g. gen_ai.client.token.usage), prefer that instrument name and its semconv attributes for ecosystem compatibility, but match the surrounding code's style.Create instruments on the meter and act on them:
createCounter('<name>_total')->add($n, $attrs) — monotonic.createUpDownCounter('<name>')->add(±1, $attrs) — gauge-like running count (e.g. consumers_active, messages_handling).createHistogram('<name>_<unit>', '<unit>', '<desc>')->record($v, $attrs) — distributions (e.g. messages_time_to_consume_seconds).createGauge('<name>_<unit>', null, '<desc>')->record($v, $attrs) — point-in-time (e.g. memory_usage_bytes).Naming: snake_case, unit suffix (_seconds, _bytes), _total suffix for monotonic counters. Reference: src/Metrics/EventSubscriber/MessageEventSubscriber.php, ConsumerEventSubscriber.php, RequestEventSubscriber.php.
Add src/Metrics/EventSubscriber/<Feature>EventSubscriber.php implementing EventSubscriberInterface, with MeterProviderInterface injected. Cache the meter in the constructor; create instruments lazily where recorded (the existing code calls createCounter(...) at the record site — that's fine, instruments are idempotent by name).
src/DependencyInjection/config/metrics/<feature>.php — register the subscriber with ->autoconfigure() and ->args([service(MeterProviderInterface::class)]). Model: config/metrics/message.php.src/DependencyInjection/Configuration.php — add arrayNode('<feature>')->addDefaultsIfNotSet() under metrics, with booleanNode('enabled')->defaultFalse() and any blacklist node (see the metrics tree, lines 180–205).src/DependencyInjection/Extension.php → loadMetrics() — add if ($this->isConfigEnabled($container, $config['<feature>'])) { ...; $loader->load('<feature>.php'); }. Set metrics.<feature>.blacklist param first if the subscriber filters (model: the request branch, lines 230–233). The core metrics.php (MeterProvider) is always loaded.MeterProviderInterface, never a static accessor._total convention.metrics.<feature>.enabled node and loaded in loadMetrics().tests/Metrics/ using an in-memory metric reader, mirroring existing metric tests.How can I help you explore Laravel packages today?