@tclohm/yoban 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/README.MD +43 -0
- package/index.js +1 -0
- package/package.json +27 -0
- package/src/notifiers/pagerduty.js +19 -0
- package/src/notifiers/slack.js +9 -0
- package/src/yoban.js +82 -0
package/README.MD
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @tclohm/yoban
|
|
2
|
+
|
|
3
|
+
> Lightweight Express middleware for real-time API performance monitoring and SLA alerting.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
```
|
|
7
|
+
npm install @tclohm/yoban
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```javascript
|
|
12
|
+
import { yoban } from '@tclohm/yoban';
|
|
13
|
+
|
|
14
|
+
const metrics = new yoban({
|
|
15
|
+
service: "payments",
|
|
16
|
+
flushInterval: 10000, // flush every 10s
|
|
17
|
+
violationThreshold: 0.5, // alert if >50% of requests violate SLA
|
|
18
|
+
sla: { premium: 400, standard: 800 },
|
|
19
|
+
notify: {
|
|
20
|
+
slack: process.env.SLACK_WEBHOOK,
|
|
21
|
+
pagerduty: process.env.PAGERDUTY_KEY
|
|
22
|
+
},
|
|
23
|
+
enrichEvent: (req) => ({
|
|
24
|
+
tier: req.user?.tier ?? 'standard'
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.use(metrics.middleware());
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Config
|
|
32
|
+
| Option | Type | Default | Description |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `service` | string | required | Name of your service |
|
|
35
|
+
| `flushInterval` | number | 10000 | How often to flush in ms |
|
|
36
|
+
| `violationThreshold` | number | 0.5 | Violation rate to trigger alert |
|
|
37
|
+
| `sla` | object | — | SLA thresholds per tier in ms |
|
|
38
|
+
| `notify.slack` | string | — | Slack webhook URL |
|
|
39
|
+
| `notify.pagerduty` | string | — | PagerDuty routing key |
|
|
40
|
+
| `enrichEvent` | function | — | Add custom metadata per request |
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
MIT © tclohm
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { yoban } from './src/yoban.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tclohm/yoban",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight middleware for real-time API performance monitoring and SLA alerting",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"monitoring",
|
|
7
|
+
"metrics",
|
|
8
|
+
"sla",
|
|
9
|
+
"middleware",
|
|
10
|
+
"alerting"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/tclohm/yoban#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/tclohm/yoban/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/tclohm/yoban.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "tclohm",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function notifyPagerDuty(routingKey, entry, violationRate) {
|
|
2
|
+
await fetch("https://events.pagerduty.com/v2/enqueue", {
|
|
3
|
+
method: "POST",
|
|
4
|
+
headers: { "Content-Type": "application/json" },
|
|
5
|
+
body: JSON.stringify({
|
|
6
|
+
routing_key: routingKey,
|
|
7
|
+
event_action: "trigger",
|
|
8
|
+
payload: {
|
|
9
|
+
summary: `${entry.service} SLA violation on ${entry.route} (${entry.tier})`,
|
|
10
|
+
severity: "critical",
|
|
11
|
+
custom_details: {
|
|
12
|
+
violationRate: `${(violationRate * 100).toFixed(2)}%`,
|
|
13
|
+
mean: entry.mean,
|
|
14
|
+
median: entry.median
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function notifySlack(webhookUrl, entry, violationRate) {
|
|
2
|
+
await fetch(webhookUrl, {
|
|
3
|
+
method: "POST",
|
|
4
|
+
headers: { "Content-Type": "application/json" },
|
|
5
|
+
body: JSON.stringify({
|
|
6
|
+
text: `[yoban ALERT]: \`${entry.service}\` on \`${entry.route}\` (${entry.tier}) has a *${(violationRate * 100).toFixed(2)}%* SLA violation rate!`
|
|
7
|
+
})
|
|
8
|
+
});
|
|
9
|
+
}
|
package/src/yoban.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { notifySlack } from './notifiers/slack.js';
|
|
2
|
+
import { notifyPagerDuty } from './notifiers/pagerduty.js';
|
|
3
|
+
|
|
4
|
+
export class yoban {
|
|
5
|
+
#buffer = [];
|
|
6
|
+
#aggregated = [];
|
|
7
|
+
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.#startFlushing();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
middleware() {
|
|
14
|
+
return (req, res, next) => {
|
|
15
|
+
const start = Date.now();
|
|
16
|
+
res.on('finish', () => {
|
|
17
|
+
const duration = Date.now() - start;
|
|
18
|
+
const extra = this.config.enrichEvent ? this.config.enrichEvent(req) : {};
|
|
19
|
+
this.#buffer.push({
|
|
20
|
+
service: this.config.service,
|
|
21
|
+
method: req.method,
|
|
22
|
+
route: req.route?.path ?? req.path,
|
|
23
|
+
status: res.statusCode,
|
|
24
|
+
duration,
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
...extra
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
next();
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#startFlushing() {
|
|
34
|
+
setInterval(() => this.flush(), this.config.flushInterval ?? 10000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
flush() {
|
|
38
|
+
const events = this.#buffer.splice(0);
|
|
39
|
+
if (events.length === 0) return;
|
|
40
|
+
this.#aggregated = [...this.#aggregated, ...this.#parse(events)];
|
|
41
|
+
this.#checkAlerts();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#parse(events) {
|
|
45
|
+
const groups = new Map();
|
|
46
|
+
for (const event of events) {
|
|
47
|
+
const key = `${event.service} ${event.route} ${event.tier}`;
|
|
48
|
+
if (!groups.has(key)) {
|
|
49
|
+
groups.set(key, { service: event.service, route: event.route, tier: event.tier, durations: [] });
|
|
50
|
+
}
|
|
51
|
+
groups.get(key).durations.push(event.duration);
|
|
52
|
+
}
|
|
53
|
+
return Array.from(groups.values()).map(({ service, route, tier, durations }) => {
|
|
54
|
+
const sorted = [...durations].sort((a, b) => a - b);
|
|
55
|
+
const mean = durations.reduce((sum, d) => sum + d, 0) / durations.length;
|
|
56
|
+
const mid = Math.floor(sorted.length / 2);
|
|
57
|
+
const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
58
|
+
return { service, route, tier, mean: Math.round(mean), median, durations };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async #checkAlerts() {
|
|
63
|
+
for (const entry of this.#aggregated) {
|
|
64
|
+
const threshold = this.config.sla?.[entry.tier];
|
|
65
|
+
if (!threshold) continue;
|
|
66
|
+
|
|
67
|
+
const violations = entry.durations.filter(d => d > threshold).length;
|
|
68
|
+
const violationRate = violations / entry.durations.length;
|
|
69
|
+
|
|
70
|
+
if (violationRate > (this.config.violationThreshold ?? 0.5)) {
|
|
71
|
+
console.log(`[ yoban ALERT ]: ${entry.service} ${entry.route} (${entry.tier}) violation rate ${(violationRate * 100).toFixed(2)}%`);
|
|
72
|
+
|
|
73
|
+
if (this.config.notify?.slack) {
|
|
74
|
+
await notifySlack(this.config.notify.slack, entry, violationRate);
|
|
75
|
+
}
|
|
76
|
+
if (this.config.notify?.pagerduty) {
|
|
77
|
+
await notifyPagerDuty(this.config.notify.pagerduty, entry, violationRate);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|