@atrim/instrument-node 0.5.0-c05e3a1-20251119131235 → 0.5.1-1451fcf-20260105212505

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.
@@ -13,9 +13,9 @@ var zod = require('zod');
13
13
  var exporterTraceOtlpHttp = require('@opentelemetry/exporter-trace-otlp-http');
14
14
  var promises = require('fs/promises');
15
15
  var path = require('path');
16
- var platformNode = require('@effect/platform-node');
17
- var platform = require('@effect/platform');
16
+ var module$1 = require('module');
18
17
 
18
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
19
19
  function _interopNamespace(e) {
20
20
  if (e && e.__esModule) return e;
21
21
  var n = Object.create(null);
@@ -107,11 +107,50 @@ var InstrumentationConfigSchema = zod.z.object({
107
107
  ignore_patterns: zod.z.array(PatternConfigSchema)
108
108
  }),
109
109
  effect: zod.z.object({
110
+ // Enable/disable Effect tracing entirely
111
+ // When false, EffectInstrumentationLive returns Layer.empty
112
+ enabled: zod.z.boolean().default(true),
113
+ // Exporter mode:
114
+ // - "unified": Use global TracerProvider from Node SDK (recommended, enables filtering)
115
+ // - "standalone": Use Effect's own OTLP exporter (bypasses Node SDK filtering)
116
+ exporter: zod.z.enum(["unified", "standalone"]).default("unified"),
110
117
  auto_extract_metadata: zod.z.boolean(),
111
118
  auto_isolation: AutoIsolationConfigSchema.optional()
112
119
  }).optional(),
113
120
  http: HttpFilteringConfigSchema.optional()
114
121
  });
