@saga-bus/middleware-metrics 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Dean Foran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @saga-bus/middleware-metrics
2
+
3
+ Prometheus metrics middleware for saga-bus.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @saga-bus/middleware-metrics prom-client
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createMetricsMiddleware } from "@saga-bus/middleware-metrics";
15
+ import { createBus } from "@saga-bus/core";
16
+ import { register } from "prom-client";
17
+
18
+ const metricsMiddleware = createMetricsMiddleware();
19
+
20
+ const bus = createBus({
21
+ middleware: [metricsMiddleware],
22
+ sagas: [...],
23
+ transport,
24
+ });
25
+
26
+ // Expose metrics endpoint (e.g., with Express)
27
+ app.get("/metrics", async (req, res) => {
28
+ res.set("Content-Type", register.contentType);
29
+ res.end(await register.metrics());
30
+ });
31
+ ```
32
+
33
+ ## Features
34
+
35
+ - Message processing counter (success/failure)
36
+ - Processing duration histogram
37
+ - Saga lifecycle tracking (created/completed)
38
+ - Customizable metric prefix
39
+ - Configurable histogram buckets
40
+ - Per-saga and per-message-type labels
41
+
42
+ ## Metrics
43
+
44
+ | Metric | Type | Labels | Description |
45
+ |--------|------|--------|-------------|
46
+ | `saga_bus_messages_processed_total` | Counter | `message_type`, `saga_name` | Successfully processed messages |
47
+ | `saga_bus_messages_failed_total` | Counter | `message_type`, `saga_name`, `error_type` | Failed message processing |
48
+ | `saga_bus_message_processing_duration_ms` | Histogram | `message_type`, `saga_name` | Processing duration in milliseconds |
49
+ | `saga_bus_sagas_created_total` | Counter | `saga_name`, `message_type` | New saga instances created |
50
+ | `saga_bus_sagas_completed_total` | Counter | `saga_name` | Completed saga instances |
51
+
52
+ ## Configuration
53
+
54
+ | Option | Type | Default | Description |
55
+ |--------|------|---------|-------------|
56
+ | `registry` | `Registry` | default | Prometheus registry |
57
+ | `prefix` | `string` | `"saga_bus"` | Metric name prefix |
58
+ | `durationBuckets` | `number[]` | `[1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]` | Histogram buckets (ms) |
59
+ | `recordSagaLabels` | `boolean` | `true` | Include saga_name label |
60
+
61
+ ## Custom Registry
62
+
63
+ ```typescript
64
+ import { Registry } from "prom-client";
65
+
66
+ const customRegistry = new Registry();
67
+
68
+ const metricsMiddleware = createMetricsMiddleware({
69
+ registry: customRegistry,
70
+ prefix: "myapp",
71
+ });
72
+
73
+ // Use custom registry for metrics endpoint
74
+ app.get("/metrics", async (req, res) => {
75
+ res.set("Content-Type", customRegistry.contentType);
76
+ res.end(await customRegistry.metrics());
77
+ });
78
+ ```
79
+
80
+ ## Direct Metric Access
81
+
82
+ Use `createMetricsMiddlewareWithMetrics` when you need direct access to metric objects:
83
+
84
+ ```typescript
85
+ import { createMetricsMiddlewareWithMetrics } from "@saga-bus/middleware-metrics";
86
+
87
+ const { middleware, metrics } = createMetricsMiddlewareWithMetrics();
88
+
89
+ // Access metrics directly
90
+ const processedCount = await metrics.messagesProcessed.get();
91
+ console.log(`Processed: ${processedCount.values[0]?.value}`);
92
+ ```
93
+
94
+ ## Grafana Dashboard
95
+
96
+ Example PromQL queries for dashboards:
97
+
98
+ ```promql
99
+ # Message throughput (per minute)
100
+ rate(saga_bus_messages_processed_total[1m])
101
+
102
+ # P95 processing duration
103
+ histogram_quantile(0.95, rate(saga_bus_message_processing_duration_ms_bucket[5m]))
104
+
105
+ # Error rate
106
+ rate(saga_bus_messages_failed_total[5m]) / rate(saga_bus_messages_processed_total[5m])
107
+
108
+ # Sagas in flight (created - completed)
109
+ saga_bus_sagas_created_total - saga_bus_sagas_completed_total
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createMetricsMiddleware: () => createMetricsMiddleware,
24
+ createMetricsMiddlewareWithMetrics: () => createMetricsMiddlewareWithMetrics
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/MetricsMiddleware.ts
29
+ var import_prom_client = require("prom-client");
30
+ var DEFAULT_DURATION_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3];
31
+ function createMetricsMiddleware(options = {}) {
32
+ const registry = options.registry ?? import_prom_client.register;
33
+ const prefix = options.prefix ?? "saga_bus";
34
+ const durationBuckets = options.durationBuckets ?? DEFAULT_DURATION_BUCKETS;
35
+ const recordSagaLabels = options.recordSagaLabels ?? true;
36
+ const baseLabelNames = ["message_type"];
37
+ const sagaLabelNames = recordSagaLabels ? [...baseLabelNames, "saga_name"] : baseLabelNames;
38
+ const messagesProcessed = new import_prom_client.Counter({
39
+ name: `${prefix}_messages_processed_total`,
40
+ help: "Total number of messages successfully processed",
41
+ labelNames: sagaLabelNames,
42
+ registers: [registry]
43
+ });
44
+ const messagesFailed = new import_prom_client.Counter({
45
+ name: `${prefix}_messages_failed_total`,
46
+ help: "Total number of messages that failed processing",
47
+ labelNames: [...sagaLabelNames, "error_type"],
48
+ registers: [registry]
49
+ });
50
+ const processingDuration = new import_prom_client.Histogram({
51
+ name: `${prefix}_message_processing_duration_ms`,
52
+ help: "Message processing duration in milliseconds",
53
+ labelNames: sagaLabelNames,
54
+ buckets: durationBuckets,
55
+ registers: [registry]
56
+ });
57
+ const sagasCreated = new import_prom_client.Counter({
58
+ name: `${prefix}_sagas_created_total`,
59
+ help: "Total number of new saga instances created",
60
+ labelNames: ["saga_name", "message_type"],
61
+ registers: [registry]
62
+ });
63
+ const sagasCompleted = new import_prom_client.Counter({
64
+ name: `${prefix}_sagas_completed_total`,
65
+ help: "Total number of saga instances completed",
66
+ labelNames: ["saga_name"],
67
+ registers: [registry]
68
+ });
69
+ return async (ctx, next) => {
70
+ const startTime = performance.now();
71
+ const messageType = ctx.envelope.type;
72
+ const sagaName = ctx.sagaName;
73
+ const isNewSaga = !ctx.existingState;
74
+ const labels = recordSagaLabels ? { message_type: messageType, saga_name: sagaName } : { message_type: messageType };
75
+ try {
76
+ await next();
77
+ const durationMs = performance.now() - startTime;
78
+ messagesProcessed.inc(labels);
79
+ processingDuration.observe(labels, durationMs);
80
+ if (isNewSaga && ctx.postState) {
81
+ sagasCreated.inc({ saga_name: sagaName, message_type: messageType });
82
+ }
83
+ if (ctx.postState?.metadata?.isCompleted) {
84
+ sagasCompleted.inc({ saga_name: sagaName });
85
+ }
86
+ } catch (error) {
87
+ const durationMs = performance.now() - startTime;
88
+ const errorType = getErrorType(error);
89
+ messagesFailed.inc({ ...labels, error_type: errorType });
90
+ processingDuration.observe(labels, durationMs);
91
+ throw error;
92
+ }
93
+ };
94
+ }
95
+ function getErrorType(error) {
96
+ if (error instanceof Error) {
97
+ return error.name || "Error";
98
+ }
99
+ return "Unknown";
100
+ }
101
+ function createMetricsMiddlewareWithMetrics(options = {}) {
102
+ const registry = options.registry ?? import_prom_client.register;
103
+ const prefix = options.prefix ?? "saga_bus";
104
+ const durationBuckets = options.durationBuckets ?? DEFAULT_DURATION_BUCKETS;
105
+ const recordSagaLabels = options.recordSagaLabels ?? true;
106
+ const baseLabelNames = ["message_type"];
107
+ const sagaLabelNames = recordSagaLabels ? [...baseLabelNames, "saga_name"] : baseLabelNames;
108
+ const messagesProcessed = new import_prom_client.Counter({
109
+ name: `${prefix}_messages_processed_total`,
110
+ help: "Total number of messages successfully processed",
111
+ labelNames: sagaLabelNames,
112
+ registers: [registry]
113
+ });
114
+ const messagesFailed = new import_prom_client.Counter({
115
+ name: `${prefix}_messages_failed_total`,
116
+ help: "Total number of messages that failed processing",
117
+ labelNames: [...sagaLabelNames, "error_type"],
118
+ registers: [registry]
119
+ });
120
+ const processingDuration = new import_prom_client.Histogram({
121
+ name: `${prefix}_message_processing_duration_ms`,
122
+ help: "Message processing duration in milliseconds",
123
+ labelNames: sagaLabelNames,
124
+ buckets: durationBuckets,
125
+ registers: [registry]
126
+ });
127
+ const sagasCreated = new import_prom_client.Counter({
128
+ name: `${prefix}_sagas_created_total`,
129
+ help: "Total number of new saga instances created",
130
+ labelNames: ["saga_name", "message_type"],
131
+ registers: [registry]
132
+ });
133
+ const sagasCompleted = new import_prom_client.Counter({
134
+ name: `${prefix}_sagas_completed_total`,
135
+ help: "Total number of saga instances completed",
136
+ labelNames: ["saga_name"],
137
+ registers: [registry]
138
+ });
139
+ const metrics = {
140
+ messagesProcessed,
141
+ messagesFailed,
142
+ processingDuration,
143
+ sagasCreated,
144
+ sagasCompleted
145
+ };
146
+ const middleware = async (ctx, next) => {
147
+ const startTime = performance.now();
148
+ const messageType = ctx.envelope.type;
149
+ const sagaName = ctx.sagaName;
150
+ const isNewSaga = !ctx.existingState;
151
+ const labels = recordSagaLabels ? { message_type: messageType, saga_name: sagaName } : { message_type: messageType };
152
+ try {
153
+ await next();
154
+ const durationMs = performance.now() - startTime;
155
+ messagesProcessed.inc(labels);
156
+ processingDuration.observe(labels, durationMs);
157
+ if (isNewSaga && ctx.postState) {
158
+ sagasCreated.inc({ saga_name: sagaName, message_type: messageType });
159
+ }
160
+ if (ctx.postState?.metadata?.isCompleted) {
161
+ sagasCompleted.inc({ saga_name: sagaName });
162
+ }
163
+ } catch (error) {
164
+ const durationMs = performance.now() - startTime;
165
+ const errorType = getErrorType(error);
166
+ messagesFailed.inc({ ...labels, error_type: errorType });
167
+ processingDuration.observe(labels, durationMs);
168
+ throw error;
169
+ }
170
+ };
171
+ return { middleware, metrics };
172
+ }
173
+ // Annotate the CommonJS export names for ESM import in node:
174
+ 0 && (module.exports = {
175
+ createMetricsMiddleware,
176
+ createMetricsMiddlewareWithMetrics
177
+ });
178
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/MetricsMiddleware.ts"],"sourcesContent":["export {\n createMetricsMiddleware,\n createMetricsMiddlewareWithMetrics,\n} from \"./MetricsMiddleware.js\";\nexport type { MetricsMiddlewareOptions, SagaBusMetrics } from \"./types.js\";\n","import {\n Counter,\n Histogram,\n register as defaultRegister,\n} from \"prom-client\";\nimport type { SagaMiddleware, SagaPipelineContext } from \"@saga-bus/core\";\nimport type { MetricsMiddlewareOptions, SagaBusMetrics } from \"./types.js\";\n\nconst DEFAULT_DURATION_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000];\n\n/**\n * Creates a Prometheus metrics middleware for saga-bus.\n *\n * Records:\n * - Messages processed (counter)\n * - Messages failed (counter)\n * - Processing duration (histogram)\n * - Sagas created (counter)\n * - Sagas completed (counter)\n *\n * @example\n * ```typescript\n * import { createMetricsMiddleware } from \"@saga-bus/middleware-metrics\";\n * import { register } from \"prom-client\";\n *\n * const metricsMiddleware = createMetricsMiddleware({\n * prefix: \"my_app\",\n * });\n *\n * const bus = createBus({\n * middleware: [metricsMiddleware],\n * // ...\n * });\n *\n * // Expose metrics endpoint\n * app.get(\"/metrics\", async (req, res) => {\n * res.set(\"Content-Type\", register.contentType);\n * res.end(await register.metrics());\n * });\n * ```\n */\nexport function createMetricsMiddleware(\n options: MetricsMiddlewareOptions = {}\n): SagaMiddleware {\n const registry = options.registry ?? defaultRegister;\n const prefix = options.prefix ?? \"saga_bus\";\n const durationBuckets = options.durationBuckets ?? DEFAULT_DURATION_BUCKETS;\n const recordSagaLabels = options.recordSagaLabels ?? true;\n\n // Build label names\n const baseLabelNames = [\"message_type\"];\n const sagaLabelNames = recordSagaLabels\n ? [...baseLabelNames, \"saga_name\"]\n : baseLabelNames;\n\n // Create metrics\n const messagesProcessed = new Counter({\n name: `${prefix}_messages_processed_total`,\n help: \"Total number of messages successfully processed\",\n labelNames: sagaLabelNames,\n registers: [registry],\n });\n\n const messagesFailed = new Counter({\n name: `${prefix}_messages_failed_total`,\n help: \"Total number of messages that failed processing\",\n labelNames: [...sagaLabelNames, \"error_type\"],\n registers: [registry],\n });\n\n const processingDuration = new Histogram({\n name: `${prefix}_message_processing_duration_ms`,\n help: \"Message processing duration in milliseconds\",\n labelNames: sagaLabelNames,\n buckets: durationBuckets,\n registers: [registry],\n });\n\n const sagasCreated = new Counter({\n name: `${prefix}_sagas_created_total`,\n help: \"Total number of new saga instances created\",\n labelNames: [\"saga_name\", \"message_type\"],\n registers: [registry],\n });\n\n const sagasCompleted = new Counter({\n name: `${prefix}_sagas_completed_total`,\n help: \"Total number of saga instances completed\",\n labelNames: [\"saga_name\"],\n registers: [registry],\n });\n\n return async (ctx: SagaPipelineContext, next: () => Promise<void>) => {\n const startTime = performance.now();\n const messageType = ctx.envelope.type;\n const sagaName = ctx.sagaName;\n const isNewSaga = !ctx.existingState;\n\n const labels = recordSagaLabels\n ? { message_type: messageType, saga_name: sagaName }\n : { message_type: messageType };\n\n try {\n await next();\n\n // Record successful processing\n const durationMs = performance.now() - startTime;\n messagesProcessed.inc(labels);\n processingDuration.observe(labels, durationMs);\n\n // Track saga lifecycle\n if (isNewSaga && ctx.postState) {\n sagasCreated.inc({ saga_name: sagaName, message_type: messageType });\n }\n\n if (ctx.postState?.metadata?.isCompleted) {\n sagasCompleted.inc({ saga_name: sagaName });\n }\n } catch (error) {\n // Record failed processing\n const durationMs = performance.now() - startTime;\n const errorType = getErrorType(error);\n\n messagesFailed.inc({ ...labels, error_type: errorType });\n processingDuration.observe(labels, durationMs);\n\n throw error;\n }\n };\n}\n\nfunction getErrorType(error: unknown): string {\n if (error instanceof Error) {\n return error.name || \"Error\";\n }\n return \"Unknown\";\n}\n\n/**\n * Creates metrics and returns both the middleware and the metrics objects.\n * Useful when you need direct access to the metric objects.\n *\n * @example\n * ```typescript\n * const { middleware, metrics } = createMetricsMiddlewareWithMetrics();\n *\n * // Access metrics directly\n * console.log(await metrics.messagesProcessed.get());\n * ```\n */\nexport function createMetricsMiddlewareWithMetrics(\n options: MetricsMiddlewareOptions = {}\n): { middleware: SagaMiddleware; metrics: SagaBusMetrics } {\n const registry = options.registry ?? defaultRegister;\n const prefix = options.prefix ?? \"saga_bus\";\n const durationBuckets = options.durationBuckets ?? DEFAULT_DURATION_BUCKETS;\n const recordSagaLabels = options.recordSagaLabels ?? true;\n\n // Build label names\n const baseLabelNames = [\"message_type\"];\n const sagaLabelNames = recordSagaLabels\n ? [...baseLabelNames, \"saga_name\"]\n : baseLabelNames;\n\n // Create metrics\n const messagesProcessed = new Counter({\n name: `${prefix}_messages_processed_total`,\n help: \"Total number of messages successfully processed\",\n labelNames: sagaLabelNames,\n registers: [registry],\n });\n\n const messagesFailed = new Counter({\n name: `${prefix}_messages_failed_total`,\n help: \"Total number of messages that failed processing\",\n labelNames: [...sagaLabelNames, \"error_type\"],\n registers: [registry],\n });\n\n const processingDuration = new Histogram({\n name: `${prefix}_message_processing_duration_ms`,\n help: \"Message processing duration in milliseconds\",\n labelNames: sagaLabelNames,\n buckets: durationBuckets,\n registers: [registry],\n });\n\n const sagasCreated = new Counter({\n name: `${prefix}_sagas_created_total`,\n help: \"Total number of new saga instances created\",\n labelNames: [\"saga_name\", \"message_type\"],\n registers: [registry],\n });\n\n const sagasCompleted = new Counter({\n name: `${prefix}_sagas_completed_total`,\n help: \"Total number of saga instances completed\",\n labelNames: [\"saga_name\"],\n registers: [registry],\n });\n\n const metrics: SagaBusMetrics = {\n messagesProcessed,\n messagesFailed,\n processingDuration,\n sagasCreated,\n sagasCompleted,\n };\n\n const middleware: SagaMiddleware = async (\n ctx: SagaPipelineContext,\n next: () => Promise<void>\n ) => {\n const startTime = performance.now();\n const messageType = ctx.envelope.type;\n const sagaName = ctx.sagaName;\n const isNewSaga = !ctx.existingState;\n\n const labels = recordSagaLabels\n ? { message_type: messageType, saga_name: sagaName }\n : { message_type: messageType };\n\n try {\n await next();\n\n const durationMs = performance.now() - startTime;\n messagesProcessed.inc(labels);\n processingDuration.observe(labels, durationMs);\n\n if (isNewSaga && ctx.postState) {\n sagasCreated.inc({ saga_name: sagaName, message_type: messageType });\n }\n\n if (ctx.postState?.metadata?.isCompleted) {\n sagasCompleted.inc({ saga_name: sagaName });\n }\n } catch (error) {\n const durationMs = performance.now() - startTime;\n const errorType = getErrorType(error);\n\n messagesFailed.inc({ ...labels, error_type: errorType });\n processingDuration.observe(labels, durationMs);\n\n throw error;\n }\n };\n\n return { middleware, metrics };\n}\n"],"mappings":"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","names":["defaultRegister"]}
@@ -0,0 +1,106 @@
1
+ import { SagaMiddleware } from '@saga-bus/core';
2
+ import { Registry, Counter, Histogram } from 'prom-client';
3
+
4
+ /**
5
+ * Options for configuring the metrics middleware.
6
+ */
7
+ interface MetricsMiddlewareOptions {
8
+ /**
9
+ * Prometheus registry to use. Defaults to the global default registry.
10
+ */
11
+ registry?: Registry;
12
+ /**
13
+ * Prefix for metric names. Defaults to "saga_bus".
14
+ */
15
+ prefix?: string;
16
+ /**
17
+ * Custom labels to add to all metrics.
18
+ */
19
+ customLabels?: Record<string, string>;
20
+ /**
21
+ * Histogram buckets for processing duration in milliseconds.
22
+ * Defaults to [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
23
+ */
24
+ durationBuckets?: number[];
25
+ /**
26
+ * Whether to record saga-related labels (saga_name, is_new, is_completed).
27
+ * Defaults to true.
28
+ */
29
+ recordSagaLabels?: boolean;
30
+ }
31
+ /**
32
+ * Metrics exposed by the middleware.
33
+ */
34
+ interface SagaBusMetrics {
35
+ /**
36
+ * Counter for total messages processed.
37
+ */
38
+ messagesProcessed: Counter;
39
+ /**
40
+ * Counter for total messages failed.
41
+ */
42
+ messagesFailed: Counter;
43
+ /**
44
+ * Histogram for message processing duration.
45
+ */
46
+ processingDuration: Histogram;
47
+ /**
48
+ * Counter for sagas created.
49
+ */
50
+ sagasCreated: Counter;
51
+ /**
52
+ * Counter for sagas completed.
53
+ */
54
+ sagasCompleted: Counter;
55
+ }
56
+
57
+ /**
58
+ * Creates a Prometheus metrics middleware for saga-bus.
59
+ *
60
+ * Records:
61
+ * - Messages processed (counter)
62
+ * - Messages failed (counter)
63
+ * - Processing duration (histogram)
64
+ * - Sagas created (counter)
65
+ * - Sagas completed (counter)
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * import { createMetricsMiddleware } from "@saga-bus/middleware-metrics";
70
+ * import { register } from "prom-client";
71
+ *
72
+ * const metricsMiddleware = createMetricsMiddleware({
73
+ * prefix: "my_app",
74
+ * });
75
+ *
76
+ * const bus = createBus({
77
+ * middleware: [metricsMiddleware],
78
+ * // ...
79
+ * });
80
+ *
81
+ * // Expose metrics endpoint
82
+ * app.get("/metrics", async (req, res) => {
83
+ * res.set("Content-Type", register.contentType);
84
+ * res.end(await register.metrics());
85
+ * });
86
+ * ```
87
+ */
88
+ declare function createMetricsMiddleware(options?: MetricsMiddlewareOptions): SagaMiddleware;
89
+ /**
90
+ * Creates metrics and returns both the middleware and the metrics objects.
91
+ * Useful when you need direct access to the metric objects.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const { middleware, metrics } = createMetricsMiddlewareWithMetrics();
96
+ *
97
+ * // Access metrics directly
98
+ * console.log(await metrics.messagesProcessed.get());
99
+ * ```
100
+ */
101
+ declare function createMetricsMiddlewareWithMetrics(options?: MetricsMiddlewareOptions): {
102
+ middleware: SagaMiddleware;
103
+ metrics: SagaBusMetrics;
104
+ };
105
+
106
+ export { type MetricsMiddlewareOptions, type SagaBusMetrics, createMetricsMiddleware, createMetricsMiddlewareWithMetrics };
@@ -0,0 +1,106 @@
1
+ import { SagaMiddleware } from '@saga-bus/core';
2
+ import { Registry, Counter, Histogram } from 'prom-client';
3
+
4
+ /**
5
+ * Options for configuring the metrics middleware.
6
+ */
7
+ interface MetricsMiddlewareOptions {
8
+ /**
9
+ * Prometheus registry to use. Defaults to the global default registry.
10
+ */
11
+ registry?: Registry;
12
+ /**
13
+ * Prefix for metric names. Defaults to "saga_bus".
14
+ */
15
+ prefix?: string;
16
+ /**
17
+ * Custom labels to add to all metrics.
18
+ */
19
+ customLabels?: Record<string, string>;
20
+ /**
21
+ * Histogram buckets for processing duration in milliseconds.
22
+ * Defaults to [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
23
+ */
24
+ durationBuckets?: number[];
25
+ /**
26
+ * Whether to record saga-related labels (saga_name, is_new, is_completed).
27
+ * Defaults to true.
28
+ */
29
+ recordSagaLabels?: boolean;
30
+ }
31
+ /**
32
+ * Metrics exposed by the middleware.
33
+ */
34
+ interface SagaBusMetrics {
35
+ /**
36
+ * Counter for total messages processed.
37
+ */
38
+ messagesProcessed: Counter;
39
+ /**
40
+ * Counter for total messages failed.
41
+ */
42
+ messagesFailed: Counter;
43
+ /**
44
+ * Histogram for message processing duration.
45
+ */
46
+ processingDuration: Histogram;
47
+ /**
48
+ * Counter for sagas created.
49
+ */
50
+ sagasCreated: Counter;
51
+ /**
52
+ * Counter for sagas completed.
53
+ */
54
+ sagasCompleted: Counter;
55
+ }
56
+
57
+ /**
58
+ * Creates a Prometheus metrics middleware for saga-bus.
59
+ *
60
+ * Records:
61
+ * - Messages processed (counter)
62
+ * - Messages failed (counter)
63
+ * - Processing duration (histogram)
64
+ * - Sagas created (counter)
65
+ * - Sagas completed (counter)
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * import { createMetricsMiddleware } from "@saga-bus/middleware-metrics";
70
+ * import { register } from "prom-client";
71
+ *
72
+ * const metricsMiddleware = createMetricsMiddleware({
73
+ * prefix: "my_app",
74
+ * });
75
+ *
76
+ * const bus = createBus({
77
+ * middleware: [metricsMiddleware],
78
+ * // ...
79
+ * });
80
+ *
81
+ * // Expose metrics endpoint
82
+ * app.get("/metrics", async (req, res) => {
83
+ * res.set("Content-Type", register.contentType);
84
+ * res.end(await register.metrics());
85
+ * });
86
+ * ```
87
+ */
88
+ declare function createMetricsMiddleware(options?: MetricsMiddlewareOptions): SagaMiddleware;
89
+ /**
90
+ * Creates metrics and returns both the middleware and the metrics objects.
91
+ * Useful when you need direct access to the metric objects.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const { middleware, metrics } = createMetricsMiddlewareWithMetrics();
96
+ *
97
+ * // Access metrics directly
98
+ * console.log(await metrics.messagesProcessed.get());
99
+ * ```
100
+ */
101
+ declare function createMetricsMiddlewareWithMetrics(options?: MetricsMiddlewareOptions): {
102
+ middleware: SagaMiddleware;
103
+ metrics: SagaBusMetrics;
104
+ };
105
+
106
+ export { type MetricsMiddlewareOptions, type SagaBusMetrics, createMetricsMiddleware, createMetricsMiddlewareWithMetrics };
package/dist/index.js ADDED
@@ -0,0 +1,154 @@
1
+ // src/MetricsMiddleware.ts
2
+ import {
3
+ Counter,
4
+ Histogram,
5
+ register as defaultRegister
6
+ } from "prom-client";
7
+ var DEFAULT_DURATION_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3];
8
+ function createMetricsMiddleware(options = {}) {
9
+ const registry = options.registry ?? defaultRegister;
10
+ const prefix = options.prefix ?? "saga_bus";
11
+ const durationBuckets = options.durationBuckets ?? DEFAULT_DURATION_BUCKETS;
12
+ const recordSagaLabels = options.recordSagaLabels ?? true;
13
+ const baseLabelNames = ["message_type"];
14
+ const sagaLabelNames = recordSagaLabels ? [...baseLabelNames, "saga_name"] : baseLabelNames;
15
+ const messagesProcessed = new Counter({
16
+ name: `${prefix}_messages_processed_total`,
17
+ help: "Total number of messages successfully processed",
18
+ labelNames: sagaLabelNames,
19
+ registers: [registry]
20
+ });
21
+ const messagesFailed = new Counter({
22
+ name: `${prefix}_messages_failed_total`,
23
+ help: "Total number of messages that failed processing",
24
+ labelNames: [...sagaLabelNames, "error_type"],
25
+ registers: [registry]
26
+ });
27
+ const processingDuration = new Histogram({
28
+ name: `${prefix}_message_processing_duration_ms`,
29
+ help: "Message processing duration in milliseconds",
30
+ labelNames: sagaLabelNames,
31
+ buckets: durationBuckets,
32
+ registers: [registry]
33
+ });
34
+ const sagasCreated = new Counter({
35
+ name: `${prefix}_sagas_created_total`,
36
+ help: "Total number of new saga instances created",
37
+ labelNames: ["saga_name", "message_type"],
38
+ registers: [registry]
39
+ });
40
+ const sagasCompleted = new Counter({
41
+ name: `${prefix}_sagas_completed_total`,
42
+ help: "Total number of saga instances completed",
43
+ labelNames: ["saga_name"],
44
+ registers: [registry]
45
+ });
46
+ return async (ctx, next) => {
47
+ const startTime = performance.now();
48
+ const messageType = ctx.envelope.type;
49
+ const sagaName = ctx.sagaName;
50
+ const isNewSaga = !ctx.existingState;
51
+ const labels = recordSagaLabels ? { message_type: messageType, saga_name: sagaName } : { message_type: messageType };
52
+ try {
53
+ await next();
54
+ const durationMs = performance.now() - startTime;
55
+ messagesProcessed.inc(labels);
56
+ processingDuration.observe(labels, durationMs);
57
+ if (isNewSaga && ctx.postState) {
58
+ sagasCreated.inc({ saga_name: sagaName, message_type: messageType });
59
+ }
60
+ if (ctx.postState?.metadata?.isCompleted) {
61
+ sagasCompleted.inc({ saga_name: sagaName });
62
+ }
63
+ } catch (error) {
64
+ const durationMs = performance.now() - startTime;
65
+ const errorType = getErrorType(error);
66
+ messagesFailed.inc({ ...labels, error_type: errorType });
67
+ processingDuration.observe(labels, durationMs);
68
+ throw error;
69
+ }
70
+ };
71
+ }
72
+ function getErrorType(error) {
73
+ if (error instanceof Error) {
74
+ return error.name || "Error";
75
+ }
76
+ return "Unknown";
77
+ }
78
+ function createMetricsMiddlewareWithMetrics(options = {}) {
79
+ const registry = options.registry ?? defaultRegister;
80
+ const prefix = options.prefix ?? "saga_bus";
81
+ const durationBuckets = options.durationBuckets ?? DEFAULT_DURATION_BUCKETS;
82
+ const recordSagaLabels = options.recordSagaLabels ?? true;
83
+ const baseLabelNames = ["message_type"];
84
+ const sagaLabelNames = recordSagaLabels ? [...baseLabelNames, "saga_name"] : baseLabelNames;
85
+ const messagesProcessed = new Counter({
86
+ name: `${prefix}_messages_processed_total`,
87
+ help: "Total number of messages successfully processed",
88
+ labelNames: sagaLabelNames,
89
+ registers: [registry]
90
+ });
91
+ const messagesFailed = new Counter({
92
+ name: `${prefix}_messages_failed_total`,
93
+ help: "Total number of messages that failed processing",
94
+ labelNames: [...sagaLabelNames, "error_type"],
95
+ registers: [registry]
96
+ });
97
+ const processingDuration = new Histogram({
98
+ name: `${prefix}_message_processing_duration_ms`,
99
+ help: "Message processing duration in milliseconds",
100
+ labelNames: sagaLabelNames,
101
+ buckets: durationBuckets,
102
+ registers: [registry]
103
+ });
104
+ const sagasCreated = new Counter({
105
+ name: `${prefix}_sagas_created_total`,
106
+ help: "Total number of new saga instances created",
107
+ labelNames: ["saga_name", "message_type"],
108
+ registers: [registry]
109
+ });
110
+ const sagasCompleted = new Counter({
111
+ name: `${prefix}_sagas_completed_total`,
112
+ help: "Total number of saga instances completed",
113
+ labelNames: ["saga_name"],
114
+ registers: [registry]
115
+ });
116
+ const metrics = {
117
+ messagesProcessed,
118
+ messagesFailed,
119
+ processingDuration,
120
+ sagasCreated,
121
+ sagasCompleted
122
+ };
123
+ const middleware = async (ctx, next) => {
124
+ const startTime = performance.now();
125
+ const messageType = ctx.envelope.type;
126
+ const sagaName = ctx.sagaName;
127
+ const isNewSaga = !ctx.existingState;
128
+ const labels = recordSagaLabels ? { message_type: messageType, saga_name: sagaName } : { message_type: messageType };
129
+ try {
130
+ await next();
131
+ const durationMs = performance.now() - startTime;
132
+ messagesProcessed.inc(labels);
133
+ processingDuration.observe(labels, durationMs);
134
+ if (isNewSaga && ctx.postState) {
135
+ sagasCreated.inc({ saga_name: sagaName, message_type: messageType });
136
+ }
137
+ if (ctx.postState?.metadata?.isCompleted) {
138
+ sagasCompleted.inc({ saga_name: sagaName });
139
+ }
140
+ } catch (error) {
141
+ const durationMs = performance.now() - startTime;
142
+ const errorType = getErrorType(error);
143
+ messagesFailed.inc({ ...labels, error_type: errorType });
144
+ processingDuration.observe(labels, durationMs);
145
+ throw error;
146
+ }
147
+ };
148
+ return { middleware, metrics };
149
+ }
150
+ export {
151
+ createMetricsMiddleware,
152
+ createMetricsMiddlewareWithMetrics
153
+ };
154
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/MetricsMiddleware.ts"],"sourcesContent":["import {\n Counter,\n Histogram,\n register as defaultRegister,\n} from \"prom-client\";\nimport type { SagaMiddleware, SagaPipelineContext } from \"@saga-bus/core\";\nimport type { MetricsMiddlewareOptions, SagaBusMetrics } from \"./types.js\";\n\nconst DEFAULT_DURATION_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000];\n\n/**\n * Creates a Prometheus metrics middleware for saga-bus.\n *\n * Records:\n * - Messages processed (counter)\n * - Messages failed (counter)\n * - Processing duration (histogram)\n * - Sagas created (counter)\n * - Sagas completed (counter)\n *\n * @example\n * ```typescript\n * import { createMetricsMiddleware } from \"@saga-bus/middleware-metrics\";\n * import { register } from \"prom-client\";\n *\n * const metricsMiddleware = createMetricsMiddleware({\n * prefix: \"my_app\",\n * });\n *\n * const bus = createBus({\n * middleware: [metricsMiddleware],\n * // ...\n * });\n *\n * // Expose metrics endpoint\n * app.get(\"/metrics\", async (req, res) => {\n * res.set(\"Content-Type\", register.contentType);\n * res.end(await register.metrics());\n * });\n * ```\n */\nexport function createMetricsMiddleware(\n options: MetricsMiddlewareOptions = {}\n): SagaMiddleware {\n const registry = options.registry ?? defaultRegister;\n const prefix = options.prefix ?? \"saga_bus\";\n const durationBuckets = options.durationBuckets ?? DEFAULT_DURATION_BUCKETS;\n const recordSagaLabels = options.recordSagaLabels ?? true;\n\n // Build label names\n const baseLabelNames = [\"message_type\"];\n const sagaLabelNames = recordSagaLabels\n ? [...baseLabelNames, \"saga_name\"]\n : baseLabelNames;\n\n // Create metrics\n const messagesProcessed = new Counter({\n name: `${prefix}_messages_processed_total`,\n help: \"Total number of messages successfully processed\",\n labelNames: sagaLabelNames,\n registers: [registry],\n });\n\n const messagesFailed = new Counter({\n name: `${prefix}_messages_failed_total`,\n help: \"Total number of messages that failed processing\",\n labelNames: [...sagaLabelNames, \"error_type\"],\n registers: [registry],\n });\n\n const processingDuration = new Histogram({\n name: `${prefix}_message_processing_duration_ms`,\n help: \"Message processing duration in milliseconds\",\n labelNames: sagaLabelNames,\n buckets: durationBuckets,\n registers: [registry],\n });\n\n const sagasCreated = new Counter({\n name: `${prefix}_sagas_created_total`,\n help: \"Total number of new saga instances created\",\n labelNames: [\"saga_name\", \"message_type\"],\n registers: [registry],\n });\n\n const sagasCompleted = new Counter({\n name: `${prefix}_sagas_completed_total`,\n help: \"Total number of saga instances completed\",\n labelNames: [\"saga_name\"],\n registers: [registry],\n });\n\n return async (ctx: SagaPipelineContext, next: () => Promise<void>) => {\n const startTime = performance.now();\n const messageType = ctx.envelope.type;\n const sagaName = ctx.sagaName;\n const isNewSaga = !ctx.existingState;\n\n const labels = recordSagaLabels\n ? { message_type: messageType, saga_name: sagaName }\n : { message_type: messageType };\n\n try {\n await next();\n\n // Record successful processing\n const durationMs = performance.now() - startTime;\n messagesProcessed.inc(labels);\n processingDuration.observe(labels, durationMs);\n\n // Track saga lifecycle\n if (isNewSaga && ctx.postState) {\n sagasCreated.inc({ saga_name: sagaName, message_type: messageType });\n }\n\n if (ctx.postState?.metadata?.isCompleted) {\n sagasCompleted.inc({ saga_name: sagaName });\n }\n } catch (error) {\n // Record failed processing\n const durationMs = performance.now() - startTime;\n const errorType = getErrorType(error);\n\n messagesFailed.inc({ ...labels, error_type: errorType });\n processingDuration.observe(labels, durationMs);\n\n throw error;\n }\n };\n}\n\nfunction getErrorType(error: unknown): string {\n if (error instanceof Error) {\n return error.name || \"Error\";\n }\n return \"Unknown\";\n}\n\n/**\n * Creates metrics and returns both the middleware and the metrics objects.\n * Useful when you need direct access to the metric objects.\n *\n * @example\n * ```typescript\n * const { middleware, metrics } = createMetricsMiddlewareWithMetrics();\n *\n * // Access metrics directly\n * console.log(await metrics.messagesProcessed.get());\n * ```\n */\nexport function createMetricsMiddlewareWithMetrics(\n options: MetricsMiddlewareOptions = {}\n): { middleware: SagaMiddleware; metrics: SagaBusMetrics } {\n const registry = options.registry ?? defaultRegister;\n const prefix = options.prefix ?? \"saga_bus\";\n const durationBuckets = options.durationBuckets ?? DEFAULT_DURATION_BUCKETS;\n const recordSagaLabels = options.recordSagaLabels ?? true;\n\n // Build label names\n const baseLabelNames = [\"message_type\"];\n const sagaLabelNames = recordSagaLabels\n ? [...baseLabelNames, \"saga_name\"]\n : baseLabelNames;\n\n // Create metrics\n const messagesProcessed = new Counter({\n name: `${prefix}_messages_processed_total`,\n help: \"Total number of messages successfully processed\",\n labelNames: sagaLabelNames,\n registers: [registry],\n });\n\n const messagesFailed = new Counter({\n name: `${prefix}_messages_failed_total`,\n help: \"Total number of messages that failed processing\",\n labelNames: [...sagaLabelNames, \"error_type\"],\n registers: [registry],\n });\n\n const processingDuration = new Histogram({\n name: `${prefix}_message_processing_duration_ms`,\n help: \"Message processing duration in milliseconds\",\n labelNames: sagaLabelNames,\n buckets: durationBuckets,\n registers: [registry],\n });\n\n const sagasCreated = new Counter({\n name: `${prefix}_sagas_created_total`,\n help: \"Total number of new saga instances created\",\n labelNames: [\"saga_name\", \"message_type\"],\n registers: [registry],\n });\n\n const sagasCompleted = new Counter({\n name: `${prefix}_sagas_completed_total`,\n help: \"Total number of saga instances completed\",\n labelNames: [\"saga_name\"],\n registers: [registry],\n });\n\n const metrics: SagaBusMetrics = {\n messagesProcessed,\n messagesFailed,\n processingDuration,\n sagasCreated,\n sagasCompleted,\n };\n\n const middleware: SagaMiddleware = async (\n ctx: SagaPipelineContext,\n next: () => Promise<void>\n ) => {\n const startTime = performance.now();\n const messageType = ctx.envelope.type;\n const sagaName = ctx.sagaName;\n const isNewSaga = !ctx.existingState;\n\n const labels = recordSagaLabels\n ? { message_type: messageType, saga_name: sagaName }\n : { message_type: messageType };\n\n try {\n await next();\n\n const durationMs = performance.now() - startTime;\n messagesProcessed.inc(labels);\n processingDuration.observe(labels, durationMs);\n\n if (isNewSaga && ctx.postState) {\n sagasCreated.inc({ saga_name: sagaName, message_type: messageType });\n }\n\n if (ctx.postState?.metadata?.isCompleted) {\n sagasCompleted.inc({ saga_name: sagaName });\n }\n } catch (error) {\n const durationMs = performance.now() - startTime;\n const errorType = getErrorType(error);\n\n messagesFailed.inc({ ...labels, error_type: errorType });\n processingDuration.observe(labels, durationMs);\n\n throw error;\n }\n };\n\n return { middleware, metrics };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@saga-bus/middleware-metrics",
3
+ "version": "0.1.1",
4
+ "description": "Prometheus metrics middleware for saga-bus",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/d-e-a-n-f/saga-bus.git",
26
+ "directory": "packages/middleware-metrics"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/d-e-a-n-f/saga-bus/issues"
30
+ },
31
+ "homepage": "https://github.com/d-e-a-n-f/saga-bus#readme",
32
+ "keywords": [
33
+ "saga",
34
+ "message-bus",
35
+ "middleware",
36
+ "metrics",
37
+ "prometheus",
38
+ "observability"
39
+ ],
40
+ "dependencies": {
41
+ "prom-client": "^15.1.0",
42
+ "@saga-bus/core": "0.1.1"
43
+ },
44
+ "devDependencies": {
45
+ "tsup": "^8.0.0",
46
+ "typescript": "^5.9.2",
47
+ "vitest": "^3.0.0",
48
+ "@repo/eslint-config": "0.0.0",
49
+ "@repo/typescript-config": "0.0.0"
50
+ },
51
+ "peerDependencies": {
52
+ "@saga-bus/core": ">=0.1.1",
53
+ "prom-client": ">=14.0.0"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "dev": "tsup --watch",
58
+ "lint": "eslint src/",
59
+ "check-types": "tsc --noEmit",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest"
62
+ }
63
+ }