@atrim/instrument-node 0.5.2-dev.ac2fbfe.20251221205322 → 0.7.0-14fdea7-20260108225522

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);
@@ -76,6 +76,69 @@ var AutoIsolationConfigSchema = zod.z.object({
76
76
  add_metadata: zod.z.boolean().default(true)
77
77
  }).default({})
78
78
  });
79
+ var SpanNamingRuleSchema = zod.z.object({
80
+ // Match criteria (all specified criteria must match)
81
+ match: zod.z.object({
82
+ // Regex pattern to match file path
83
+ file: zod.z.string().optional(),
84
+ // Regex pattern to match function name
85
+ function: zod.z.string().optional(),
86
+ // Regex pattern to match module name
87
+ module: zod.z.string().optional()
88
+ }),
89
+ // Span name template with variables:
90
+ // {fiber_id} - Fiber ID
91
+ // {function} - Function name
92
+ // {module} - Module name
93
+ // {file} - File path
94
+ // {line} - Line number
95
+ // {operator} - Effect operator (gen, all, forEach, etc.)
96
+ // {match:field:N} - Captured regex group from match
97
+ name: zod.z.string()
98
+ });
99
+ var AutoInstrumentationConfigSchema = zod.z.object({
100
+ // Enable/disable auto-instrumentation
101
+ enabled: zod.z.boolean().default(false),
102
+ // Tracing granularity
103
+ // - 'fiber': Trace at fiber creation (recommended, lower overhead)
104
+ // - 'operator': Trace each Effect operator (higher granularity, more overhead)
105
+ granularity: zod.z.enum(["fiber", "operator"]).default("fiber"),
106
+ // Smart span naming configuration
107
+ span_naming: zod.z.object({
108
+ // Default span name template when no rules match
109
+ default: zod.z.string().default("effect.fiber.{fiber_id}"),
110
+ // Infer span names from source code (requires stack trace parsing)
111
+ // Adds ~50-100μs overhead per fiber
112
+ infer_from_source: zod.z.boolean().default(true),
113
+ // Naming rules (first match wins)
114
+ rules: zod.z.array(SpanNamingRuleSchema).default([])
115
+ }).default({}),
116
+ // Pattern-based filtering
117
+ filter: zod.z.object({
118
+ // Only trace spans matching these patterns (empty = trace all)
119
+ include: zod.z.array(zod.z.string()).default([]),
120
+ // Never trace spans matching these patterns
121
+ exclude: zod.z.array(zod.z.string()).default([])
122
+ }).default({}),
123
+ // Performance controls
124
+ performance: zod.z.object({
125
+ // Sample rate (0.0 - 1.0)
126
+ sampling_rate: zod.z.number().min(0).max(1).default(1),
127
+ // Skip fibers shorter than this duration (e.g., "10ms", "100 millis")
128
+ min_duration: zod.z.string().default("0ms"),
129
+ // Maximum concurrent traced fibers (0 = unlimited)
130
+ max_concurrent: zod.z.number().default(0)
131
+ }).default({}),
132
+ // Automatic metadata extraction
133
+ metadata: zod.z.object({
134
+ // Extract Effect fiber information
135
+ fiber_info: zod.z.boolean().default(true),
136
+ // Extract source location (file:line)
137
+ source_location: zod.z.boolean().default(true),
138
+ // Extract parent fiber information
139
+ parent_fiber: zod.z.boolean().default(true)
140
+ }).default({})
141
+ });
79
142
  var HttpFilteringConfigSchema = zod.z.object({
80
143
  // Patterns to ignore for outgoing HTTP requests (string patterns only in YAML)
81
144
  ignore_outgoing_urls: zod.z.array(zod.z.string()).optional(),
@@ -97,6 +160,30 @@ var HttpFilteringConfigSchema = zod.z.object({
97
160
  include_urls: zod.z.array(zod.z.string()).optional()
98
161
  }).optional()
99
162
  });
