@ryanzeng/nest-observe 0.1.1 → 0.1.2

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/register.js CHANGED
@@ -93,15 +93,14 @@ function resolveObserveConfig(options = {}, env = process.env) {
93
93
  allowedHeaders,
94
94
  exportTimeoutMillis: positiveInteger(options.exportTimeoutMillis ?? env.OTEL_EXPORTER_OTLP_TIMEOUT, 1e4),
95
95
  metricExportIntervalMillis: positiveInteger(options.metricExportIntervalMillis ?? env.OTEL_METRIC_EXPORT_INTERVAL, 6e4),
96
- resourceAttributes
96
+ resourceAttributes,
97
+ diagnosticLogging: booleanValue(options.diagnosticLogging, true),
98
+ failFast: booleanValue(options.failFast, false)
97
99
  };
98
100
  if (options.exporters) result.exporters = options.exporters;
99
101
  return result;
100
102
  }
101
103
 
102
- // src/exceptions/process-exception-capture.ts
103
- var import_api = require("@opentelemetry/api");
104
-
105
104
  // src/security/redaction.ts
106
105
  var REDACTED = "[REDACTED]";
107
106
  var SENSITIVE_KEY = /(?:authorization|proxy-authorization|cookie|set-cookie|pass(?:word|wd)?|secret|token|api[-_]?key|phone|mobile)/i;
@@ -135,7 +134,115 @@ function redact(value) {
135
134
  return visit(value, /* @__PURE__ */ new WeakSet());
136
135
  }
137
136
 
137
+ // src/diagnostics.ts
138
+ function toError(value) {
139
+ if (value instanceof Error) return value;
140
+ if (typeof value === "string") return new Error(value);
141
+ try {
142
+ return new Error(JSON.stringify(value));
143
+ } catch {
144
+ return new Error(String(value));
145
+ }
146
+ }
147
+ function sanitizeError(error) {
148
+ const sanitized = new Error(redactText(error.message));
149
+ sanitized.name = error.name;
150
+ if (error.stack) sanitized.stack = redactText(error.stack);
151
+ return sanitized;
152
+ }
153
+ var ObserveDiagnostics = class {
154
+ constructor(logging, onError) {
155
+ this.logging = logging;
156
+ this.onError = onError;
157
+ }
158
+ logging;
159
+ onError;
160
+ currentStatus = "starting";
161
+ currentError;
162
+ failedSignals = /* @__PURE__ */ new Set();
163
+ reported = /* @__PURE__ */ new Set();
164
+ get status() {
165
+ return this.currentStatus;
166
+ }
167
+ get lastError() {
168
+ return this.currentError;
169
+ }
170
+ activate() {
171
+ if (this.currentStatus === "starting") this.currentStatus = "active";
172
+ }
173
+ inactive() {
174
+ this.currentStatus = "inactive";
175
+ }
176
+ stop() {
177
+ this.currentStatus = "stopped";
178
+ }
179
+ success(signal) {
180
+ this.failedSignals.delete(signal);
181
+ for (const fingerprint of this.reported) {
182
+ if (fingerprint.startsWith(`${signal}\0`)) this.reported.delete(fingerprint);
183
+ }
184
+ if (this.currentStatus === "degraded" && this.failedSignals.size === 0) {
185
+ this.currentStatus = "active";
186
+ }
187
+ }
188
+ failure(signal, stage, value) {
189
+ const error = sanitizeError(toError(value));
190
+ const event = { signal, stage, error, timestamp: Date.now() };
191
+ this.currentError = event;
192
+ this.failedSignals.add(signal);
193
+ if (this.currentStatus !== "inactive" && this.currentStatus !== "stopped") {
194
+ this.currentStatus = "degraded";
195
+ }
196
+ const fingerprint = `${signal}\0${stage}\0${error.name}\0${error.message}`;
197
+ if (this.reported.has(fingerprint)) return;
198
+ this.reported.add(fingerprint);
199
+ try {
200
+ this.onError?.(event);
201
+ } catch {
202
+ }
203
+ if (!this.logging) return;
204
+ try {
205
+ process.stderr.write(
206
+ `[nest-observe] ${signal} ${stage} failed: ${redactText(error.message)}
207
+ `
208
+ );
209
+ } catch {
210
+ }
211
+ }
212
+ };
213
+ function withExporterDiagnostics(exporter, signal, diagnostics) {
214
+ const delegate = exporter;
215
+ return new Proxy(exporter, {
216
+ get(target, property) {
217
+ if (property === "export") {
218
+ return (items, callback) => {
219
+ try {
220
+ delegate.export.call(target, items, (result) => {
221
+ if (result.code === 0) {
222
+ diagnostics.success(signal);
223
+ } else {
224
+ diagnostics.failure(
225
+ signal,
226
+ "export",
227
+ result.error ?? new Error(`${signal} export failed`)
228
+ );
229
+ }
230
+ callback(result);
231
+ });
232
+ } catch (error) {
233
+ diagnostics.failure(signal, "export", error);
234
+ throw error;
235
+ }
236
+ };
237
+ }
238
+ const value = Reflect.get(target, property, target);
239
+ return typeof value === "function" ? value.bind(target) : value;
240
+ }
241
+ });
242
+ }
243
+
138
244
  // src/exceptions/process-exception-capture.ts
