@ryanzeng/nest-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.mjs ADDED
@@ -0,0 +1,1073 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
12
+
13
+ // src/config.ts
14
+ import { randomUUID } from "crypto";
15
+ var DEFAULT_ALLOWED_HEADERS = ["accept", "content-type", "user-agent", "x-request-id", "traceparent"];
16
+ function booleanValue(value, fallback) {
17
+ if (typeof value === "boolean") return value;
18
+ if (value?.toLowerCase() === "true") return true;
19
+ if (value?.toLowerCase() === "false") return false;
20
+ return fallback;
21
+ }
22
+ function positiveInteger(value, fallback) {
23
+ const number = Number(value);
24
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
25
+ }
26
+ function samplingValue(value) {
27
+ const number = Number(value);
28
+ if (!Number.isFinite(number)) return 1;
29
+ return Math.min(1, Math.max(0, number));
30
+ }
31
+ function environmentSampling(env) {
32
+ const sampler = env.OTEL_TRACES_SAMPLER?.toLowerCase();
33
+ if (sampler === "always_off" || sampler === "parentbased_always_off") return 0;
34
+ if (sampler === "always_on" || sampler === "parentbased_always_on") return 1;
35
+ return samplingValue(env.OTEL_TRACES_SAMPLER_ARG);
36
+ }
37
+ function decode(value) {
38
+ try {
39
+ return decodeURIComponent(value.trim());
40
+ } catch {
41
+ return value.trim();
42
+ }
43
+ }
44
+ function parseKeyValueList(value) {
45
+ if (!value) return {};
46
+ return Object.fromEntries(value.split(",").flatMap((entry) => {
47
+ const separator = entry.indexOf("=");
48
+ if (separator <= 0) return [];
49
+ const key = decode(entry.slice(0, separator));
50
+ const itemValue = decode(entry.slice(separator + 1));
51
+ return key ? [[key, itemValue]] : [];
52
+ }));
53
+ }
54
+ function appendSignalPath(endpoint, signal) {
55
+ if (!endpoint) return void 0;
56
+ return `${endpoint.replace(/\/+$/, "")}/v1/${signal}`;
57
+ }
58
+ function optionalString(value) {
59
+ const result = value?.trim();
60
+ return result ? result : void 0;
61
+ }
62
+ function resolveObserveConfig(options = {}, env = process.env) {
63
+ const otelResources = parseKeyValueList(env.OTEL_RESOURCE_ATTRIBUTES);
64
+ const resourceAttributes = { ...otelResources, ...options.resourceAttributes };
65
+ const genericEndpoint = optionalString(options.endpoint ?? env.OTEL_EXPORTER_OTLP_ENDPOINT);
66
+ const sampling = options.sampling === void 0 ? environmentSampling(env) : samplingValue(options.sampling);
67
+ const allowedHeaders = Array.from(new Set([
68
+ ...DEFAULT_ALLOWED_HEADERS,
69
+ ...options.allowedHeaders ?? []
70
+ ].map((header) => header.toLowerCase().trim()).filter(Boolean)));
71
+ const result = {
72
+ enabled: booleanValue(options.enabled ?? env.OBSERVE_ENABLED, true),
73
+ serviceName: options.serviceName ?? env.OTEL_SERVICE_NAME ?? otelResources["service.name"] ?? "nest-application",
74
+ serviceVersion: options.serviceVersion ?? env.OTEL_SERVICE_VERSION ?? otelResources["service.version"] ?? "unknown",
75
+ environment: options.environment ?? env.OBSERVE_ENVIRONMENT ?? otelResources["deployment.environment.name"] ?? env.NODE_ENV ?? "development",
76
+ instanceId: options.instanceId ?? otelResources["service.instance.id"] ?? env.OTEL_SERVICE_INSTANCE_ID ?? env.HOSTNAME ?? randomUUID(),
77
+ endpoints: {
78
+ traces: optionalString(env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) ?? appendSignalPath(genericEndpoint, "traces"),
79
+ metrics: optionalString(env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) ?? appendSignalPath(genericEndpoint, "metrics"),
80
+ logs: optionalString(env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) ?? appendSignalPath(genericEndpoint, "logs")
81
+ },
82
+ headers: { ...parseKeyValueList(env.OTEL_EXPORTER_OTLP_HEADERS), ...options.headers },
83
+ traces: booleanValue(options.traces, true),
84
+ logs: booleanValue(options.logs, true),
85
+ metrics: booleanValue(options.metrics, true),
86
+ providerTracing: booleanValue(options.providerTracing, true),
87
+ controllerTracing: booleanValue(options.controllerTracing, true),
88
+ sampling,
89
+ allowedHeaders,
90
+ exportTimeoutMillis: positiveInteger(options.exportTimeoutMillis ?? env.OTEL_EXPORTER_OTLP_TIMEOUT, 1e4),
91
+ metricExportIntervalMillis: positiveInteger(options.metricExportIntervalMillis ?? env.OTEL_METRIC_EXPORT_INTERVAL, 6e4),
92
+ resourceAttributes
93
+ };
94
+ if (options.exporters) result.exporters = options.exporters;
95
+ return result;
96
+ }
97
+
98
+ // src/decorators/metadata.ts
99
+ var ignoredTargets = /* @__PURE__ */ new WeakSet();
100
+ var tracedTargets = /* @__PURE__ */ new WeakSet();
101
+ function markTraceIgnored(target) {
102
+ ignoredTargets.add(target);
103
+ }
104
+ function markTraceDecorated(target) {
105
+ tracedTargets.add(target);
106
+ }
107
+ function isTraceIgnored(method, type) {
108
+ return Boolean(method && ignoredTargets.has(method) || type && ignoredTargets.has(type));
109
+ }
110
+ function isTraceDecorated(method) {
111
+ return Boolean(method && tracedTargets.has(method));
112
+ }
113
+
114
+ // src/decorators/ignore-trace.decorator.ts
115
+ function IgnoreTrace() {
116
+ return ((target, propertyKey, descriptor) => {
117
+ if (propertyKey !== void 0 && descriptor?.value && typeof descriptor.value === "function") {
118
+ markTraceIgnored(descriptor.value);
119
+ return descriptor;
120
+ }
121
+ markTraceIgnored(target);
122
+ return target;
123
+ });
124
+ }
125
+
126
+ // src/decorators/trace.decorator.ts
127
+ import { trace } from "@opentelemetry/api";
128
+
129
+ // src/decorators/invoke.ts
130
+ import { context, SpanStatusCode } from "@opentelemetry/api";
131
+ import { isObservable, Observable } from "rxjs";
132
+ import { finalize, tap } from "rxjs/operators";
133
+ function fail(span, error) {
134
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
135
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) });
136
+ }
137
+ function invokeWithSpan(tracer, spanName, attributes, invoke, onFinish) {
138
+ return tracer.startActiveSpan(spanName, { attributes }, (span) => {
139
+ const started = process.hrtime.bigint();
140
+ const finish = (error) => {
141
+ if (error !== void 0) fail(span, error);
142
+ else span.setStatus({ code: SpanStatusCode.OK });
143
+ onFinish?.(Number(process.hrtime.bigint() - started) / 1e9, error);
144
+ span.end();
145
+ };
146
+ try {
147
+ const result = invoke();
148
+ if (result instanceof Promise) {
149
+ return result.then(
150
+ (value) => {
151
+ finish();
152
+ return value;
153
+ },
154
+ (error) => {
155
+ finish(error);
156
+ throw error;
157
+ }
158
+ );
159
+ }
160
+ if (isObservable(result)) {
161
+ const spanContext = context.active();
162
+ const source = result;
163
+ return new Observable((subscriber) => context.with(spanContext, () => {
164
+ let streamError;
165
+ return source.pipe(
166
+ tap({ error: (error) => {
167
+ streamError = error;
168
+ } }),
169
+ finalize(() => finish(streamError))
170
+ ).subscribe(subscriber);
171
+ }));
172
+ }
173
+ finish();
174
+ return result;
175
+ } catch (error) {
176
+ finish(error);
177
+ throw error;
178
+ }
179
+ });
180
+ }
181
+
182
+ // src/decorators/trace.decorator.ts
183
+ function decorateMethod(target, propertyKey, descriptor, options) {
184
+ const original = descriptor.value;
185
+ if (typeof original !== "function" || isTraceIgnored(original, target.constructor)) return;
186
+ const className = target.constructor?.name || "Anonymous";
187
+ const methodName = String(propertyKey);
188
+ const wrapped = function(...args) {
189
+ const tracer = trace.getTracer("@ryanzen9/nest-observe");
190
+ return invokeWithSpan(tracer, options.name ?? `${className}.${methodName}`, {
191
+ "code.function.name": methodName,
192
+ "nestjs.class": className,
193
+ "nestjs.method": methodName,
194
+ ...options.attributes
195
+ }, () => original.apply(this, args));
196
+ };
197
+ Object.defineProperty(wrapped, "name", { value: original.name, configurable: true });
198
+ markTraceDecorated(wrapped);
199
+ descriptor.value = wrapped;
200
+ }
201
+ function Trace(nameOrOptions = {}) {
202
+ const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions;
203
+ return ((target, propertyKey, descriptor) => {
204
+ if (propertyKey !== void 0 && descriptor) {
205
+ decorateMethod(target, propertyKey, descriptor, options);
206
+ return descriptor;
207
+ }
208
+ const prototype = target.prototype;
209
+ if (!prototype) return target;
210
+ for (const key of Object.getOwnPropertyNames(prototype)) {
211
+ if (key === "constructor") continue;
212
+ const item = Object.getOwnPropertyDescriptor(prototype, key);
213
+ if (!item || typeof item.value !== "function") continue;
214
+ decorateMethod(prototype, key, item, options);
215
+ Object.defineProperty(prototype, key, item);
216
+ }
217
+ return target;
218
+ });
219
+ }
220
+
221
+ // src/logs/nest-logger-instrumentation.ts
222
+ import { ConsoleLogger } from "@nestjs/common";
223
+ import { trace as trace2 } from "@opentelemetry/api";
224
+
225
+ // src/security/redaction.ts
226
+ var REDACTED = "[REDACTED]";
227
+ var SENSITIVE_KEY = /(?:authorization|proxy-authorization|cookie|set-cookie|pass(?:word|wd)?|secret|token|api[-_]?key|phone|mobile)/i;
228
+ function isSensitiveKey(name) {
229
+ return SENSITIVE_KEY.test(name);
230
+ }
231
+ function redactText(value) {
232
+ return value.replace(/(?<!\d)1[3-9]\d{9}(?!\d)/g, REDACTED).replace(/\b(authorization|proxy-authorization)\s*[:=]\s*(?:bearer\s+|basic\s+)?[^\s,;]+/gi, `$1=${REDACTED}`).replace(/\b(password|passwd|secret|token|access_token|api[-_]?key|cookie)\s*[:=]\s*[^\s,;]+/gi, `$1=${REDACTED}`).replace(/\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED}`);
233
+ }
234
+ function errorValue(error) {
235
+ return {
236
+ name: error.name,
237
+ message: redactText(error.message),
238
+ ...error.stack ? { stack: redactText(error.stack) } : {}
239
+ };
240
+ }
241
+ function visit(value, seen) {
242
+ if (typeof value === "string") return redactText(value);
243
+ if (value === null || typeof value !== "object") return value;
244
+ if (value instanceof Error) return errorValue(value);
245
+ if (seen.has(value)) return "[Circular]";
246
+ seen.add(value);
247
+ if (Array.isArray(value)) return value.map((item) => visit(item, seen));
248
+ const output = {};
249
+ for (const [key, item] of Object.entries(value)) {
250
+ output[key] = SENSITIVE_KEY.test(key) ? REDACTED : visit(item, seen);
251
+ }
252
+ return output;
253
+ }
254
+ function redact(value) {
255
+ return visit(value, /* @__PURE__ */ new WeakSet());
256
+ }
257
+ function sanitizeHeaders(headers, allowedHeaders) {
258
+ const allowed = new Set(allowedHeaders.map((header) => header.toLowerCase()));
259
+ const output = {};
260
+ for (const [originalName, value] of Object.entries(headers)) {
261
+ const name = originalName.toLowerCase();
262
+ if (!allowed.has(name) || value === void 0) continue;
263
+ output[name] = SENSITIVE_KEY.test(name) ? REDACTED : Array.isArray(value) ? value.join(", ") : value;
264
+ }
265
+ return output;
266
+ }
267
+
268
+ // src/logs/nest-logger-instrumentation.ts
269
+ var SEVERITY = {
270
+ verbose: "TRACE",
271
+ debug: "DEBUG",
272
+ log: "INFO",
273
+ warn: "WARN",
274
+ error: "ERROR",
275
+ fatal: "FATAL"
276
+ };
277
+ function bodyValue(message) {
278
+ if (message instanceof Error) return redactText(message.stack ?? `${message.name}: ${message.message}`);
279
+ return redact(message);
280
+ }
281
+ var NestLoggerInstrumentation = class {
282
+ constructor(emitter, resourceAttributes = {}) {
283
+ this.emitter = emitter;
284
+ this.resourceAttributes = resourceAttributes;
285
+ }
286
+ emitter;
287
+ resourceAttributes;
288
+ originals = /* @__PURE__ */ new Map();
289
+ enabled = false;
290
+ enable() {
291
+ if (this.enabled) return;
292
+ this.enabled = true;
293
+ const prototype = ConsoleLogger.prototype;
294
+ for (const method of Object.keys(SEVERITY)) {
295
+ const original = prototype[method];
296
+ if (typeof original !== "function") continue;
297
+ this.originals.set(method, original);
298
+ const emitter = this.emitter;
299
+ const resourceAttributes = this.resourceAttributes;
300
+ prototype[method] = function(message, ...args) {
301
+ try {
302
+ const spanContext = trace2.getActiveSpan()?.spanContext();
303
+ const contextName = this.context ?? (typeof args.at(-1) === "string" ? String(args.at(-1)) : void 0);
304
+ const attributes = { ...resourceAttributes };
305
+ if (contextName) attributes["nestjs.context"] = contextName;
306
+ if (spanContext?.traceId) attributes.trace_id = spanContext.traceId;
307
+ if (spanContext?.spanId) attributes.span_id = spanContext.spanId;
308
+ emitter.emit({
309
+ body: bodyValue(message),
310
+ severityText: SEVERITY[method],
311
+ attributes,
312
+ timestamp: Date.now()
313
+ });
314
+ } catch {
315
+ }
316
+ return original.call(this, message, ...args);
317
+ };
318
+ }
319
+ }
320
+ disable() {
321
+ if (!this.enabled) return;
322
+ const prototype = ConsoleLogger.prototype;
323
+ for (const [method, original] of this.originals) prototype[method] = original;
324
+ this.originals.clear();
325
+ this.enabled = false;
326
+ }
327
+ };
328
+
329
+ // src/logs/otel-log-emitter.ts
330
+ import { context as context2 } from "@opentelemetry/api";
331
+ import { logs, SeverityNumber } from "@opentelemetry/api-logs";
332
+ var SEVERITY_NUMBER = {
333
+ TRACE: SeverityNumber.TRACE,
334
+ DEBUG: SeverityNumber.DEBUG,
335
+ INFO: SeverityNumber.INFO,
336
+ WARN: SeverityNumber.WARN,
337
+ ERROR: SeverityNumber.ERROR,
338
+ FATAL: SeverityNumber.FATAL
339
+ };
340
+ var OpenTelemetryLogEmitter = class {
341
+ logger;
342
+ constructor(name = "@ryanzen9/nest-observe", version, logger) {
343
+ this.logger = logger ?? logs.getLogger(name, version);
344
+ }
345
+ emit(record) {
346
+ this.logger.emit({
347
+ body: record.body,
348
+ severityText: record.severityText,
349
+ severityNumber: SEVERITY_NUMBER[record.severityText],
350
+ attributes: record.attributes,
351
+ ...record.timestamp === void 0 ? {} : { timestamp: record.timestamp },
352
+ context: context2.active()
353
+ });
354
+ }
355
+ };
356
+
357
+ // src/instrumentation/nest-compatible-instrumentation.ts
358
+ import {
359
+ InstrumentationNodeModuleDefinition,
360
+ InstrumentationNodeModuleFile
361
+ } from "@opentelemetry/instrumentation";
362
+ import { NestInstrumentation } from "@opentelemetry/instrumentation-nestjs-core";
363
+
364
+ // src/nest/method-instrumenter.ts
365
+ var NestMethodInstrumenter = class {
366
+ constructor(tracer, meter) {
367
+ this.tracer = tracer;
368
+ this.calls = meter.createCounter("nestjs.method.calls", { unit: "{call}" });
369
+ this.duration = meter.createHistogram("nestjs.method.duration", {
370
+ unit: "s",
371
+ advice: { explicitBucketBoundaries: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5] }
372
+ });
373
+ this.errors = meter.createCounter("nestjs.method.errors", { unit: "{error}" });
374
+ }
375
+ tracer;
376
+ calls;
377
+ duration;
378
+ errors;
379
+ instrumented = /* @__PURE__ */ new WeakMap();
380
+ instrumentInstance(instance, kind, componentName) {
381
+ const type = instance.constructor;
382
+ const prototype = Object.getPrototypeOf(instance);
383
+ if (!prototype) return;
384
+ this.instrumentTarget(instance, prototype, type, kind, componentName);
385
+ }
386
+ instrumentPrototype(type, kind, componentName) {
387
+ const prototype = type.prototype;
388
+ if (!prototype) return;
389
+ this.instrumentTarget(prototype, prototype, type, kind, componentName);
390
+ }
391
+ instrumentTarget(target, prototype, type, kind, componentName) {
392
+ if (isTraceIgnored(type)) return;
393
+ const completed = this.instrumented.get(target) ?? /* @__PURE__ */ new Set();
394
+ this.instrumented.set(target, completed);
395
+ for (const methodName of Object.getOwnPropertyNames(prototype)) {
396
+ if (methodName === "constructor" || completed.has(methodName) || methodName.startsWith("onModule")) continue;
397
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
398
+ const original = descriptor?.value;
399
+ if (!descriptor || typeof original !== "function" || isTraceIgnored(original, type) || isTraceDecorated(original)) continue;
400
+ const existing = Reflect.get(target, methodName);
401
+ if (typeof existing === "function" && existing !== original && isTraceDecorated(existing)) continue;
402
+ const name = componentName || type.name || "Anonymous";
403
+ const attributes = {
404
+ [`nestjs.${kind}`]: name,
405
+ "nestjs.method": methodName
406
+ };
407
+ const instrumenter = this;
408
+ const wrapped = function(...args) {
409
+ instrumenter.calls.add(1, attributes);
410
+ return invokeWithSpan(
411
+ instrumenter.tracer,
412
+ `${name}.${methodName}`,
413
+ attributes,
414
+ () => original.apply(this, args),
415
+ (seconds, error) => {
416
+ instrumenter.duration.record(seconds, attributes);
417
+ if (error !== void 0) instrumenter.errors.add(1, attributes);
418
+ }
419
+ );
420
+ };
421
+ markTraceDecorated(wrapped);
422
+ try {
423
+ Object.defineProperty(target, methodName, { ...descriptor, value: wrapped });
424
+ completed.add(methodName);
425
+ } catch {
426
+ }
427
+ }
428
+ }
429
+ };
430
+
431
+ // src/instrumentation/nest-compatible-instrumentation.ts
432
+ var SUPPORTED_NEST_VERSIONS = [">=4.0.0 <13"];
433
+ var CompatibleNestInstrumentation = class extends NestInstrumentation {
434
+ constructor(observeConfig = {}) {
435
+ super(observeConfig);
436
+ this.observeConfig = observeConfig;
437
+ }
438
+ observeConfig;
439
+ methodInstrumenter;
440
+ init() {
441
+ const definition = new InstrumentationNodeModuleDefinition("@nestjs/core", SUPPORTED_NEST_VERSIONS);
442
+ definition.files.push(
443
+ this.getNestFactoryFileInstrumentation(SUPPORTED_NEST_VERSIONS),
444
+ this.getRouterExecutionContextFileInstrumentation(SUPPORTED_NEST_VERSIONS),
445
+ new InstrumentationNodeModuleFile(
446
+ "@nestjs/core/injector/injector.js",
447
+ SUPPORTED_NEST_VERSIONS,
448
+ (moduleExports) => {
449
+ const instrumentation = this;
450
+ this._wrap(moduleExports.Injector.prototype, "instantiateClass", (original) => {
451
+ return async function(...args) {
452
+ const instance = await original.apply(this, args);
453
+ try {
454
+ const wrapper = args[1];
455
+ if (!instance || typeof instance !== "object" || !wrapper?.metatype) return instance;
456
+ if (["InternalCoreModule", "ObserveModule", "DiscoveryModule"].includes(wrapper.host?.name ?? "")) {
457
+ return instance;
458
+ }
459
+ const isController = wrapper.host?.controllers?.get(wrapper.token) === wrapper;
460
+ if (isController && instrumentation.observeConfig.controllerTracing === false || !isController && instrumentation.observeConfig.providerTracing === false) {
461
+ return instance;
462
+ }
463
+ instrumentation.getMethodInstrumenter().instrumentInstance(
464
+ instance,
465
+ isController ? "controller" : "provider",
466
+ wrapper.name ?? wrapper.metatype.name
467
+ );
468
+ } catch {
469
+ }
470
+ return instance;
471
+ };
472
+ });
473
+ return moduleExports;
474
+ },
475
+ (moduleExports) => {
476
+ this._unwrap(moduleExports.Injector.prototype, "instantiateClass");
477
+ }
478
+ )
479
+ );
480
+ return definition;
481
+ }
482
+ getMethodInstrumenter() {
483
+ return this.methodInstrumenter ??= new NestMethodInstrumenter(this.tracer, this.meter);
484
+ }
485
+ };
486
+
487
+ // src/metrics/http-request-metrics.ts
488
+ function routeFor(request) {
489
+ const route = request.route?.path ?? request.routerPath ?? request.routeOptions?.url;
490
+ if (!route) return "<unmatched>";
491
+ return `${request.baseUrl ?? ""}${route}` || "<unmatched>";
492
+ }
493
+ var HttpRequestMetrics = class {
494
+ constructor(meter, serviceName) {
495
+ this.serviceName = serviceName;
496
+ this.requestCount = meter.createCounter("http.server.request.count", {
497
+ description: "Number of completed inbound HTTP requests",
498
+ unit: "{request}"
499
+ });
500
+ this.requestDuration = meter.createHistogram("http.server.request.duration", {
501
+ description: "Inbound HTTP request duration",
502
+ unit: "s",
503
+ advice: { explicitBucketBoundaries: [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] }
504
+ });
505
+ this.errorCount = meter.createCounter("http.server.error.count", {
506
+ description: "Number of failed inbound HTTP requests",
507
+ unit: "{error}"
508
+ });
509
+ }
510
+ serviceName;
511
+ requestCount;
512
+ requestDuration;
513
+ errorCount;
514
+ startedAt = /* @__PURE__ */ new WeakMap();
515
+ start(request) {
516
+ this.startedAt.set(request, process.hrtime.bigint());
517
+ }
518
+ record(request, response) {
519
+ const statusCode = Number.isFinite(response.statusCode) ? response.statusCode : 0;
520
+ const attributes = {
521
+ "service.name": this.serviceName,
522
+ "http.route": routeFor(request),
523
+ "http.request.method": request.method ?? "UNKNOWN",
524
+ "http.response.status_code": statusCode
525
+ };
526
+ this.requestCount.add(1, attributes);
527
+ const started = this.startedAt.get(request);
528
+ if (started !== void 0) {
529
+ this.requestDuration.record(Number(process.hrtime.bigint() - started) / 1e9, attributes);
530
+ this.startedAt.delete(request);
531
+ }
532
+ if (statusCode >= 500) this.errorCount.add(1, attributes);
533
+ }
534
+ };
535
+
536
+ // src/metrics/runtime-metrics.ts
537
+ import { cpus } from "os";
538
+ import { monitorEventLoopDelay, PerformanceObserver, performance } from "perf_hooks";
539
+ var RuntimeMetrics = class {
540
+ constructor(meter) {
541
+ this.meter = meter;
542
+ }
543
+ meter;
544
+ callbacks = [];
545
+ eventLoopDelay;
546
+ gcObserver;
547
+ started = false;
548
+ previousElu = performance.eventLoopUtilization();
549
+ observe(instrument, callback) {
550
+ instrument.addCallback(callback);
551
+ this.callbacks.push({ instrument, callback });
552
+ }
553
+ start() {
554
+ if (this.started) return;
555
+ this.started = true;
556
+ const systemCpu = this.meter.createObservableGauge("system.cpu.utilization", { unit: "1" });
557
+ this.observe(systemCpu, (result) => {
558
+ const cores = cpus();
559
+ let idle = 0;
560
+ let total = 0;
561
+ for (const core of cores) {
562
+ idle += core.times.idle;
563
+ total += Object.values(core.times).reduce((sum, value) => sum + value, 0);
564
+ }
565
+ result.observe(total ? (total - idle) / total : 0);
566
+ });
567
+ const processCpu = this.meter.createObservableCounter("process.cpu.time", { unit: "s" });
568
+ this.observe(processCpu, (result) => {
569
+ const cpu = process.cpuUsage();
570
+ result.observe((cpu.user + cpu.system) / 1e6);
571
+ });
572
+ const rss = this.meter.createObservableGauge("process.memory.rss", { unit: "By" });
573
+ const heapUsed = this.meter.createObservableGauge("nodejs.memory.heap.used", { unit: "By" });
574
+ const heapTotal = this.meter.createObservableGauge("nodejs.memory.heap.total", { unit: "By" });
575
+ this.observe(rss, (result) => result.observe(process.memoryUsage().rss));
576
+ this.observe(heapUsed, (result) => result.observe(process.memoryUsage().heapUsed));
577
+ this.observe(heapTotal, (result) => result.observe(process.memoryUsage().heapTotal));
578
+ this.eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
579
+ this.eventLoopDelay.enable();
580
+ const delay = this.meter.createObservableGauge("nodejs.eventloop.delay", { unit: "s" });
581
+ this.observe(delay, (result) => {
582
+ const mean = this.eventLoopDelay?.mean ?? 0;
583
+ result.observe(Number.isFinite(mean) ? mean / 1e9 : 0);
584
+ this.eventLoopDelay?.reset();
585
+ });
586
+ const utilization = this.meter.createObservableGauge("nodejs.eventloop.utilization", { unit: "1" });
587
+ this.observe(utilization, (result) => {
588
+ const current = performance.eventLoopUtilization(this.previousElu);
589
+ this.previousElu = performance.eventLoopUtilization();
590
+ result.observe(current.utilization);
591
+ });
592
+ const gc = this.meter.createHistogram("nodejs.gc.duration", { unit: "s" });
593
+ this.gcObserver = new PerformanceObserver((list) => {
594
+ for (const entry of list.getEntries()) {
595
+ const detail = entry.detail;
596
+ gc.record(entry.duration / 1e3, { "nodejs.gc.type": String(detail?.kind ?? "unknown") });
597
+ }
598
+ });
599
+ try {
600
+ this.gcObserver.observe({ entryTypes: ["gc"] });
601
+ } catch {
602
+ }
603
+ const uptime = this.meter.createObservableGauge("process.uptime", { unit: "s" });
604
+ this.observe(uptime, (result) => result.observe(process.uptime()));
605
+ }
606
+ stop() {
607
+ for (const { instrument, callback } of this.callbacks) instrument.removeCallback(callback);
608
+ this.callbacks.length = 0;
609
+ this.eventLoopDelay?.disable();
610
+ this.eventLoopDelay = void 0;
611
+ this.gcObserver?.disconnect();
612
+ this.gcObserver = void 0;
613
+ this.started = false;
614
+ }
615
+ };
616
+
617
+ // src/nest/observe.module.ts
618
+ import { Global, Inject, Injectable, Module } from "@nestjs/common";
619
+ import { DiscoveryModule, DiscoveryService } from "@nestjs/core";
620
+ import { metrics as metrics2, trace as trace5 } from "@opentelemetry/api";
621
+
622
+ // src/sdk.ts
623
+ import { logs as logs2 } from "@opentelemetry/api-logs";
624
+ import { metrics } from "@opentelemetry/api";
625
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
626
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
627
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
628
+ import { registerInstrumentations } from "@opentelemetry/instrumentation";
629
+ import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
630
+ import { PrismaInstrumentation } from "@prisma/instrumentation";
631
+ import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
632
+ import { AggregationType, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
633
+ import { AlwaysOffSampler, BatchSpanProcessor, ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
634
+ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
635
+
636
+ // src/exceptions/process-exception-capture.ts
637
+ import { trace as trace3, SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
638
+ function asError(value) {
639
+ return value instanceof Error ? value : new Error(String(value));
640
+ }
641
+ var ProcessExceptionCapture = class {
642
+ constructor(emitter) {
643
+ this.emitter = emitter;
644
+ }
645
+ emitter;
646
+ listening = false;
647
+ onUncaughtException = (value) => this.capture(value, "uncaughtException");
648
+ start() {
649
+ if (this.listening) return;
650
+ this.listening = true;
651
+ process.on("uncaughtExceptionMonitor", this.onUncaughtException);
652
+ }
653
+ stop() {
654
+ if (!this.listening) return;
655
+ process.off("uncaughtExceptionMonitor", this.onUncaughtException);
656
+ this.listening = false;
657
+ }
658
+ capture(value, source = "nestjs") {
659
+ try {
660
+ const error = asError(value);
661
+ const span = trace3.getActiveSpan();
662
+ span?.recordException(error);
663
+ span?.setStatus({ code: SpanStatusCode2.ERROR, message: error.message });
664
+ const spanContext = span?.spanContext();
665
+ this.emitter?.emit({
666
+ body: redactText(error.stack ?? error.message),
667
+ severityText: "ERROR",
668
+ attributes: {
669
+ "exception.type": error.name,
670
+ "exception.message": redactText(error.message),
671
+ "exception.stacktrace": redactText(error.stack ?? ""),
672
+ "exception.source": source,
673
+ ...spanContext ? { trace_id: spanContext.traceId, span_id: spanContext.spanId } : {}
674
+ },
675
+ timestamp: Date.now()
676
+ });
677
+ } catch {
678
+ }
679
+ }
680
+ };
681
+
682
+ // src/resource.ts
683
+ import { hostname } from "os";
684
+ import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
685
+ var SDK_NAME = "@ryanzen9/nest-observe";
686
+ var SDK_VERSION = "0.1.0";
687
+ function createObserveResource(config) {
688
+ return defaultResource().merge(resourceFromAttributes({
689
+ ...config.resourceAttributes,
690
+ "service.name": config.serviceName,
691
+ "service.version": config.serviceVersion,
692
+ "deployment.environment.name": config.environment,
693
+ "service.instance.id": config.instanceId,
694
+ "telemetry.sdk.name": SDK_NAME,
695
+ "telemetry.sdk.version": SDK_VERSION,
696
+ "host.name": config.resourceAttributes["host.name"] ?? hostname()
697
+ }));
698
+ }
699
+
700
+ // src/security/span-redaction-processor.ts
701
+ var DROP_VALUE = /(?:request\.body|db\.(?:query\.)?parameters?|db\.statement\.parameters?|redis\.(?:args|value))/i;
702
+ function cleanValue(value) {
703
+ if (typeof value === "string") return redactText(value);
704
+ if (Array.isArray(value)) {
705
+ return value.map((item) => typeof item === "string" ? redactText(item) : item);
706
+ }
707
+ return value;
708
+ }
709
+ function cleanAttributes(attributes) {
710
+ if (!attributes) return;
711
+ const mutable = attributes;
712
+ for (const [key, value] of Object.entries(mutable)) {
713
+ if (value === void 0) continue;
714
+ if (DROP_VALUE.test(key) || isSensitiveKey(key)) mutable[key] = REDACTED;
715
+ else mutable[key] = cleanValue(value);
716
+ }
717
+ }
718
+ var SpanRedactionProcessor = class {
719
+ onStart(_span, _parentContext) {
720
+ }
721
+ onEnd(span) {
722
+ const mutable = span;
723
+ try {
724
+ mutable.name = redactText(mutable.name);
725
+ cleanAttributes(mutable.attributes);
726
+ if (mutable.status.message) mutable.status.message = redactText(mutable.status.message);
727
+ for (const event of mutable.events) cleanAttributes(event.attributes);
728
+ } catch {
729
+ }
730
+ }
731
+ forceFlush() {
732
+ return Promise.resolve();
733
+ }
734
+ shutdown() {
735
+ return Promise.resolve();
736
+ }
737
+ };
738
+
739
+ // src/sdk.ts
740
+ var InactiveObserveHandle = class {
741
+ constructor(config) {
742
+ this.config = config;
743
+ }
744
+ config;
745
+ started = false;
746
+ tracerProvider = void 0;
747
+ meterProvider = void 0;
748
+ loggerProvider = void 0;
749
+ forceFlush() {
750
+ return Promise.resolve();
751
+ }
752
+ shutdown() {
753
+ return Promise.resolve();
754
+ }
755
+ };
756
+ var ActiveObserveHandle = class {
757
+ constructor(config, tracerProvider, meterProvider, loggerProvider, runtimeMetrics, loggerInstrumentation, exceptionCapture, instrumentations) {
758
+ this.config = config;
759
+ this.tracerProvider = tracerProvider;
760
+ this.meterProvider = meterProvider;
761
+ this.loggerProvider = loggerProvider;
762
+ this.runtimeMetrics = runtimeMetrics;
763
+ this.loggerInstrumentation = loggerInstrumentation;
764
+ this.exceptionCapture = exceptionCapture;
765
+ this.instrumentations = instrumentations;
766
+ }
767
+ config;
768
+ tracerProvider;
769
+ meterProvider;
770
+ loggerProvider;
771
+ runtimeMetrics;
772
+ loggerInstrumentation;
773
+ exceptionCapture;
774
+ instrumentations;
775
+ started = true;
776
+ stopped = false;
777
+ async forceFlush() {
778
+ await Promise.allSettled([
779
+ this.tracerProvider?.forceFlush(),
780
+ this.meterProvider?.forceFlush(),
781
+ this.loggerProvider?.forceFlush({ timeoutMillis: this.config.exportTimeoutMillis })
782
+ ].filter((item) => Boolean(item)));
783
+ }
784
+ async shutdown() {
785
+ if (this.stopped) return;
786
+ this.stopped = true;
787
+ this.loggerInstrumentation?.disable();
788
+ this.runtimeMetrics?.stop();
789
+ this.exceptionCapture.stop();
790
+ for (const instrumentation of this.instrumentations) {
791
+ try {
792
+ instrumentation.disable();
793
+ } catch {
794
+ }
795
+ }
796
+ await this.forceFlush();
797
+ await Promise.allSettled([
798
+ this.tracerProvider?.shutdown(),
799
+ this.meterProvider?.shutdown(),
800
+ this.loggerProvider?.shutdown()
801
+ ].filter((item) => Boolean(item)));
802
+ if (activeRuntime === this) activeRuntime = void 0;
803
+ }
804
+ };
805
+ var activeRuntime;
806
+ function exporterConfig(url, config) {
807
+ return {
808
+ ...url ? { url } : {},
809
+ headers: config.headers,
810
+ timeoutMillis: config.exportTimeoutMillis
811
+ };
812
+ }
813
+ function createTraceProvider(config, resource) {
814
+ if (!config.traces && !config.metrics) return void 0;
815
+ const spanProcessors = [new SpanRedactionProcessor()];
816
+ if (config.traces) {
817
+ const exporter = config.exporters?.span ?? new OTLPTraceExporter(exporterConfig(config.endpoints.traces, config));
818
+ spanProcessors.push(new BatchSpanProcessor(exporter, {
819
+ exportTimeoutMillis: config.exportTimeoutMillis,
820
+ maxQueueSize: 2048,
821
+ maxExportBatchSize: 512
822
+ }));
823
+ }
824
+ const provider = new NodeTracerProvider({
825
+ resource,
826
+ sampler: config.traces ? new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(config.sampling) }) : new AlwaysOffSampler(),
827
+ spanProcessors,
828
+ spanLimits: { attributeCountLimit: 64, eventCountLimit: 64, linkCountLimit: 16 }
829
+ });
830
+ provider.register();
831
+ return provider;
832
+ }
833
+ function createMeterProvider(config, resource) {
834
+ if (!config.metrics) return void 0;
835
+ let reader;
836
+ if (config.exporters?.metricReader) {
837
+ reader = config.exporters.metricReader;
838
+ } else {
839
+ reader = new PeriodicExportingMetricReader({
840
+ exporter: new OTLPMetricExporter(exporterConfig(config.endpoints.metrics, config)),
841
+ exportIntervalMillis: config.metricExportIntervalMillis,
842
+ exportTimeoutMillis: Math.min(config.exportTimeoutMillis, config.metricExportIntervalMillis),
843
+ cardinalityLimits: { default: 2e3, histogram: 2e3 }
844
+ });
845
+ }
846
+ const provider = new MeterProvider({
847
+ resource,
848
+ readers: [reader],
849
+ views: [{
850
+ instrumentName: "http.server.request.duration",
851
+ meterName: "@opentelemetry/instrumentation-http",
852
+ aggregation: { type: AggregationType.DROP }
853
+ }]
854
+ });
855
+ metrics.setGlobalMeterProvider(provider);
856
+ return provider;
857
+ }
858
+ function createLoggerProvider(config, resource) {
859
+ if (!config.logs) return void 0;
860
+ const exporter = config.exporters?.log ?? new OTLPLogExporter(exporterConfig(config.endpoints.logs, config));
861
+ const provider = new LoggerProvider({
862
+ resource,
863
+ processors: [new BatchLogRecordProcessor({
864
+ exporter,
865
+ exportTimeoutMillis: config.exportTimeoutMillis,
866
+ maxQueueSize: 2048,
867
+ maxExportBatchSize: 512
868
+ })]
869
+ });
870
+ logs2.setGlobalLoggerProvider(provider);
871
+ return provider;
872
+ }
873
+ function observe(options = {}) {
874
+ if (activeRuntime?.started) return activeRuntime;
875
+ const config = resolveObserveConfig(options);
876
+ if (!config.enabled) return new InactiveObserveHandle(config);
877
+ let tracerProvider;
878
+ let meterProvider;
879
+ let loggerProvider;
880
+ let runtimeMetrics;
881
+ let loggerInstrumentation;
882
+ let exceptionCapture;
883
+ const instrumentations = [];
884
+ try {
885
+ const resource = createObserveResource(config);
886
+ meterProvider = createMeterProvider(config, resource);
887
+ const meter = meterProvider?.getMeter(SDK_NAME, SDK_VERSION);
888
+ tracerProvider = createTraceProvider(config, resource);
889
+ loggerProvider = createLoggerProvider(config, resource);
890
+ runtimeMetrics = meter ? new RuntimeMetrics(meter) : void 0;
891
+ runtimeMetrics?.start();
892
+ const otelLogger = loggerProvider?.getLogger(SDK_NAME, SDK_VERSION);
893
+ const logEmitter = config.logs ? new OpenTelemetryLogEmitter(SDK_NAME, SDK_VERSION, otelLogger) : void 0;
894
+ loggerInstrumentation = logEmitter ? new NestLoggerInstrumentation(logEmitter, {
895
+ "service.name": config.serviceName,
896
+ "service.version": config.serviceVersion,
897
+ "deployment.environment.name": config.environment
898
+ }) : void 0;
899
+ loggerInstrumentation?.enable();
900
+ exceptionCapture = new ProcessExceptionCapture(logEmitter);
901
+ exceptionCapture.start();
902
+ if (config.traces || config.metrics) {
903
+ const safeHeaders = config.allowedHeaders.filter((header) => !isSensitiveKey(header));
904
+ const httpRequestMetrics = meter ? new HttpRequestMetrics(meter, config.serviceName) : void 0;
905
+ instrumentations.push(new HttpInstrumentation({
906
+ requireParentforOutgoingSpans: false,
907
+ headersToSpanAttributes: {
908
+ client: { requestHeaders: safeHeaders, responseHeaders: safeHeaders },
909
+ server: { requestHeaders: safeHeaders, responseHeaders: safeHeaders }
910
+ },
911
+ redactedQueryParams: [
912
+ "sig",
913
+ "Signature",
914
+ "AWSAccessKeyId",
915
+ "X-Goog-Signature",
916
+ "password",
917
+ "passwd",
918
+ "token",
919
+ "access_token",
920
+ "api_key",
921
+ "secret"
922
+ ],
923
+ ...httpRequestMetrics ? {
924
+ requestHook: (_span, request) => {
925
+ if (!("getHeader" in request)) httpRequestMetrics.start(request);
926
+ },
927
+ applyCustomAttributesOnSpan: (_span, request, response) => {
928
+ if ("setHeader" in response) httpRequestMetrics.record(request, response);
929
+ }
930
+ } : {}
931
+ }));
932
+ instrumentations.push(new CompatibleNestInstrumentation({
933
+ providerTracing: config.providerTracing,
934
+ controllerTracing: config.controllerTracing
935
+ }));
936
+ if (config.traces) instrumentations.push(new PrismaInstrumentation());
937
+ registerInstrumentations({
938
+ instrumentations,
939
+ ...tracerProvider ? { tracerProvider } : {},
940
+ ...meterProvider ? { meterProvider } : {}
941
+ });
942
+ }
943
+ const runtime = new ActiveObserveHandle(
944
+ config,
945
+ tracerProvider,
946
+ meterProvider,
947
+ loggerProvider,
948
+ runtimeMetrics,
949
+ loggerInstrumentation,
950
+ exceptionCapture,
951
+ instrumentations
952
+ );
953
+ activeRuntime = runtime;
954
+ return runtime;
955
+ } catch {
956
+ loggerInstrumentation?.disable();
957
+ runtimeMetrics?.stop();
958
+ exceptionCapture?.stop();
959
+ for (const instrumentation of instrumentations) {
960
+ try {
961
+ instrumentation.disable();
962
+ } catch {
963
+ }
964
+ }
965
+ void Promise.allSettled([
966
+ tracerProvider?.shutdown(),
967
+ meterProvider?.shutdown(),
968
+ loggerProvider?.shutdown()
969
+ ].filter((item) => Boolean(item)));
970
+ return new InactiveObserveHandle(config);
971
+ }
972
+ }
973
+ function getObserveRuntime() {
974
+ return activeRuntime;
975
+ }
976
+
977
+ // src/nest/observe.module.ts
978
+ var OBSERVE_OPTIONS = /* @__PURE__ */ Symbol("OBSERVE_OPTIONS");
979
+ var OBSERVE_HANDLE = /* @__PURE__ */ Symbol("OBSERVE_HANDLE");
980
+ var NestObserveExplorer = class {
981
+ constructor(discovery, options, handle) {
982
+ this.discovery = discovery;
983
+ this.options = options;
984
+ this.handle = handle;
985
+ }
986
+ discovery;
987
+ options;
988
+ handle;
989
+ onModuleInit() {
990
+ if (!this.handle.started) return;
991
+ const runtime = getObserveRuntime();
992
+ const tracer = runtime?.tracerProvider?.getTracer(SDK_NAME, SDK_VERSION) ?? trace5.getTracer(SDK_NAME, SDK_VERSION);
993
+ const meter = runtime?.meterProvider?.getMeter(SDK_NAME, SDK_VERSION) ?? metrics2.getMeter(SDK_NAME, SDK_VERSION);
994
+ const instrumenter = new NestMethodInstrumenter(tracer, meter);
995
+ if (this.options.providerTracing !== false) {
996
+ this.instrument(this.discovery.getProviders(), instrumenter, "provider");
997
+ }
998
+ if (this.options.controllerTracing !== false) {
999
+ this.instrument(this.discovery.getControllers(), instrumenter, "controller");
1000
+ }
1001
+ }
1002
+ instrument(wrappers, instrumenter, kind) {
1003
+ for (const wrapper of wrappers) {
1004
+ if (!wrapper.metatype) continue;
1005
+ if (["ObserveModule", "DiscoveryModule", "InternalCoreModule"].includes(wrapper.host?.name ?? "")) continue;
1006
+ if (wrapper.metatype === NestObserveExplorer) continue;
1007
+ if (wrapper.instance) {
1008
+ instrumenter.instrumentInstance(wrapper.instance, kind, wrapper.name ?? wrapper.metatype.name);
1009
+ } else {
1010
+ instrumenter.instrumentPrototype(wrapper.metatype, kind, wrapper.name ?? wrapper.metatype.name);
1011
+ }
1012
+ }
1013
+ }
1014
+ async onApplicationShutdown() {
1015
+ await this.handle.shutdown();
1016
+ }
1017
+ };
1018
+ NestObserveExplorer = __decorateClass([
1019
+ Injectable(),
1020
+ __decorateParam(0, Inject(DiscoveryService)),
1021
+ __decorateParam(1, Inject(OBSERVE_OPTIONS)),
1022
+ __decorateParam(2, Inject(OBSERVE_HANDLE))
1023
+ ], NestObserveExplorer);
1024
+ var ObserveModule = class {
1025
+ static forRoot(options = {}) {
1026
+ return {
1027
+ module: ObserveModule,
1028
+ global: true,
1029
+ imports: [DiscoveryModule],
1030
+ providers: [
1031
+ { provide: OBSERVE_OPTIONS, useValue: options },
1032
+ { provide: OBSERVE_HANDLE, useFactory: () => observe(options) },
1033
+ NestObserveExplorer
1034
+ ],
1035
+ exports: [OBSERVE_HANDLE]
1036
+ };
1037
+ }
1038
+ };
1039
+ ObserveModule = __decorateClass([
1040
+ Global(),
1041
+ Module({})
1042
+ ], ObserveModule);
1043
+ export {
1044
+ CompatibleNestInstrumentation,
1045
+ HttpRequestMetrics,
1046
+ IgnoreTrace,
1047
+ NestLoggerInstrumentation,
1048
+ NestMethodInstrumenter,
1049
+ OBSERVE_HANDLE,
1050
+ OBSERVE_OPTIONS,
1051
+ ObserveModule,
1052
+ OpenTelemetryLogEmitter,
1053
+ REDACTED,
1054
+ RuntimeMetrics,
1055
+ SDK_NAME,
1056
+ SDK_VERSION,
1057
+ SpanRedactionProcessor,
1058
+ Trace,
1059
+ createObserveResource,
1060
+ getObserveRuntime,
1061
+ isSensitiveKey,
1062
+ isTraceDecorated,
1063
+ isTraceIgnored,
1064
+ markTraceDecorated,
1065
+ markTraceIgnored,
1066
+ observe,
1067
+ parseKeyValueList,
1068
+ redact,
1069
+ redactText,
1070
+ resolveObserveConfig,
1071
+ sanitizeHeaders
1072
+ };
1073
+ //# sourceMappingURL=index.mjs.map