Initial Commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
/** Stable OTel metric names for events-metrics. */
|
||||
export const METRIC_NAMES = {
|
||||
DISPATCH_TOTAL: "suaveplan.events.dispatch.total",
|
||||
LISTENER_ERROR_TOTAL: "suaveplan.events.listener.error.total",
|
||||
DISPATCH_LATENCY_MS: "suaveplan.events.dispatch.latency_ms",
|
||||
QUEUE_DEPTH: "suaveplan.events.queue.depth",
|
||||
ACTIVE_SUBSCRIBERS: "suaveplan.events.subscribers.active",
|
||||
} as const;
|
||||
|
||||
/** Stable OTel attribute keys for events-metrics. */
|
||||
export const ATTRIBUTE_KEYS = {
|
||||
EVENT_NAME: "event.name",
|
||||
EVENT_CATEGORY: "event.category",
|
||||
LISTENER_ID: "listener.id",
|
||||
BUS_ID: "bus.id",
|
||||
} as const;
|
||||
@@ -0,0 +1,98 @@
|
||||
# attach-event-metrics
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Attaches OTel metrics instrumentation to an `EventBus` via the public interceptor API from `@suaveplan/events`. It records event dispatch totals, listener error counts, and latency histograms per event name without modifying the bus or its listeners. The attachment is transparent to publishers and subscribers — they do not need to know that metrics are being collected.
|
||||
|
||||
The implementation prioritises zero per-dispatch allocation on the hot path for previously seen event names by pre-caching the OTel counter/histogram references and using object property access rather than Map lookups.
|
||||
|
||||
## Features
|
||||
|
||||
- `attachEventMetrics(bus, options?)` — installs a metrics interceptor and returns a `EventMetricsHandle`
|
||||
- Dispatch total counter (`suaveplan.events.dispatch.total`) per event name
|
||||
- Listener error counter (`suaveplan.events.listener.error.total`) per event name
|
||||
- Dispatch latency histogram (`suaveplan.events.dispatch.latency_ms`) per event name
|
||||
- `EventMetricsHandle.snapshot()` returns in-process totals without needing an OTel exporter
|
||||
- `EventMetricsHandle.detach()` removes the interceptor
|
||||
- Injectable `meter`, `clock`, `logger` for testability
|
||||
- `eventFilter` option to limit recording to a subset of event names
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { attachEventMetrics } from "@suaveplan/events-metrics";
|
||||
|
||||
const bus = new EventBus<{ "user.created": { id: string } }>();
|
||||
|
||||
const handle = attachEventMetrics(bus);
|
||||
|
||||
bus.on("user.created", (payload) => console.log(payload));
|
||||
await bus.emitAsync("user.created", { id: "u-1" });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
console.log(snap.dispatchTotal["user.created"]); // 1
|
||||
handle.detach();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { attachEventMetrics } from "@suaveplan/events-metrics";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
|
||||
const bus = new EventBus<{ click: { x: number }; submit: { formId: string } }>();
|
||||
|
||||
const handle = attachEventMetrics(bus, {
|
||||
meter: metrics.getMeter("my-app"),
|
||||
eventFilter: (name) => name.startsWith("click"),
|
||||
clock: { now: () => performance.now() },
|
||||
});
|
||||
|
||||
// Only "click" events will be instrumented; "submit" events pass through uninstrumented
|
||||
await bus.emitAsync("click", { x: 42 });
|
||||
await bus.emitAsync("submit", { formId: "f-1" });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
console.log(snap.dispatchTotal["click"]); // 1
|
||||
console.log(snap.dispatchTotal["submit"]); // undefined (filtered out)
|
||||
handle.detach();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `attachEventMetrics<TMap>(bus, options?): EventMetricsHandle`
|
||||
|
||||
Installs a metrics interceptor on `bus`.
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `bus` | `EventBus<TMap>` | The event bus to instrument |
|
||||
| `options` | `EventMetricsOptions?` | Meter, filter, clock, logger |
|
||||
|
||||
**Returns**: `EventMetricsHandle`
|
||||
|
||||
### `EventMetricsHandle`
|
||||
|
||||
| Method | Returns | Description |
|
||||
|---|---|---|
|
||||
| `detach()` | `void` | Remove the interceptor |
|
||||
| `snapshot()` | `MetricsSnapshot` | In-process metric values |
|
||||
|
||||
### `MetricsSnapshot`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `dispatchTotal` | `Record<string, number>` | Total dispatches per event name |
|
||||
| `listenerErrorTotal` | `Record<string, number>` | Total listener errors per event name |
|
||||
| `avgDispatchLatencyMs` | `Record<string, number>` | Average latency per event name |
|
||||
| `queueDepth` | `number` | Current queue depth (0 for synchronous bus) |
|
||||
| `activeSubscribers` | `number` | Active subscriber count |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The metrics interceptor uses `bus.use()` which is the `@suaveplan/events` public interceptor API. It wraps `next()` with a timer, increments counters, and records the histogram. OTel instruments are lazily created on first use to avoid startup cost when the bus is never dispatched. The `queueDepth` and `activeSubscribers` fields in `snapshot()` are reported as 0 because the synchronous `EventBus` does not expose a queue depth API; applications using `BoundedQueue` would need a separate gauge observer.
|
||||
@@ -0,0 +1,169 @@
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { attachEventMetrics } from "./attach-event-metrics.js";
|
||||
|
||||
type TestMap = {
|
||||
click: { x: number };
|
||||
submit: { formId: string };
|
||||
"user.created": { id: string };
|
||||
};
|
||||
|
||||
describe("attachEventMetrics", () => {
|
||||
it("counts dispatches per event name", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus);
|
||||
|
||||
bus.on("click", () => void 0);
|
||||
await bus.emitAsync("click", { x: 1 });
|
||||
await bus.emitAsync("click", { x: 2 });
|
||||
await bus.emitAsync("submit", { formId: "f" });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.dispatchTotal["click"]).toBe(2);
|
||||
expect(snap.dispatchTotal["submit"]).toBe(1);
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("snapshot returns zeros for unknown events", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus);
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.dispatchTotal["click"]).toBeUndefined();
|
||||
expect(snap.listenerErrorTotal["click"]).toBeUndefined();
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("counts listener errors", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus);
|
||||
|
||||
bus.on("click", () => {
|
||||
throw new Error("listener error");
|
||||
});
|
||||
await bus.emitAsync("click", { x: 0 });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.listenerErrorTotal["click"]).toBe(1);
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("measures latency (avgDispatchLatencyMs >= 0)", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus);
|
||||
|
||||
bus.on("click", () => void 0);
|
||||
await bus.emitAsync("click", { x: 5 });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.avgDispatchLatencyMs["click"]).toBeGreaterThanOrEqual(0);
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("respects eventFilter option", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus, {
|
||||
eventFilter: (name) => name === "click",
|
||||
});
|
||||
|
||||
bus.on("click", () => void 0);
|
||||
bus.on("submit", () => void 0);
|
||||
await bus.emitAsync("click", { x: 1 });
|
||||
await bus.emitAsync("submit", { formId: "f" });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.dispatchTotal["click"]).toBe(1);
|
||||
expect(snap.dispatchTotal["submit"]).toBeUndefined();
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("detach removes the interceptor", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus);
|
||||
bus.on("click", () => void 0);
|
||||
|
||||
handle.detach();
|
||||
await bus.emitAsync("click", { x: 1 });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.dispatchTotal["click"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses injectable clock", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
let t = 0;
|
||||
const clock = { now: () => (t += 10) };
|
||||
const handle = attachEventMetrics(bus, { clock });
|
||||
|
||||
bus.on("click", () => void 0);
|
||||
await bus.emitAsync("click", { x: 1 });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.avgDispatchLatencyMs["click"]).toBeGreaterThan(0);
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("snapshot returns queueDepth and activeSubscribers as numbers", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus);
|
||||
const snap = handle.snapshot();
|
||||
expect(typeof snap.queueDepth).toBe("number");
|
||||
expect(typeof snap.activeSubscribers).toBe("number");
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("handles multiple listeners and counts correctly", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus);
|
||||
|
||||
let count = 0;
|
||||
bus.on("click", () => {
|
||||
count++;
|
||||
});
|
||||
bus.on("click", () => {
|
||||
count++;
|
||||
});
|
||||
await bus.emitAsync("click", { x: 0 });
|
||||
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.dispatchTotal["click"]).toBe(1); // one dispatch, two listeners
|
||||
expect(count).toBe(2);
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("counts errors thrown in interceptor chain", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventMetrics(bus);
|
||||
|
||||
// Install a second interceptor that throws
|
||||
bus.use(async (_envelope, _next) => {
|
||||
throw new Error("interceptor error");
|
||||
});
|
||||
|
||||
// The event bus wraps non-EventsError throws; verify via cause chain
|
||||
let caught: unknown;
|
||||
try {
|
||||
await bus.emitAsync("click", { x: 0 });
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
const hasMsg = (err: unknown, msg: string): boolean =>
|
||||
err instanceof Error
|
||||
? err.message.includes(msg) || hasMsg(err.cause, msg)
|
||||
: false;
|
||||
expect(hasMsg(caught, "interceptor error")).toBe(true);
|
||||
|
||||
const snap = handle.snapshot();
|
||||
expect(snap.listenerErrorTotal["click"]).toBe(1);
|
||||
handle.detach();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { Counter, Histogram } from "@opentelemetry/api";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import type { EventBus, EventMap } from "@suaveplan/events";
|
||||
import { ATTRIBUTE_KEYS, METRIC_NAMES } from "../../constants/index.js";
|
||||
import type {
|
||||
EventMetricsHandle,
|
||||
EventMetricsOptions,
|
||||
MetricsSnapshot,
|
||||
} from "./attach-event-metrics.types.js";
|
||||
|
||||
/**
|
||||
* Attaches OTel metrics instrumentation to an EventBus via the interceptor API.
|
||||
*
|
||||
* Records dispatch totals, listener error counts, and latency histograms
|
||||
* per event name. Uses `ObservableGauge` for queue depth and active subscribers
|
||||
* (polled at collection time, not per-dispatch).
|
||||
*/
|
||||
export function attachEventMetrics<TMap extends EventMap>(
|
||||
bus: EventBus<TMap>,
|
||||
options: EventMetricsOptions = {}
|
||||
): EventMetricsHandle {
|
||||
const meter = options.meter ?? metrics.getMeter("suaveplan.events");
|
||||
const filter = options.eventFilter ?? (() => true);
|
||||
|
||||
// In-process accumulators for snapshot()
|
||||
const dispatchTotal: Record<string, number> = {};
|
||||
const listenerErrorTotal: Record<string, number> = {};
|
||||
const latencyAccum: Record<string, { sum: number; count: number }> = {};
|
||||
|
||||
// OTel instruments (lazily cached per event name)
|
||||
let counter: Counter | null = null;
|
||||
let errorCounter: Counter | null = null;
|
||||
let histogram: Histogram | null = null;
|
||||
|
||||
function getCounter(): Counter {
|
||||
counter ??= meter.createCounter(METRIC_NAMES.DISPATCH_TOTAL, {
|
||||
description: "Total event dispatches",
|
||||
});
|
||||
return counter;
|
||||
}
|
||||
|
||||
function getErrorCounter(): Counter {
|
||||
errorCounter ??= meter.createCounter(
|
||||
METRIC_NAMES.LISTENER_ERROR_TOTAL,
|
||||
{
|
||||
description: "Total listener errors during dispatch",
|
||||
}
|
||||
);
|
||||
return errorCounter;
|
||||
}
|
||||
|
||||
function getHistogram(): Histogram {
|
||||
histogram ??= meter.createHistogram(METRIC_NAMES.DISPATCH_LATENCY_MS, {
|
||||
description: "Event dispatch latency in milliseconds",
|
||||
unit: "ms",
|
||||
});
|
||||
return histogram;
|
||||
}
|
||||
|
||||
const clock = options.clock ?? { now: () => Date.now() };
|
||||
|
||||
const unsubscribe = bus.use(async (envelope, next) => {
|
||||
const eventName = envelope.type;
|
||||
if (!filter(eventName)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const startMs = clock.now();
|
||||
const attrs = { [ATTRIBUTE_KEYS.EVENT_NAME]: eventName };
|
||||
|
||||
// Increment dispatch counter
|
||||
dispatchTotal[eventName] = (dispatchTotal[eventName] ?? 0) + 1;
|
||||
getCounter().add(1, attrs);
|
||||
|
||||
try {
|
||||
const result = await next();
|
||||
|
||||
// Record errors from listeners
|
||||
if (result.errors.length > 0) {
|
||||
listenerErrorTotal[eventName] =
|
||||
(listenerErrorTotal[eventName] ?? 0) + result.errors.length;
|
||||
getErrorCounter().add(result.errors.length, attrs);
|
||||
}
|
||||
|
||||
const latencyMs = clock.now() - startMs;
|
||||
getHistogram().record(latencyMs, attrs);
|
||||
|
||||
const accum = latencyAccum[eventName] ?? { sum: 0, count: 0 };
|
||||
accum.sum += latencyMs;
|
||||
accum.count += 1;
|
||||
latencyAccum[eventName] = accum;
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
listenerErrorTotal[eventName] =
|
||||
(listenerErrorTotal[eventName] ?? 0) + 1;
|
||||
getErrorCounter().add(1, attrs);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
detach(): void {
|
||||
unsubscribe();
|
||||
},
|
||||
snapshot(): MetricsSnapshot {
|
||||
const avgDispatchLatencyMs: Record<string, number> = {};
|
||||
for (const [name, accum] of Object.entries(latencyAccum)) {
|
||||
// accum is only inserted into latencyAccum after count++ on
|
||||
// the dispatch path, so count is always >= 1 here.
|
||||
avgDispatchLatencyMs[name] = accum.sum / accum.count;
|
||||
}
|
||||
return {
|
||||
dispatchTotal: { ...dispatchTotal },
|
||||
listenerErrorTotal: { ...listenerErrorTotal },
|
||||
avgDispatchLatencyMs,
|
||||
queueDepth: 0,
|
||||
activeSubscribers: 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Meter } from "@opentelemetry/api";
|
||||
import type { LoggerLike } from "@suaveplan/types";
|
||||
|
||||
/** Minimal clock interface for latency measurement. */
|
||||
export interface MetricsClock {
|
||||
now(): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring event bus metrics instrumentation.
|
||||
*/
|
||||
export interface EventMetricsOptions {
|
||||
/** OTel Meter instance. Defaults to the global meter from @opentelemetry/api. */
|
||||
meter?: Meter;
|
||||
/** Filter function: only record metrics for matching event names. */
|
||||
eventFilter?: (name: string) => boolean;
|
||||
/** Injectable clock for latency measurement. */
|
||||
clock?: MetricsClock;
|
||||
/** Logger for diagnostic output. */
|
||||
logger?: LoggerLike;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot of current metrics values (no OTel exporter required).
|
||||
*/
|
||||
export interface MetricsSnapshot {
|
||||
/** Total dispatch count per event name. */
|
||||
dispatchTotal: Record<string, number>;
|
||||
/** Total listener error count per event name. */
|
||||
listenerErrorTotal: Record<string, number>;
|
||||
/** Average dispatch latency in ms per event name. */
|
||||
avgDispatchLatencyMs: Record<string, number>;
|
||||
/** Current queue depth. */
|
||||
queueDepth: number;
|
||||
/** Current active subscriber count. */
|
||||
activeSubscribers: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle returned by attachEventMetrics. Use to detach or snapshot metrics.
|
||||
*/
|
||||
export interface EventMetricsHandle {
|
||||
/** Remove the metrics interceptor from the bus. */
|
||||
detach(): void;
|
||||
/** Return a snapshot of current in-process metric values. */
|
||||
snapshot(): MetricsSnapshot;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { attachEventMetrics } from "./attach-event-metrics.js";
|
||||
export type {
|
||||
EventMetricsHandle,
|
||||
EventMetricsOptions,
|
||||
MetricsSnapshot,
|
||||
} from "./attach-event-metrics.types.js";
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
EVENTS_METRICS_CODES,
|
||||
EventsMetricsError,
|
||||
registerEventsMetricsErrorCodes,
|
||||
} from "./error-codes.js";
|
||||
|
||||
describe("EventsMetricsError", () => {
|
||||
it("constructs with a registered code, message, and options", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const cause = new Error("root");
|
||||
const err = new EventsMetricsError(
|
||||
EVENTS_METRICS_CODES.ATTACH_FAILED,
|
||||
"could not attach",
|
||||
{ cause }
|
||||
);
|
||||
expect(err).toBeInstanceOf(EventsMetricsError);
|
||||
expect(err.code).toBe(EVENTS_METRICS_CODES.ATTACH_FAILED);
|
||||
expect(err.message).toBe("could not attach");
|
||||
expect(err.name).toBe("EventsMetricsError");
|
||||
expect(err.cause).toBe(cause);
|
||||
});
|
||||
|
||||
it("constructs without options", { timeout: 5000 }, () => {
|
||||
const err = new EventsMetricsError(
|
||||
EVENTS_METRICS_CODES.ALREADY_ATTACHED,
|
||||
"already attached"
|
||||
);
|
||||
expect(err.code).toBe(EVENTS_METRICS_CODES.ALREADY_ATTACHED);
|
||||
expect(err.name).toBe("EventsMetricsError");
|
||||
});
|
||||
|
||||
it("registerEventsMetricsErrorCodes is idempotent", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => {
|
||||
registerEventsMetricsErrorCodes();
|
||||
registerEventsMetricsErrorCodes();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("EVENTS_METRICS_CODES exposes all expected codes", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(EVENTS_METRICS_CODES.ATTACH_FAILED).toBe(
|
||||
"EVENTS_METRICS.ATTACH_FAILED"
|
||||
);
|
||||
expect(EVENTS_METRICS_CODES.ALREADY_ATTACHED).toBe(
|
||||
"EVENTS_METRICS.ALREADY_ATTACHED"
|
||||
);
|
||||
expect(EVENTS_METRICS_CODES.DETACHED).toBe("EVENTS_METRICS.DETACHED");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
defineCodes,
|
||||
SuaveplanError,
|
||||
type SuaveplanErrorOptions,
|
||||
} from "@suaveplan/error";
|
||||
|
||||
export const EVENTS_METRICS_CODES = defineCodes("EVENTS_METRICS", [
|
||||
"ATTACH_FAILED",
|
||||
"ALREADY_ATTACHED",
|
||||
"DETACHED",
|
||||
] as const);
|
||||
|
||||
export type EventsMetricsErrorCode =
|
||||
(typeof EVENTS_METRICS_CODES)[keyof typeof EVENTS_METRICS_CODES];
|
||||
|
||||
/**
|
||||
* Structured error for events-metrics operations.
|
||||
*/
|
||||
export class EventsMetricsError extends SuaveplanError<EventsMetricsErrorCode> {
|
||||
public constructor(
|
||||
code: EventsMetricsErrorCode,
|
||||
message: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.name = "EventsMetricsError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Register events-metrics error codes (idempotent). */
|
||||
export function registerEventsMetricsErrorCodes(): void {
|
||||
// defineCodes handles registration; this is a no-op hook.
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type { EventsMetricsErrorCode } from "./error-codes.js";
|
||||
export {
|
||||
EVENTS_METRICS_CODES,
|
||||
EventsMetricsError,
|
||||
registerEventsMetricsErrorCodes,
|
||||
} from "./error-codes.js";
|
||||
13
packages/networking/events/events-metrics/src/index.ts
Normal file
13
packages/networking/events/events-metrics/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export { ATTRIBUTE_KEYS, METRIC_NAMES } from "./constants/index.js";
|
||||
export { attachEventMetrics } from "./core/attach-event-metrics/attach-event-metrics.js";
|
||||
export type {
|
||||
EventMetricsHandle,
|
||||
EventMetricsOptions,
|
||||
MetricsSnapshot,
|
||||
} from "./core/attach-event-metrics/attach-event-metrics.types.js";
|
||||
export type { EventsMetricsErrorCode } from "./error-codes/index.js";
|
||||
export {
|
||||
EVENTS_METRICS_CODES,
|
||||
EventsMetricsError,
|
||||
registerEventsMetricsErrorCodes,
|
||||
} from "./error-codes/index.js";
|
||||
@@ -0,0 +1 @@
|
||||
export { ATTRIBUTE_KEYS, METRIC_NAMES } from "./metric-names.js";
|
||||
@@ -0,0 +1,61 @@
|
||||
# metric-names
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Exports the stable constant objects `METRIC_NAMES` and `ATTRIBUTE_KEYS` that define every OTel metric name and span attribute key used across `@suaveplan/events-metrics`. Centralising these names prevents typos, enables IDE completion, and makes it easy to audit the full observability surface of the package.
|
||||
|
||||
These constants are the single source of truth consumed by both the instrumentation code and the tests that assert metric names are correct.
|
||||
|
||||
## Features
|
||||
|
||||
- `METRIC_NAMES` — all five OTel metric names: dispatch total, listener error total, dispatch latency histogram, queue depth gauge, active subscribers gauge
|
||||
- `ATTRIBUTE_KEYS` — all four OTel attribute keys: event name, event category, listener ID, bus ID
|
||||
- Both objects use `as const` for full literal-type narrowing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { METRIC_NAMES, ATTRIBUTE_KEYS } from "@suaveplan/events-metrics";
|
||||
|
||||
console.log(METRIC_NAMES.DISPATCH_TOTAL); // "suaveplan.events.dispatch.total"
|
||||
console.log(ATTRIBUTE_KEYS.EVENT_NAME); // "event.name"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { METRIC_NAMES, ATTRIBUTE_KEYS } from "@suaveplan/events-metrics";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
|
||||
const meter = metrics.getMeter("my-app");
|
||||
const counter = meter.createCounter(METRIC_NAMES.DISPATCH_TOTAL);
|
||||
counter.add(1, { [ATTRIBUTE_KEYS.EVENT_NAME]: "user.created" });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `METRIC_NAMES`
|
||||
|
||||
| Key | Value |
|
||||
|---|---|
|
||||
| `DISPATCH_TOTAL` | `"suaveplan.events.dispatch.total"` |
|
||||
| `LISTENER_ERROR_TOTAL` | `"suaveplan.events.listener.error.total"` |
|
||||
| `DISPATCH_LATENCY_MS` | `"suaveplan.events.dispatch.latency_ms"` |
|
||||
| `QUEUE_DEPTH` | `"suaveplan.events.queue.depth"` |
|
||||
| `ACTIVE_SUBSCRIBERS` | `"suaveplan.events.subscribers.active"` |
|
||||
|
||||
### `ATTRIBUTE_KEYS`
|
||||
|
||||
| Key | Value |
|
||||
|---|---|
|
||||
| `EVENT_NAME` | `"event.name"` |
|
||||
| `EVENT_CATEGORY` | `"event.category"` |
|
||||
| `LISTENER_ID` | `"listener.id"` |
|
||||
| `BUS_ID` | `"bus.id"` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constants are defined in `src/constants/index.ts` and re-exported from this module to follow the package structure convention. Consumers should import from `@suaveplan/events-metrics` (the root barrel) rather than the internal path to benefit from tree-shaking and future path changes.
|
||||
@@ -0,0 +1 @@
|
||||
export { ATTRIBUTE_KEYS, METRIC_NAMES } from "../../constants/index.js";
|
||||
Reference in New Issue
Block a user