Skip to content

feat: metrics instrumentation via typed EventEmitter#1751

Draft
tomwilkie wants to merge 10 commits into
Koenkk:masterfrom
tomwilkie:metrics-instrumentation
Draft

feat: metrics instrumentation via typed EventEmitter#1751
tomwilkie wants to merge 10 commits into
Koenkk:masterfrom
tomwilkie:metrics-instrumentation

Conversation

@tomwilkie
Copy link
Copy Markdown

@tomwilkie tomwilkie commented Apr 26, 2026

Summary

  • Replaces the injectable Metrics interface / setMetrics / noopMetrics pattern with a typed EventEmitter<MetricsEventMap> singleton exported as metrics
  • Instruments send methods (sendZclFrameToEndpoint, sendZdo, sendZclFrameToGroup, sendZclFrameToAll) directly in the controller via a shared instrumentSend helper, with timing and success/failure status
  • Adds metrics emission for adapter retries, ZDO responses, ZCL payload receives, and request queue length/duration
  • Enqueue timestamp (enqueuedAt) stored directly on Request alongside expires, replacing a parallel WeakMap on RequestQueue
  • Consumers subscribe via metrics.on("adapterSendZclUnicast", (data) => ...) etc. — metrics are always emitted unconditionally

Test plan

  • npm test — all 1744 tests pass
  • Subscribe to metrics.on(...) events and verify they fire on adapter sends

🤖 Generated with Claude Code

@Nerivec
Copy link
Copy Markdown
Collaborator

Nerivec commented Apr 26, 2026

This appears very targeted for Prometheus use (it is also not a no-op).
If we are to implement metrics in ZH, I think it should be something generic that any dependency-user (be it Z2M, iobroker, etc.) can use with the usual ZH events API (e.g. Z2M health system can make use of it, same as the rest).

We should also be very careful about possible performance degradation (be it small but widespread, or very large). Since this is "extra", it should have a very minimal impact overall.

@tomwilkie
Copy link
Copy Markdown
Author

Hi @Nerivec! Thank you for your feedback - I mostly just opened this PR to see if you were open to this and get feedback on the direction to go.

This appears very targeted for Prometheus use

I tried to make it as non-prometheus specific as I could - e.g. not encoding counter, gauge or histogram specifics, but instead specific events to measure. I'll admit I'm only familiar with Prometheus and OpenTelemetry, but if you have any other suggestions please let me know.

it is also not a no-op

I tried to make the calling sites as simple as possible (e.g. not having each one check if the metrics received is null but instead of having a no-op receiver) - but I could change that? WDYT?

I could also change the instrumentation of the adapter so that it only gets added if there is a metrics callback?

If we are to implement metrics in ZH, I think it should be something generic that any dependency-user (be it Z2M, iobroker, etc.) can use with the usual ZH events API (e.g. Z2M health system can make use of it, same as the rest).

100%. By "the usual ZH events API" do you mean e.g. making the Metrics interface follow the same pattern as ControllerEventMap and emit?

@tomwilkie tomwilkie changed the title feat: add injectable Metrics interface for instrumentation feat: metrics instrumentation via typed EventEmitter Apr 27, 2026
Comment thread src/adapter/metricsAdapter.ts Outdated
Comment thread src/controller/controller.ts Outdated
Comment thread src/utils/metrics.ts
requestQueueDuration: [data: RequestQueueDurationPayload];
}

export const metrics = new EventEmitter<MetricsEventMap>();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use of this in e.g. Z2M would require to import {metrics} from "zigbee-herdsman";, i.e. entirely separate from Controller (main entrypoint). I think that's fine for this purpose though.
Note: need to be sure we can easily add a mock in Z2M tests (similar to Controller), no matter the design we end up with.

Comment thread src/utils/metrics.ts
durationSeconds: number;
}

export interface MetricsEventMap {
Copy link
Copy Markdown
Collaborator

@Nerivec Nerivec Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this one.
Is it better to have one event, with a discriminator type in the payload?
Current would require a lot of .on("...", ...), not sure that's ideal for metrics (same "end logic").

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also identify available adapter's protocol-specific metrics.
I'm not sure if any other than ember currently have any, but currently we have a 3rd party tool to parse this from logs. Would be much better integrated with metrics directly.

Comment thread src/adapter/ember/adapter/emberAdapter.ts Outdated
@tomwilkie
Copy link
Copy Markdown
Author

Thanks for the review feedback! I'll work through it and integrate the changes into Koenkk/zigbee2mqtt#31645. I've got a build of the HA add on for testing in my local setup too, and need to build some dashboards etc.. so probably a few more days before I've got it working end to end.

tomwilkie added 10 commits May 21, 2026 09:29
Adds a Prometheus-style Metrics interface (src/utils/metrics.ts) with one
method per observable event, following the existing Logger pattern.
Instrumentation covers:

- Adapter sends (ZCL unicast/group/broadcast, ZDO): success/failure status
  and duration, via a Proxy-based wrapper (src/adapter/metricsAdapter.ts)
  that requires no changes to any concrete adapter implementation.
- RequestQueue: queue depth gauge and time-in-queue histogram per device
  endpoint, tracked with a non-enumerable WeakMap to avoid serialisation
  side-effects.
- Retries: per-adapter counters with reason labels in z-stack, ember,
  zigate, zboss, and zoh adapters (deconz has no retry logic).

Consumers inject an implementation via setMetrics(); the default is a no-op
so there is no overhead when metrics are not configured.

Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
- Update biome.json schema to 2.4.13
- Fix import sort order in metricsAdapter.ts, controller.ts, index.ts
- Replace banned Function type with (...args: never) => unknown
- Reformat instrumentSend signature to single line per biome style

Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
…rapping opt-in

Replaces the injectable `Metrics` interface and `setMetrics`/`noopMetrics` pattern
with a typed `EventEmitter<MetricsEventMap>` singleton. Adapter metrics wrapping is
now opt-in via `enableMetrics: true` in Controller options (defaults to false).
Also instruments ZDO responses and ZCL payload receives, and adds tests for the
new EventEmitter-based metrics events.

Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
Simpler and more cohesive to store enqueuedAt on the Request itself,
alongside the existing expires timestamp, rather than in a parallel
non-enumerable WeakMap on RequestQueue.

Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
…controller

Replace the Proxy-based adapter wrapper and opt-in enableMetrics option with
direct instrumentSend calls at each adapter send site in the controller,
consistent with how receive/queue/retry metrics are already emitted.

Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
@tomwilkie tomwilkie force-pushed the metrics-instrumentation branch from 94c8e6d to ee1dc06 Compare May 21, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants