@saga-bus/middleware-metrics 1.0.0
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 +21 -0
- package/README.md +114 -0
- package/dist/index.cjs +178 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +106 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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": "1.0.0",
|
|
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/deanforan/saga-bus.git",
|
|
26
|
+
"directory": "packages/middleware-metrics"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/deanforan/saga-bus/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/deanforan/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.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.9.2",
|
|
47
|
+
"vitest": "^3.0.0",
|
|
48
|
+
"@repo/typescript-config": "0.0.0",
|
|
49
|
+
"@repo/eslint-config": "0.0.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@saga-bus/core": ">=0.1.0",
|
|
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
|
+
}
|