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