122
+ var defaultConfig = {
123
+ version: "1.0",
124
+ instrumentation: {
125
+ enabled: true,
126
+ logging: "on",
127
+ description: "Default instrumentation configuration",
128
+ instrument_patterns: [
129
+ { pattern: "^app\\.", enabled: true, description: "Application operations" },
130
+ { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
131
+ { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
132
+ ],
133
+ ignore_patterns: [
134
+ { pattern: "^test\\.", description: "Test utilities" },
135
+ { pattern: "^internal\\.", description: "Internal operations" },
136
+ { pattern: "^health\\.", description: "Health checks" }
137
+ ]
138
+ },
139
+ effect: {
140
+ enabled: true,
141
+ exporter: "unified",
142
+ auto_extract_metadata: true
143
+ }
144
+ };
145
+ function parseAndValidateConfig(content) {
146
+ let parsed;
147
+ if (typeof content === "string") {
148
+ parsed = yaml.parse(content);
149
+ } else {
150
+ parsed = content;
151
+ }
152
+ return InstrumentationConfigSchema.parse(parsed);
153
+ }
115
154
  (class extends effect.Data.TaggedError("ConfigError") {
116
155
  get message() {
117
156
  return this.reason;
@@ -289,7 +328,7 @@ var makeConfigLoader = effect.Effect.gen(function* () {
289
328
  })
290
329
  });
291
330
  });
292
- var ConfigLoaderLive = effect.Layer.effect(ConfigLoader, makeConfigLoader);
331
+ effect.Layer.effect(ConfigLoader, makeConfigLoader);
293
332
  var PatternMatcher = class {
294
333
  constructor(config) {
295
334
  __publicField2(this, "ignorePatterns", []);
@@ -453,8 +492,14 @@ var PatternSpanProcessor = class {
453
492
  constructor(config, wrappedProcessor) {
454
493
  __publicField(this, "matcher");
455
494
  __publicField(this, "wrappedProcessor");
495
+ __publicField(this, "httpIgnorePatterns", []);
456
496
  this.matcher = new PatternMatcher(config);
457
497
  this.wrappedProcessor = wrappedProcessor;
498
+ if (config.http?.ignore_incoming_paths) {
499
+ this.httpIgnorePatterns = config.http.ignore_incoming_paths.map(
500
+ (pattern) => new RegExp(pattern)
501
+ );
502
+ }
458
503
  }
459
504
  /**
460
505
  * Called when a span is started
@@ -472,12 +517,40 @@ var PatternSpanProcessor = class {
472
517
  * Called when a span is ended
473
518
  *
474
519
  * This is where we make the final decision on whether to export the span.
520
+ * We check both span name patterns and HTTP path patterns.
475
521
  */
476
522
  onEnd(span) {
477
523
  const spanName = span.name;
478
- if (this.matcher.shouldInstrument(spanName)) {
479
- this.wrappedProcessor.onEnd(span);
524
+ if (!this.matcher.shouldInstrument(spanName)) {
525
+ return;
526
+ }
527
+ if (this.shouldIgnoreHttpSpan(span)) {
528
+ return;
480
529
  }
530
+ this.wrappedProcessor.onEnd(span);
531
+ }
532
+ /**
533
+ * Check if span should be ignored based on HTTP path attributes
534
+ *
535
+ * This checks the span's url.path, http.route, or http.target attributes
536
+ * against the configured http.ignore_incoming_paths patterns.
537
+ *
538
+ * This enables filtering of Effect HTTP spans (and any other HTTP spans)
539
+ * based on path patterns, which is essential for filtering out OTLP
540
+ * endpoint requests like /v1/traces, /v1/logs, /v1/metrics.
541
+ */
542
+ shouldIgnoreHttpSpan(span) {
543
+ if (this.httpIgnorePatterns.length === 0) {
544
+ return false;
545
+ }
546
+ const urlPath = span.attributes["url.path"];
547
+ const httpRoute = span.attributes["http.route"];
548
+ const httpTarget = span.attributes["http.target"];
549
+ const pathToCheck = urlPath || httpRoute || httpTarget;
550
+ if (!pathToCheck) {
551
+ return false;
552
+ }
553
+ return this.httpIgnorePatterns.some((pattern) => pattern.test(pathToCheck));
481
554
  }
482
555
  /**
483
556
  * Shutdown the processor
@@ -706,88 +779,69 @@ var getServiceInfoWithFallback = detectServiceInfo.pipe(
706
779
  })
707
780
  )
708
781
  );
709
- var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
710
- effect.Layer.provide(effect.Layer.mergeAll(platformNode.NodeContext.layer, platform.FetchHttpClient.layer))
711
- );
712
- var cachedLoaderPromise = null;
713
- function getCachedLoader() {
714
- if (!cachedLoaderPromise) {
715
- cachedLoaderPromise = effect.Effect.runPromise(
716
- effect.Effect.gen(function* () {
717
- return yield* ConfigLoader;
718
- }).pipe(effect.Effect.provide(NodeConfigLoaderLive))
719
- );
720
- }
721
- return cachedLoaderPromise;
782
+ async function detectServiceInfoAsync() {
783
+ return effect.Effect.runPromise(getServiceInfoWithFallback);
722
784
  }
723
- function _resetConfigLoaderCache() {
724
- cachedLoaderPromise = null;
785
+ async function getServiceNameAsync() {
786
+ return effect.Effect.runPromise(getServiceName);
725
787
  }
726
- async function loadConfig(uri, options) {
727
- if (options?.cacheTimeout === 0) {
728
- const program = effect.Effect.gen(function* () {
729
- const loader2 = yield* ConfigLoader;
730
- return yield* loader2.loadFromUri(uri);
731
- });
732
- return effect.Effect.runPromise(program.pipe(effect.Effect.provide(NodeConfigLoaderLive)));
788
+ async function getServiceVersionAsync() {
789
+ return effect.Effect.runPromise(getServiceVersion);
790
+ }
791
+ async function loadFromFile(filePath) {
792
+ const { readFile: readFile2 } = await import('fs/promises');
793
+ const content = await readFile2(filePath, "utf-8");
794
+ return parseAndValidateConfig(content);
795
+ }
796
+ async function loadFromUrl(url) {
797
+ const response = await fetch(url);
798
+ if (!response.ok) {
799
+ throw new Error(`Failed to fetch config from ${url}: ${response.statusText}`);
733
800
  }
734
- const loader = await getCachedLoader();
735
- return effect.Effect.runPromise(loader.loadFromUri(uri));
801
+ const content = await response.text();
802
+ return parseAndValidateConfig(content);
803
+ }
804
+ async function loadConfig(uri, _options) {
805
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
806
+ return loadFromUrl(uri);
807
+ }
808
+ if (uri.startsWith("file://")) {
809
+ const filePath = uri.slice(7);
810
+ return loadFromFile(filePath);
811
+ }
812
+ return loadFromFile(uri);
736
813
  }
737
814
  async function loadConfigFromInline(content) {
738
- const loader = await getCachedLoader();
739
- return effect.Effect.runPromise(loader.loadFromInline(content));
815
+ return parseAndValidateConfig(content);
740
816
  }
741
- function getDefaultConfig() {
742
- return {
743
- version: "1.0",
744
- instrumentation: {
745
- enabled: true,
746
- logging: "on",
747
- description: "Default instrumentation configuration",
748
- instrument_patterns: [
749
- { pattern: "^app\\.", enabled: true, description: "Application operations" },
750
- { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
751
- { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
752
- ],
753
- ignore_patterns: [
754
- { pattern: "^test\\.", description: "Test utilities" },
755
- { pattern: "^internal\\.", description: "Internal operations" },
756
- { pattern: "^health\\.", description: "Health checks" }
757
- ]
758
- },
759
- effect: {
760
- auto_extract_metadata: true
761
- }
762
- };
817
+ function _resetConfigLoaderCache() {
763
818
  }
764
819
  async function loadConfigWithOptions(options = {}) {
765
- const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
766
820
  if (options.config) {
767
821
  return loadConfigFromInline(options.config);
768
822
  }
769
823
  const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
770
824
  if (envConfigPath) {
771
- return loadConfig(envConfigPath, loadOptions);
825
+ return loadConfig(envConfigPath);
772
826
  }
773
827
  if (options.configUrl) {
774
- return loadConfig(options.configUrl, loadOptions);
828
+ return loadConfig(options.configUrl);
775
829
  }
776
830
  if (options.configPath) {
777
- return loadConfig(options.configPath, loadOptions);
831
+ return loadConfig(options.configPath);
778
832
  }
779
833
  const { existsSync } = await import('fs');
780
834
  const { join: join2 } = await import('path');
781
835
  const defaultPath = join2(process.cwd(), "instrumentation.yaml");
782
836
  if (existsSync(defaultPath)) {
783
- return loadConfig(defaultPath, loadOptions);
837
+ return loadConfig(defaultPath);
784
838
  }
785
- return getDefaultConfig();
839
+ return defaultConfig;
786
840
  }
787
841
 
788
842
  // src/core/sdk-initializer.ts
789
843
  var sdkInstance = null;
790
- var initializationDeferred = null;
844
+ var initializationPromise = null;
791
845
  function buildHttpInstrumentationConfig(options, config, _otlpEndpoint) {
792
846
  const httpConfig = { enabled: true };
793
847
  const programmaticPatterns = options.http?.ignoreOutgoingUrls || [];
@@ -903,38 +957,27 @@ function isTracingAlreadyInitialized() {
903
957
  return false;
904
958
  }
905
959
  }
906
- var initializeSdkEffect = (options = {}) => effect.Effect.gen(function* () {
960
+ async function initializeSdk(options = {}) {
907
961
  if (sdkInstance) {
908
962
  logger.warn("@atrim/instrumentation: SDK already initialized. Returning existing instance.");
909
963
  return sdkInstance;
910
964
  }
911
- if (initializationDeferred) {
965
+ if (initializationPromise) {
912
966
  logger.log(
913
- "@atrim/instrumentation: SDK initialization in progress, waiting for completion..."
967
+ "@atrim/instrumentation: SDK already initialized, waiting for initialization to complete..."
914
968
  );
915
- return yield* effect.Deferred.await(initializationDeferred);
916
- }
917
- const deferred = yield* effect.Deferred.make();
918
- initializationDeferred = deferred;
919
- const result = yield* performInitializationEffect(options).pipe(
920
- effect.Effect.tap((sdk) => effect.Deferred.succeed(deferred, sdk)),
921
- effect.Effect.tapError((error) => effect.Deferred.fail(deferred, error)),
922
- effect.Effect.ensuring(
923
- effect.Effect.sync(() => {
924
- initializationDeferred = null;
925
- })
926
- )
927
- );
928
- return result;
929
- });
930
- var performInitializationEffect = (options) => effect.Effect.gen(function* () {
931
- const config = yield* effect.Effect.tryPromise({
932
- try: () => loadConfigWithOptions(options),
933
- catch: (error) => new InitializationError2({
934
- reason: "Failed to load configuration",
935
- cause: error
936
- })
937
- });
969
+ return initializationPromise;
970
+ }
971
+ initializationPromise = performInitialization(options);
972
+ try {
973
+ const result = await initializationPromise;
974
+ return result;
975
+ } finally {
976
+ initializationPromise = null;
977
+ }
978
+ }
979
+ async function performInitialization(options) {
980
+ const config = await loadConfigWithOptions(options);
938
981
  const loggingLevel = config.instrumentation.logging || "on";
939
982
  logger.setLevel(loggingLevel);
940
983
  const alreadyInitialized = isTracingAlreadyInitialized();
@@ -950,25 +993,14 @@ var performInitializationEffect = (options) => effect.Effect.gen(function* () {
950
993
  logger.log("");
951
994
  return null;
952
995
  }
953
- const serviceInfo = yield* detectServiceInfo.pipe(
954
- effect.Effect.catchAll(
955
- () => effect.Effect.succeed({
956
- name: "unknown-service",
957
- version: void 0
958
- })
959
- )
960
- );
996
+ const serviceInfo = await detectServiceInfoAsync();
961
997
  const serviceName = options.serviceName || serviceInfo.name;
962
998
  const serviceVersion = options.serviceVersion || serviceInfo.version;
963
- const rawExporter = yield* effect.Effect.sync(() => createOtlpExporter(options.otlp));
964
- const exporter = yield* effect.Effect.sync(() => new SafeSpanExporter(rawExporter));
999
+ const rawExporter = createOtlpExporter(options.otlp);
1000
+ const exporter = new SafeSpanExporter(rawExporter);
965
1001
  const useSimpleProcessor = process.env.NODE_ENV === "test" || process.env.OTEL_USE_SIMPLE_PROCESSOR === "true";
966
- const baseProcessor = yield* effect.Effect.sync(
967
- () => useSimpleProcessor ? new sdkTraceBase.SimpleSpanProcessor(exporter) : new sdkTraceBase.BatchSpanProcessor(exporter)
968
- );
969
- const patternProcessor = yield* effect.Effect.sync(
970
- () => new PatternSpanProcessor(config, baseProcessor)
971
- );
1002
+ const baseProcessor = useSimpleProcessor ? new sdkTraceBase.SimpleSpanProcessor(exporter) : new sdkTraceBase.BatchSpanProcessor(exporter);
1003
+ const patternProcessor = new PatternSpanProcessor(config, baseProcessor);
972
1004
  const instrumentations = [];
973
1005
  const hasWebFramework = hasWebFrameworkInstalled();
974
1006
  const enableAutoInstrumentation = shouldEnableAutoInstrumentation(
@@ -981,11 +1013,15 @@ var performInitializationEffect = (options) => effect.Effect.gen(function* () {
981
1013
  const undiciConfig = buildUndiciInstrumentationConfig(options, config);
982
1014
  instrumentations.push(
983
1015
  ...autoInstrumentationsNode.getNodeAutoInstrumentations({
1016
+ // Enable HTTP instrumentation with filtering (for http/https modules)
984
1017
  "@opentelemetry/instrumentation-http": httpConfig,
1018
+ // Enable undici instrumentation with filtering (for fetch API)
985
1019
  "@opentelemetry/instrumentation-undici": undiciConfig,
1020
+ // Enable web framework instrumentations
986
1021
  "@opentelemetry/instrumentation-express": { enabled: true },
987
1022
  "@opentelemetry/instrumentation-fastify": { enabled: true },
988
1023
  "@opentelemetry/instrumentation-koa": { enabled: true },
1024
+ // Disable noisy instrumentations by default
989
1025
  "@opentelemetry/instrumentation-fs": { enabled: false },
990
1026
  "@opentelemetry/instrumentation-dns": { enabled: false }
991
1027
  })
@@ -1013,20 +1049,18 @@ var performInitializationEffect = (options) => effect.Effect.gen(function* () {
1013
1049
  serviceName,
1014
1050
  ...serviceVersion && { serviceVersion },
1015
1051
  instrumentations,
1052
+ // Allow advanced overrides
1016
1053
  ...options.sdk
1017
1054
  };
1018
- const sdk = yield* effect.Effect.sync(() => {
1019
- const s = new sdkNode.NodeSDK(sdkConfig);
1020
- s.start();
1021
- return s;
1022
- });
1055
+ const sdk = new sdkNode.NodeSDK(sdkConfig);
1056
+ sdk.start();
1023
1057
  sdkInstance = sdk;
1024
1058
  if (!options.disableAutoShutdown) {
1025
- yield* effect.Effect.sync(() => registerShutdownHandlers(sdk));
1059
+ registerShutdownHandlers(sdk);
1026
1060
  }
1027
1061
  logInitialization(config, serviceName, serviceVersion, options, enableAutoInstrumentation);
1028
1062
  return sdk;
1029
- });
1063
+ }
1030
1064
  function getSdkInstance() {
1031
1065
  return sdkInstance;
1032
1066
  }
@@ -1039,7 +1073,7 @@ async function shutdownSdk() {
1039
1073
  }
1040
1074
  function resetSdk() {
1041
1075
  sdkInstance = null;
1042
- initializationDeferred = null;
1076
+ initializationPromise = null;
1043
1077
  }
1044
1078
  function registerShutdownHandlers(sdk) {
1045
1079
  const shutdown = async (signal) => {
@@ -1094,10 +1128,63 @@ function logInitialization(config, serviceName, serviceVersion, options, autoIns
1094
1128
  logger.log(` - OTLP endpoint: ${endpoint}`);
1095
1129
  logger.log("");
1096
1130
  }
1131
+ var require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
1132
+ function validateOpenTelemetryApi() {
1133
+ try {
1134
+ require2.resolve("@opentelemetry/api");
1135
+ } catch {
1136
+ throw new Error(
1137
+ "@atrim/instrument-node requires @opentelemetry/api as a peer dependency.\n\nInstall it with:\n npm install @opentelemetry/api\n\nOr with your preferred package manager:\n pnpm add @opentelemetry/api\n yarn add @opentelemetry/api\n bun add @opentelemetry/api"
1138
+ );
1139
+ }
1140
+ }
1141
+ function validateEffectDependencies() {
1142
+ const packages = ["effect", "@effect/opentelemetry", "@effect/platform"];
1143
+ for (const pkg of packages) {
1144
+ try {
1145
+ require2.resolve(pkg);
1146
+ } catch {
1147
+ return false;
1148
+ }
1149
+ }
1150
+ return true;
1151
+ }
1152
+ var validateDependencies = effect.Effect.try({
1153
+ try: () => validateOpenTelemetryApi(),
1154
+ catch: (error) => new InitializationError2({
1155
+ reason: error instanceof Error ? error.message : "Dependency validation failed",
1156
+ cause: error
1157
+ })
1158
+ });
1159
+ effect.Effect.sync(() => validateEffectDependencies());
1097
1160
 
1098
1161
  // src/api.ts
1099
- var initializeInstrumentation = (options = {}) => effect.Effect.gen(function* () {
1100
- const sdk = yield* initializeSdkEffect(options);
1162
+ async function initializeInstrumentation(options = {}) {
1163
+ validateOpenTelemetryApi();
1164
+ const sdk = await initializeSdk(options);
1165
+ if (sdk) {
1166
+ const config = await loadConfigWithOptions(options);
1167
+ initializePatternMatcher(config);
1168
+ }
1169
+ return sdk;
1170
+ }
1171
+ async function initializePatternMatchingOnly(options = {}) {
1172
+ const config = await loadConfigWithOptions(options);
1173
+ initializePatternMatcher(config);
1174
+ logger.log("@atrim/instrumentation: Pattern matching initialized (legacy mode)");
1175
+ logger.log(
1176
+ " Note: NodeSDK is not initialized. Use initializeInstrumentation() for complete setup."
1177
+ );
1178
+ }
1179
+ var initializeInstrumentationEffect = (options = {}) => effect.Effect.gen(function* () {
1180
+ yield* validateDependencies;
1181
+ const sdk = yield* effect.Effect.tryPromise({
1182
+ try: () => initializeSdk(options),
1183
+ catch: (error) => new InitializationError2({
1184
+ reason: "SDK initialization failed",
1185
+ cause: error
1186
+ })
1187
+ });
1101
1188
  if (sdk) {
1102
1189
  yield* effect.Effect.tryPromise({
1103
1190
  try: () => loadConfigWithOptions(options),
@@ -1115,7 +1202,7 @@ var initializeInstrumentation = (options = {}) => effect.Effect.gen(function* ()
1115
1202
  }
1116
1203
  return sdk;
1117
1204
  });
1118
- var initializePatternMatchingOnly = (options = {}) => effect.Effect.gen(function* () {
1205
+ var initializePatternMatchingOnlyEffect = (options = {}) => effect.Effect.gen(function* () {
1119
1206
  const config = yield* effect.Effect.tryPromise({
1120
1207
  try: () => loadConfigWithOptions(options),
1121
1208
  catch: (error) => new ConfigError2({
@@ -1125,7 +1212,7 @@ var initializePatternMatchingOnly = (options = {}) => effect.Effect.gen(function
1125
1212
  });
1126
1213
  yield* effect.Effect.sync(() => {
1127
1214
  initializePatternMatcher(config);
1128
- logger.log("@atrim/instrumentation: Pattern matching initialized (pattern-only mode)");
1215
+ logger.log("@atrim/instrumentation: Pattern matching initialized (legacy mode)");
1129
1216
  logger.log(
1130
1217
  " Note: NodeSDK is not initialized. Use initializeInstrumentation() for complete setup."
1131
1218
  );
@@ -1241,15 +1328,20 @@ exports.annotateDbQuery = annotateDbQuery;
1241
1328
  exports.annotateHttpRequest = annotateHttpRequest;
1242
1329
  exports.clearConfigCache = _resetConfigLoaderCache;
1243
1330
  exports.createOtlpExporter = createOtlpExporter;
1244
- exports.detectServiceInfo = detectServiceInfo;
1331
+ exports.detectServiceInfo = detectServiceInfoAsync;
1332
+ exports.detectServiceInfoEffect = detectServiceInfo;
1245
1333
  exports.getOtlpEndpoint = getOtlpEndpoint;
1246
1334
  exports.getPatternMatcher = getPatternMatcher;
1247
1335
  exports.getSdkInstance = getSdkInstance;
1248
1336
  exports.getServiceInfoWithFallback = getServiceInfoWithFallback;
1249
- exports.getServiceName = getServiceName;
1250
- exports.getServiceVersion = getServiceVersion;
1337
+ exports.getServiceName = getServiceNameAsync;
1338
+ exports.getServiceNameEffect = getServiceName;
1339
+ exports.getServiceVersion = getServiceVersionAsync;
1340
+ exports.getServiceVersionEffect = getServiceVersion;
1251
1341
  exports.initializeInstrumentation = initializeInstrumentation;
1342
+ exports.initializeInstrumentationEffect = initializeInstrumentationEffect;
1252
1343
  exports.initializePatternMatchingOnly = initializePatternMatchingOnly;
1344
+ exports.initializePatternMatchingOnlyEffect = initializePatternMatchingOnlyEffect;
1253
1345
  exports.loadConfig = loadConfig;
1254
1346
  exports.loadConfigFromInline = loadConfigFromInline;
1255
1347
  exports.loadConfigWithOptions = loadConfigWithOptions;