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