245
+ var import_api = require("@opentelemetry/api");
139
246
  function asError(value) {
140
247
  return value instanceof Error ? value : new Error(String(value));
141
248
  }
@@ -386,7 +493,13 @@ var SEVERITY = {
386
493
  };
387
494
  function bodyValue(message) {
388
495
  if (message instanceof Error) return redactText(message.stack ?? `${message.name}: ${message.message}`);
389
- return redact(message);
496
+ const sanitized = redact(message);
497
+ if (sanitized === null || typeof sanitized !== "object") return sanitized;
498
+ try {
499
+ return JSON.stringify(sanitized, (_key, value) => typeof value === "bigint" ? value.toString() : value);
500
+ } catch {
501
+ return redactText(String(sanitized));
502
+ }
390
503
  }
391
504
  var NestLoggerInstrumentation = class {
392
505
  constructor(emitter, resourceAttributes = {}) {
@@ -493,9 +606,14 @@ var HttpRequestMetrics = class {
493
606
  errorCount;
494
607
  startedAt = /* @__PURE__ */ new WeakMap();
495
608
  start(request) {
609
+ if (this.startedAt.has(request)) return false;
496
610
  this.startedAt.set(request, process.hrtime.bigint());
611
+ return true;
497
612
  }
498
613
  record(request, response) {
614
+ const started = this.startedAt.get(request);
615
+ if (started === void 0) return false;
616
+ this.startedAt.delete(request);
499
617
  const statusCode = Number.isFinite(response.statusCode) ? response.statusCode : 0;
500
618
  const attributes = {
501
619
  "service.name": this.serviceName,
@@ -504,12 +622,9 @@ var HttpRequestMetrics = class {
504
622
  "http.response.status_code": statusCode
505
623
  };
506
624
  this.requestCount.add(1, attributes);
507
- const started = this.startedAt.get(request);
508
- if (started !== void 0) {
509
- this.requestDuration.record(Number(process.hrtime.bigint() - started) / 1e9, attributes);
510
- this.startedAt.delete(request);
511
- }
625
+ this.requestDuration.record(Number(process.hrtime.bigint() - started) / 1e9, attributes);
512
626
  if (statusCode >= 500) this.errorCount.add(1, attributes);
627
+ return true;
513
628
  }
514
629
  };
515
630
 
@@ -597,8 +712,118 @@ var RuntimeMetrics = class {
597
712
  // src/resource.ts
598
713
  var import_resources = require("@opentelemetry/resources");
599
714
  var import_node_os2 = require("os");
600
- var SDK_NAME = "@ryanzeng/nest-observe";
601
- var SDK_VERSION = "0.1.0";
715
+
716
+ // package.json
717
+ var package_default = {
718
+ name: "@ryanzeng/nest-observe",
719
+ version: "0.1.2",
720
+ description: "Zero-config, vendor-neutral OpenTelemetry observability for NestJS",
721
+ keywords: [
722
+ "nestjs",
723
+ "opentelemetry",
724
+ "observability",
725
+ "otlp",
726
+ "tracing",
727
+ "metrics",
728
+ "logging"
729
+ ],
730
+ license: "MIT",
731
+ homepage: "https://github.com/ryanzen9/nest-observe#readme",
732
+ bugs: {
733
+ url: "https://github.com/ryanzen9/nest-observe/issues"
734
+ },
735
+ repository: {
736
+ type: "git",
737
+ url: "git+https://github.com/ryanzen9/nest-observe.git"
738
+ },
739
+ packageManager: "pnpm@10.13.1",
740
+ sideEffects: [
741
+ "./dist/register.js",
742
+ "./dist/register.mjs"
743
+ ],
744
+ main: "./dist/index.js",
745
+ module: "./dist/index.mjs",
746
+ types: "./dist/index.d.ts",
747
+ exports: {
748
+ ".": {
749
+ import: {
750
+ types: "./dist/index.d.mts",
751
+ default: "./dist/index.mjs"
752
+ },
753
+ require: {
754
+ types: "./dist/index.d.ts",
755
+ default: "./dist/index.js"
756
+ }
757
+ },
758
+ "./register": {
759
+ import: {
760
+ types: "./dist/register.d.mts",
761
+ default: "./dist/register.mjs"
762
+ },
763
+ require: {
764
+ types: "./dist/register.d.ts",
765
+ default: "./dist/register.js"
766
+ }
767
+ }
768
+ },
769
+ files: [
770
+ "dist",
771
+ "README.md",
772
+ "CHANGELOG.md",
773
+ "LICENSE"
774
+ ],
775
+ scripts: {
776
+ build: "tsup",
777
+ test: "vitest run",
778
+ "test:watch": "vitest",
779
+ typecheck: "tsc --noEmit",
780
+ check: "pnpm typecheck && pnpm test && pnpm build"
781
+ },
782
+ engines: {
783
+ node: ">=20"
784
+ },
785
+ peerDependencies: {
786
+ "@nestjs/common": ">=10 <13",
787
+ "@nestjs/core": ">=10 <13",
788
+ "reflect-metadata": ">=0.1.12 <1",
789
+ rxjs: "^7.1.0"
790
+ },
791
+ dependencies: {
792
+ "@opentelemetry/api": "^1.9.0",
793
+ "@opentelemetry/api-logs": "^0.221.0",
794
+ "@opentelemetry/exporter-logs-otlp-proto": "^0.221.0",
795
+ "@opentelemetry/exporter-metrics-otlp-proto": "^0.221.0",
796
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.221.0",
797
+ "@opentelemetry/instrumentation": "^0.221.0",
798
+ "@opentelemetry/instrumentation-http": "^0.221.0",
799
+ "@opentelemetry/instrumentation-nestjs-core": "^0.67.0",
800
+ "@opentelemetry/resources": "^2.10.0",
801
+ "@opentelemetry/sdk-logs": "^0.221.0",
802
+ "@opentelemetry/sdk-metrics": "^2.10.0",
803
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
804
+ "@opentelemetry/sdk-trace-node": "^2.10.0",
805
+ "@prisma/instrumentation": "^7.10.0"
806
+ },
807
+ devDependencies: {
808
+ "@nestjs/common": "^12.0.1",
809
+ "@nestjs/core": "^12.0.1",
810
+ "@nestjs/testing": "^12.0.1",
811
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
812
+ "@types/node": "^24.0.0",
813
+ "reflect-metadata": "^0.2.2",
814
+ rxjs: "^7.8.2",
815
+ tsup: "^8.5.1",
816
+ typescript: "^5.9.3",
817
+ vitest: "^4.1.11"
818
+ },
819
+ publishConfig: {
820
+ access: "public"
821
+ }
822
+ };
823
+
824
+ // src/resource.ts
825
+ var SDK_NAME = package_default.name;
826
+ var SDK_VERSION = package_default.version;
602
827
  function createObserveResource(config) {
603
828
  return (0, import_resources.defaultResource)().merge(
604
829
  (0, import_resources.resourceFromAttributes)({
@@ -655,14 +880,23 @@ var SpanRedactionProcessor = class {
655
880
 
656
881
  // src/sdk.ts
657
882
  var InactiveObserveHandle = class {
658
- constructor(config) {
883
+ constructor(config, diagnostics) {
659
884
  this.config = config;
885
+ this.diagnostics = diagnostics;
660
886
  }
661
887
  config;
888
+ diagnostics;
662
889
  started = false;
663
890
  tracerProvider = void 0;
664
891
  meterProvider = void 0;
665
892
  loggerProvider = void 0;
893
+ httpRequestMetrics = void 0;
894
+ get status() {
895
+ return this.diagnostics.status;
896
+ }
897
+ get lastError() {
898
+ return this.diagnostics.lastError;
899
+ }
666
900
  forceFlush() {
667
901
  return Promise.resolve();
668
902
  }
@@ -671,52 +905,73 @@ var InactiveObserveHandle = class {
671
905
  }
672
906
  };
673
907
  var ActiveObserveHandle = class {
674
- constructor(config, tracerProvider, meterProvider, loggerProvider, runtimeMetrics, loggerInstrumentation, exceptionCapture, instrumentations) {
908
+ constructor(config, tracerProvider, meterProvider, loggerProvider, httpRequestMetrics, runtimeMetrics, loggerInstrumentation, exceptionCapture, instrumentations, diagnostics) {
675
909
  this.config = config;
676
910
  this.tracerProvider = tracerProvider;
677
911
  this.meterProvider = meterProvider;
678
912
  this.loggerProvider = loggerProvider;
913
+ this.httpRequestMetrics = httpRequestMetrics;
679
914
  this.runtimeMetrics = runtimeMetrics;
680
915
  this.loggerInstrumentation = loggerInstrumentation;
681
916
  this.exceptionCapture = exceptionCapture;
682
917
  this.instrumentations = instrumentations;
918
+ this.diagnostics = diagnostics;
683
919
  }
684
920
  config;
685
921
  tracerProvider;
686
922
  meterProvider;
687
923
  loggerProvider;
924
+ httpRequestMetrics;
688
925
  runtimeMetrics;
689
926
  loggerInstrumentation;
690
927
  exceptionCapture;
691
928
  instrumentations;
929
+ diagnostics;
692
930
  started = true;
693
931
  stopped = false;
932
+ get status() {
933
+ return this.diagnostics.status;
934
+ }
935
+ get lastError() {
936
+ return this.diagnostics.lastError;
937
+ }
694
938
  async forceFlush() {
695
- await Promise.allSettled([
696
- this.tracerProvider?.forceFlush(),
697
- this.meterProvider?.forceFlush(),
698
- this.loggerProvider?.forceFlush({ timeoutMillis: this.config.exportTimeoutMillis })
699
- ].filter((item) => Boolean(item)));
939
+ await this.runSafely("forceFlush", [
940
+ ...this.config.traces && this.tracerProvider ? [["traces", () => this.tracerProvider.forceFlush()]] : [],
941
+ ...this.config.metrics && this.meterProvider ? [["metrics", () => this.meterProvider.forceFlush()]] : [],
942
+ ...this.config.logs && this.loggerProvider ? [["logs", () => this.loggerProvider.forceFlush({ timeoutMillis: this.config.exportTimeoutMillis })]] : []
943
+ ]);
700
944
  }
701
945
  async shutdown() {
702
946
  if (this.stopped) return;
703
947
  this.stopped = true;
704
948
  this.loggerInstrumentation?.disable();
705
- this.runtimeMetrics?.stop();
706
949
  this.exceptionCapture.stop();
707
950
  for (const instrumentation of this.instrumentations) {
708
951
  try {
709
952
  instrumentation.disable();
710
- } catch {
953
+ } catch (error) {
954
+ this.diagnostics.failure("sdk", "shutdown", error);
711
955
  }
712
956
  }
713
957
  await this.forceFlush();
714
- await Promise.allSettled([
715
- this.tracerProvider?.shutdown(),
716
- this.meterProvider?.shutdown(),
717
- this.loggerProvider?.shutdown()
718
- ].filter((item) => Boolean(item)));
958
+ this.runtimeMetrics?.stop();
959
+ await this.runSafely("shutdown", [
960
+ ...this.tracerProvider ? [["traces", () => this.tracerProvider.shutdown()]] : [],
961
+ ...this.meterProvider ? [["metrics", () => this.meterProvider.shutdown()]] : [],
962
+ ...this.loggerProvider ? [["logs", () => this.loggerProvider.shutdown()]] : []
963
+ ]);
719
964
  if (activeRuntime === this) activeRuntime = void 0;
965
+ this.diagnostics.stop();
966
+ }
967
+ async runSafely(stage, operations) {
968
+ await Promise.all(operations.map(async ([signal, operation]) => {
969
+ try {
970
+ await operation();
971
+ } catch (error) {
972
+ this.diagnostics.failure(signal, stage, error);
973
+ }
974
+ }));
720
975
  }
721
976
  };
722
977
  var activeRuntime;
@@ -727,12 +982,16 @@ function exporterConfig(url, config) {
727
982
  timeoutMillis: config.exportTimeoutMillis
728
983
  };
729
984
  }
730
- function createTraceProvider(config, resource) {
985
+ function createTraceProvider(config, resource, diagnostics) {
731
986
  if (!config.traces && !config.metrics) return void 0;
732
987
  const spanProcessors = [new SpanRedactionProcessor()];
733
988
  if (config.traces) {
734
989
  const exporter = config.exporters?.span ?? new import_exporter_trace_otlp_proto.OTLPTraceExporter(exporterConfig(config.endpoints.traces, config));
735
- spanProcessors.push(new import_sdk_trace_base.BatchSpanProcessor(exporter, {
990
+ spanProcessors.push(new import_sdk_trace_base.BatchSpanProcessor(withExporterDiagnostics(
991
+ exporter,
992
+ "traces",
993
+ diagnostics
994
+ ), {
736
995
  exportTimeoutMillis: config.exportTimeoutMillis,
737
996
  maxQueueSize: 2048,
738
997
  maxExportBatchSize: 512
@@ -747,14 +1006,18 @@ function createTraceProvider(config, resource) {
747
1006
  provider.register();
748
1007
  return provider;
749
1008
  }
750
- function createMeterProvider(config, resource) {
1009
+ function createMeterProvider(config, resource, diagnostics) {
751
1010
  if (!config.metrics) return void 0;
752
1011
  let reader;
753
1012
  if (config.exporters?.metricReader) {
754
1013
  reader = config.exporters.metricReader;
755
1014
  } else {
756
1015
  reader = new import_sdk_metrics.PeriodicExportingMetricReader({
757
- exporter: new import_exporter_metrics_otlp_proto.OTLPMetricExporter(exporterConfig(config.endpoints.metrics, config)),
1016
+ exporter: withExporterDiagnostics(
1017
+ new import_exporter_metrics_otlp_proto.OTLPMetricExporter(exporterConfig(config.endpoints.metrics, config)),
1018
+ "metrics",
1019
+ diagnostics
1020
+ ),
758
1021
  exportIntervalMillis: config.metricExportIntervalMillis,
759
1022
  exportTimeoutMillis: Math.min(config.exportTimeoutMillis, config.metricExportIntervalMillis),
760
1023
  cardinalityLimits: { default: 2e3, histogram: 2e3 }
@@ -772,13 +1035,13 @@ function createMeterProvider(config, resource) {
772
1035
  import_api5.metrics.setGlobalMeterProvider(provider);
773
1036
  return provider;
774
1037
  }
775
- function createLoggerProvider(config, resource) {
1038
+ function createLoggerProvider(config, resource, diagnostics) {
776
1039
  if (!config.logs) return void 0;
777
1040
  const exporter = config.exporters?.log ?? new import_exporter_logs_otlp_proto.OTLPLogExporter(exporterConfig(config.endpoints.logs, config));
778
1041
  const provider = new import_sdk_logs.LoggerProvider({
779
1042
  resource,
780
1043
  processors: [new import_sdk_logs.BatchLogRecordProcessor({
781
- exporter,
1044
+ exporter: withExporterDiagnostics(exporter, "logs", diagnostics),
782
1045
  exportTimeoutMillis: config.exportTimeoutMillis,
783
1046
  maxQueueSize: 2048,
784
1047
  maxExportBatchSize: 512
@@ -790,7 +1053,11 @@ function createLoggerProvider(config, resource) {
790
1053
  function observe(options = {}) {
791
1054
  if (activeRuntime?.started) return activeRuntime;
792
1055
  const config = resolveObserveConfig(options);
793
- if (!config.enabled) return new InactiveObserveHandle(config);
1056
+ const diagnostics = new ObserveDiagnostics(config.diagnosticLogging, options.onError);
1057
+ if (!config.enabled) {
1058
+ diagnostics.inactive();
1059
+ return new InactiveObserveHandle(config, diagnostics);
1060
+ }
794
1061
  let tracerProvider;
795
1062
  let meterProvider;
796
1063
  let loggerProvider;
@@ -800,10 +1067,11 @@ function observe(options = {}) {
800
1067
  const instrumentations = [];
801
1068
  try {
802
1069
  const resource = createObserveResource(config);
803
- meterProvider = createMeterProvider(config, resource);
1070
+ meterProvider = createMeterProvider(config, resource, diagnostics);
804
1071
  const meter = meterProvider?.getMeter(SDK_NAME, SDK_VERSION);
805
- tracerProvider = createTraceProvider(config, resource);
806
- loggerProvider = createLoggerProvider(config, resource);
1072
+ const httpRequestMetrics = meter ? new HttpRequestMetrics(meter, config.serviceName) : void 0;
1073
+ tracerProvider = createTraceProvider(config, resource, diagnostics);
1074
+ loggerProvider = createLoggerProvider(config, resource, diagnostics);
807
1075
  runtimeMetrics = meter ? new RuntimeMetrics(meter) : void 0;
808
1076
  runtimeMetrics?.start();
809
1077
  const otelLogger = loggerProvider?.getLogger(SDK_NAME, SDK_VERSION);
@@ -818,7 +1086,6 @@ function observe(options = {}) {
818
1086
  exceptionCapture.start();
819
1087
  if (config.traces || config.metrics) {
820
1088
  const safeHeaders = config.allowedHeaders.filter((header) => !isSensitiveKey(header));
821
- const httpRequestMetrics = meter ? new HttpRequestMetrics(meter, config.serviceName) : void 0;
822
1089
  instrumentations.push(new import_instrumentation_http.HttpInstrumentation({
823
1090
  requireParentforOutgoingSpans: false,
824
1091
  headersToSpanAttributes: {
@@ -862,14 +1129,18 @@ function observe(options = {}) {
862
1129
  tracerProvider,
863
1130
  meterProvider,
864
1131
  loggerProvider,
1132
+ httpRequestMetrics,
865
1133
  runtimeMetrics,
866
1134
  loggerInstrumentation,
867
1135
  exceptionCapture,
868
- instrumentations
1136
+ instrumentations,
1137
+ diagnostics
869
1138
  );
1139
+ diagnostics.activate();
870
1140
  activeRuntime = runtime;
871
1141
  return runtime;
872
- } catch {
1142
+ } catch (error) {
1143
+ diagnostics.failure("sdk", "initialization", error);
873
1144
  loggerInstrumentation?.disable();
874
1145
  runtimeMetrics?.stop();
875
1146
  exceptionCapture?.stop();
@@ -884,7 +1155,9 @@ function observe(options = {}) {
884
1155
  meterProvider?.shutdown(),
885
1156
  loggerProvider?.shutdown()
886
1157
  ].filter((item) => Boolean(item)));
887
- return new InactiveObserveHandle(config);
1158
+ diagnostics.inactive();
1159
+ if (config.failFast) throw toError(error);
1160
+ return new InactiveObserveHandle(config, diagnostics);
888
1161
  }
889
1162
  }
890
1163