@backendkit-labs/observability 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/README.md +367 -0
- package/dist/index.cjs +592 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +216 -0
- package/dist/index.d.ts +216 -0
- package/dist/index.js +562 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { Injectable, Inject, Optional, Catch, Module, Logger, HttpException, HttpStatus } from '@nestjs/common';
|
|
2
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import * as winston from 'winston';
|
|
5
|
+
import TransportStream from 'winston-transport';
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import * as http from 'http';
|
|
8
|
+
import * as https from 'https';
|
|
9
|
+
import { CircuitBreaker, CircuitBreakerState, CircuitBreakerOpenError } from '@backendkit-labs/circuit-breaker';
|
|
10
|
+
import { Observable } from 'rxjs';
|
|
11
|
+
import { tap } from 'rxjs/operators';
|
|
12
|
+
|
|
13
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
14
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
15
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
16
|
+
}) : x)(function(x) {
|
|
17
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
18
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
19
|
+
});
|
|
20
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
21
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
22
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
23
|
+
if (decorator = decorators[i])
|
|
24
|
+
result = (decorator(result)) || result;
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
28
|
+
|
|
29
|
+
// src/internal/otel.ts
|
|
30
|
+
var otel = null;
|
|
31
|
+
try {
|
|
32
|
+
otel = __require("@opentelemetry/api");
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
var getTracer = (name) => otel?.trace?.getTracer(name) ?? noopTracer;
|
|
36
|
+
var getActiveSpan = () => otel ? otel.trace.getSpan(otel.context.active()) : void 0;
|
|
37
|
+
var runInOtelContext = async (span, fn) => {
|
|
38
|
+
if (!otel || !span) return fn();
|
|
39
|
+
return otel.context.with(otel.trace.setSpan(otel.context.active(), span), fn);
|
|
40
|
+
};
|
|
41
|
+
var noopSpan = {
|
|
42
|
+
end: () => {
|
|
43
|
+
},
|
|
44
|
+
setAttribute: () => {
|
|
45
|
+
},
|
|
46
|
+
setAttributes: () => {
|
|
47
|
+
},
|
|
48
|
+
recordException: () => {
|
|
49
|
+
},
|
|
50
|
+
setStatus: () => {
|
|
51
|
+
},
|
|
52
|
+
spanContext: () => ({ traceId: "", spanId: "" })
|
|
53
|
+
};
|
|
54
|
+
var noopTracer = {
|
|
55
|
+
startSpan: () => noopSpan,
|
|
56
|
+
startActiveSpan: (_name, fn) => fn(noopSpan)
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/correlation/correlation.service.ts
|
|
60
|
+
var storage = new AsyncLocalStorage();
|
|
61
|
+
var CorrelationIdService = class {
|
|
62
|
+
/**
|
|
63
|
+
* Run `fn` inside a context that carries `correlationId`.
|
|
64
|
+
* All code executed within `fn` (including async continuations) can call
|
|
65
|
+
* `get()` and receive the same ID without passing it explicitly.
|
|
66
|
+
*/
|
|
67
|
+
run(correlationId, fn) {
|
|
68
|
+
return storage.run(correlationId, fn);
|
|
69
|
+
}
|
|
70
|
+
/** Current correlation ID, or a fresh UUID when called outside a context. */
|
|
71
|
+
get() {
|
|
72
|
+
return storage.getStore() ?? randomUUID();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Current correlation ID, or `undefined` when called outside a context.
|
|
76
|
+
* Prefer `get()` for logging; use this only when you need to distinguish
|
|
77
|
+
* "no context" from "context with a random ID".
|
|
78
|
+
*/
|
|
79
|
+
getOrUndefined() {
|
|
80
|
+
return storage.getStore();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Active OTel trace + span IDs when @opentelemetry/api is installed and a
|
|
84
|
+
* span is active; `undefined` otherwise.
|
|
85
|
+
*/
|
|
86
|
+
getTraceContext() {
|
|
87
|
+
const span = getActiveSpan();
|
|
88
|
+
if (!span) return void 0;
|
|
89
|
+
const ctx = span.spanContext?.();
|
|
90
|
+
if (!ctx?.traceId) return void 0;
|
|
91
|
+
return { traceId: ctx.traceId, spanId: ctx.spanId };
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
CorrelationIdService = __decorateClass([
|
|
95
|
+
Injectable()
|
|
96
|
+
], CorrelationIdService);
|
|
97
|
+
|
|
98
|
+
// src/observability.constants.ts
|
|
99
|
+
var OBSERVABILITY_OPTIONS = "OBSERVABILITY_OPTIONS";
|
|
100
|
+
var TRANSPORT_CB_DEFAULTS = {
|
|
101
|
+
failureThreshold: 60,
|
|
102
|
+
slowCallThreshold: 100,
|
|
103
|
+
slowCallDurationMs: 6e4,
|
|
104
|
+
minimumCalls: 3,
|
|
105
|
+
slidingWindowSize: 5,
|
|
106
|
+
halfOpenMaxCalls: 1,
|
|
107
|
+
openTimeoutMs: 3e4
|
|
108
|
+
};
|
|
109
|
+
var WinstonHttpTransport = class extends TransportStream {
|
|
110
|
+
client;
|
|
111
|
+
cb;
|
|
112
|
+
buffer = [];
|
|
113
|
+
batchSize;
|
|
114
|
+
maxBufferSize;
|
|
115
|
+
flushTimer;
|
|
116
|
+
constructor(opts) {
|
|
117
|
+
super(opts);
|
|
118
|
+
this.batchSize = opts.batchSize ?? 100;
|
|
119
|
+
this.maxBufferSize = opts.maxBufferSize ?? 2e3;
|
|
120
|
+
const keepAlive = new http.Agent({ keepAlive: true });
|
|
121
|
+
const keepAliveHttps = new https.Agent({ keepAlive: true });
|
|
122
|
+
this.client = axios.create({
|
|
123
|
+
baseURL: opts.url,
|
|
124
|
+
timeout: opts.timeoutMs ?? 5e3,
|
|
125
|
+
httpAgent: keepAlive,
|
|
126
|
+
httpsAgent: keepAliveHttps,
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
...opts.authToken ? { Authorization: `Bearer ${opts.authToken}` } : {},
|
|
130
|
+
...opts.headers
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
this.cb = new CircuitBreaker({
|
|
134
|
+
...TRANSPORT_CB_DEFAULTS,
|
|
135
|
+
...opts.circuitBreaker,
|
|
136
|
+
name: "WinstonHttpTransport",
|
|
137
|
+
isFailure: () => true
|
|
138
|
+
});
|
|
139
|
+
this.flushTimer = setInterval(
|
|
140
|
+
() => {
|
|
141
|
+
void this.flush();
|
|
142
|
+
},
|
|
143
|
+
opts.flushIntervalMs ?? 5e3
|
|
144
|
+
);
|
|
145
|
+
this.flushTimer.unref?.();
|
|
146
|
+
}
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
log(info, callback) {
|
|
149
|
+
setImmediate(() => this.emit("logged", info));
|
|
150
|
+
if (this.buffer.length < this.maxBufferSize) {
|
|
151
|
+
this.buffer.push(info);
|
|
152
|
+
}
|
|
153
|
+
if (this.buffer.length >= this.batchSize) {
|
|
154
|
+
void this.flush();
|
|
155
|
+
}
|
|
156
|
+
callback();
|
|
157
|
+
}
|
|
158
|
+
/** Flush remaining buffer on graceful shutdown. */
|
|
159
|
+
async close() {
|
|
160
|
+
clearInterval(this.flushTimer);
|
|
161
|
+
await this.flush();
|
|
162
|
+
}
|
|
163
|
+
async flush() {
|
|
164
|
+
if (this.buffer.length === 0) return;
|
|
165
|
+
const batch = this.buffer.splice(0, this.batchSize);
|
|
166
|
+
try {
|
|
167
|
+
await this.cb.execute(() => this.client.post("", batch));
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const room = this.maxBufferSize - this.buffer.length;
|
|
170
|
+
if (room > 0) this.buffer.unshift(...batch.slice(0, room));
|
|
171
|
+
if (!(err instanceof CircuitBreakerOpenError)) {
|
|
172
|
+
console.error(`[WinstonHttpTransport] flush failed \u2014 re-queued ${batch.length} entries`, err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/logger/logger.service.ts
|
|
179
|
+
var LoggerService = class {
|
|
180
|
+
constructor(opts, correlationSvc) {
|
|
181
|
+
this.opts = opts;
|
|
182
|
+
this.correlationSvc = correlationSvc;
|
|
183
|
+
const transports2 = [
|
|
184
|
+
new winston.transports.Console({
|
|
185
|
+
format: winston.format.combine(
|
|
186
|
+
winston.format.timestamp(),
|
|
187
|
+
winston.format.colorize(),
|
|
188
|
+
winston.format.printf(({ level, message, timestamp, ...meta }) => {
|
|
189
|
+
const extra = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
|
|
190
|
+
return `${timestamp} [${level}] ${message}${extra}`;
|
|
191
|
+
})
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
];
|
|
195
|
+
if (opts.http) {
|
|
196
|
+
transports2.push(
|
|
197
|
+
new WinstonHttpTransport({
|
|
198
|
+
...opts.http,
|
|
199
|
+
format: winston.format.json()
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
this.winston = winston.createLogger({
|
|
204
|
+
level: opts.logLevel ?? "info",
|
|
205
|
+
transports: transports2,
|
|
206
|
+
format: winston.format.json()
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
opts;
|
|
210
|
+
correlationSvc;
|
|
211
|
+
winston;
|
|
212
|
+
log(message, context) {
|
|
213
|
+
this.winston.info(message, this.buildMeta(context));
|
|
214
|
+
}
|
|
215
|
+
error(message, trace, context) {
|
|
216
|
+
this.winston.error(message, { ...this.buildMeta(context), trace });
|
|
217
|
+
}
|
|
218
|
+
warn(message, context) {
|
|
219
|
+
this.winston.warn(message, this.buildMeta(context));
|
|
220
|
+
}
|
|
221
|
+
debug(message, context) {
|
|
222
|
+
this.winston.debug(message, this.buildMeta(context));
|
|
223
|
+
}
|
|
224
|
+
verbose(message, context) {
|
|
225
|
+
this.winston.verbose(message, this.buildMeta(context));
|
|
226
|
+
}
|
|
227
|
+
/** Log with additional arbitrary metadata. */
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
229
|
+
logWithMeta(level, message, meta) {
|
|
230
|
+
this.winston.log(level, message, { ...this.buildMeta(), ...meta });
|
|
231
|
+
}
|
|
232
|
+
buildMeta(context) {
|
|
233
|
+
const base = {
|
|
234
|
+
service: this.opts.serviceName,
|
|
235
|
+
environment: this.opts.environment ?? "production",
|
|
236
|
+
correlationId: this.correlationSvc?.get()
|
|
237
|
+
};
|
|
238
|
+
if (context) base.context = context;
|
|
239
|
+
return base;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
LoggerService = __decorateClass([
|
|
243
|
+
Injectable(),
|
|
244
|
+
__decorateParam(0, Inject(OBSERVABILITY_OPTIONS)),
|
|
245
|
+
__decorateParam(1, Optional())
|
|
246
|
+
], LoggerService);
|
|
247
|
+
var TRANSPORT_CB_DEFAULTS2 = {
|
|
248
|
+
failureThreshold: 60,
|
|
249
|
+
slowCallThreshold: 100,
|
|
250
|
+
slowCallDurationMs: 6e4,
|
|
251
|
+
minimumCalls: 3,
|
|
252
|
+
slidingWindowSize: 5,
|
|
253
|
+
halfOpenMaxCalls: 1,
|
|
254
|
+
openTimeoutMs: 3e4
|
|
255
|
+
};
|
|
256
|
+
var MetricsService = class {
|
|
257
|
+
constructor(opts, correlationSvc) {
|
|
258
|
+
this.opts = opts;
|
|
259
|
+
this.correlationSvc = correlationSvc;
|
|
260
|
+
if (!opts.metrics) return;
|
|
261
|
+
const m = opts.metrics;
|
|
262
|
+
this.maxBufferSize = m.maxBufferSize ?? 5e3;
|
|
263
|
+
const keepAlive = new http.Agent({ keepAlive: true });
|
|
264
|
+
const keepAliveHttps = new https.Agent({ keepAlive: true });
|
|
265
|
+
this.client = axios.create({
|
|
266
|
+
baseURL: m.url,
|
|
267
|
+
timeout: m.timeoutMs ?? 5e3,
|
|
268
|
+
httpAgent: keepAlive,
|
|
269
|
+
httpsAgent: keepAliveHttps,
|
|
270
|
+
headers: {
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
...m.authToken ? { Authorization: `Bearer ${m.authToken}` } : {},
|
|
273
|
+
...m.headers
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
this.cb = new CircuitBreaker({
|
|
277
|
+
...TRANSPORT_CB_DEFAULTS2,
|
|
278
|
+
...m.circuitBreaker,
|
|
279
|
+
name: "MetricsService",
|
|
280
|
+
isFailure: () => true,
|
|
281
|
+
onStateChange: (from, to, metrics) => {
|
|
282
|
+
if (to === CircuitBreakerState.OPEN) {
|
|
283
|
+
this.logger.warn(
|
|
284
|
+
`[MetricsService] circuit breaker OPEN \u2014 pausing metric sends for ${(m.circuitBreaker?.openTimeoutMs ?? TRANSPORT_CB_DEFAULTS2.openTimeoutMs) / 1e3}s`,
|
|
285
|
+
metrics
|
|
286
|
+
);
|
|
287
|
+
} else if (to === CircuitBreakerState.CLOSED && from !== CircuitBreakerState.HALF_OPEN) {
|
|
288
|
+
this.logger.log(`[MetricsService] circuit breaker CLOSED \u2014 recovered`);
|
|
289
|
+
}
|
|
290
|
+
m.circuitBreaker?.onStateChange?.(from, to, metrics);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
this.flushTimer = setInterval(
|
|
294
|
+
() => {
|
|
295
|
+
void this.flush();
|
|
296
|
+
},
|
|
297
|
+
m.flushIntervalMs ?? 1e4
|
|
298
|
+
);
|
|
299
|
+
this.flushTimer.unref?.();
|
|
300
|
+
}
|
|
301
|
+
opts;
|
|
302
|
+
correlationSvc;
|
|
303
|
+
client = null;
|
|
304
|
+
cb = null;
|
|
305
|
+
logger = new Logger(MetricsService.name);
|
|
306
|
+
buffer = [];
|
|
307
|
+
maxBufferSize = 5e3;
|
|
308
|
+
flushTimer = null;
|
|
309
|
+
/**
|
|
310
|
+
* Enqueue a metric event. Fire-and-forget; batched and sent on the next
|
|
311
|
+
* flush interval or when the buffer reaches `maxBufferSize`.
|
|
312
|
+
*/
|
|
313
|
+
record(name, value, options) {
|
|
314
|
+
if (!this.client) return;
|
|
315
|
+
if (this.buffer.length >= this.maxBufferSize) {
|
|
316
|
+
this.logger.warn("[MetricsService] buffer full \u2014 dropping metric");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
this.buffer.push({
|
|
320
|
+
name,
|
|
321
|
+
value,
|
|
322
|
+
unit: options?.unit,
|
|
323
|
+
tags: options?.tags,
|
|
324
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
325
|
+
serviceName: this.opts.serviceName,
|
|
326
|
+
environment: this.opts.environment ?? "production",
|
|
327
|
+
correlationId: this.correlationSvc?.getOrUndefined()
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Flush on graceful shutdown. */
|
|
331
|
+
async onModuleDestroy() {
|
|
332
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
333
|
+
await this.flush();
|
|
334
|
+
}
|
|
335
|
+
async flush() {
|
|
336
|
+
if (!this.client || this.buffer.length === 0) return;
|
|
337
|
+
const batch = this.buffer.splice(0, 500);
|
|
338
|
+
try {
|
|
339
|
+
await this.cb.execute(() => this.client.post("", batch));
|
|
340
|
+
} catch (err) {
|
|
341
|
+
const room = this.maxBufferSize - this.buffer.length;
|
|
342
|
+
if (room > 0) this.buffer.unshift(...batch.slice(0, room));
|
|
343
|
+
if (!(err instanceof CircuitBreakerOpenError)) {
|
|
344
|
+
this.logger.warn(
|
|
345
|
+
`[MetricsService] flush failed \u2014 re-queueing ${batch.length} events`,
|
|
346
|
+
err
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
MetricsService = __decorateClass([
|
|
353
|
+
Injectable(),
|
|
354
|
+
__decorateParam(0, Inject(OBSERVABILITY_OPTIONS)),
|
|
355
|
+
__decorateParam(1, Optional())
|
|
356
|
+
], MetricsService);
|
|
357
|
+
var CORRELATION_HEADER = "x-correlation-id";
|
|
358
|
+
var CorrelationInterceptor = class {
|
|
359
|
+
constructor(correlationSvc) {
|
|
360
|
+
this.correlationSvc = correlationSvc;
|
|
361
|
+
}
|
|
362
|
+
correlationSvc;
|
|
363
|
+
intercept(ctx, next) {
|
|
364
|
+
const req = ctx.switchToHttp().getRequest();
|
|
365
|
+
const res = ctx.switchToHttp().getResponse();
|
|
366
|
+
const incomingId = req.headers?.[CORRELATION_HEADER];
|
|
367
|
+
const correlationId = typeof incomingId === "string" && incomingId ? incomingId : randomUUID();
|
|
368
|
+
res.setHeader(CORRELATION_HEADER, correlationId);
|
|
369
|
+
return new Observable((subscriber) => {
|
|
370
|
+
this.correlationSvc.run(correlationId, () => {
|
|
371
|
+
next.handle().pipe(
|
|
372
|
+
tap({ error: () => {
|
|
373
|
+
} })
|
|
374
|
+
).subscribe(subscriber);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
CorrelationInterceptor = __decorateClass([
|
|
380
|
+
Injectable(),
|
|
381
|
+
__decorateParam(0, Inject(CorrelationIdService))
|
|
382
|
+
], CorrelationInterceptor);
|
|
383
|
+
var PerformanceInterceptor = class {
|
|
384
|
+
constructor(logger, metrics, correlationSvc) {
|
|
385
|
+
this.logger = logger;
|
|
386
|
+
this.metrics = metrics;
|
|
387
|
+
this.correlationSvc = correlationSvc;
|
|
388
|
+
}
|
|
389
|
+
logger;
|
|
390
|
+
metrics;
|
|
391
|
+
correlationSvc;
|
|
392
|
+
intercept(ctx, next) {
|
|
393
|
+
const start = Date.now();
|
|
394
|
+
const req = ctx.switchToHttp().getRequest();
|
|
395
|
+
const method = req.method ?? "UNKNOWN";
|
|
396
|
+
const path = req.url ?? "UNKNOWN";
|
|
397
|
+
return next.handle().pipe(
|
|
398
|
+
tap({
|
|
399
|
+
next: () => {
|
|
400
|
+
const durationMs = Date.now() - start;
|
|
401
|
+
this.record(method, path, "success", durationMs);
|
|
402
|
+
},
|
|
403
|
+
error: () => {
|
|
404
|
+
const durationMs = Date.now() - start;
|
|
405
|
+
this.record(method, path, "error", durationMs);
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
record(method, path, outcome, durationMs) {
|
|
411
|
+
const correlationId = this.correlationSvc?.getOrUndefined();
|
|
412
|
+
const span = getActiveSpan();
|
|
413
|
+
span?.setAttribute("http.method", method);
|
|
414
|
+
span?.setAttribute("http.target", path);
|
|
415
|
+
span?.setAttribute("http.duration_ms", durationMs);
|
|
416
|
+
this.logger.logWithMeta("info", `${method} ${path} [${outcome}] ${durationMs}ms`, {
|
|
417
|
+
method,
|
|
418
|
+
path,
|
|
419
|
+
outcome,
|
|
420
|
+
durationMs,
|
|
421
|
+
correlationId
|
|
422
|
+
});
|
|
423
|
+
this.metrics?.record("http.request.duration", durationMs, {
|
|
424
|
+
unit: "ms",
|
|
425
|
+
tags: { method, path, outcome }
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
PerformanceInterceptor = __decorateClass([
|
|
430
|
+
Injectable(),
|
|
431
|
+
__decorateParam(0, Inject(LoggerService)),
|
|
432
|
+
__decorateParam(1, Optional()),
|
|
433
|
+
__decorateParam(1, Inject(MetricsService)),
|
|
434
|
+
__decorateParam(2, Optional()),
|
|
435
|
+
__decorateParam(2, Inject(CorrelationIdService))
|
|
436
|
+
], PerformanceInterceptor);
|
|
437
|
+
var AllExceptionsFilter = class {
|
|
438
|
+
constructor(logger, correlationSvc) {
|
|
439
|
+
this.logger = logger;
|
|
440
|
+
this.correlationSvc = correlationSvc;
|
|
441
|
+
}
|
|
442
|
+
logger;
|
|
443
|
+
correlationSvc;
|
|
444
|
+
mappers = [];
|
|
445
|
+
/**
|
|
446
|
+
* Register a custom error mapper.
|
|
447
|
+
* Mappers are tried in order; the first non-null result wins.
|
|
448
|
+
* Return `null` to fall through to the next mapper.
|
|
449
|
+
*/
|
|
450
|
+
addMapper(mapper) {
|
|
451
|
+
this.mappers.push(mapper);
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
catch(exception, host) {
|
|
455
|
+
const ctx = host.switchToHttp();
|
|
456
|
+
const res = ctx.getResponse();
|
|
457
|
+
const mapped = this.resolveError(exception);
|
|
458
|
+
const correlationId = this.correlationSvc?.getOrUndefined();
|
|
459
|
+
const span = getActiveSpan();
|
|
460
|
+
span?.recordException(exception instanceof Error ? exception : new Error(String(exception)));
|
|
461
|
+
this.logger.error(
|
|
462
|
+
mapped.message,
|
|
463
|
+
exception instanceof Error ? exception.stack : void 0,
|
|
464
|
+
AllExceptionsFilter.name
|
|
465
|
+
);
|
|
466
|
+
res.status(mapped.statusCode).json({
|
|
467
|
+
ok: false,
|
|
468
|
+
statusCode: mapped.statusCode,
|
|
469
|
+
message: mapped.message,
|
|
470
|
+
code: mapped.code,
|
|
471
|
+
correlationId,
|
|
472
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
resolveError(exception) {
|
|
476
|
+
for (const mapper of this.mappers) {
|
|
477
|
+
const result = mapper(exception);
|
|
478
|
+
if (result !== null) return result;
|
|
479
|
+
}
|
|
480
|
+
if (exception instanceof HttpException) {
|
|
481
|
+
const body = exception.getResponse();
|
|
482
|
+
const message = typeof body === "string" ? body : body.message ?? exception.message;
|
|
483
|
+
return { statusCode: exception.getStatus(), message };
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
487
|
+
message: "Internal server error"
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
AllExceptionsFilter = __decorateClass([
|
|
492
|
+
Catch(),
|
|
493
|
+
__decorateParam(0, Inject(LoggerService)),
|
|
494
|
+
__decorateParam(1, Optional()),
|
|
495
|
+
__decorateParam(1, Inject(CorrelationIdService))
|
|
496
|
+
], AllExceptionsFilter);
|
|
497
|
+
|
|
498
|
+
// src/observability.module.ts
|
|
499
|
+
var ObservabilityModule = class {
|
|
500
|
+
static forRoot(options) {
|
|
501
|
+
const optionsProvider = {
|
|
502
|
+
provide: OBSERVABILITY_OPTIONS,
|
|
503
|
+
useValue: options
|
|
504
|
+
};
|
|
505
|
+
const providers = [
|
|
506
|
+
optionsProvider,
|
|
507
|
+
CorrelationIdService,
|
|
508
|
+
LoggerService,
|
|
509
|
+
MetricsService,
|
|
510
|
+
CorrelationInterceptor,
|
|
511
|
+
PerformanceInterceptor,
|
|
512
|
+
AllExceptionsFilter
|
|
513
|
+
];
|
|
514
|
+
return {
|
|
515
|
+
module: ObservabilityModule,
|
|
516
|
+
global: true,
|
|
517
|
+
providers,
|
|
518
|
+
exports: [
|
|
519
|
+
CorrelationIdService,
|
|
520
|
+
LoggerService,
|
|
521
|
+
MetricsService,
|
|
522
|
+
CorrelationInterceptor,
|
|
523
|
+
PerformanceInterceptor,
|
|
524
|
+
AllExceptionsFilter
|
|
525
|
+
]
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
ObservabilityModule = __decorateClass([
|
|
530
|
+
Module({})
|
|
531
|
+
], ObservabilityModule);
|
|
532
|
+
|
|
533
|
+
// src/decorators/track-performance.decorator.ts
|
|
534
|
+
function TrackPerformance(options = {}) {
|
|
535
|
+
return function(target, propertyKey, descriptor) {
|
|
536
|
+
const original = descriptor.value;
|
|
537
|
+
const operationName = options.operation ?? `${target.constructor.name}.${String(propertyKey)}`;
|
|
538
|
+
descriptor.value = async function(...args) {
|
|
539
|
+
const tracer = getTracer("@backendkit-labs/observability");
|
|
540
|
+
const span = tracer.startSpan(operationName);
|
|
541
|
+
if (options.attributes) {
|
|
542
|
+
span.setAttributes(options.attributes);
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const result = await runInOtelContext(span, () => original.apply(this, args));
|
|
546
|
+
span.setStatus({ code: 1 });
|
|
547
|
+
return result;
|
|
548
|
+
} catch (err) {
|
|
549
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
550
|
+
span.setStatus({ code: 2, message: String(err) });
|
|
551
|
+
throw err;
|
|
552
|
+
} finally {
|
|
553
|
+
span.end();
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
return descriptor;
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export { AllExceptionsFilter, CorrelationIdService, CorrelationInterceptor, LoggerService, MetricsService, OBSERVABILITY_OPTIONS, ObservabilityModule, PerformanceInterceptor, TrackPerformance, WinstonHttpTransport };
|
|
561
|
+
//# sourceMappingURL=index.js.map
|
|
562
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/otel.ts","../src/correlation/correlation.service.ts","../src/observability.constants.ts","../src/logger/winston-http.transport.ts","../src/logger/logger.service.ts","../src/metrics/metrics.service.ts","../src/interceptors/correlation.interceptor.ts","../src/interceptors/performance.interceptor.ts","../src/filters/all-exceptions.filter.ts","../src/observability.module.ts","../src/decorators/track-performance.decorator.ts"],"names":["transports","Injectable","TRANSPORT_CB_DEFAULTS","http2","https2","axios","CircuitBreaker","CircuitBreakerOpenError","Inject","Optional","randomUUID","tap"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Optional OpenTelemetry shim.\n * If @opentelemetry/api is not installed all operations become no-ops,\n * so the package works without any tracing backend.\n */\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet otel: any = null;\n\ntry {\n // Dynamic require keeps OTel out of the bundle when not installed\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n otel = require('@opentelemetry/api');\n} catch {\n // OTel not installed — spans will be no-ops\n}\n\nexport const isOtelAvailable = (): boolean => otel !== null;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const getTracer = (name: string): any =>\n otel?.trace?.getTracer(name) ?? noopTracer;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const getActiveSpan = (): any =>\n otel ? otel.trace.getSpan(otel.context.active()) : undefined;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const runInOtelContext = async <T>(span: any, fn: () => Promise<T>): Promise<T> => {\n if (!otel || !span) return fn();\n return otel.context.with(otel.trace.setSpan(otel.context.active(), span), fn);\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst noopSpan: any = {\n end: () => {},\n setAttribute: () => {},\n setAttributes: () => {},\n recordException: () => {},\n setStatus: () => {},\n spanContext: () => ({ traceId: '', spanId: '' }),\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst noopTracer: any = {\n startSpan: () => noopSpan,\n startActiveSpan: (_name: string, fn: (span: unknown) => unknown) => fn(noopSpan),\n};\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport { Injectable } from '@nestjs/common';\nimport { randomUUID } from 'node:crypto';\nimport { getActiveSpan } from '../internal/otel.js';\n\nconst storage = new AsyncLocalStorage<string>();\n\n@Injectable()\nexport class CorrelationIdService {\n /**\n * Run `fn` inside a context that carries `correlationId`.\n * All code executed within `fn` (including async continuations) can call\n * `get()` and receive the same ID without passing it explicitly.\n */\n run<T>(correlationId: string, fn: () => T): T {\n return storage.run(correlationId, fn);\n }\n\n /** Current correlation ID, or a fresh UUID when called outside a context. */\n get(): string {\n return storage.getStore() ?? randomUUID();\n }\n\n /**\n * Current correlation ID, or `undefined` when called outside a context.\n * Prefer `get()` for logging; use this only when you need to distinguish\n * \"no context\" from \"context with a random ID\".\n */\n getOrUndefined(): string | undefined {\n return storage.getStore();\n }\n\n /**\n * Active OTel trace + span IDs when @opentelemetry/api is installed and a\n * span is active; `undefined` otherwise.\n */\n getTraceContext(): { traceId: string; spanId: string } | undefined {\n const span = getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext?.();\n if (!ctx?.traceId) return undefined;\n return { traceId: ctx.traceId, spanId: ctx.spanId };\n }\n}\n","export const OBSERVABILITY_OPTIONS = 'OBSERVABILITY_OPTIONS' as const;\n","import TransportStream, { TransportStreamOptions } from 'winston-transport';\nimport axios, { AxiosInstance } from 'axios';\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport {\n CircuitBreaker,\n CircuitBreakerConfig,\n CircuitBreakerOpenError,\n} from '@backendkit-labs/circuit-breaker';\n\nexport interface WinstonHttpTransportOptions extends TransportStreamOptions {\n /** Full URL of the log-ingest endpoint. */\n url: string;\n\n /** Bearer token sent in `Authorization` header. */\n authToken?: string;\n\n /** Additional static headers merged into every request. */\n headers?: Record<string, string>;\n\n /** Flush batch when it reaches this many entries (default 100). */\n batchSize?: number;\n\n /** Maximum entries held in the in-memory buffer (default 2000). */\n maxBufferSize?: number;\n\n /** Flush interval in ms — also flushes on `close()` (default 5000). */\n flushIntervalMs?: number;\n\n /** Request timeout in ms (default 5000). */\n timeoutMs?: number;\n\n /**\n * Override any circuit breaker config fields.\n * `name` and `isFailure` are set internally and cannot be overridden.\n *\n * Transport defaults: failureThreshold 60%, slidingWindowSize 5,\n * minimumCalls 3, openTimeoutMs 30 000, halfOpenMaxCalls 1.\n */\n circuitBreaker?: Partial<Omit<CircuitBreakerConfig, 'name' | 'isFailure'>>;\n}\n\ninterface LogEntry {\n level: string;\n message: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\nconst TRANSPORT_CB_DEFAULTS: Omit<CircuitBreakerConfig, 'name' | 'isFailure'> = {\n failureThreshold: 60,\n slowCallThreshold: 100,\n slowCallDurationMs: 60_000,\n minimumCalls: 3,\n slidingWindowSize: 5,\n halfOpenMaxCalls: 1,\n openTimeoutMs: 30_000,\n};\n\nexport class WinstonHttpTransport extends TransportStream {\n private readonly client: AxiosInstance;\n private readonly cb: CircuitBreaker;\n private readonly buffer: LogEntry[] = [];\n private readonly batchSize: number;\n private readonly maxBufferSize: number;\n private readonly flushTimer: ReturnType<typeof setInterval>;\n\n constructor(opts: WinstonHttpTransportOptions) {\n super(opts);\n\n this.batchSize = opts.batchSize ?? 100;\n this.maxBufferSize = opts.maxBufferSize ?? 2_000;\n\n const keepAlive = new http.Agent({ keepAlive: true });\n const keepAliveHttps = new https.Agent({ keepAlive: true });\n\n this.client = axios.create({\n baseURL: opts.url,\n timeout: opts.timeoutMs ?? 5_000,\n httpAgent: keepAlive,\n httpsAgent: keepAliveHttps,\n headers: {\n 'Content-Type': 'application/json',\n ...(opts.authToken ? { Authorization: `Bearer ${opts.authToken}` } : {}),\n ...opts.headers,\n },\n });\n\n this.cb = new CircuitBreaker({\n ...TRANSPORT_CB_DEFAULTS,\n ...opts.circuitBreaker,\n name: 'WinstonHttpTransport',\n isFailure: () => true,\n });\n\n this.flushTimer = setInterval(\n () => { void this.flush(); },\n opts.flushIntervalMs ?? 5_000,\n );\n this.flushTimer.unref?.();\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n override log(info: any, callback: () => void): void {\n setImmediate(() => this.emit('logged', info));\n\n if (this.buffer.length < this.maxBufferSize) {\n this.buffer.push(info as LogEntry);\n }\n // silently drop when full — buffer-full warn would cause infinite recursion\n\n if (this.buffer.length >= this.batchSize) {\n void this.flush();\n }\n\n callback();\n }\n\n /** Flush remaining buffer on graceful shutdown. */\n override async close(): Promise<void> {\n clearInterval(this.flushTimer);\n await this.flush();\n }\n\n private async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n\n const batch = this.buffer.splice(0, this.batchSize);\n\n try {\n await this.cb.execute(() => this.client.post('', batch));\n } catch (err) {\n // Re-queue the batch (whether CB was open or the request failed)\n const room = this.maxBufferSize - this.buffer.length;\n if (room > 0) this.buffer.unshift(...batch.slice(0, room));\n\n if (!(err instanceof CircuitBreakerOpenError)) {\n // Network errors are worth logging; CB-open state was already surfaced\n // via onStateChange in the CircuitBreaker itself\n console.error(`[WinstonHttpTransport] flush failed — re-queued ${batch.length} entries`, err);\n }\n }\n }\n}\n","import { Injectable, Inject, Optional, LoggerService as NestLoggerService } from '@nestjs/common';\nimport * as winston from 'winston';\nimport { CorrelationIdService } from '../correlation/correlation.service.js';\nimport { OBSERVABILITY_OPTIONS } from '../observability.constants.js';\nimport { ObservabilityOptions } from '../observability.types.js';\nimport { WinstonHttpTransport } from './winston-http.transport.js';\n\n@Injectable()\nexport class LoggerService implements NestLoggerService {\n private readonly winston: winston.Logger;\n\n constructor(\n @Inject(OBSERVABILITY_OPTIONS)\n private readonly opts: ObservabilityOptions,\n @Optional()\n private readonly correlationSvc?: CorrelationIdService,\n ) {\n const transports: winston.transport[] = [\n new winston.transports.Console({\n format: winston.format.combine(\n winston.format.timestamp(),\n winston.format.colorize(),\n winston.format.printf(({ level, message, timestamp, ...meta }) => {\n const extra = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';\n return `${timestamp} [${level}] ${message}${extra}`;\n }),\n ),\n }),\n ];\n\n if (opts.http) {\n transports.push(\n new WinstonHttpTransport({\n ...opts.http,\n format: winston.format.json(),\n }),\n );\n }\n\n this.winston = winston.createLogger({\n level: opts.logLevel ?? 'info',\n transports,\n format: winston.format.json(),\n });\n }\n\n log(message: string, context?: string): void {\n this.winston.info(message, this.buildMeta(context));\n }\n\n error(message: string, trace?: string, context?: string): void {\n this.winston.error(message, { ...this.buildMeta(context), trace });\n }\n\n warn(message: string, context?: string): void {\n this.winston.warn(message, this.buildMeta(context));\n }\n\n debug(message: string, context?: string): void {\n this.winston.debug(message, this.buildMeta(context));\n }\n\n verbose(message: string, context?: string): void {\n this.winston.verbose(message, this.buildMeta(context));\n }\n\n /** Log with additional arbitrary metadata. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n logWithMeta(level: string, message: string, meta: Record<string, any>): void {\n this.winston.log(level, message, { ...this.buildMeta(), ...meta });\n }\n\n private buildMeta(context?: string): Record<string, unknown> {\n const base: Record<string, unknown> = {\n service: this.opts.serviceName,\n environment: this.opts.environment ?? 'production',\n correlationId: this.correlationSvc?.get(),\n };\n if (context) base.context = context;\n return base;\n }\n}\n","import { Injectable, Inject, Optional, OnModuleDestroy, Logger } from '@nestjs/common';\nimport axios, { AxiosInstance } from 'axios';\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport {\n CircuitBreaker,\n CircuitBreakerConfig,\n CircuitBreakerOpenError,\n CircuitBreakerState,\n} from '@backendkit-labs/circuit-breaker';\nimport { CorrelationIdService } from '../correlation/correlation.service.js';\nimport { OBSERVABILITY_OPTIONS } from '../observability.constants.js';\nimport { ObservabilityOptions, MetricEvent } from '../observability.types.js';\n\nconst TRANSPORT_CB_DEFAULTS: Omit<CircuitBreakerConfig, 'name' | 'isFailure'> = {\n failureThreshold: 60,\n slowCallThreshold: 100,\n slowCallDurationMs: 60_000,\n minimumCalls: 3,\n slidingWindowSize: 5,\n halfOpenMaxCalls: 1,\n openTimeoutMs: 30_000,\n};\n\n@Injectable()\nexport class MetricsService implements OnModuleDestroy {\n private readonly client: AxiosInstance | null = null;\n private readonly cb: CircuitBreaker | null = null;\n private readonly logger = new Logger(MetricsService.name);\n private readonly buffer: MetricEvent[] = [];\n private readonly maxBufferSize: number = 5_000;\n private readonly flushTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(\n @Inject(OBSERVABILITY_OPTIONS)\n private readonly opts: ObservabilityOptions,\n @Optional()\n private readonly correlationSvc?: CorrelationIdService,\n ) {\n if (!opts.metrics) return;\n\n const m = opts.metrics;\n this.maxBufferSize = m.maxBufferSize ?? 5_000;\n\n const keepAlive = new http.Agent({ keepAlive: true });\n const keepAliveHttps = new https.Agent({ keepAlive: true });\n\n this.client = axios.create({\n baseURL: m.url,\n timeout: m.timeoutMs ?? 5_000,\n httpAgent: keepAlive,\n httpsAgent: keepAliveHttps,\n headers: {\n 'Content-Type': 'application/json',\n ...(m.authToken ? { Authorization: `Bearer ${m.authToken}` } : {}),\n ...m.headers,\n },\n });\n\n this.cb = new CircuitBreaker({\n ...TRANSPORT_CB_DEFAULTS,\n ...m.circuitBreaker,\n name: 'MetricsService',\n isFailure: () => true,\n onStateChange: (from, to, metrics) => {\n if (to === CircuitBreakerState.OPEN) {\n this.logger.warn(\n `[MetricsService] circuit breaker OPEN — pausing metric sends for ${(m.circuitBreaker?.openTimeoutMs ?? TRANSPORT_CB_DEFAULTS.openTimeoutMs) / 1_000}s`,\n metrics,\n );\n } else if (to === CircuitBreakerState.CLOSED && from !== CircuitBreakerState.HALF_OPEN) {\n this.logger.log(`[MetricsService] circuit breaker CLOSED — recovered`);\n }\n m.circuitBreaker?.onStateChange?.(from, to, metrics);\n },\n });\n\n this.flushTimer = setInterval(\n () => { void this.flush(); },\n m.flushIntervalMs ?? 10_000,\n );\n this.flushTimer.unref?.();\n }\n\n /**\n * Enqueue a metric event. Fire-and-forget; batched and sent on the next\n * flush interval or when the buffer reaches `maxBufferSize`.\n */\n record(\n name: string,\n value: number,\n options?: { unit?: string; tags?: Record<string, string> },\n ): void {\n if (!this.client) return;\n\n if (this.buffer.length >= this.maxBufferSize) {\n this.logger.warn('[MetricsService] buffer full — dropping metric');\n return;\n }\n\n this.buffer.push({\n name,\n value,\n unit: options?.unit,\n tags: options?.tags,\n timestamp: new Date().toISOString(),\n serviceName: this.opts.serviceName,\n environment: this.opts.environment ?? 'production',\n correlationId: this.correlationSvc?.getOrUndefined(),\n });\n }\n\n /** Flush on graceful shutdown. */\n async onModuleDestroy(): Promise<void> {\n if (this.flushTimer) clearInterval(this.flushTimer);\n await this.flush();\n }\n\n private async flush(): Promise<void> {\n if (!this.client || this.buffer.length === 0) return;\n\n const batch = this.buffer.splice(0, 500);\n\n try {\n await this.cb!.execute(() => this.client!.post('', batch));\n } catch (err) {\n // Re-queue in both cases (CB open or network error)\n const room = this.maxBufferSize - this.buffer.length;\n if (room > 0) this.buffer.unshift(...batch.slice(0, room));\n\n if (!(err instanceof CircuitBreakerOpenError)) {\n this.logger.warn(\n `[MetricsService] flush failed — re-queueing ${batch.length} events`,\n err,\n );\n }\n }\n }\n}\n","import {\n Injectable,\n NestInterceptor,\n ExecutionContext,\n CallHandler,\n Inject,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { tap } from 'rxjs/operators';\nimport { randomUUID } from 'node:crypto';\nimport { CorrelationIdService } from '../correlation/correlation.service.js';\n\nconst CORRELATION_HEADER = 'x-correlation-id';\n\n@Injectable()\nexport class CorrelationInterceptor implements NestInterceptor {\n constructor(\n @Inject(CorrelationIdService)\n private readonly correlationSvc: CorrelationIdService,\n ) {}\n\n intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {\n const req = ctx.switchToHttp().getRequest<Record<string, unknown>>();\n const res = ctx.switchToHttp().getResponse<{\n setHeader(name: string, value: string): void;\n }>();\n\n const incomingId =\n (req.headers as Record<string, string | undefined>)?.[CORRELATION_HEADER];\n const correlationId = (typeof incomingId === 'string' && incomingId)\n ? incomingId\n : randomUUID();\n\n res.setHeader(CORRELATION_HEADER, correlationId);\n\n return new Observable(subscriber => {\n this.correlationSvc.run(correlationId, () => {\n next.handle().pipe(\n tap({ error: () => {} }),\n ).subscribe(subscriber);\n });\n });\n }\n}\n","import {\n Injectable,\n NestInterceptor,\n ExecutionContext,\n CallHandler,\n Inject,\n Optional,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { tap } from 'rxjs/operators';\nimport { LoggerService } from '../logger/logger.service.js';\nimport { MetricsService } from '../metrics/metrics.service.js';\nimport { CorrelationIdService } from '../correlation/correlation.service.js';\nimport { getActiveSpan } from '../internal/otel.js';\n\n@Injectable()\nexport class PerformanceInterceptor implements NestInterceptor {\n constructor(\n @Inject(LoggerService)\n private readonly logger: LoggerService,\n @Optional()\n @Inject(MetricsService)\n private readonly metrics: MetricsService | undefined,\n @Optional()\n @Inject(CorrelationIdService)\n private readonly correlationSvc: CorrelationIdService | undefined,\n ) {}\n\n intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {\n const start = Date.now();\n const req = ctx.switchToHttp().getRequest<{\n method: string;\n url: string;\n }>();\n const method = req.method ?? 'UNKNOWN';\n const path = req.url ?? 'UNKNOWN';\n\n return next.handle().pipe(\n tap({\n next: () => {\n const durationMs = Date.now() - start;\n this.record(method, path, 'success', durationMs);\n },\n error: () => {\n const durationMs = Date.now() - start;\n this.record(method, path, 'error', durationMs);\n },\n }),\n );\n }\n\n private record(\n method: string,\n path: string,\n outcome: 'success' | 'error',\n durationMs: number,\n ): void {\n const correlationId = this.correlationSvc?.getOrUndefined();\n const span = getActiveSpan();\n\n span?.setAttribute('http.method', method);\n span?.setAttribute('http.target', path);\n span?.setAttribute('http.duration_ms', durationMs);\n\n this.logger.logWithMeta('info', `${method} ${path} [${outcome}] ${durationMs}ms`, {\n method,\n path,\n outcome,\n durationMs,\n correlationId,\n });\n\n this.metrics?.record('http.request.duration', durationMs, {\n unit: 'ms',\n tags: { method, path, outcome },\n });\n }\n}\n","import {\n ExceptionFilter,\n Catch,\n ArgumentsHost,\n HttpException,\n HttpStatus,\n Inject,\n Optional,\n} from '@nestjs/common';\nimport { LoggerService } from '../logger/logger.service.js';\nimport { CorrelationIdService } from '../correlation/correlation.service.js';\nimport { getActiveSpan } from '../internal/otel.js';\n\nexport type ErrorMapper = (error: unknown) => {\n statusCode: number;\n message: string;\n code?: string;\n} | null;\n\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n private readonly mappers: ErrorMapper[] = [];\n\n constructor(\n @Inject(LoggerService)\n private readonly logger: LoggerService,\n @Optional()\n @Inject(CorrelationIdService)\n private readonly correlationSvc: CorrelationIdService | undefined,\n ) {}\n\n /**\n * Register a custom error mapper.\n * Mappers are tried in order; the first non-null result wins.\n * Return `null` to fall through to the next mapper.\n */\n addMapper(mapper: ErrorMapper): this {\n this.mappers.push(mapper);\n return this;\n }\n\n catch(exception: unknown, host: ArgumentsHost): void {\n const ctx = host.switchToHttp();\n const res = ctx.getResponse<{\n status(code: number): { json(body: unknown): void };\n }>();\n\n const mapped = this.resolveError(exception);\n\n const correlationId = this.correlationSvc?.getOrUndefined();\n const span = getActiveSpan();\n span?.recordException(exception instanceof Error ? exception : new Error(String(exception)));\n\n this.logger.error(\n mapped.message,\n exception instanceof Error ? exception.stack : undefined,\n AllExceptionsFilter.name,\n );\n\n res.status(mapped.statusCode).json({\n ok: false,\n statusCode: mapped.statusCode,\n message: mapped.message,\n code: mapped.code,\n correlationId,\n timestamp: new Date().toISOString(),\n });\n }\n\n private resolveError(exception: unknown): {\n statusCode: number;\n message: string;\n code?: string;\n } {\n for (const mapper of this.mappers) {\n const result = mapper(exception);\n if (result !== null) return result;\n }\n\n if (exception instanceof HttpException) {\n const body = exception.getResponse();\n const message =\n typeof body === 'string'\n ? body\n : (body as { message?: string }).message ?? exception.message;\n return { statusCode: exception.getStatus(), message };\n }\n\n return {\n statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n message: 'Internal server error',\n };\n }\n}\n","import { DynamicModule, Module, Provider } from '@nestjs/common';\nimport { CorrelationIdService } from './correlation/correlation.service.js';\nimport { LoggerService } from './logger/logger.service.js';\nimport { MetricsService } from './metrics/metrics.service.js';\nimport { CorrelationInterceptor } from './interceptors/correlation.interceptor.js';\nimport { PerformanceInterceptor } from './interceptors/performance.interceptor.js';\nimport { AllExceptionsFilter } from './filters/all-exceptions.filter.js';\nimport { OBSERVABILITY_OPTIONS } from './observability.constants.js';\nimport { ObservabilityOptions } from './observability.types.js';\n\n@Module({})\nexport class ObservabilityModule {\n static forRoot(options: ObservabilityOptions): DynamicModule {\n const optionsProvider: Provider = {\n provide: OBSERVABILITY_OPTIONS,\n useValue: options,\n };\n\n const providers: Provider[] = [\n optionsProvider,\n CorrelationIdService,\n LoggerService,\n MetricsService,\n CorrelationInterceptor,\n PerformanceInterceptor,\n AllExceptionsFilter,\n ];\n\n return {\n module: ObservabilityModule,\n global: true,\n providers,\n exports: [\n CorrelationIdService,\n LoggerService,\n MetricsService,\n CorrelationInterceptor,\n PerformanceInterceptor,\n AllExceptionsFilter,\n ],\n };\n }\n}\n","import { getTracer, runInOtelContext } from '../internal/otel.js';\n\nexport interface TrackPerformanceOptions {\n /** OTel span / log operation name. Defaults to `ClassName.methodName`. */\n operation?: string;\n\n /** Attributes added to the OTel span. */\n attributes?: Record<string, string | number | boolean>;\n}\n\n/**\n * Method decorator that wraps the decorated async method in an OTel span\n * (when @opentelemetry/api is available) and records its duration.\n * Works with regular methods and NestJS service methods.\n *\n * @example\n * \\@TrackPerformance()\n * async processPayment(id: string) { ... }\n */\nexport function TrackPerformance(options: TrackPerformanceOptions = {}): MethodDecorator {\n return function (\n target: object,\n propertyKey: string | symbol,\n descriptor: PropertyDescriptor,\n ): PropertyDescriptor {\n const original = descriptor.value as (...args: unknown[]) => Promise<unknown>;\n const operationName =\n options.operation ?? `${target.constructor.name}.${String(propertyKey)}`;\n\n descriptor.value = async function (...args: unknown[]): Promise<unknown> {\n const tracer = getTracer('@backendkit-labs/observability');\n const span = tracer.startSpan(operationName);\n\n if (options.attributes) {\n span.setAttributes(options.attributes);\n }\n\n try {\n const result = await runInOtelContext(span, () => original.apply(this, args));\n span.setStatus({ code: 1 }); // SpanStatusCode.OK\n return result;\n } catch (err) {\n span.recordException(err instanceof Error ? err : new Error(String(err)));\n span.setStatus({ code: 2, message: String(err) }); // SpanStatusCode.ERROR\n throw err;\n } finally {\n span.end();\n }\n };\n\n return descriptor;\n };\n}\n"]}
|