@atrim/instrument-node 0.5.0 → 0.5.1-3a86b84-20260105170223

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.
@@ -46,35 +46,33 @@ declare function createOtlpExporter(options?: OtlpExporterOptions): OTLPTraceExp
46
46
  declare function getOtlpEndpoint(options?: OtlpExporterOptions): string;
47
47
 
48
48
  /**
49
- * Node.js configuration loader using Effect Platform
49
+ * Node.js configuration loader
50
50
  *
51
- * Provides FileSystem and HttpClient layers for the core ConfigLoader service
51
+ * Provides configuration loading using native Node.js APIs (fs, fetch)
52
+ * This module doesn't require Effect Platform, making it work without Effect installed.
52
53
  */
53
54
 
54
55
  /**
55
- * Reset the cached loader (for testing purposes)
56
- * @internal
57
- */
58
- declare function _resetConfigLoaderCache(): void;
59
- /**
60
- * Load configuration from URI (Promise-based convenience API)
61
- *
62
- * Automatically provides Node.js platform layers (FileSystem + HttpClient)
56
+ * Load configuration from URI (file path or URL)
63
57
  *
64
58
  * @param uri - Configuration URI (file://, http://, https://, or relative path)
65
- * @param options - Optional loading options (e.g., to disable caching)
66
59
  * @returns Promise that resolves to validated configuration
67
60
  */
