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