163
+ var ExporterConfigSchema = zod.z.object({
164
+ // Exporter type: 'otlp' | 'console' | 'none'
165
+ // - 'otlp': Export to OTLP endpoint (production)
166
+ // - 'console': Log spans to console (development)
167
+ // - 'none': No export (disable tracing)
168
+ type: zod.z.enum(["otlp", "console", "none"]).default("otlp"),
169
+ // OTLP endpoint URL (for type: otlp)
170
+ // Defaults to OTEL_EXPORTER_OTLP_ENDPOINT env var or http://localhost:4318
171
+ endpoint: zod.z.string().optional(),
172
+ // Custom headers to send with OTLP requests (for type: otlp)
173
+ // Useful for authentication (x-api-key, Authorization, etc.)
174
+ headers: zod.z.record(zod.z.string()).optional(),
175
+ // Span processor type
176
+ // - 'batch': Batch spans for export (production, lower overhead)
177
+ // - 'simple': Export immediately (development, no batching delay)
178
+ processor: zod.z.enum(["batch", "simple"]).default("batch"),
179
+ // Batch processor settings (for processor: batch)
180
+ batch: zod.z.object({
181
+ // Max time to wait before exporting (milliseconds)
182
+ scheduled_delay_millis: zod.z.number().default(1e3),
183
+ // Max batch size
184
+ max_export_batch_size: zod.z.number().default(100)
185
+ }).optional()
186
+ });
100
187
  var InstrumentationConfigSchema = zod.z.object({
101
188
  version: zod.z.string(),
102
189
  instrumentation: zod.z.object({
@@ -107,11 +194,54 @@ var InstrumentationConfigSchema = zod.z.object({
107
194
  ignore_patterns: zod.z.array(PatternConfigSchema)
108
195
  }),
109
196
  effect: zod.z.object({
197
+ // Enable/disable Effect tracing entirely
198
+ // When false, EffectInstrumentationLive returns Layer.empty
199
+ enabled: zod.z.boolean().default(true),
200
+ // Exporter mode (legacy - use exporter.type instead):
201
+ // - "unified": Use global TracerProvider from Node SDK (recommended, enables filtering)
202
+ // - "standalone": Use Effect's own OTLP exporter (bypasses Node SDK filtering)
203
+ exporter: zod.z.enum(["unified", "standalone"]).default("unified"),
204
+ // Exporter configuration (for auto-instrumentation)
205
+ exporter_config: ExporterConfigSchema.optional(),
110
206
  auto_extract_metadata: zod.z.boolean(),
111
- auto_isolation: AutoIsolationConfigSchema.optional()
207
+ auto_isolation: AutoIsolationConfigSchema.optional(),
208
+ // Auto-instrumentation: automatic tracing of all Effect fibers
209
+ auto_instrumentation: AutoInstrumentationConfigSchema.optional()
112
210
  }).optional(),
113
211
  http: HttpFilteringConfigSchema.optional()
114
212
  });
213
+ var defaultConfig = {
214
+ version: "1.0",
215
+ instrumentation: {
216
+ enabled: true,
217
+ logging: "on",
218
+ description: "Default instrumentation configuration",
219
+ instrument_patterns: [
220
+ { pattern: "^app\\.", enabled: true, description: "Application operations" },
221
+ { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
222
+ { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
223
+ ],
224
+ ignore_patterns: [
225
+ { pattern: "^test\\.", description: "Test utilities" },
226
+ { pattern: "^internal\\.", description: "Internal operations" },
227
+ { pattern: "^health\\.", description: "Health checks" }
228
+ ]
229
+ },
230
+ effect: {
231
+ enabled: true,
232
+ exporter: "unified",
233
+ auto_extract_metadata: true
234
+ }
235
+ };
236
+ function parseAndValidateConfig(content) {
237
+ let parsed;
238
+ if (typeof content === "string") {
239
+ parsed = yaml.parse(content);
240
+ } else {
241
+ parsed = content;
242
+ }
243
+ return InstrumentationConfigSchema.parse(parsed);
244
+ }
115
245
  (class extends effect.Data.TaggedError("ConfigError") {
116
246
  get message() {
117
247
  return this.reason;
@@ -289,7 +419,7 @@ var makeConfigLoader = effect.Effect.gen(function* () {
289
419
  })
290
420
  });
291
421
  });
292
- var ConfigLoaderLive = effect.Layer.effect(ConfigLoader, makeConfigLoader);
422
+ effect.Layer.effect(ConfigLoader, makeConfigLoader);
293
423
  var PatternMatcher = class {
294
424
  constructor(config) {
295
425
  __publicField2(this, "ignorePatterns", []);
@@ -453,8 +583,14 @@ var PatternSpanProcessor = class {
453
583
  constructor(config, wrappedProcessor) {
454
584
  __publicField(this, "matcher");
455
585
  __publicField(this, "wrappedProcessor");
586
+ __publicField(this, "httpIgnorePatterns", []);
456
587
  this.matcher = new PatternMatcher(config);
457
588
  this.wrappedProcessor = wrappedProcessor;
589
+ if (config.http?.ignore_incoming_paths) {
590
+ this.httpIgnorePatterns = config.http.ignore_incoming_paths.map(
591
+ (pattern) => new RegExp(pattern)
592
+ );
593
+ }
458
594
  }
459
595
  /**
460
596
  * Called when a span is started
@@ -472,12 +608,40 @@ var PatternSpanProcessor = class {
472
608
  * Called when a span is ended
473
609
  *
474
610
  * This is where we make the final decision on whether to export the span.
611
+ * We check both span name patterns and HTTP path patterns.
475
612
  */
476
613
  onEnd(span) {
477
614
  const spanName = span.name;
478
- if (this.matcher.shouldInstrument(spanName)) {
479
- this.wrappedProcessor.onEnd(span);
615
+ if (!this.matcher.shouldInstrument(spanName)) {
616
+ return;
617
+ }
618
+ if (this.shouldIgnoreHttpSpan(span)) {
619
+ return;
620
+ }
621
+ this.wrappedProcessor.onEnd(span);
622
+ }
623
+ /**
624
+ * Check if span should be ignored based on HTTP path attributes
625
+ *
626
+ * This checks the span's url.path, http.route, or http.target attributes
627
+ * against the configured http.ignore_incoming_paths patterns.
628
+ *
629
+ * This enables filtering of Effect HTTP spans (and any other HTTP spans)
630
+ * based on path patterns, which is essential for filtering out OTLP
631
+ * endpoint requests like /v1/traces, /v1/logs, /v1/metrics.
632
+ */
633
+ shouldIgnoreHttpSpan(span) {
634
+ if (this.httpIgnorePatterns.length === 0) {
635
+ return false;
636
+ }
637
+ const urlPath = span.attributes["url.path"];
638
+ const httpRoute = span.attributes["http.route"];
639
+ const httpTarget = span.attributes["http.target"];
640
+ const pathToCheck = urlPath || httpRoute || httpTarget;
641
+ if (!pathToCheck) {
642
+ return false;
480
643
  }
644
+ return this.httpIgnorePatterns.some((pattern) => pattern.test(pathToCheck));
481
645
  }
482
646
  /**
483
647
  * Shutdown the processor
@@ -715,83 +879,55 @@ async function getServiceNameAsync() {
715
879
  async function getServiceVersionAsync() {
716
880
  return effect.Effect.runPromise(getServiceVersion);
717
881
  }
718
- var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
719
- effect.Layer.provide(effect.Layer.mergeAll(platformNode.NodeContext.layer, platform.FetchHttpClient.layer))
720
- );
721
- var cachedLoaderPromise = null;
722
- function getCachedLoader() {
723
- if (!cachedLoaderPromise) {
724
- cachedLoaderPromise = effect.Effect.runPromise(
725
- effect.Effect.gen(function* () {
726
- return yield* ConfigLoader;
727
- }).pipe(effect.Effect.provide(NodeConfigLoaderLive))
728
- );
729
- }
730
- return cachedLoaderPromise;
882
+ async function loadFromFile(filePath) {
883
+ const { readFile: readFile2 } = await import('fs/promises');
884
+ const content = await readFile2(filePath, "utf-8");
885
+ return parseAndValidateConfig(content);
731
886
  }
732
- function _resetConfigLoaderCache() {
733
- cachedLoaderPromise = null;
887
+ async function loadFromUrl(url) {
888
+ const response = await fetch(url);
889
+ if (!response.ok) {
890
+ throw new Error(`Failed to fetch config from ${url}: ${response.statusText}`);
891
+ }
892
+ const content = await response.text();
893
+ return parseAndValidateConfig(content);
734
894
  }
735
- async function loadConfig(uri, options) {
736
- if (options?.cacheTimeout === 0) {
737
- const program = effect.Effect.gen(function* () {
738
- const loader2 = yield* ConfigLoader;
739
- return yield* loader2.loadFromUri(uri);
740
- });
741
- return effect.Effect.runPromise(program.pipe(effect.Effect.provide(NodeConfigLoaderLive)));
895
+ async function loadConfig(uri, _options) {
896
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
897
+ return loadFromUrl(uri);
742
898
  }
743
- const loader = await getCachedLoader();
744
- return effect.Effect.runPromise(loader.loadFromUri(uri));
899
+ if (uri.startsWith("file://")) {
900
+ const filePath = uri.slice(7);
901
+ return loadFromFile(filePath);
902
+ }
903
+ return loadFromFile(uri);
745
904
  }
746
905
  async function loadConfigFromInline(content) {
747
- const loader = await getCachedLoader();
748
- return effect.Effect.runPromise(loader.loadFromInline(content));
906
+ return parseAndValidateConfig(content);
749
907
  }
750
- function getDefaultConfig() {
751
- return {
752
- version: "1.0",
753
- instrumentation: {
754
- enabled: true,
755
- logging: "on",
756
- description: "Default instrumentation configuration",
757
- instrument_patterns: [
758
- { pattern: "^app\\.", enabled: true, description: "Application operations" },
759
- { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
760
- { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
761
- ],
762
- ignore_patterns: [
763
- { pattern: "^test\\.", description: "Test utilities" },
764
- { pattern: "^internal\\.", description: "Internal operations" },
765
- { pattern: "^health\\.", description: "Health checks" }
766
- ]
767
- },
768
- effect: {
769
- auto_extract_metadata: true
770
- }
771
- };
908
+ function _resetConfigLoaderCache() {
772
909
  }
773
910
  async function loadConfigWithOptions(options = {}) {
774
- const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
775
911
  if (options.config) {
776
912
  return loadConfigFromInline(options.config);
777
913
  }
778
914
  const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
779
915
  if (envConfigPath) {
780
- return loadConfig(envConfigPath, loadOptions);
916
+ return loadConfig(envConfigPath);
781
917
  }
782
918
  if (options.configUrl) {
783
- return loadConfig(options.configUrl, loadOptions);
919
+ return loadConfig(options.configUrl);
784
920
  }
785
921
  if (options.configPath) {
786
- return loadConfig(options.configPath, loadOptions);
922
+ return loadConfig(options.configPath);
787
923
  }
788
924
  const { existsSync } = await import('fs');
789
925
  const { join: join2 } = await import('path');
790
926
  const defaultPath = join2(process.cwd(), "instrumentation.yaml");
791
927
  if (existsSync(defaultPath)) {
792
- return loadConfig(defaultPath, loadOptions);
928
+ return loadConfig(defaultPath);
793
929
  }
794
- return getDefaultConfig();
930
+ return defaultConfig;
795
931
  }
796
932
 
797
933
  // src/core/sdk-initializer.ts
@@ -1083,9 +1219,39 @@ function logInitialization(config, serviceName, serviceVersion, options, autoIns
1083
1219
  logger.log(` - OTLP endpoint: ${endpoint}`);
1084
1220
  logger.log("");
1085
1221
  }
1222
+ 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)));
1223
+ function validateOpenTelemetryApi() {
1224
+ try {
1225
+ require2.resolve("@opentelemetry/api");
1226
+ } catch {
1227
+ throw new Error(
1228
+ "@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"
1229
+ );
1230
+ }
1231
+ }
1232
+ function validateEffectDependencies() {
1233
+ const packages = ["effect", "@effect/opentelemetry", "@effect/platform"];
1234
+ for (const pkg of packages) {
1235
+ try {
1236
+ require2.resolve(pkg);
1237
+ } catch {
1238
+ return false;
1239
+ }
1240
+ }
1241
+ return true;
1242
+ }
1243
+ var validateDependencies = effect.Effect.try({
1244
+ try: () => validateOpenTelemetryApi(),
1245
+ catch: (error) => new InitializationError2({
1246
+ reason: error instanceof Error ? error.message : "Dependency validation failed",
1247
+ cause: error
1248
+ })
1249
+ });
1250
+ effect.Effect.sync(() => validateEffectDependencies());
1086
1251
 
1087
1252
  // src/api.ts
1088
1253
  async function initializeInstrumentation(options = {}) {
1254
+ validateOpenTelemetryApi();
1089
1255
  const sdk = await initializeSdk(options);
1090
1256
  if (sdk) {
1091
1257
  const config = await loadConfigWithOptions(options);
@@ -1102,6 +1268,7 @@ async function initializePatternMatchingOnly(options = {}) {
1102
1268
  );
1103
1269
  }
1104
1270
  var initializeInstrumentationEffect = (options = {}) => effect.Effect.gen(function* () {
1271
+ yield* validateDependencies;
1105
1272
  const sdk = yield* effect.Effect.tryPromise({
1106
1273
  try: () => initializeSdk(options),
1107
1274
  catch: (error) => new InitializationError2({