node-observe 0.1.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/dist/index.js ADDED
@@ -0,0 +1,454 @@
1
+ // src/histogram.ts
2
+ var Histogram = class {
3
+ buffer = [];
4
+ head = 0;
5
+ cap;
6
+ count = 0;
7
+ errors = 0;
8
+ sum = 0;
9
+ minV = Infinity;
10
+ maxV = -Infinity;
11
+ constructor(cap = 1024) {
12
+ this.cap = Math.max(1, cap);
13
+ }
14
+ /** Record one duration (ms). Mark `isError` to count it toward the error tally. */
15
+ record(value, isError = false) {
16
+ if (!Number.isFinite(value) || value < 0) return;
17
+ this.count++;
18
+ this.sum += value;
19
+ if (isError) this.errors++;
20
+ if (value < this.minV) this.minV = value;
21
+ if (value > this.maxV) this.maxV = value;
22
+ if (this.buffer.length < this.cap) {
23
+ this.buffer.push(value);
24
+ } else {
25
+ this.buffer[this.head] = value;
26
+ this.head = (this.head + 1) % this.cap;
27
+ }
28
+ }
29
+ /** Linear-interpolation percentile over the retained window. `p` in [0,100]. */
30
+ percentile(p) {
31
+ const n = this.buffer.length;
32
+ if (n === 0) return 0;
33
+ const sorted = [...this.buffer].sort((a, b) => a - b);
34
+ if (n === 1) return sorted[0];
35
+ const rank = p / 100 * (n - 1);
36
+ const lo = Math.floor(rank);
37
+ const hi = Math.ceil(rank);
38
+ const frac = rank - lo;
39
+ return sorted[lo] * (1 - frac) + sorted[hi] * frac;
40
+ }
41
+ snapshot() {
42
+ const round = (n) => Math.round(n * 100) / 100;
43
+ return {
44
+ count: this.count,
45
+ errors: this.errors,
46
+ mean: this.count ? round(this.sum / this.count) : 0,
47
+ min: this.count ? round(this.minV) : 0,
48
+ max: this.count ? round(this.maxV) : 0,
49
+ p50: round(this.percentile(50)),
50
+ p95: round(this.percentile(95)),
51
+ p99: round(this.percentile(99))
52
+ };
53
+ }
54
+ };
55
+ var MetricsRegistry = class {
56
+ constructor(cap = 1024) {
57
+ this.cap = cap;
58
+ }
59
+ cap;
60
+ histograms = /* @__PURE__ */ new Map();
61
+ counters = /* @__PURE__ */ new Map();
62
+ record(name, value, isError = false) {
63
+ let h = this.histograms.get(name);
64
+ if (!h) {
65
+ h = new Histogram(this.cap);
66
+ this.histograms.set(name, h);
67
+ }
68
+ h.record(value, isError);
69
+ }
70
+ increment(name, by = 1) {
71
+ this.counters.set(name, (this.counters.get(name) ?? 0) + by);
72
+ }
73
+ snapshot() {
74
+ const out = {};
75
+ for (const [name, h] of this.histograms) out[name] = h.snapshot();
76
+ return out;
77
+ }
78
+ counterSnapshot() {
79
+ return Object.fromEntries(this.counters);
80
+ }
81
+ reset() {
82
+ this.histograms.clear();
83
+ this.counters.clear();
84
+ }
85
+ };
86
+
87
+ // src/memory.ts
88
+ var MemoryMonitor = class {
89
+ samples = [];
90
+ window;
91
+ constructor(window = 30) {
92
+ this.window = Math.max(2, window);
93
+ }
94
+ /** Add the current memory reading. `now`/`mem` are injectable for testing. */
95
+ sample(now, mem) {
96
+ this.samples.push({ t: now, heapUsed: mem.heapUsed });
97
+ if (this.samples.length > this.window) this.samples.shift();
98
+ }
99
+ /** Heap growth rate in MB per minute over the retained window. */
100
+ growthMbPerMin() {
101
+ if (this.samples.length < 2) return 0;
102
+ const n = this.samples.length;
103
+ const t0 = this.samples[0].t;
104
+ let sx = 0;
105
+ let sy = 0;
106
+ let sxx = 0;
107
+ let sxy = 0;
108
+ for (const s of this.samples) {
109
+ const x = (s.t - t0) / 6e4;
110
+ const y = s.heapUsed / (1024 * 1024);
111
+ sx += x;
112
+ sy += y;
113
+ sxx += x * x;
114
+ sxy += x * y;
115
+ }
116
+ const denom = n * sxx - sx * sx;
117
+ if (denom === 0) return 0;
118
+ const slope = (n * sxy - sx * sy) / denom;
119
+ return Math.round(slope * 100) / 100;
120
+ }
121
+ snapshot(mem) {
122
+ const mb = (b) => Math.round(b / (1024 * 1024) * 100) / 100;
123
+ return {
124
+ heapUsedMb: mb(mem.heapUsed),
125
+ heapTotalMb: mb(mem.heapTotal),
126
+ rssMb: mb(mem.rss),
127
+ externalMb: mb(mem.external),
128
+ growthMbPerMin: this.growthMbPerMin()
129
+ };
130
+ }
131
+ /** Number of samples currently retained. */
132
+ get size() {
133
+ return this.samples.length;
134
+ }
135
+ };
136
+
137
+ // src/slack.ts
138
+ var EMOJI = {
139
+ "slow-request": "\u{1F422}",
140
+ "slow-mongo": "\u{1F343}",
141
+ "slow-redis": "\u{1F9F1}",
142
+ "memory-leak": "\u{1F4C8}",
143
+ "memory-high": "\u{1F6A8}"
144
+ };
145
+ function formatSlackMessage(alert, service) {
146
+ const emoji = EMOJI[alert.type] ?? "\u26A0\uFE0F";
147
+ return `${emoji} *[${service}] ${alert.type}*
148
+ ${alert.message}
149
+ \u2022 value: \`${alert.value}\` \u2022 threshold: \`${alert.threshold}\` \u2022 at: ${alert.at}`;
150
+ }
151
+ async function postToSlack(webhook, alert, service, logger) {
152
+ if (typeof fetch !== "function") {
153
+ logger?.warn?.("node-observe: global fetch unavailable (need Node 18+); skipping Slack alert");
154
+ return;
155
+ }
156
+ try {
157
+ const res = await fetch(webhook, {
158
+ method: "POST",
159
+ headers: { "content-type": "application/json" },
160
+ body: JSON.stringify({ text: formatSlackMessage(alert, service) })
161
+ });
162
+ if (!res.ok) {
163
+ logger?.error?.(`node-observe: Slack webhook returned ${res.status}`);
164
+ }
165
+ } catch (err) {
166
+ logger?.error?.(`node-observe: Slack alert failed: ${err.message}`);
167
+ }
168
+ }
169
+
170
+ // src/util.ts
171
+ function normalizePath(path) {
172
+ const clean = path.split("?")[0] ?? path;
173
+ return clean.split("/").map((seg) => {
174
+ if (seg === "") return seg;
175
+ if (/^\d+$/.test(seg)) return ":id";
176
+ if (/^[0-9a-f]{24}$/i.test(seg)) return ":id";
177
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return ":id";
178
+ if (/^[0-9a-f]{32,}$/i.test(seg)) return ":id";
179
+ return seg;
180
+ }).join("/");
181
+ }
182
+ function elapsedMs(start) {
183
+ return Number(process.hrtime.bigint() - start) / 1e6;
184
+ }
185
+
186
+ // src/express.ts
187
+ function instrumentExpress(insights) {
188
+ return function nodeObserveMiddleware(req, res, next) {
189
+ const start = process.hrtime.bigint();
190
+ let done = false;
191
+ const finish2 = () => {
192
+ if (done) return;
193
+ done = true;
194
+ const ms = elapsedMs(start);
195
+ const method = req.method ?? "GET";
196
+ const route = routeOf(req);
197
+ insights.recordRequest(method, route, res.statusCode ?? 0, ms);
198
+ };
199
+ res.on("finish", finish2);
200
+ res.on("close", finish2);
201
+ next();
202
+ };
203
+ }
204
+ function routeOf(req) {
205
+ if (req.route?.path) {
206
+ const base = req.baseUrl ?? "";
207
+ return base + req.route.path || req.route.path;
208
+ }
209
+ return req.path ?? (req.originalUrl ?? req.url ?? "unknown").split("?")[0] ?? "unknown";
210
+ }
211
+
212
+ // src/mongo.ts
213
+ var START = /* @__PURE__ */ Symbol("nodeObserveStart");
214
+ var QUERY_HOOK = /^(find|findOne|findOneAnd|count|countDocuments|estimatedDocumentCount|update|updateOne|updateMany|delete|deleteOne|deleteMany|replaceOne)/;
215
+ function instrumentMongoose(mongoose, insights) {
216
+ const m = mongoose;
217
+ if (!m || typeof m.plugin !== "function") {
218
+ insights.logger.warn?.("node-observe: instrumentMongoose called with a non-mongoose value; skipping");
219
+ return;
220
+ }
221
+ m.plugin((schema) => {
222
+ schema.pre(QUERY_HOOK, function() {
223
+ this[START] = process.hrtime.bigint();
224
+ });
225
+ schema.post(QUERY_HOOK, function() {
226
+ finish(insights, modelName(this), this.op ?? "query", this[START]);
227
+ });
228
+ schema.pre("aggregate", function() {
229
+ this[START] = process.hrtime.bigint();
230
+ });
231
+ schema.post("aggregate", function() {
232
+ finish(insights, aggregateModelName(this), "aggregate", this[START]);
233
+ });
234
+ schema.pre("save", function() {
235
+ this[START] = process.hrtime.bigint();
236
+ });
237
+ schema.post("save", function() {
238
+ finish(insights, this?.constructor?.modelName ?? "Document", "save", this[START]);
239
+ });
240
+ });
241
+ }
242
+ function finish(insights, model, op, start) {
243
+ if (typeof start !== "bigint") return;
244
+ insights.recordMongo(`${model}.${op}`, elapsedMs(start));
245
+ }
246
+ function modelName(query) {
247
+ return query?.model?.modelName ?? query?.mongooseCollection?.name ?? "Model";
248
+ }
249
+ function aggregateModelName(agg) {
250
+ return agg?._model?.modelName ?? agg?.model?.()?.modelName ?? "Model";
251
+ }
252
+
253
+ // src/redis.ts
254
+ var PATCHED = /* @__PURE__ */ Symbol.for("nodeObserve.redisPatched");
255
+ function instrumentRedis(client, insights) {
256
+ const c = client;
257
+ if (!c || typeof c.sendCommand !== "function") {
258
+ insights.logger.warn?.("node-observe: instrumentRedis called with a non-ioredis value; skipping");
259
+ return;
260
+ }
261
+ if (c[PATCHED]) return;
262
+ c[PATCHED] = true;
263
+ const original = c.sendCommand.bind(c);
264
+ c.sendCommand = function patchedSendCommand(command, ...rest) {
265
+ const start = process.hrtime.bigint();
266
+ const name = (command?.name ?? "unknown").toLowerCase();
267
+ const result = original(command, ...rest);
268
+ const promise = command?.promise ?? result;
269
+ if (promise && typeof promise.then === "function") {
270
+ promise.then(
271
+ () => insights.recordRedis(name, elapsedMs(start), false),
272
+ () => insights.recordRedis(name, elapsedMs(start), true)
273
+ );
274
+ }
275
+ return result;
276
+ };
277
+ }
278
+
279
+ // src/insights.ts
280
+ var consoleLogger = {
281
+ info: (m) => console.log(m),
282
+ error: (m) => console.error(m),
283
+ warn: (m) => console.warn(m),
284
+ debug: (m) => console.debug(m)
285
+ };
286
+ var DEFAULT_THRESHOLDS = {
287
+ requestMs: 1e3,
288
+ mongoMs: 300,
289
+ redisMs: 100,
290
+ heapGrowthMbPerMin: 50,
291
+ heapUsedMb: 0
292
+ // disabled by default
293
+ };
294
+ var Insights = class {
295
+ http;
296
+ mongo;
297
+ redis;
298
+ counters;
299
+ memory;
300
+ serviceName;
301
+ thresholds;
302
+ logger;
303
+ opts;
304
+ cooldownMs;
305
+ lastAlertAt = /* @__PURE__ */ new Map();
306
+ memoryTimer;
307
+ startedAt = Date.now();
308
+ constructor(options = {}) {
309
+ this.opts = options;
310
+ this.serviceName = options.serviceName ?? "node-observe";
311
+ this.thresholds = { ...DEFAULT_THRESHOLDS, ...options.thresholds ?? {} };
312
+ this.logger = options.logger ?? consoleLogger;
313
+ this.cooldownMs = options.alertCooldownMs ?? 6e4;
314
+ this.memory = new MemoryMonitor();
315
+ const cap = options.sampleSize ?? 1024;
316
+ this.http = new MetricsRegistry(cap);
317
+ this.mongo = new MetricsRegistry(cap);
318
+ this.redis = new MetricsRegistry(cap);
319
+ this.counters = new MetricsRegistry(cap);
320
+ }
321
+ // ---- recording (used by the instrumentations) -----------------------------
322
+ /** Record an HTTP request. `route` is normalized when `normalizePaths` is on. */
323
+ recordRequest(method, route, statusCode, ms) {
324
+ const path = this.opts.normalizePaths === false ? route : normalizePath(route);
325
+ const isError = statusCode >= 500;
326
+ const key = `${method.toUpperCase()} ${path}`;
327
+ this.http.record(key, ms, isError);
328
+ this.counters.increment(`http.status.${Math.floor(statusCode / 100)}xx`);
329
+ if (ms > this.thresholds.requestMs) {
330
+ this.raise("slow-request", key, `${key} took ${ms.toFixed(0)}ms`, ms, this.thresholds.requestMs);
331
+ }
332
+ }
333
+ /** Record a Mongo operation, e.g. `User.find`. */
334
+ recordMongo(operation, ms, isError = false) {
335
+ this.mongo.record(operation, ms, isError);
336
+ if (ms > this.thresholds.mongoMs) {
337
+ this.raise("slow-mongo", operation, `Mongo ${operation} took ${ms.toFixed(0)}ms`, ms, this.thresholds.mongoMs);
338
+ }
339
+ }
340
+ /** Record a Redis command, e.g. `get`. */
341
+ recordRedis(command, ms, isError = false) {
342
+ this.redis.record(command, ms, isError);
343
+ if (ms > this.thresholds.redisMs) {
344
+ this.raise("slow-redis", command, `Redis ${command} took ${ms.toFixed(0)}ms`, ms, this.thresholds.redisMs);
345
+ }
346
+ }
347
+ // ---- instrumentation attach points ----------------------------------------
348
+ /** Express/Connect middleware that times every request. */
349
+ express() {
350
+ return instrumentExpress(this);
351
+ }
352
+ /** Instrument a Mongoose instance so every query/aggregate/save is timed. */
353
+ instrumentMongoose(mongoose) {
354
+ instrumentMongoose(mongoose, this);
355
+ }
356
+ /** Instrument an ioredis client so every command is timed. */
357
+ instrumentRedis(client) {
358
+ instrumentRedis(client, this);
359
+ }
360
+ /** Begin periodic memory sampling + leak detection. Returns a stop function. */
361
+ startMemoryMonitor() {
362
+ if (this.memoryTimer) return () => this.stopMemoryMonitor();
363
+ const interval = this.opts.memorySampleIntervalMs ?? 15e3;
364
+ this.memoryTimer = setInterval(() => this.checkMemory(), interval);
365
+ this.memoryTimer.unref?.();
366
+ return () => this.stopMemoryMonitor();
367
+ }
368
+ stopMemoryMonitor() {
369
+ if (this.memoryTimer) {
370
+ clearInterval(this.memoryTimer);
371
+ this.memoryTimer = void 0;
372
+ }
373
+ }
374
+ /**
375
+ * Take one memory reading and evaluate the leak/high-heap thresholds.
376
+ * `now`/`mem` are injectable for testing; both default to live process values.
377
+ */
378
+ checkMemory(now = Date.now(), mem = process.memoryUsage()) {
379
+ this.memory.sample(now, mem);
380
+ const snap = this.memory.snapshot(mem);
381
+ if (this.memory.size >= 5 && snap.growthMbPerMin > this.thresholds.heapGrowthMbPerMin) {
382
+ this.raise(
383
+ "memory-leak",
384
+ "heap",
385
+ `Heap growing ${snap.growthMbPerMin}MB/min (now ${snap.heapUsedMb}MB)`,
386
+ snap.growthMbPerMin,
387
+ this.thresholds.heapGrowthMbPerMin
388
+ );
389
+ }
390
+ if (this.thresholds.heapUsedMb > 0 && snap.heapUsedMb > this.thresholds.heapUsedMb) {
391
+ this.raise(
392
+ "memory-high",
393
+ "heap",
394
+ `Heap usage ${snap.heapUsedMb}MB`,
395
+ snap.heapUsedMb,
396
+ this.thresholds.heapUsedMb
397
+ );
398
+ }
399
+ }
400
+ // ---- output ---------------------------------------------------------------
401
+ snapshot() {
402
+ const mem = process.memoryUsage();
403
+ return {
404
+ service: this.serviceName,
405
+ uptimeSec: Math.round((Date.now() - this.startedAt) / 1e3),
406
+ http: this.http.snapshot(),
407
+ mongo: this.mongo.snapshot(),
408
+ redis: this.redis.snapshot(),
409
+ counters: this.counters.counterSnapshot(),
410
+ memory: this.memory.snapshot(mem),
411
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
412
+ };
413
+ }
414
+ /** Express handler that responds with the JSON snapshot. */
415
+ handler() {
416
+ return (_req, res) => {
417
+ const body = this.snapshot();
418
+ if (typeof res.json === "function") {
419
+ res.json(body);
420
+ } else {
421
+ res.setHeader?.("Content-Type", "application/json");
422
+ res.end?.(JSON.stringify(body));
423
+ }
424
+ };
425
+ }
426
+ // ---- alerting -------------------------------------------------------------
427
+ raise(type, key, message, value, threshold) {
428
+ const dedupeKey = `${type}:${key}`;
429
+ const now = Date.now();
430
+ const last = this.lastAlertAt.get(dedupeKey) ?? 0;
431
+ if (now - last < this.cooldownMs) return;
432
+ this.lastAlertAt.set(dedupeKey, now);
433
+ const alert = {
434
+ type,
435
+ key,
436
+ message,
437
+ value: Math.round(value * 100) / 100,
438
+ threshold,
439
+ at: new Date(now).toISOString()
440
+ };
441
+ try {
442
+ this.opts.onAlert?.(alert);
443
+ } catch (err) {
444
+ this.logger.error(`node-observe: onAlert handler threw: ${err.message}`);
445
+ }
446
+ if (this.opts.slackWebhook) {
447
+ void postToSlack(this.opts.slackWebhook, alert, this.serviceName, this.logger);
448
+ }
449
+ }
450
+ };
451
+
452
+ export { Histogram, Insights, MemoryMonitor, MetricsRegistry, elapsedMs, formatSlackMessage, instrumentExpress, instrumentMongoose, instrumentRedis, normalizePath, postToSlack };
453
+ //# sourceMappingURL=index.js.map
454
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/histogram.ts","../src/memory.ts","../src/slack.ts","../src/util.ts","../src/express.ts","../src/mongo.ts","../src/redis.ts","../src/insights.ts"],"names":["finish"],"mappings":"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","file":"index.js","sourcesContent":["import type { HistogramSnapshot } from \"./types\";\n\n/**\n * A bounded latency histogram. Keeps the most recent `cap` samples in a ring\n * buffer (so memory is constant) while tracking lifetime count/min/max/sum and\n * an error tally. Percentiles are computed over the retained window.\n */\nexport class Histogram {\n private readonly buffer: number[] = [];\n private head = 0;\n private readonly cap: number;\n\n count = 0;\n errors = 0;\n private sum = 0;\n private minV = Infinity;\n private maxV = -Infinity;\n\n constructor(cap = 1024) {\n this.cap = Math.max(1, cap);\n }\n\n /** Record one duration (ms). Mark `isError` to count it toward the error tally. */\n record(value: number, isError = false): void {\n if (!Number.isFinite(value) || value < 0) return;\n this.count++;\n this.sum += value;\n if (isError) this.errors++;\n if (value < this.minV) this.minV = value;\n if (value > this.maxV) this.maxV = value;\n\n if (this.buffer.length < this.cap) {\n this.buffer.push(value);\n } else {\n this.buffer[this.head] = value;\n this.head = (this.head + 1) % this.cap;\n }\n }\n\n /** Linear-interpolation percentile over the retained window. `p` in [0,100]. */\n percentile(p: number): number {\n const n = this.buffer.length;\n if (n === 0) return 0;\n const sorted = [...this.buffer].sort((a, b) => a - b);\n if (n === 1) return sorted[0] as number;\n const rank = (p / 100) * (n - 1);\n const lo = Math.floor(rank);\n const hi = Math.ceil(rank);\n const frac = rank - lo;\n return (sorted[lo] as number) * (1 - frac) + (sorted[hi] as number) * frac;\n }\n\n snapshot(): HistogramSnapshot {\n const round = (n: number) => Math.round(n * 100) / 100;\n return {\n count: this.count,\n errors: this.errors,\n mean: this.count ? round(this.sum / this.count) : 0,\n min: this.count ? round(this.minV) : 0,\n max: this.count ? round(this.maxV) : 0,\n p50: round(this.percentile(50)),\n p95: round(this.percentile(95)),\n p99: round(this.percentile(99)),\n };\n }\n}\n\n/** A named set of histograms plus simple counters. */\nexport class MetricsRegistry {\n private readonly histograms = new Map<string, Histogram>();\n private readonly counters = new Map<string, number>();\n\n constructor(private readonly cap = 1024) {}\n\n record(name: string, value: number, isError = false): void {\n let h = this.histograms.get(name);\n if (!h) {\n h = new Histogram(this.cap);\n this.histograms.set(name, h);\n }\n h.record(value, isError);\n }\n\n increment(name: string, by = 1): void {\n this.counters.set(name, (this.counters.get(name) ?? 0) + by);\n }\n\n snapshot(): Record<string, HistogramSnapshot> {\n const out: Record<string, HistogramSnapshot> = {};\n for (const [name, h] of this.histograms) out[name] = h.snapshot();\n return out;\n }\n\n counterSnapshot(): Record<string, number> {\n return Object.fromEntries(this.counters);\n }\n\n reset(): void {\n this.histograms.clear();\n this.counters.clear();\n }\n}\n","import type { MemorySnapshot } from \"./types\";\n\ninterface Sample {\n t: number; // ms timestamp\n heapUsed: number; // bytes\n}\n\n/**\n * Tracks heap-used over time and estimates the growth trend via least-squares\n * linear regression. A sustained positive slope is the signal of a memory leak —\n * far more reliable than reacting to a single high reading (which is usually\n * just GC timing).\n */\nexport class MemoryMonitor {\n private readonly samples: Sample[] = [];\n private readonly window: number;\n\n constructor(window = 30) {\n this.window = Math.max(2, window);\n }\n\n /** Add the current memory reading. `now`/`mem` are injectable for testing. */\n sample(now: number, mem: NodeJS.MemoryUsage): void {\n this.samples.push({ t: now, heapUsed: mem.heapUsed });\n if (this.samples.length > this.window) this.samples.shift();\n }\n\n /** Heap growth rate in MB per minute over the retained window. */\n growthMbPerMin(): number {\n if (this.samples.length < 2) return 0;\n const n = this.samples.length;\n const t0 = this.samples[0]!.t;\n let sx = 0;\n let sy = 0;\n let sxx = 0;\n let sxy = 0;\n for (const s of this.samples) {\n const x = (s.t - t0) / 60000; // minutes since window start\n const y = s.heapUsed / (1024 * 1024); // MB\n sx += x;\n sy += y;\n sxx += x * x;\n sxy += x * y;\n }\n const denom = n * sxx - sx * sx;\n if (denom === 0) return 0;\n const slope = (n * sxy - sx * sy) / denom; // MB per minute\n return Math.round(slope * 100) / 100;\n }\n\n snapshot(mem: NodeJS.MemoryUsage): MemorySnapshot {\n const mb = (b: number) => Math.round((b / (1024 * 1024)) * 100) / 100;\n return {\n heapUsedMb: mb(mem.heapUsed),\n heapTotalMb: mb(mem.heapTotal),\n rssMb: mb(mem.rss),\n externalMb: mb(mem.external),\n growthMbPerMin: this.growthMbPerMin(),\n };\n }\n\n /** Number of samples currently retained. */\n get size(): number {\n return this.samples.length;\n }\n}\n","import type { Alert, Logger } from \"./types\";\n\nconst EMOJI: Record<string, string> = {\n \"slow-request\": \"🐢\",\n \"slow-mongo\": \"🍃\",\n \"slow-redis\": \"🧱\",\n \"memory-leak\": \"📈\",\n \"memory-high\": \"🚨\",\n};\n\n/** Format an alert as Slack message text. */\nexport function formatSlackMessage(alert: Alert, service: string): string {\n const emoji = EMOJI[alert.type] ?? \"⚠️\";\n return (\n `${emoji} *[${service}] ${alert.type}*\\n` +\n `${alert.message}\\n` +\n `• value: \\`${alert.value}\\` • threshold: \\`${alert.threshold}\\` • at: ${alert.at}`\n );\n}\n\n/**\n * Post an alert to a Slack incoming webhook. Uses the global `fetch` (Node 18+);\n * failures are logged, never thrown — observability must not crash the app.\n */\nexport async function postToSlack(\n webhook: string,\n alert: Alert,\n service: string,\n logger?: Logger,\n): Promise<void> {\n if (typeof fetch !== \"function\") {\n logger?.warn?.(\"node-observe: global fetch unavailable (need Node 18+); skipping Slack alert\");\n return;\n }\n try {\n const res = await fetch(webhook, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ text: formatSlackMessage(alert, service) }),\n });\n if (!res.ok) {\n logger?.error?.(`node-observe: Slack webhook returned ${res.status}`);\n }\n } catch (err) {\n logger?.error?.(`node-observe: Slack alert failed: ${(err as Error).message}`);\n }\n}\n","/**\n * Collapse high-cardinality path segments so routes group together in metrics.\n * Numeric ids, Mongo ObjectIds, UUIDs, and long hex tokens all become `:id`.\n *\n * `/users/64b8.../posts/12` → `/users/:id/posts/:id`\n */\nexport function normalizePath(path: string): string {\n const clean = path.split(\"?\")[0] ?? path;\n return clean\n .split(\"/\")\n .map((seg) => {\n if (seg === \"\") return seg;\n if (/^\\d+$/.test(seg)) return \":id\";\n if (/^[0-9a-f]{24}$/i.test(seg)) return \":id\"; // ObjectId\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return \":id\"; // UUID\n if (/^[0-9a-f]{32,}$/i.test(seg)) return \":id\"; // long hex token\n return seg;\n })\n .join(\"/\");\n}\n\n/** Elapsed milliseconds since an `process.hrtime.bigint()` start, as a float. */\nexport function elapsedMs(start: bigint): number {\n return Number(process.hrtime.bigint() - start) / 1e6;\n}\n","import type { Insights } from \"./insights\";\nimport { elapsedMs } from \"./util\";\n\n/** Minimal shapes so we don't need `express` as a dependency. */\nexport interface ObserveRequest {\n method?: string;\n route?: { path?: string };\n baseUrl?: string;\n originalUrl?: string;\n path?: string;\n url?: string;\n}\nexport interface ObserveResponse {\n statusCode?: number;\n on(event: \"finish\" | \"close\", listener: () => void): void;\n}\nexport type ObserveNext = () => void;\n\n/**\n * Express/Connect middleware that records request latency on response finish.\n * Prefers the matched route pattern (`req.route.path`) to keep cardinality low,\n * falling back to the raw path.\n */\nexport function instrumentExpress(insights: Insights) {\n return function nodeObserveMiddleware(req: ObserveRequest, res: ObserveResponse, next: ObserveNext): void {\n const start = process.hrtime.bigint();\n let done = false;\n const finish = () => {\n if (done) return;\n done = true;\n const ms = elapsedMs(start);\n const method = req.method ?? \"GET\";\n const route = routeOf(req);\n insights.recordRequest(method, route, res.statusCode ?? 0, ms);\n };\n res.on(\"finish\", finish);\n res.on(\"close\", finish);\n next();\n };\n}\n\nfunction routeOf(req: ObserveRequest): string {\n // `req.route.path` is the matched pattern (e.g. \"/users/:id\") — best for grouping.\n if (req.route?.path) {\n const base = req.baseUrl ?? \"\";\n return base + req.route.path || req.route.path;\n }\n return req.path ?? (req.originalUrl ?? req.url ?? \"unknown\").split(\"?\")[0] ?? \"unknown\";\n}\n","import type { Insights } from \"./insights\";\nimport { elapsedMs } from \"./util\";\n\nconst START = Symbol(\"nodeObserveStart\");\n\n/** Minimal Mongoose shapes (avoids a hard dependency on `mongoose`). */\ninterface SchemaLike {\n pre(hook: string | RegExp | string[], fn: (this: any) => void): void;\n post(hook: string | RegExp | string[], fn: (this: any, ...args: any[]) => void): void;\n}\ninterface MongooseLike {\n plugin(fn: (schema: SchemaLike) => void): unknown;\n}\n\nconst QUERY_HOOK = /^(find|findOne|findOneAnd|count|countDocuments|estimatedDocumentCount|update|updateOne|updateMany|delete|deleteOne|deleteMany|replaceOne)/;\n\n/**\n * Register a global Mongoose plugin that times every query, aggregate, and\n * document save, recording `<Model>.<op>` durations on the given Insights.\n *\n * Call this **before** your models are compiled (i.e. right after you import\n * mongoose) so the plugin applies to every schema.\n *\n * @example\n * ```ts\n * import mongoose from \"mongoose\";\n * insights.instrumentMongoose(mongoose);\n * ```\n */\nexport function instrumentMongoose(mongoose: unknown, insights: Insights): void {\n const m = mongoose as MongooseLike;\n if (!m || typeof m.plugin !== \"function\") {\n insights.logger.warn?.(\"node-observe: instrumentMongoose called with a non-mongoose value; skipping\");\n return;\n }\n\n m.plugin((schema: SchemaLike) => {\n // --- query middleware (find/update/delete/count/...) ---\n schema.pre(QUERY_HOOK, function (this: any) {\n this[START] = process.hrtime.bigint();\n });\n schema.post(QUERY_HOOK, function (this: any) {\n finish(insights, modelName(this), this.op ?? \"query\", this[START]);\n });\n\n // --- aggregate middleware ---\n schema.pre(\"aggregate\", function (this: any) {\n this[START] = process.hrtime.bigint();\n });\n schema.post(\"aggregate\", function (this: any) {\n finish(insights, aggregateModelName(this), \"aggregate\", this[START]);\n });\n\n // --- document save middleware ---\n schema.pre(\"save\", function (this: any) {\n this[START] = process.hrtime.bigint();\n });\n schema.post(\"save\", function (this: any) {\n finish(insights, this?.constructor?.modelName ?? \"Document\", \"save\", this[START]);\n });\n });\n}\n\nfunction finish(insights: Insights, model: string, op: string, start: bigint | undefined): void {\n if (typeof start !== \"bigint\") return;\n insights.recordMongo(`${model}.${op}`, elapsedMs(start));\n}\n\nfunction modelName(query: any): string {\n return query?.model?.modelName ?? query?.mongooseCollection?.name ?? \"Model\";\n}\n\nfunction aggregateModelName(agg: any): string {\n return agg?._model?.modelName ?? agg?.model?.()?.modelName ?? \"Model\";\n}\n","import type { Insights } from \"./insights\";\nimport { elapsedMs } from \"./util\";\n\nconst PATCHED = Symbol.for(\"nodeObserve.redisPatched\");\n\ninterface CommandLike {\n name?: string;\n promise?: Promise<unknown>;\n}\ninterface RedisLike {\n sendCommand?: (command: CommandLike, ...rest: unknown[]) => unknown;\n [PATCHED]?: boolean;\n}\n\n/**\n * Instrument an ioredis client by wrapping `sendCommand`, timing every command\n * (`get`, `set`, `hgetall`, …) via the command's own promise. Safe to call once\n * per client; re-instrumenting the same client is a no-op.\n *\n * @example\n * ```ts\n * import Redis from \"ioredis\";\n * const redis = new Redis(url);\n * insights.instrumentRedis(redis);\n * ```\n */\nexport function instrumentRedis(client: unknown, insights: Insights): void {\n const c = client as RedisLike;\n if (!c || typeof c.sendCommand !== \"function\") {\n insights.logger.warn?.(\"node-observe: instrumentRedis called with a non-ioredis value; skipping\");\n return;\n }\n if (c[PATCHED]) return;\n c[PATCHED] = true;\n\n const original = c.sendCommand.bind(c);\n c.sendCommand = function patchedSendCommand(command: CommandLike, ...rest: unknown[]) {\n const start = process.hrtime.bigint();\n const name = (command?.name ?? \"unknown\").toLowerCase();\n const result = original(command, ...rest);\n const promise = command?.promise ?? (result as Promise<unknown> | undefined);\n if (promise && typeof (promise as Promise<unknown>).then === \"function\") {\n (promise as Promise<unknown>).then(\n () => insights.recordRedis(name, elapsedMs(start), false),\n () => insights.recordRedis(name, elapsedMs(start), true),\n );\n }\n return result;\n };\n}\n","import { MetricsRegistry } from \"./histogram\";\nimport { MemoryMonitor } from \"./memory\";\nimport { postToSlack } from \"./slack\";\nimport { normalizePath } from \"./util\";\nimport { instrumentExpress } from \"./express\";\nimport { instrumentMongoose } from \"./mongo\";\nimport { instrumentRedis } from \"./redis\";\nimport type { Alert, AlertType, InsightsOptions, InsightsSnapshot, Logger } from \"./types\";\n\nconst consoleLogger: Logger = {\n info: (m) => console.log(m),\n error: (m) => console.error(m),\n warn: (m) => console.warn(m),\n debug: (m) => console.debug(m),\n};\n\nconst DEFAULT_THRESHOLDS = {\n requestMs: 1000,\n mongoMs: 300,\n redisMs: 100,\n heapGrowthMbPerMin: 50,\n heapUsedMb: 0, // disabled by default\n};\n\n/**\n * The central observability handle. Create one per process, attach the\n * instrumentations you want, and read {@link Insights.snapshot} (or mount\n * {@link Insights.handler}) to see latency percentiles, query timings, memory\n * trend, and counters. Threshold breaches raise {@link Alert}s, optionally\n * pushed to Slack.\n *\n * @example\n * ```ts\n * const insights = new Insights({ serviceName: \"api\", slackWebhook: process.env.SLACK_URL });\n * app.use(insights.express());\n * insights.instrumentMongoose(mongoose);\n * insights.instrumentRedis(redis);\n * insights.startMemoryMonitor();\n * app.get(\"/insights\", insights.handler());\n * ```\n */\nexport class Insights {\n readonly http: MetricsRegistry;\n readonly mongo: MetricsRegistry;\n readonly redis: MetricsRegistry;\n private readonly counters: MetricsRegistry;\n private readonly memory: MemoryMonitor;\n\n readonly serviceName: string;\n readonly thresholds: Required<NonNullable<InsightsOptions[\"thresholds\"]>>;\n readonly logger: Logger;\n private readonly opts: InsightsOptions;\n private readonly cooldownMs: number;\n private readonly lastAlertAt = new Map<string, number>();\n private memoryTimer: ReturnType<typeof setInterval> | undefined;\n private readonly startedAt = Date.now();\n\n constructor(options: InsightsOptions = {}) {\n this.opts = options;\n this.serviceName = options.serviceName ?? \"node-observe\";\n this.thresholds = { ...DEFAULT_THRESHOLDS, ...(options.thresholds ?? {}) };\n this.logger = options.logger ?? consoleLogger;\n this.cooldownMs = options.alertCooldownMs ?? 60_000;\n this.memory = new MemoryMonitor();\n const cap = options.sampleSize ?? 1024;\n this.http = new MetricsRegistry(cap);\n this.mongo = new MetricsRegistry(cap);\n this.redis = new MetricsRegistry(cap);\n this.counters = new MetricsRegistry(cap);\n }\n\n // ---- recording (used by the instrumentations) -----------------------------\n\n /** Record an HTTP request. `route` is normalized when `normalizePaths` is on. */\n recordRequest(method: string, route: string, statusCode: number, ms: number): void {\n const path = this.opts.normalizePaths === false ? route : normalizePath(route);\n const isError = statusCode >= 500;\n const key = `${method.toUpperCase()} ${path}`;\n this.http.record(key, ms, isError);\n this.counters.increment(`http.status.${Math.floor(statusCode / 100)}xx`);\n if (ms > this.thresholds.requestMs) {\n this.raise(\"slow-request\", key, `${key} took ${ms.toFixed(0)}ms`, ms, this.thresholds.requestMs);\n }\n }\n\n /** Record a Mongo operation, e.g. `User.find`. */\n recordMongo(operation: string, ms: number, isError = false): void {\n this.mongo.record(operation, ms, isError);\n if (ms > this.thresholds.mongoMs) {\n this.raise(\"slow-mongo\", operation, `Mongo ${operation} took ${ms.toFixed(0)}ms`, ms, this.thresholds.mongoMs);\n }\n }\n\n /** Record a Redis command, e.g. `get`. */\n recordRedis(command: string, ms: number, isError = false): void {\n this.redis.record(command, ms, isError);\n if (ms > this.thresholds.redisMs) {\n this.raise(\"slow-redis\", command, `Redis ${command} took ${ms.toFixed(0)}ms`, ms, this.thresholds.redisMs);\n }\n }\n\n // ---- instrumentation attach points ----------------------------------------\n\n /** Express/Connect middleware that times every request. */\n express() {\n return instrumentExpress(this);\n }\n\n /** Instrument a Mongoose instance so every query/aggregate/save is timed. */\n instrumentMongoose(mongoose: unknown): void {\n instrumentMongoose(mongoose, this);\n }\n\n /** Instrument an ioredis client so every command is timed. */\n instrumentRedis(client: unknown): void {\n instrumentRedis(client, this);\n }\n\n /** Begin periodic memory sampling + leak detection. Returns a stop function. */\n startMemoryMonitor(): () => void {\n if (this.memoryTimer) return () => this.stopMemoryMonitor();\n const interval = this.opts.memorySampleIntervalMs ?? 15_000;\n this.memoryTimer = setInterval(() => this.checkMemory(), interval);\n // Don't keep the event loop alive just for monitoring.\n this.memoryTimer.unref?.();\n return () => this.stopMemoryMonitor();\n }\n\n stopMemoryMonitor(): void {\n if (this.memoryTimer) {\n clearInterval(this.memoryTimer);\n this.memoryTimer = undefined;\n }\n }\n\n /**\n * Take one memory reading and evaluate the leak/high-heap thresholds.\n * `now`/`mem` are injectable for testing; both default to live process values.\n */\n checkMemory(now: number = Date.now(), mem: NodeJS.MemoryUsage = process.memoryUsage()): void {\n this.memory.sample(now, mem);\n const snap = this.memory.snapshot(mem);\n\n if (\n this.memory.size >= 5 &&\n snap.growthMbPerMin > this.thresholds.heapGrowthMbPerMin\n ) {\n this.raise(\n \"memory-leak\",\n \"heap\",\n `Heap growing ${snap.growthMbPerMin}MB/min (now ${snap.heapUsedMb}MB)`,\n snap.growthMbPerMin,\n this.thresholds.heapGrowthMbPerMin,\n );\n }\n if (this.thresholds.heapUsedMb > 0 && snap.heapUsedMb > this.thresholds.heapUsedMb) {\n this.raise(\n \"memory-high\",\n \"heap\",\n `Heap usage ${snap.heapUsedMb}MB`,\n snap.heapUsedMb,\n this.thresholds.heapUsedMb,\n );\n }\n }\n\n // ---- output ---------------------------------------------------------------\n\n snapshot(): InsightsSnapshot {\n const mem = process.memoryUsage();\n return {\n service: this.serviceName,\n uptimeSec: Math.round((Date.now() - this.startedAt) / 1000),\n http: this.http.snapshot(),\n mongo: this.mongo.snapshot(),\n redis: this.redis.snapshot(),\n counters: this.counters.counterSnapshot(),\n memory: this.memory.snapshot(mem),\n generatedAt: new Date().toISOString(),\n };\n }\n\n /** Express handler that responds with the JSON snapshot. */\n handler() {\n return (_req: unknown, res: { json?: (b: unknown) => void; setHeader?: (k: string, v: string) => void; end?: (s: string) => void }) => {\n const body = this.snapshot();\n if (typeof res.json === \"function\") {\n res.json(body);\n } else {\n res.setHeader?.(\"Content-Type\", \"application/json\");\n res.end?.(JSON.stringify(body));\n }\n };\n }\n\n // ---- alerting -------------------------------------------------------------\n\n private raise(type: AlertType, key: string, message: string, value: number, threshold: number): void {\n const dedupeKey = `${type}:${key}`;\n const now = Date.now();\n const last = this.lastAlertAt.get(dedupeKey) ?? 0;\n if (now - last < this.cooldownMs) return; // within cooldown — suppress\n this.lastAlertAt.set(dedupeKey, now);\n\n const alert: Alert = {\n type,\n key,\n message,\n value: Math.round(value * 100) / 100,\n threshold,\n at: new Date(now).toISOString(),\n };\n\n try {\n this.opts.onAlert?.(alert);\n } catch (err) {\n this.logger.error(`node-observe: onAlert handler threw: ${(err as Error).message}`);\n }\n if (this.opts.slackWebhook) {\n void postToSlack(this.opts.slackWebhook, alert, this.serviceName, this.logger);\n }\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "node-observe",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight backend observability for Node.js — API latency, MongoDB & Redis query timing, memory-leak detection, and Slack alerts. Datadog-lite for small teams, with no agent and minimal dependencies.",
5
+ "keywords": [
6
+ "observability",
7
+ "monitoring",
8
+ "apm",
9
+ "metrics",
10
+ "latency",
11
+ "performance",
12
+ "express",
13
+ "mongoose",
14
+ "mongodb",
15
+ "redis",
16
+ "ioredis",
17
+ "memory-leak",
18
+ "slack",
19
+ "alerts",
20
+ "percentiles"
21
+ ],
22
+ "license": "MIT",
23
+ "author": "sriramadari",
24
+ "homepage": "https://github.com/sriramadari/node-observe#readme",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/sriramadari/node-observe.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/sriramadari/node-observe/issues"
31
+ },
32
+ "type": "module",
33
+ "main": "./dist/index.cjs",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "require": "./dist/index.cjs"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "prepublishOnly": "npm run build"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^20.14.0",
61
+ "tsup": "^8.2.0",
62
+ "typescript": "^5.5.0",
63
+ "vitest": "^2.0.0"
64
+ }
65
+ }