68
- declare function loadConfig(uri: string, options?: {
61
+ declare function loadConfig(uri: string, _options?: {
69
62
  cacheTimeout?: number;
70
63
  }): Promise<InstrumentationConfig>;
71
64
  /**
72
- * Load configuration from inline content (Promise-based convenience API)
65
+ * Load configuration from inline content
73
66
  *
74
67
  * @param content - YAML string, JSON string, or plain object
75
68
  * @returns Promise that resolves to validated configuration
76
69
  */
77
70
  declare function loadConfigFromInline(content: string | unknown): Promise<InstrumentationConfig>;
71
+ /**
72
+ * Reset the config loader cache (no-op for native implementation)
73
+ * @internal
74
+ */
75
+ declare function _resetConfigLoaderCache(): void;
78
76
  /**
79
77
  * Legacy options interface for backward compatibility
80
78
  */
@@ -548,6 +546,10 @@ declare function getServiceVersionAsync(): Promise<string | undefined>;
548
546
  * This processor filters spans based on configured patterns before they are exported.
549
547
  * It wraps another processor (typically BatchSpanProcessor) and only forwards spans
550
548
  * that match the instrumentation patterns.
549
+ *
550
+ * Filtering is applied at two levels:
551
+ * 1. Span name patterns (instrument_patterns / ignore_patterns)
552
+ * 2. HTTP path patterns (http.ignore_incoming_paths) - for HTTP spans
551
553
  */
552
554
 
553
555
  /**
@@ -570,6 +572,7 @@ declare function getServiceVersionAsync(): Promise<string | undefined>;
570
572
  declare class PatternSpanProcessor implements SpanProcessor {
571
573
  private matcher;
572
574
  private wrappedProcessor;
575
+ private httpIgnorePatterns;
573
576
  constructor(config: InstrumentationConfig, wrappedProcessor: SpanProcessor);
574
577
  /**
575
578
  * Called when a span is started
@@ -582,8 +585,20 @@ declare class PatternSpanProcessor implements SpanProcessor {
582
585
  * Called when a span is ended
583
586
  *
584
587
  * This is where we make the final decision on whether to export the span.
588
+ * We check both span name patterns and HTTP path patterns.
585
589
  */
586
590
  onEnd(span: ReadableSpan): void;
591
+ /**
592
+ * Check if span should be ignored based on HTTP path attributes
593
+ *
594
+ * This checks the span's url.path, http.route, or http.target attributes
595
+ * against the configured http.ignore_incoming_paths patterns.
596
+ *
597
+ * This enables filtering of Effect HTTP spans (and any other HTTP spans)
598
+ * based on path patterns, which is essential for filtering out OTLP
599
+ * endpoint requests like /v1/traces, /v1/logs, /v1/metrics.
600
+ */
601
+ private shouldIgnoreHttpSpan;
587
602
  /**
588
603
  * Shutdown the processor
589
604
  */
@@ -46,35 +46,33 @@ declare function createOtlpExporter(options?: OtlpExporterOptions): OTLPTraceExp
46
46
  declare function getOtlpEndpoint(options?: OtlpExporterOptions): string;
47
47
 
48
48
  /**
49
- * Node.js configuration loader using Effect Platform
49
+ * Node.js configuration loader
50
50
  *
51
- * Provides FileSystem and HttpClient layers for the core ConfigLoader service
51
+ * Provides configuration loading using native Node.js APIs (fs, fetch)
52
+ * This module doesn't require Effect Platform, making it work without Effect installed.
52
53
  */
53
54
 
54
55
  /**
55
- * Reset the cached loader (for testing purposes)
56
- * @internal
57
- */
58
- declare function _resetConfigLoaderCache(): void;
59
- /**
60
- * Load configuration from URI (Promise-based convenience API)
61
- *
62
- * Automatically provides Node.js platform layers (FileSystem + HttpClient)
56
+ * Load configuration from URI (file path or URL)
63
57
  *
64
58
  * @param uri - Configuration URI (file://, http://, https://, or relative path)
65
- * @param options - Optional loading options (e.g., to disable caching)
66
59
  * @returns Promise that resolves to validated configuration
67
60
  */
68
- declare function loadConfig(uri: string, options?: {
61
+ declare function loadConfig(uri: string, _options?: {
69
62
  cacheTimeout?: number;
70
63
  }): Promise<InstrumentationConfig>;
71
64
  /**
72
- * Load configuration from inline content (Promise-based convenience API)
65
+ * Load configuration from inline content
73
66
  *
74
67
  * @param content - YAML string, JSON string, or plain object
75
68
  * @returns Promise that resolves to validated configuration
76
69
  */
77
70
  declare function loadConfigFromInline(content: string | unknown): Promise<InstrumentationConfig>;
71
+ /**
72
+ * Reset the config loader cache (no-op for native implementation)
73
+ * @internal
74
+ */
75
+ declare function _resetConfigLoaderCache(): void;
78
76
  /**
79
77
  * Legacy options interface for backward compatibility
80
78
  */
@@ -548,6 +546,10 @@ declare function getServiceVersionAsync(): Promise<string | undefined>;
548
546
  * This processor filters spans based on configured patterns before they are exported.
549
547
  * It wraps another processor (typically BatchSpanProcessor) and only forwards spans
550
548
  * that match the instrumentation patterns.
549
+ *
550
+ * Filtering is applied at two levels:
551
+ * 1. Span name patterns (instrument_patterns / ignore_patterns)
552
+ * 2. HTTP path patterns (http.ignore_incoming_paths) - for HTTP spans
551
553
  */
552
554
 
553
555
  /**
@@ -570,6 +572,7 @@ declare function getServiceVersionAsync(): Promise<string | undefined>;
570
572
  declare class PatternSpanProcessor implements SpanProcessor {
571
573
  private matcher;
572
574
  private wrappedProcessor;
575
+ private httpIgnorePatterns;
573
576
  constructor(config: InstrumentationConfig, wrappedProcessor: SpanProcessor);
574
577
  /**
575
578
  * Called when a span is started
@@ -582,8 +585,20 @@ declare class PatternSpanProcessor implements SpanProcessor {
582
585
  * Called when a span is ended
583
586
  *
584
587
  * This is where we make the final decision on whether to export the span.
588
+ * We check both span name patterns and HTTP path patterns.
585
589
  */
586
590
  onEnd(span: ReadableSpan): void;
591
+ /**
592
+ * Check if span should be ignored based on HTTP path attributes
593
+ *
594
+ * This checks the span's url.path, http.route, or http.target attributes
595
+ * against the configured http.ignore_incoming_paths patterns.
596
+ *
597
+ * This enables filtering of Effect HTTP spans (and any other HTTP spans)
598
+ * based on path patterns, which is essential for filtering out OTLP
599
+ * endpoint requests like /v1/traces, /v1/logs, /v1/metrics.
600
+ */
601
+ private shouldIgnoreHttpSpan;
587
602
  /**
588
603
  * Shutdown the processor
589
604
  */
@@ -11,8 +11,7 @@ import { z } from 'zod';
11
11
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
12
12
  import { readFile } from 'fs/promises';
13
13
  import { join } from 'path';
14
- import { NodeContext } from '@effect/platform-node';
15
- import { FetchHttpClient } from '@effect/platform';
14
+ import { createRequire } from 'module';
16
15
 
17
16
  var __defProp = Object.defineProperty;
18
17
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -59,7 +58,20 @@ var HttpFilteringConfigSchema = z.object({
59
58
  // Patterns to ignore for incoming HTTP requests (string patterns only in YAML)
60
59
  ignore_incoming_paths: z.array(z.string()).optional(),
61
60
  // Require parent span for outgoing requests (prevents root spans for HTTP calls)
62
- require_parent_for_outgoing_spans: z.boolean().optional()
61
+ require_parent_for_outgoing_spans: z.boolean().optional(),
62
+ // Trace context propagation configuration
63
+ // Controls which cross-origin requests receive W3C Trace Context headers (traceparent, tracestate)
64
+ propagate_trace_context: z.object({
65
+ // Strategy for trace propagation
66
+ // - "all": Propagate to all cross-origin requests (may cause CORS errors)
67
+ // - "none": Never propagate trace headers
68
+ // - "same-origin": Only propagate to same-origin requests (default, safe)
69
+ // - "patterns": Propagate based on include_urls patterns
70
+ strategy: z.enum(["all", "none", "same-origin", "patterns"]).default("same-origin"),
71
+ // URL patterns to include when strategy is "patterns"
72
+ // Supports regex patterns (e.g., "^https://api\\.myapp\\.com")
73
+ include_urls: z.array(z.string()).optional()
74
+ }).optional()
63
75
  });
64
76
  var InstrumentationConfigSchema = z.object({
65
77
  version: z.string(),
@@ -71,11 +83,50 @@ var InstrumentationConfigSchema = z.object({
71
83
  ignore_patterns: z.array(PatternConfigSchema)
72
84
  }),
73
85
  effect: z.object({
86
+ // Enable/disable Effect tracing entirely
87
+ // When false, EffectInstrumentationLive returns Layer.empty
88
+ enabled: z.boolean().default(true),
89
+ // Exporter mode:
90
+ // - "unified": Use global TracerProvider from Node SDK (recommended, enables filtering)
91
+ // - "standalone": Use Effect's own OTLP exporter (bypasses Node SDK filtering)
92
+ exporter: z.enum(["unified", "standalone"]).default("unified"),
74
93
  auto_extract_metadata: z.boolean(),
75
94
  auto_isolation: AutoIsolationConfigSchema.optional()
76
95
  }).optional(),
77
96
  http: HttpFilteringConfigSchema.optional()
78
97
  });
98
+ var defaultConfig = {
99
+ version: "1.0",
100
+ instrumentation: {
101
+ enabled: true,
102
+ logging: "on",
103
+ description: "Default instrumentation configuration",
104
+ instrument_patterns: [
105
+ { pattern: "^app\\.", enabled: true, description: "Application operations" },
106
+ { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
107
+ { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
108
+ ],
109
+ ignore_patterns: [
110
+ { pattern: "^test\\.", description: "Test utilities" },
111
+ { pattern: "^internal\\.", description: "Internal operations" },
112
+ { pattern: "^health\\.", description: "Health checks" }
113
+ ]
114
+ },
115
+ effect: {
116
+ enabled: true,
117
+ exporter: "unified",
118
+ auto_extract_metadata: true
119
+ }
120
+ };
121
+ function parseAndValidateConfig(content) {
122
+ let parsed;
123
+ if (typeof content === "string") {
124
+ parsed = parse(content);
125
+ } else {
126
+ parsed = content;
127
+ }
128
+ return InstrumentationConfigSchema.parse(parsed);
129
+ }
79
130
  (class extends Data.TaggedError("ConfigError") {
80
131
  get message() {
81
132
  return this.reason;
@@ -253,7 +304,7 @@ var makeConfigLoader = Effect.gen(function* () {
253
304
  })
254
305
  });
255
306
  });
256
- var ConfigLoaderLive = Layer.effect(ConfigLoader, makeConfigLoader);
307
+ Layer.effect(ConfigLoader, makeConfigLoader);
257
308
  var PatternMatcher = class {
258
309
  constructor(config) {
259
310
  __publicField2(this, "ignorePatterns", []);
@@ -417,8 +468,14 @@ var PatternSpanProcessor = class {
417
468
  constructor(config, wrappedProcessor) {
418
469
  __publicField(this, "matcher");
419
470
  __publicField(this, "wrappedProcessor");
471
+ __publicField(this, "httpIgnorePatterns", []);
420
472
  this.matcher = new PatternMatcher(config);
421
473
  this.wrappedProcessor = wrappedProcessor;
474
+ if (config.http?.ignore_incoming_paths) {
475
+ this.httpIgnorePatterns = config.http.ignore_incoming_paths.map(
476
+ (pattern) => new RegExp(pattern)
477
+ );
478
+ }
422
479
  }
423
480
  /**
424
481
  * Called when a span is started
@@ -436,12 +493,40 @@ var PatternSpanProcessor = class {
436
493
  * Called when a span is ended
437
494
  *
438
495
  * This is where we make the final decision on whether to export the span.
496
+ * We check both span name patterns and HTTP path patterns.
439
497
  */
440
498
  onEnd(span) {
441
499
  const spanName = span.name;
442
- if (this.matcher.shouldInstrument(spanName)) {
443
- this.wrappedProcessor.onEnd(span);
500
+ if (!this.matcher.shouldInstrument(spanName)) {
501
+ return;
502
+ }
503
+ if (this.shouldIgnoreHttpSpan(span)) {
504
+ return;
444
505
  }
506
+ this.wrappedProcessor.onEnd(span);
507
+ }
508
+ /**
509
+ * Check if span should be ignored based on HTTP path attributes
510
+ *
511
+ * This checks the span's url.path, http.route, or http.target attributes
512
+ * against the configured http.ignore_incoming_paths patterns.
513
+ *
514
+ * This enables filtering of Effect HTTP spans (and any other HTTP spans)
515
+ * based on path patterns, which is essential for filtering out OTLP
516
+ * endpoint requests like /v1/traces, /v1/logs, /v1/metrics.
517
+ */
518
+ shouldIgnoreHttpSpan(span) {
519
+ if (this.httpIgnorePatterns.length === 0) {
520
+ return false;
521
+ }
522
+ const urlPath = span.attributes["url.path"];
523
+ const httpRoute = span.attributes["http.route"];
524
+ const httpTarget = span.attributes["http.target"];
525
+ const pathToCheck = urlPath || httpRoute || httpTarget;
526
+ if (!pathToCheck) {
527
+ return false;
528
+ }
529
+ return this.httpIgnorePatterns.some((pattern) => pattern.test(pathToCheck));
445
530
  }
446
531
  /**
447
532
  * Shutdown the processor
@@ -679,83 +764,55 @@ async function getServiceNameAsync() {
679
764
  async function getServiceVersionAsync() {
680
765
  return Effect.runPromise(getServiceVersion);
681
766
  }
682
- var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
683
- Layer.provide(Layer.mergeAll(NodeContext.layer, FetchHttpClient.layer))
684
- );
685
- var cachedLoaderPromise = null;
686
- function getCachedLoader() {
687
- if (!cachedLoaderPromise) {
688
- cachedLoaderPromise = Effect.runPromise(
689
- Effect.gen(function* () {
690
- return yield* ConfigLoader;
691
- }).pipe(Effect.provide(NodeConfigLoaderLive))
692
- );
693
- }
694
- return cachedLoaderPromise;
767
+ async function loadFromFile(filePath) {
768
+ const { readFile: readFile2 } = await import('fs/promises');
769
+ const content = await readFile2(filePath, "utf-8");
770
+ return parseAndValidateConfig(content);
695
771
  }
696
- function _resetConfigLoaderCache() {
697
- cachedLoaderPromise = null;
772
+ async function loadFromUrl(url) {
773
+ const response = await fetch(url);
774
+ if (!response.ok) {
775
+ throw new Error(`Failed to fetch config from ${url}: ${response.statusText}`);
776
+ }
777
+ const content = await response.text();
778
+ return parseAndValidateConfig(content);
698
779
  }
699
- async function loadConfig(uri, options) {
700
- if (options?.cacheTimeout === 0) {
701
- const program = Effect.gen(function* () {
702
- const loader2 = yield* ConfigLoader;
703
- return yield* loader2.loadFromUri(uri);
704
- });
705
- return Effect.runPromise(program.pipe(Effect.provide(NodeConfigLoaderLive)));
780
+ async function loadConfig(uri, _options) {
781
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
782
+ return loadFromUrl(uri);
783
+ }
784
+ if (uri.startsWith("file://")) {
785
+ const filePath = uri.slice(7);
786
+ return loadFromFile(filePath);
706
787
  }
707
- const loader = await getCachedLoader();
708
- return Effect.runPromise(loader.loadFromUri(uri));
788
+ return loadFromFile(uri);
709
789
  }
710
790
  async function loadConfigFromInline(content) {
711
- const loader = await getCachedLoader();
712
- return Effect.runPromise(loader.loadFromInline(content));
791
+ return parseAndValidateConfig(content);
713
792
  }
714
- function getDefaultConfig() {
715
- return {
716
- version: "1.0",
717
- instrumentation: {
718
- enabled: true,
719
- logging: "on",
720
- description: "Default instrumentation configuration",
721
- instrument_patterns: [
722
- { pattern: "^app\\.", enabled: true, description: "Application operations" },
723
- { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
724
- { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
725
- ],
726
- ignore_patterns: [
727
- { pattern: "^test\\.", description: "Test utilities" },
728
- { pattern: "^internal\\.", description: "Internal operations" },
729
- { pattern: "^health\\.", description: "Health checks" }
730
- ]
731
- },
732
- effect: {
733
- auto_extract_metadata: true
734
- }
735
- };
793
+ function _resetConfigLoaderCache() {
736
794
  }
737
795
  async function loadConfigWithOptions(options = {}) {
738
- const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
739
796
  if (options.config) {
740
797
  return loadConfigFromInline(options.config);
741
798
  }
742
799
  const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
743
800
  if (envConfigPath) {
744
- return loadConfig(envConfigPath, loadOptions);
801
+ return loadConfig(envConfigPath);
745
802
  }
746
803
  if (options.configUrl) {
747
- return loadConfig(options.configUrl, loadOptions);
804
+ return loadConfig(options.configUrl);
748
805
  }
749
806
  if (options.configPath) {
750
- return loadConfig(options.configPath, loadOptions);
807
+ return loadConfig(options.configPath);
751
808
  }
752
809
  const { existsSync } = await import('fs');
753
810
  const { join: join2 } = await import('path');
754
811
  const defaultPath = join2(process.cwd(), "instrumentation.yaml");
755
812
  if (existsSync(defaultPath)) {
756
- return loadConfig(defaultPath, loadOptions);
813
+ return loadConfig(defaultPath);
757
814
  }
758
- return getDefaultConfig();
815
+ return defaultConfig;
759
816
  }
760
817
 
761
818
  // src/core/sdk-initializer.ts
@@ -1047,9 +1104,39 @@ function logInitialization(config, serviceName, serviceVersion, options, autoIns
1047
1104
  logger.log(` - OTLP endpoint: ${endpoint}`);
1048
1105
  logger.log("");
1049
1106
  }
1107
+ var require2 = createRequire(import.meta.url);
1108
+ function validateOpenTelemetryApi() {
1109
+ try {
1110
+ require2.resolve("@opentelemetry/api");
1111
+ } catch {
1112
+ throw new Error(
1113
+ "@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"
1114
+ );
1115
+ }
1116
+ }
1117
+ function validateEffectDependencies() {
1118
+ const packages = ["effect", "@effect/opentelemetry", "@effect/platform"];
1119
+ for (const pkg of packages) {
1120
+ try {
1121
+ require2.resolve(pkg);
1122
+ } catch {
1123
+ return false;
1124
+ }
1125
+ }
1126
+ return true;
1127
+ }
1128
+ var validateDependencies = Effect.try({
1129
+ try: () => validateOpenTelemetryApi(),
1130
+ catch: (error) => new InitializationError2({
1131
+ reason: error instanceof Error ? error.message : "Dependency validation failed",
1132
+ cause: error
1133
+ })
1134
+ });
1135
+ Effect.sync(() => validateEffectDependencies());
1050
1136
 
1051
1137
  // src/api.ts
1052
1138
  async function initializeInstrumentation(options = {}) {
1139
+ validateOpenTelemetryApi();
1053
1140
  const sdk = await initializeSdk(options);
1054
1141
  if (sdk) {
1055
1142
  const config = await loadConfigWithOptions(options);
@@ -1066,6 +1153,7 @@ async function initializePatternMatchingOnly(options = {}) {
1066
1153
  );
1067
1154
  }
1068
1155
  var initializeInstrumentationEffect = (options = {}) => Effect.gen(function* () {
1156
+ yield* validateDependencies;
1069
1157
  const sdk = yield* Effect.tryPromise({
1070
1158
  try: () => initializeSdk(options),
1071
1159
  catch: (error) => new InitializationError2({