@atrim/instrument-node 0.5.0-14fdea7-20260108232035 → 0.5.0-14fdea7-20260109020503

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.
@@ -0,0 +1,1385 @@
1
+ import { Data, Context, Effect, Layer } from 'effect';
2
+ import { NodeSDK } from '@opentelemetry/sdk-node';
3
+ import { SimpleSpanProcessor, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
4
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
5
+ import { trace } from '@opentelemetry/api';
6
+ import { FileSystem } from '@effect/platform/FileSystem';
7
+ import * as HttpClient from '@effect/platform/HttpClient';
8
+ import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
9
+ import { parse } from 'yaml';
10
+ import { z } from 'zod';
11
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
12
+ import { readFile } from 'fs/promises';
13
+ import { join } from 'path';
14
+ import { createRequire } from 'module';
15
+
16
+ var __defProp = Object.defineProperty;
17
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
18
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
19
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
20
+ }) : x)(function(x) {
21
+ if (typeof require !== "undefined") return require.apply(this, arguments);
22
+ throw Error('Dynamic require of "' + x + '" is not supported');
23
+ });
24
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
25
+ var __defProp2 = Object.defineProperty;
26
+ var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
27
+ var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
28
+ var PatternConfigSchema = z.object({
29
+ pattern: z.string(),
30
+ enabled: z.boolean().optional(),
31
+ description: z.string().optional()
32
+ });
33
+ var AutoIsolationConfigSchema = z.object({
34
+ // Global enable/disable for auto-isolation
35
+ enabled: z.boolean().default(false),
36
+ // Which operators to auto-isolate
37
+ operators: z.object({
38
+ fiberset_run: z.boolean().default(true),
39
+ effect_fork: z.boolean().default(true),
40
+ effect_fork_daemon: z.boolean().default(true),
41
+ effect_fork_in: z.boolean().default(false)
42
+ }).default({}),
43
+ // Virtual parent tracking configuration
44
+ tracking: z.object({
45
+ use_span_links: z.boolean().default(true),
46
+ use_attributes: z.boolean().default(true),
47
+ capture_logical_parent: z.boolean().default(true)
48
+ }).default({}),
49
+ // Span categorization
50
+ attributes: z.object({
51
+ category: z.string().default("background_task"),
52
+ add_metadata: z.boolean().default(true)
53
+ }).default({})
54
+ });
55
+ var SpanNamingRuleSchema = z.object({
56
+ // Match criteria (all specified criteria must match)
57
+ match: z.object({
58
+ // Regex pattern to match file path
59
+ file: z.string().optional(),
60
+ // Regex pattern to match function name
61
+ function: z.string().optional(),
62
+ // Regex pattern to match module name
63
+ module: z.string().optional()
64
+ }),
65
+ // Span name template with variables:
66
+ // {fiber_id} - Fiber ID
67
+ // {function} - Function name
68
+ // {module} - Module name
69
+ // {file} - File path
70
+ // {line} - Line number
71
+ // {operator} - Effect operator (gen, all, forEach, etc.)
72
+ // {match:field:N} - Captured regex group from match
73
+ name: z.string()
74
+ });
75
+ var AutoInstrumentationConfigSchema = z.object({
76
+ // Enable/disable auto-instrumentation
77
+ enabled: z.boolean().default(false),
78
+ // Tracing granularity
79
+ // - 'fiber': Trace at fiber creation (recommended, lower overhead)
80
+ // - 'operator': Trace each Effect operator (higher granularity, more overhead)
81
+ granularity: z.enum(["fiber", "operator"]).default("fiber"),
82
+ // Smart span naming configuration
83
+ span_naming: z.object({
84
+ // Default span name template when no rules match
85
+ default: z.string().default("effect.fiber.{fiber_id}"),
86
+ // Infer span names from source code (requires stack trace parsing)
87
+ // Adds ~50-100μs overhead per fiber
88
+ infer_from_source: z.boolean().default(true),
89
+ // Naming rules (first match wins)
90
+ rules: z.array(SpanNamingRuleSchema).default([])
91
+ }).default({}),
92
+ // Pattern-based filtering
93
+ filter: z.object({
94
+ // Only trace spans matching these patterns (empty = trace all)
95
+ include: z.array(z.string()).default([]),
96
+ // Never trace spans matching these patterns
97
+ exclude: z.array(z.string()).default([])
98
+ }).default({}),
99
+ // Performance controls
100
+ performance: z.object({
101
+ // Sample rate (0.0 - 1.0)
102
+ sampling_rate: z.number().min(0).max(1).default(1),
103
+ // Skip fibers shorter than this duration (e.g., "10ms", "100 millis")
104
+ min_duration: z.string().default("0ms"),
105
+ // Maximum concurrent traced fibers (0 = unlimited)
106
+ max_concurrent: z.number().default(0)
107
+ }).default({}),
108
+ // Automatic metadata extraction
109
+ metadata: z.object({
110
+ // Extract Effect fiber information
111
+ fiber_info: z.boolean().default(true),
112
+ // Extract source location (file:line)
113
+ source_location: z.boolean().default(true),
114
+ // Extract parent fiber information
115
+ parent_fiber: z.boolean().default(true)
116
+ }).default({})
117
+ });
118
+ var HttpFilteringConfigSchema = z.object({
119
+ // Patterns to ignore for outgoing HTTP requests (string patterns only in YAML)
120
+ ignore_outgoing_urls: z.array(z.string()).optional(),
121
+ // Patterns to ignore for incoming HTTP requests (string patterns only in YAML)
122
+ ignore_incoming_paths: z.array(z.string()).optional(),
123
+ // Require parent span for outgoing requests (prevents root spans for HTTP calls)
124
+ require_parent_for_outgoing_spans: z.boolean().optional(),
125
+ // Trace context propagation configuration
126
+ // Controls which cross-origin requests receive W3C Trace Context headers (traceparent, tracestate)
127
+ propagate_trace_context: z.object({
128
+ // Strategy for trace propagation
129
+ // - "all": Propagate to all cross-origin requests (may cause CORS errors)
130
+ // - "none": Never propagate trace headers
131
+ // - "same-origin": Only propagate to same-origin requests (default, safe)
132
+ // - "patterns": Propagate based on include_urls patterns
133
+ strategy: z.enum(["all", "none", "same-origin", "patterns"]).default("same-origin"),
134
+ // URL patterns to include when strategy is "patterns"
135
+ // Supports regex patterns (e.g., "^https://api\\.myapp\\.com")
136
+ include_urls: z.array(z.string()).optional()
137
+ }).optional()
138
+ });
139
+ var ExporterConfigSchema = z.object({
140
+ // Exporter type: 'otlp' | 'console' | 'none'
141
+ // - 'otlp': Export to OTLP endpoint (production)
142
+ // - 'console': Log spans to console (development)
143
+ // - 'none': No export (disable tracing)
144
+ type: z.enum(["otlp", "console", "none"]).default("otlp"),
145
+ // OTLP endpoint URL (for type: otlp)
146
+ // Defaults to OTEL_EXPORTER_OTLP_ENDPOINT env var or http://localhost:4318
147
+ endpoint: z.string().optional(),
148
+ // Custom headers to send with OTLP requests (for type: otlp)
149
+ // Useful for authentication (x-api-key, Authorization, etc.)
150
+ headers: z.record(z.string()).optional(),
151
+ // Span processor type
152
+ // - 'batch': Batch spans for export (production, lower overhead)
153
+ // - 'simple': Export immediately (development, no batching delay)
154
+ processor: z.enum(["batch", "simple"]).default("batch"),
155
+ // Batch processor settings (for processor: batch)
156
+ batch: z.object({
157
+ // Max time to wait before exporting (milliseconds)
158
+ scheduled_delay_millis: z.number().default(1e3),
159
+ // Max batch size
160
+ max_export_batch_size: z.number().default(100)
161
+ }).optional()
162
+ });
163
+ var InstrumentationConfigSchema = z.object({
164
+ version: z.string(),
165
+ instrumentation: z.object({
166
+ enabled: z.boolean(),
167
+ description: z.string().optional(),
168
+ logging: z.enum(["on", "off", "minimal"]).optional().default("on"),
169
+ instrument_patterns: z.array(PatternConfigSchema),
170
+ ignore_patterns: z.array(PatternConfigSchema)
171
+ }),
172
+ effect: z.object({
173
+ // Enable/disable Effect tracing entirely
174
+ // When false, EffectInstrumentationLive returns Layer.empty
175
+ enabled: z.boolean().default(true),
176
+ // Exporter mode (legacy - use exporter.type instead):
177
+ // - "unified": Use global TracerProvider from Node SDK (recommended, enables filtering)
178
+ // - "standalone": Use Effect's own OTLP exporter (bypasses Node SDK filtering)
179
+ exporter: z.enum(["unified", "standalone"]).default("unified"),
180
+ // Exporter configuration (for auto-instrumentation)
181
+ exporter_config: ExporterConfigSchema.optional(),
182
+ auto_extract_metadata: z.boolean(),
183
+ auto_isolation: AutoIsolationConfigSchema.optional(),
184
+ // Auto-instrumentation: automatic tracing of all Effect fibers
185
+ auto_instrumentation: AutoInstrumentationConfigSchema.optional()
186
+ }).optional(),
187
+ http: HttpFilteringConfigSchema.optional()
188
+ });
189
+ var defaultConfig = {
190
+ version: "1.0",
191
+ instrumentation: {
192
+ enabled: true,
193
+ logging: "on",
194
+ description: "Default instrumentation configuration",
195
+ instrument_patterns: [
196
+ { pattern: "^app\\.", enabled: true, description: "Application operations" },
197
+ { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
198
+ { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
199
+ ],
200
+ ignore_patterns: [
201
+ { pattern: "^test\\.", description: "Test utilities" },
202
+ { pattern: "^internal\\.", description: "Internal operations" },
203
+ { pattern: "^health\\.", description: "Health checks" }
204
+ ]
205
+ },
206
+ effect: {
207
+ enabled: true,
208
+ exporter: "unified",
209
+ auto_extract_metadata: true
210
+ }
211
+ };
212
+ function parseAndValidateConfig(content) {
213
+ let parsed;
214
+ if (typeof content === "string") {
215
+ parsed = parse(content);
216
+ } else {
217
+ parsed = content;
218
+ }
219
+ return InstrumentationConfigSchema.parse(parsed);
220
+ }
221
+ (class extends Data.TaggedError("ConfigError") {
222
+ get message() {
223
+ return this.reason;
224
+ }
225
+ });
226
+ var ConfigUrlError = class extends Data.TaggedError("ConfigUrlError") {
227
+ get message() {
228
+ return this.reason;
229
+ }
230
+ };
231
+ var ConfigValidationError = class extends Data.TaggedError("ConfigValidationError") {
232
+ get message() {
233
+ return this.reason;
234
+ }
235
+ };
236
+ var ConfigFileError = class extends Data.TaggedError("ConfigFileError") {
237
+ get message() {
238
+ return this.reason;
239
+ }
240
+ };
241
+ (class extends Data.TaggedError("ServiceDetectionError") {
242
+ get message() {
243
+ return this.reason;
244
+ }
245
+ });
246
+ (class extends Data.TaggedError("InitializationError") {
247
+ get message() {
248
+ return this.reason;
249
+ }
250
+ });
251
+ (class extends Data.TaggedError("ExportError") {
252
+ get message() {
253
+ return this.reason;
254
+ }
255
+ });
256
+ (class extends Data.TaggedError("ShutdownError") {
257
+ get message() {
258
+ return this.reason;
259
+ }
260
+ });
261
+ var SECURITY_DEFAULTS = {
262
+ maxConfigSize: 1e6,
263
+ // 1MB
264
+ requestTimeout: 5e3
265
+ // 5 seconds
266
+ };
267
+ var ConfigLoader = class extends Context.Tag("ConfigLoader")() {
268
+ };
269
+ var parseYamlContent = (content, uri) => Effect.gen(function* () {
270
+ const parsed = yield* Effect.try({
271
+ try: () => parse(content),
272
+ catch: (error) => new ConfigValidationError({
273
+ reason: uri ? `Failed to parse YAML from ${uri}` : "Failed to parse YAML",
274
+ cause: error
275
+ })
276
+ });
277
+ return yield* Effect.try({
278
+ try: () => InstrumentationConfigSchema.parse(parsed),
279
+ catch: (error) => new ConfigValidationError({
280
+ reason: uri ? `Invalid configuration schema from ${uri}` : "Invalid configuration schema",
281
+ cause: error
282
+ })
283
+ });
284
+ });
285
+ var loadFromFileWithFs = (fs, path, uri) => Effect.gen(function* () {
286
+ const content = yield* fs.readFileString(path).pipe(
287
+ Effect.mapError(
288
+ (error) => new ConfigFileError({
289
+ reason: `Failed to read config file at ${uri}`,
290
+ cause: error
291
+ })
292
+ )
293
+ );
294
+ if (content.length > SECURITY_DEFAULTS.maxConfigSize) {
295
+ return yield* Effect.fail(
296
+ new ConfigFileError({
297
+ reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
298
+ })
299
+ );
300
+ }
301
+ return yield* parseYamlContent(content, uri);
302
+ });
303
+ var loadFromHttpWithClient = (client, url) => Effect.scoped(
304
+ Effect.gen(function* () {
305
+ if (url.startsWith("http://")) {
306
+ return yield* Effect.fail(
307
+ new ConfigUrlError({
308
+ reason: "Insecure protocol: only HTTPS URLs are allowed"
309
+ })
310
+ );
311
+ }
312
+ const request = HttpClientRequest.get(url).pipe(
313
+ HttpClientRequest.setHeaders({
314
+ Accept: "application/yaml, text/yaml, application/x-yaml"
315
+ })
316
+ );
317
+ const response = yield* client.execute(request).pipe(
318
+ Effect.timeout(`${SECURITY_DEFAULTS.requestTimeout} millis`),
319
+ Effect.mapError((error) => {
320
+ if (error._tag === "TimeoutException") {
321
+ return new ConfigUrlError({
322
+ reason: `Config fetch timeout after ${SECURITY_DEFAULTS.requestTimeout}ms from ${url}`
323
+ });
324
+ }
325
+ return new ConfigUrlError({
326
+ reason: `Failed to load config from URL: ${url}`,
327
+ cause: error
328
+ });
329
+ })
330
+ );
331
+ if (response.status >= 400) {
332
+ return yield* Effect.fail(
333
+ new ConfigUrlError({
334
+ reason: `HTTP ${response.status} from ${url}`
335
+ })
336
+ );
337
+ }
338
+ const text = yield* response.text.pipe(
339
+ Effect.mapError(
340
+ (error) => new ConfigUrlError({
341
+ reason: `Failed to read response body from ${url}`,
342
+ cause: error
343
+ })
344
+ )
345
+ );
346
+ if (text.length > SECURITY_DEFAULTS.maxConfigSize) {
347
+ return yield* Effect.fail(
348
+ new ConfigUrlError({
349
+ reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
350
+ })
351
+ );
352
+ }
353
+ return yield* parseYamlContent(text, url);
354
+ })
355
+ );
356
+ var makeConfigLoader = Effect.gen(function* () {
357
+ const fs = yield* Effect.serviceOption(FileSystem);
358
+ const http = yield* HttpClient.HttpClient;
359
+ const loadFromUriUncached = (uri) => Effect.gen(function* () {
360
+ if (uri.startsWith("file://")) {
361
+ const path = uri.slice(7);
362
+ if (fs._tag === "None") {
363
+ return yield* Effect.fail(
364
+ new ConfigFileError({
365
+ reason: "FileSystem not available (browser environment?)",
366
+ cause: { uri }
367
+ })
368
+ );
369
+ }
370
+ return yield* loadFromFileWithFs(fs.value, path, uri);
371
+ }
372
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
373
+ return yield* loadFromHttpWithClient(http, uri);
374
+ }
375
+ if (fs._tag === "Some") {
376
+ return yield* loadFromFileWithFs(fs.value, uri, uri);
377
+ } else {
378
+ return yield* loadFromHttpWithClient(http, uri);
379
+ }
380
+ });
381
+ const loadFromUriCached = yield* Effect.cachedFunction(loadFromUriUncached);
382
+ return ConfigLoader.of({
383
+ loadFromUri: loadFromUriCached,
384
+ loadFromInline: (content) => Effect.gen(function* () {
385
+ if (typeof content === "string") {
386
+ return yield* parseYamlContent(content);
387
+ }
388
+ return yield* Effect.try({
389
+ try: () => InstrumentationConfigSchema.parse(content),
390
+ catch: (error) => new ConfigValidationError({
391
+ reason: "Invalid configuration schema",
392
+ cause: error
393
+ })
394
+ });
395
+ })
396
+ });
397
+ });
398
+ Layer.effect(ConfigLoader, makeConfigLoader);
399
+ var PatternMatcher = class {
400
+ constructor(config) {
401
+ __publicField2(this, "ignorePatterns", []);
402
+ __publicField2(this, "instrumentPatterns", []);
403
+ __publicField2(this, "enabled", true);
404
+ this.enabled = config.instrumentation.enabled;
405
+ this.ignorePatterns = config.instrumentation.ignore_patterns.map((p) => this.compilePattern(p));
406
+ this.instrumentPatterns = config.instrumentation.instrument_patterns.filter((p) => p.enabled !== false).map((p) => this.compilePattern(p));
407
+ }
408
+ /**
409
+ * Compile a pattern configuration into a RegExp
410
+ */
411
+ compilePattern(pattern) {
412
+ try {
413
+ const compiled = {
414
+ regex: new RegExp(pattern.pattern),
415
+ enabled: pattern.enabled !== false
416
+ };
417
+ if (pattern.description !== void 0) {
418
+ compiled.description = pattern.description;
419
+ }
420
+ return compiled;
421
+ } catch (error) {
422
+ throw new Error(
423
+ `Failed to compile pattern "${pattern.pattern}": ${error instanceof Error ? error.message : String(error)}`
424
+ );
425
+ }
426
+ }
427
+ /**
428
+ * Check if a span should be instrumented
429
+ *
430
+ * Returns true if the span should be created, false otherwise.
431
+ *
432
+ * Logic:
433
+ * 1. If instrumentation disabled globally, return false
434
+ * 2. Check ignore patterns - if any match, return false
435
+ * 3. Check instrument patterns - if any match, return true
436
+ * 4. Default: return true (fail-open - create span if no patterns match)
437
+ */
438
+ shouldInstrument(spanName) {
439
+ if (!this.enabled) {
440
+ return false;
441
+ }
442
+ for (const pattern of this.ignorePatterns) {
443
+ if (pattern.regex.test(spanName)) {
444
+ return false;
445
+ }
446
+ }
447
+ for (const pattern of this.instrumentPatterns) {
448
+ if (pattern.enabled && pattern.regex.test(spanName)) {
449
+ return true;
450
+ }
451
+ }
452
+ return true;
453
+ }
454
+ /**
455
+ * Get statistics about pattern matching (for debugging/monitoring)
456
+ */
457
+ getStats() {
458
+ return {
459
+ enabled: this.enabled,
460
+ ignorePatternCount: this.ignorePatterns.length,
461
+ instrumentPatternCount: this.instrumentPatterns.filter((p) => p.enabled).length
462
+ };
463
+ }
464
+ };
465
+ var globalMatcher = null;
466
+ function initializePatternMatcher(config) {
467
+ globalMatcher = new PatternMatcher(config);
468
+ }
469
+ function shouldInstrumentSpan(spanName) {
470
+ if (!globalMatcher) {
471
+ return true;
472
+ }
473
+ return globalMatcher.shouldInstrument(spanName);
474
+ }
475
+ function getPatternMatcher() {
476
+ return globalMatcher;
477
+ }
478
+ var Logger = class {
479
+ constructor() {
480
+ __publicField2(this, "level", "on");
481
+ __publicField2(this, "hasLoggedMinimal", false);
482
+ }
483
+ /**
484
+ * Set the logging level
485
+ */
486
+ setLevel(level) {
487
+ this.level = level;
488
+ this.hasLoggedMinimal = false;
489
+ }
490
+ /**
491
+ * Get the current logging level
492
+ */
493
+ getLevel() {
494
+ return this.level;
495
+ }
496
+ /**
497
+ * Log a minimal initialization message (only shown once in minimal mode)
498
+ */
499
+ minimal(message) {
500
+ if (this.level === "off") {
501
+ return;
502
+ }
503
+ if (this.level === "minimal" && !this.hasLoggedMinimal) {
504
+ console.log(message);
505
+ this.hasLoggedMinimal = true;
506
+ return;
507
+ }
508
+ if (this.level === "on") {
509
+ console.log(message);
510
+ }
511
+ }
512
+ /**
513
+ * Log an informational message
514
+ */
515
+ log(...args) {
516
+ if (this.level === "on") {
517
+ console.log(...args);
518
+ }
519
+ }
520
+ /**
521
+ * Log a warning message (shown in minimal mode)
522
+ */
523
+ warn(...args) {
524
+ if (this.level !== "off") {
525
+ console.warn(...args);
526
+ }
527
+ }
528
+ /**
529
+ * Log an error message (shown in minimal mode)
530
+ */
531
+ error(...args) {
532
+ if (this.level !== "off") {
533
+ console.error(...args);
534
+ }
535
+ }
536
+ /**
537
+ * Check if full logging is enabled
538
+ */
539
+ isEnabled() {
540
+ return this.level === "on";
541
+ }
542
+ /**
543
+ * Check if minimal logging is enabled
544
+ */
545
+ isMinimal() {
546
+ return this.level === "minimal";
547
+ }
548
+ /**
549
+ * Check if logging is completely disabled
550
+ */
551
+ isDisabled() {
552
+ return this.level === "off";
553
+ }
554
+ };
555
+ var logger = new Logger();
556
+
557
+ // src/core/span-processor.ts
558
+ var PatternSpanProcessor = class {
559
+ constructor(config, wrappedProcessor) {
560
+ __publicField(this, "matcher");
561
+ __publicField(this, "wrappedProcessor");
562
+ __publicField(this, "httpIgnorePatterns", []);
563
+ this.matcher = new PatternMatcher(config);
564
+ this.wrappedProcessor = wrappedProcessor;
565
+ if (config.http?.ignore_incoming_paths) {
566
+ this.httpIgnorePatterns = config.http.ignore_incoming_paths.map(
567
+ (pattern) => new RegExp(pattern)
568
+ );
569
+ }
570
+ }
571
+ /**
572
+ * Called when a span is started
573
+ *
574
+ * We check if the span should be instrumented here. If not, we can mark it
575
+ * to be dropped later in onEnd().
576
+ */
577
+ onStart(span, parentContext) {
578
+ const spanName = span.name;
579
+ if (this.matcher.shouldInstrument(spanName)) {
580
+ this.wrappedProcessor.onStart(span, parentContext);
581
+ }
582
+ }
583
+ /**
584
+ * Called when a span is ended
585
+ *
586
+ * This is where we make the final decision on whether to export the span.
587
+ * We check both span name patterns and HTTP path patterns.
588
+ */
589
+ onEnd(span) {
590
+ const spanName = span.name;
591
+ if (!this.matcher.shouldInstrument(spanName)) {
592
+ return;
593
+ }
594
+ if (this.shouldIgnoreHttpSpan(span)) {
595
+ return;
596
+ }
597
+ this.wrappedProcessor.onEnd(span);
598
+ }
599
+ /**
600
+ * Check if span should be ignored based on HTTP path attributes
601
+ *
602
+ * This checks the span's url.path, http.route, or http.target attributes
603
+ * against the configured http.ignore_incoming_paths patterns.
604
+ *
605
+ * This enables filtering of Effect HTTP spans (and any other HTTP spans)
606
+ * based on path patterns, which is essential for filtering out OTLP
607
+ * endpoint requests like /v1/traces, /v1/logs, /v1/metrics.
608
+ */
609
+ shouldIgnoreHttpSpan(span) {
610
+ if (this.httpIgnorePatterns.length === 0) {
611
+ return false;
612
+ }
613
+ const urlPath = span.attributes["url.path"];
614
+ const httpRoute = span.attributes["http.route"];
615
+ const httpTarget = span.attributes["http.target"];
616
+ const pathToCheck = urlPath || httpRoute || httpTarget;
617
+ if (!pathToCheck) {
618
+ return false;
619
+ }
620
+ return this.httpIgnorePatterns.some((pattern) => pattern.test(pathToCheck));
621
+ }
622
+ /**
623
+ * Shutdown the processor
624
+ */
625
+ async shutdown() {
626
+ return this.wrappedProcessor.shutdown();
627
+ }
628
+ /**
629
+ * Force flush any pending spans
630
+ */
631
+ async forceFlush() {
632
+ return this.wrappedProcessor.forceFlush();
633
+ }
634
+ /**
635
+ * Get the pattern matcher (for debugging/testing)
636
+ */
637
+ getPatternMatcher() {
638
+ return this.matcher;
639
+ }
640
+ };
641
+ var DEFAULT_OTLP_ENDPOINT = "http://localhost:4318/v1/traces";
642
+ function createOtlpExporter(options = {}) {
643
+ const endpoint = options.endpoint || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTLP_ENDPOINT;
644
+ const normalizedEndpoint = normalizeEndpoint(endpoint);
645
+ const config = {
646
+ url: normalizedEndpoint
647
+ };
648
+ if (options.headers) {
649
+ config.headers = options.headers;
650
+ }
651
+ return new OTLPTraceExporter(config);
652
+ }
653
+ function normalizeEndpoint(endpoint) {
654
+ try {
655
+ const url = new URL(endpoint);
656
+ if (!url.pathname || url.pathname === "/") {
657
+ url.pathname = "/v1/traces";
658
+ return url.toString();
659
+ }
660
+ return endpoint;
661
+ } catch {
662
+ return endpoint;
663
+ }
664
+ }
665
+ function getOtlpEndpoint(options = {}) {
666
+ const endpoint = options.endpoint || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTLP_ENDPOINT;
667
+ return normalizeEndpoint(endpoint);
668
+ }
669
+
670
+ // src/core/safe-exporter.ts
671
+ var SafeSpanExporter = class {
672
+ // Log errors max once per minute
673
+ constructor(exporter) {
674
+ __publicField(this, "exporter");
675
+ __publicField(this, "errorCount", 0);
676
+ __publicField(this, "lastErrorTime", 0);
677
+ __publicField(this, "errorThrottleMs", 6e4);
678
+ this.exporter = exporter;
679
+ }
680
+ /**
681
+ * Export spans with error handling
682
+ */
683
+ export(spans, resultCallback) {
684
+ try {
685
+ this.exporter.export(spans, (result) => {
686
+ if (result.code !== 0) {
687
+ this.handleExportError(result.error);
688
+ }
689
+ resultCallback(result);
690
+ });
691
+ } catch (error) {
692
+ this.handleExportError(error);
693
+ resultCallback({
694
+ code: 2,
695
+ // FAILED
696
+ error: error instanceof Error ? error : new Error(String(error))
697
+ });
698
+ }
699
+ }
700
+ /**
701
+ * Shutdown with error handling
702
+ */
703
+ async shutdown() {
704
+ try {
705
+ await this.exporter.shutdown();
706
+ } catch (error) {
707
+ logger.error(
708
+ "@atrim/instrumentation: Error during exporter shutdown (non-critical):",
709
+ error instanceof Error ? error.message : String(error)
710
+ );
711
+ }
712
+ }
713
+ /**
714
+ * Force flush with error handling
715
+ */
716
+ async forceFlush() {
717
+ if (!this.exporter.forceFlush) {
718
+ return;
719
+ }
720
+ try {
721
+ await this.exporter.forceFlush();
722
+ } catch (error) {
723
+ this.handleExportError(error);
724
+ }
725
+ }
726
+ /**
727
+ * Handle export errors with throttling
728
+ */
729
+ handleExportError(error) {
730
+ this.errorCount++;
731
+ const now = Date.now();
732
+ const shouldLog = now - this.lastErrorTime > this.errorThrottleMs;
733
+ if (shouldLog) {
734
+ const errorMessage = error instanceof Error ? error.message : String(error);
735
+ if (this.isConnectionError(error)) {
736
+ logger.warn(`@atrim/instrumentation: Unable to export spans - collector not available`);
737
+ logger.warn(` Error: ${errorMessage}`);
738
+ logger.warn(` Spans will be dropped. Ensure OTEL collector is running.`);
739
+ } else {
740
+ logger.error("@atrim/instrumentation: Span export failed:", errorMessage);
741
+ }
742
+ if (this.errorCount > 1) {
743
+ logger.warn(` (${this.errorCount} errors total, throttled to 1/min)`);
744
+ }
745
+ this.lastErrorTime = now;
746
+ this.errorCount = 0;
747
+ }
748
+ }
749
+ /**
750
+ * Check if error is a connection error (ECONNREFUSED, ENOTFOUND, etc.)
751
+ */
752
+ isConnectionError(error) {
753
+ if (!error || typeof error !== "object") {
754
+ return false;
755
+ }
756
+ const err = error;
757
+ if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND" || err.code === "ETIMEDOUT") {
758
+ return true;
759
+ }
760
+ if (Array.isArray(err.errors)) {
761
+ return err.errors.every((e) => this.isConnectionError(e));
762
+ }
763
+ if (err.message) {
764
+ const msg = err.message.toLowerCase();
765
+ return msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("etimedout") || msg.includes("connection refused");
766
+ }
767
+ return false;
768
+ }
769
+ };
770
+ var ConfigError2 = class extends Data.TaggedError("ConfigError") {
771
+ };
772
+ var ConfigUrlError2 = class extends Data.TaggedError("ConfigUrlError") {
773
+ };
774
+ var ConfigValidationError2 = class extends Data.TaggedError("ConfigValidationError") {
775
+ };
776
+ var ConfigFileError2 = class extends Data.TaggedError("ConfigFileError") {
777
+ };
778
+ var ServiceDetectionError2 = class extends Data.TaggedError("ServiceDetectionError") {
779
+ };
780
+ var InitializationError2 = class extends Data.TaggedError("InitializationError") {
781
+ };
782
+ var ExportError2 = class extends Data.TaggedError("ExportError") {
783
+ };
784
+ var ShutdownError2 = class extends Data.TaggedError("ShutdownError") {
785
+ };
786
+
787
+ // src/core/service-detector.ts
788
+ var detectServiceInfo = Effect.gen(
789
+ function* () {
790
+ const envServiceName = process.env.OTEL_SERVICE_NAME;
791
+ const envServiceVersion = process.env.OTEL_SERVICE_VERSION;
792
+ if (envServiceName) {
793
+ return {
794
+ name: envServiceName,
795
+ version: envServiceVersion
796
+ };
797
+ }
798
+ const packageJsonPath = join(process.cwd(), "package.json");
799
+ const packageJsonContent = yield* Effect.tryPromise({
800
+ try: () => readFile(packageJsonPath, "utf-8"),
801
+ catch: (error) => new ServiceDetectionError2({
802
+ reason: `Failed to read package.json at ${packageJsonPath}`,
803
+ cause: error
804
+ })
805
+ });
806
+ let parsed;
807
+ try {
808
+ parsed = JSON.parse(packageJsonContent);
809
+ } catch (error) {
810
+ yield* Effect.fail(
811
+ new ServiceDetectionError2({
812
+ reason: "Invalid JSON in package.json",
813
+ cause: error
814
+ })
815
+ );
816
+ }
817
+ if (typeof parsed === "object" && parsed !== null) {
818
+ const packageJson = parsed;
819
+ if (packageJson.name) {
820
+ return {
821
+ name: packageJson.name,
822
+ version: envServiceVersion || packageJson.version
823
+ };
824
+ }
825
+ }
826
+ return yield* Effect.fail(
827
+ new ServiceDetectionError2({
828
+ reason: 'package.json exists but has no "name" field'
829
+ })
830
+ );
831
+ }
832
+ );
833
+ var getServiceName = detectServiceInfo.pipe(
834
+ Effect.map((info) => info.name),
835
+ Effect.catchAll(() => Effect.succeed("unknown-service"))
836
+ );
837
+ var getServiceVersion = detectServiceInfo.pipe(
838
+ Effect.map((info) => info.version),
839
+ Effect.catchAll(() => Effect.succeed(void 0))
840
+ );
841
+ var getServiceInfoWithFallback = detectServiceInfo.pipe(
842
+ Effect.catchAll(
843
+ () => Effect.succeed({
844
+ name: "unknown-service",
845
+ version: process.env.OTEL_SERVICE_VERSION
846
+ })
847
+ )
848
+ );
849
+ async function detectServiceInfoAsync() {
850
+ return Effect.runPromise(getServiceInfoWithFallback);
851
+ }
852
+ async function getServiceNameAsync() {
853
+ return Effect.runPromise(getServiceName);
854
+ }
855
+ async function getServiceVersionAsync() {
856
+ return Effect.runPromise(getServiceVersion);
857
+ }
858
+ async function loadFromFile(filePath) {
859
+ const { readFile: readFile2 } = await import('fs/promises');
860
+ const content = await readFile2(filePath, "utf-8");
861
+ return parseAndValidateConfig(content);
862
+ }
863
+ async function loadFromUrl(url) {
864
+ const response = await fetch(url);
865
+ if (!response.ok) {
866
+ throw new Error(`Failed to fetch config from ${url}: ${response.statusText}`);
867
+ }
868
+ const content = await response.text();
869
+ return parseAndValidateConfig(content);
870
+ }
871
+ async function loadConfig(uri, _options) {
872
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
873
+ return loadFromUrl(uri);
874
+ }
875
+ if (uri.startsWith("file://")) {
876
+ const filePath = uri.slice(7);
877
+ return loadFromFile(filePath);
878
+ }
879
+ return loadFromFile(uri);
880
+ }
881
+ async function loadConfigFromInline(content) {
882
+ return parseAndValidateConfig(content);
883
+ }
884
+ function _resetConfigLoaderCache() {
885
+ }
886
+ async function loadConfigWithOptions(options = {}) {
887
+ if (options.config) {
888
+ return loadConfigFromInline(options.config);
889
+ }
890
+ const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
891
+ if (envConfigPath) {
892
+ return loadConfig(envConfigPath);
893
+ }
894
+ if (options.configUrl) {
895
+ return loadConfig(options.configUrl);
896
+ }
897
+ if (options.configPath) {
898
+ return loadConfig(options.configPath);
899
+ }
900
+ const { existsSync } = await import('fs');
901
+ const { join: join2 } = await import('path');
902
+ const defaultPath = join2(process.cwd(), "instrumentation.yaml");
903
+ if (existsSync(defaultPath)) {
904
+ return loadConfig(defaultPath);
905
+ }
906
+ return defaultConfig;
907
+ }
908
+
909
+ // src/core/sdk-initializer.ts
910
+ var sdkInstance = null;
911
+ var initializationPromise = null;
912
+ function buildHttpInstrumentationConfig(options, config, _otlpEndpoint) {
913
+ const httpConfig = { enabled: true };
914
+ const programmaticPatterns = options.http?.ignoreOutgoingUrls || [];
915
+ const yamlPatterns = config.http?.ignore_outgoing_urls || [];
916
+ const allOutgoingPatterns = [
917
+ ...programmaticPatterns.map((p) => typeof p === "string" ? new RegExp(p) : p),
918
+ ...yamlPatterns.map((p) => new RegExp(p))
919
+ ];
920
+ logger.log(`HTTP filtering: ${allOutgoingPatterns.length} outgoing patterns configured`);
921
+ if (options.http?.ignoreOutgoingRequestHook) {
922
+ httpConfig.ignoreOutgoingRequestHook = options.http.ignoreOutgoingRequestHook;
923
+ } else if (allOutgoingPatterns.length > 0) {
924
+ httpConfig.ignoreOutgoingRequestHook = (req) => {
925
+ const hostname = req.hostname || req.host || "";
926
+ const port = req.port || "";
927
+ const protocol = req.protocol || "http:";
928
+ const path = req.path || "";
929
+ const portStr = port ? `:${port}` : "";
930
+ const url = `${protocol}//${hostname}${portStr}${path}`;
931
+ const matchesPattern = allOutgoingPatterns.some(
932
+ (pattern) => pattern.test(url) || pattern.test(path)
933
+ );
934
+ return matchesPattern;
935
+ };
936
+ }
937
+ const programmaticIncomingPatterns = options.http?.ignoreIncomingPaths || [];
938
+ const yamlIncomingPatterns = config.http?.ignore_incoming_paths || [];
939
+ const allIncomingPatterns = [
940
+ ...programmaticIncomingPatterns.map((p) => typeof p === "string" ? new RegExp(p) : p),
941
+ ...yamlIncomingPatterns.map((p) => new RegExp(p))
942
+ ];
943
+ if (options.http?.ignoreIncomingRequestHook) {
944
+ httpConfig.ignoreIncomingRequestHook = options.http.ignoreIncomingRequestHook;
945
+ } else if (allIncomingPatterns.length > 0) {
946
+ httpConfig.ignoreIncomingRequestHook = (req) => {
947
+ const path = req.url || "";
948
+ return allIncomingPatterns.some((pattern) => pattern.test(path));
949
+ };
950
+ }
951
+ if (options.http?.requireParentForOutgoingSpans !== void 0 || config.http?.require_parent_for_outgoing_spans !== void 0) {
952
+ httpConfig.requireParentforOutgoingSpans = options.http?.requireParentForOutgoingSpans ?? config.http?.require_parent_for_outgoing_spans ?? false;
953
+ }
954
+ return httpConfig;
955
+ }
956
+ function buildUndiciInstrumentationConfig(options, config, _otlpEndpoint) {
957
+ const undiciConfig = { enabled: true };
958
+ const programmaticPatterns = options.http?.ignoreOutgoingUrls || [];
959
+ const yamlPatterns = config.http?.ignore_outgoing_urls || [];
960
+ const allPatterns = [
961
+ ...programmaticPatterns.map((p) => typeof p === "string" ? new RegExp(p) : p),
962
+ ...yamlPatterns.map((p) => new RegExp(p))
963
+ ];
964
+ if (allPatterns.length > 0) {
965
+ undiciConfig.ignoreRequestHook = (request) => {
966
+ const origin = request.origin;
967
+ const path = request.path;
968
+ const url = `${origin}${path}`;
969
+ const matchesPattern = allPatterns.some((pattern) => pattern.test(url) || pattern.test(path));
970
+ return matchesPattern;
971
+ };
972
+ }
973
+ return undiciConfig;
974
+ }
975
+ function isEffectProject() {
976
+ try {
977
+ __require.resolve("effect");
978
+ return true;
979
+ } catch {
980
+ return false;
981
+ }
982
+ }
983
+ function shouldEnableAutoInstrumentation(explicitValue, hasWebFramework) {
984
+ if (explicitValue !== void 0) {
985
+ return explicitValue;
986
+ }
987
+ const isEffect = isEffectProject();
988
+ if (isEffect && !hasWebFramework) {
989
+ logger.log("@atrim/instrumentation: Detected Effect-TS without web framework");
990
+ logger.log(" - Auto-instrumentation disabled by default");
991
+ logger.log(" - Effect.withSpan() will create spans");
992
+ return false;
993
+ }
994
+ return true;
995
+ }
996
+ function hasWebFrameworkInstalled() {
997
+ const frameworks = ["express", "fastify", "koa", "@hono/node-server", "restify"];
998
+ for (const framework of frameworks) {
999
+ try {
1000
+ __require.resolve(framework);
1001
+ return true;
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ return false;
1006
+ }
1007
+ function isTracingAlreadyInitialized() {
1008
+ try {
1009
+ const provider = trace.getTracerProvider();
1010
+ const providerWithDelegate = provider;
1011
+ const delegate = providerWithDelegate._delegate || providerWithDelegate.getDelegate?.();
1012
+ if (delegate) {
1013
+ const delegateName = delegate.constructor.name;
1014
+ if (!delegateName.includes("Noop")) {
1015
+ return true;
1016
+ }
1017
+ }
1018
+ const providerWithProps = provider;
1019
+ const hasResource = providerWithProps.resource !== void 0;
1020
+ const hasActiveSpanProcessor = providerWithProps.activeSpanProcessor !== void 0;
1021
+ const hasTracers = providerWithProps._tracers !== void 0;
1022
+ return hasResource || hasActiveSpanProcessor || hasTracers;
1023
+ } catch {
1024
+ return false;
1025
+ }
1026
+ }
1027
+ async function initializeSdk(options = {}) {
1028
+ if (sdkInstance) {
1029
+ logger.warn("@atrim/instrumentation: SDK already initialized. Returning existing instance.");
1030
+ return sdkInstance;
1031
+ }
1032
+ if (initializationPromise) {
1033
+ logger.log(
1034
+ "@atrim/instrumentation: SDK already initialized, waiting for initialization to complete..."
1035
+ );
1036
+ return initializationPromise;
1037
+ }
1038
+ initializationPromise = performInitialization(options);
1039
+ try {
1040
+ const result = await initializationPromise;
1041
+ return result;
1042
+ } finally {
1043
+ initializationPromise = null;
1044
+ }
1045
+ }
1046
+ async function performInitialization(options) {
1047
+ const config = await loadConfigWithOptions(options);
1048
+ const loggingLevel = config.instrumentation.logging || "on";
1049
+ logger.setLevel(loggingLevel);
1050
+ const alreadyInitialized = isTracingAlreadyInitialized();
1051
+ if (alreadyInitialized) {
1052
+ logger.log("@atrim/instrumentation: Detected existing OpenTelemetry initialization.");
1053
+ logger.log(" - Skipping NodeSDK setup");
1054
+ logger.log(" - Setting up pattern-based filtering only");
1055
+ logger.log("");
1056
+ initializePatternMatcher(config);
1057
+ logger.log("@atrim/instrumentation: Pattern filtering initialized");
1058
+ logger.log(" \u26A0\uFE0F Note: Pattern filtering will only work with manual spans");
1059
+ logger.log(" \u26A0\uFE0F Auto-instrumentation must be configured separately");
1060
+ logger.log("");
1061
+ return null;
1062
+ }
1063
+ const serviceInfo = await detectServiceInfoAsync();
1064
+ const serviceName = options.serviceName || serviceInfo.name;
1065
+ const serviceVersion = options.serviceVersion || serviceInfo.version;
1066
+ const rawExporter = createOtlpExporter(options.otlp);
1067
+ const exporter = new SafeSpanExporter(rawExporter);
1068
+ const useSimpleProcessor = process.env.NODE_ENV === "test" || process.env.OTEL_USE_SIMPLE_PROCESSOR === "true";
1069
+ const baseProcessor = useSimpleProcessor ? new SimpleSpanProcessor(exporter) : new BatchSpanProcessor(exporter);
1070
+ const patternProcessor = new PatternSpanProcessor(config, baseProcessor);
1071
+ const instrumentations = [];
1072
+ const hasWebFramework = hasWebFrameworkInstalled();
1073
+ const enableAutoInstrumentation = shouldEnableAutoInstrumentation(
1074
+ options.autoInstrument,
1075
+ hasWebFramework
1076
+ );
1077
+ if (enableAutoInstrumentation) {
1078
+ options.otlp?.endpoint || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318/v1/traces";
1079
+ const httpConfig = buildHttpInstrumentationConfig(options, config);
1080
+ const undiciConfig = buildUndiciInstrumentationConfig(options, config);
1081
+ instrumentations.push(
1082
+ ...getNodeAutoInstrumentations({
1083
+ // Enable HTTP instrumentation with filtering (for http/https modules)
1084
+ "@opentelemetry/instrumentation-http": httpConfig,
1085
+ // Enable undici instrumentation with filtering (for fetch API)
1086
+ "@opentelemetry/instrumentation-undici": undiciConfig,
1087
+ // Enable web framework instrumentations
1088
+ "@opentelemetry/instrumentation-express": { enabled: true },
1089
+ "@opentelemetry/instrumentation-fastify": { enabled: true },
1090
+ "@opentelemetry/instrumentation-koa": { enabled: true },
1091
+ // Disable noisy instrumentations by default
1092
+ "@opentelemetry/instrumentation-fs": { enabled: false },
1093
+ "@opentelemetry/instrumentation-dns": { enabled: false }
1094
+ })
1095
+ );
1096
+ logger.log(`Auto-instrumentation: ${instrumentations.length} instrumentations enabled`);
1097
+ }
1098
+ if (options.instrumentations) {
1099
+ instrumentations.push(...options.instrumentations);
1100
+ }
1101
+ if (!enableAutoInstrumentation && instrumentations.length === 0) {
1102
+ const wasExplicit = options.autoInstrument === false;
1103
+ const detectionMessage = wasExplicit ? "@atrim/instrumentation: Auto-instrumentation: disabled" : "@atrim/instrumentation: Pure Effect-TS app detected (auto-detected)";
1104
+ logger.log(detectionMessage);
1105
+ logger.log(" - Skipping NodeSDK setup");
1106
+ logger.log(" - Pattern matching configured from instrumentation.yaml");
1107
+ if (!wasExplicit) {
1108
+ logger.log(" - Use EffectInstrumentationLive for tracing");
1109
+ }
1110
+ logger.log("");
1111
+ initializePatternMatcher(config);
1112
+ return null;
1113
+ }
1114
+ const sdkConfig = {
1115
+ spanProcessor: patternProcessor,
1116
+ serviceName,
1117
+ ...serviceVersion && { serviceVersion },
1118
+ instrumentations,
1119
+ // Allow advanced overrides
1120
+ ...options.sdk
1121
+ };
1122
+ const sdk = new NodeSDK(sdkConfig);
1123
+ sdk.start();
1124
+ sdkInstance = sdk;
1125
+ if (!options.disableAutoShutdown) {
1126
+ registerShutdownHandlers(sdk);
1127
+ }
1128
+ logInitialization(config, serviceName, serviceVersion, options, enableAutoInstrumentation);
1129
+ return sdk;
1130
+ }
1131
+ function getSdkInstance() {
1132
+ return sdkInstance;
1133
+ }
1134
+ async function shutdownSdk() {
1135
+ if (!sdkInstance) {
1136
+ return;
1137
+ }
1138
+ await sdkInstance.shutdown();
1139
+ sdkInstance = null;
1140
+ }
1141
+ function resetSdk() {
1142
+ sdkInstance = null;
1143
+ initializationPromise = null;
1144
+ }
1145
+ function registerShutdownHandlers(sdk) {
1146
+ const shutdown = async (signal) => {
1147
+ logger.log(`
1148
+ @atrim/instrumentation: Received ${signal}, shutting down gracefully...`);
1149
+ try {
1150
+ await sdk.shutdown();
1151
+ logger.log("@atrim/instrumentation: Shutdown complete");
1152
+ process.exit(0);
1153
+ } catch (error) {
1154
+ logger.error(
1155
+ "@atrim/instrumentation: Error during shutdown:",
1156
+ error instanceof Error ? error.message : String(error)
1157
+ );
1158
+ process.exit(1);
1159
+ }
1160
+ };
1161
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
1162
+ process.on("SIGINT", () => shutdown("SIGINT"));
1163
+ process.on("uncaughtException", async (error) => {
1164
+ logger.error("@atrim/instrumentation: Uncaught exception:", error);
1165
+ await sdk.shutdown();
1166
+ process.exit(1);
1167
+ });
1168
+ process.on("unhandledRejection", async (reason) => {
1169
+ logger.error("@atrim/instrumentation: Unhandled rejection:", reason);
1170
+ await sdk.shutdown();
1171
+ process.exit(1);
1172
+ });
1173
+ }
1174
+ function logInitialization(config, serviceName, serviceVersion, options, autoInstrumentEnabled) {
1175
+ logger.minimal("@atrim/instrumentation: SDK initialized successfully");
1176
+ logger.log(` - Service: ${serviceName}${serviceVersion ? ` v${serviceVersion}` : ""}`);
1177
+ if (config.instrumentation.enabled) {
1178
+ const instrumentCount = config.instrumentation.instrument_patterns.filter(
1179
+ (p) => p.enabled !== false
1180
+ ).length;
1181
+ const ignoreCount = config.instrumentation.ignore_patterns.length;
1182
+ logger.log(` - Pattern filtering: enabled`);
1183
+ logger.log(` - Instrument patterns: ${instrumentCount}`);
1184
+ logger.log(` - Ignore patterns: ${ignoreCount}`);
1185
+ } else {
1186
+ logger.log(` - Pattern filtering: disabled`);
1187
+ }
1188
+ const autoInstrumentLabel = autoInstrumentEnabled ? "enabled" : "disabled";
1189
+ const autoDetected = options.autoInstrument === void 0 ? " (auto-detected)" : "";
1190
+ logger.log(` - Auto-instrumentation: ${autoInstrumentLabel}${autoDetected}`);
1191
+ if (options.instrumentations && options.instrumentations.length > 0) {
1192
+ logger.log(` - Custom instrumentations: ${options.instrumentations.length}`);
1193
+ }
1194
+ const endpoint = options.otlp?.endpoint || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318/v1/traces";
1195
+ logger.log(` - OTLP endpoint: ${endpoint}`);
1196
+ logger.log("");
1197
+ }
1198
+ var require2 = createRequire(import.meta.url);
1199
+ function validateOpenTelemetryApi() {
1200
+ try {
1201
+ require2.resolve("@opentelemetry/api");
1202
+ } catch {
1203
+ throw new Error(
1204
+ "@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"
1205
+ );
1206
+ }
1207
+ }
1208
+ function validateEffectDependencies() {
1209
+ const packages = ["effect", "@effect/opentelemetry", "@effect/platform"];
1210
+ for (const pkg of packages) {
1211
+ try {
1212
+ require2.resolve(pkg);
1213
+ } catch {
1214
+ return false;
1215
+ }
1216
+ }
1217
+ return true;
1218
+ }
1219
+ var validateDependencies = Effect.try({
1220
+ try: () => validateOpenTelemetryApi(),
1221
+ catch: (error) => new InitializationError2({
1222
+ reason: error instanceof Error ? error.message : "Dependency validation failed",
1223
+ cause: error
1224
+ })
1225
+ });
1226
+ Effect.sync(() => validateEffectDependencies());
1227
+
1228
+ // src/api.ts
1229
+ async function initializeInstrumentation(options = {}) {
1230
+ validateOpenTelemetryApi();
1231
+ const sdk = await initializeSdk(options);
1232
+ if (sdk) {
1233
+ const config = await loadConfigWithOptions(options);
1234
+ initializePatternMatcher(config);
1235
+ }
1236
+ return sdk;
1237
+ }
1238
+ async function initializePatternMatchingOnly(options = {}) {
1239
+ const config = await loadConfigWithOptions(options);
1240
+ initializePatternMatcher(config);
1241
+ logger.log("@atrim/instrumentation: Pattern matching initialized (legacy mode)");
1242
+ logger.log(
1243
+ " Note: NodeSDK is not initialized. Use initializeInstrumentation() for complete setup."
1244
+ );
1245
+ }
1246
+ var initializeInstrumentationEffect = (options = {}) => Effect.gen(function* () {
1247
+ yield* validateDependencies;
1248
+ const sdk = yield* Effect.tryPromise({
1249
+ try: () => initializeSdk(options),
1250
+ catch: (error) => new InitializationError2({
1251
+ reason: "SDK initialization failed",
1252
+ cause: error
1253
+ })
1254
+ });
1255
+ if (sdk) {
1256
+ yield* Effect.tryPromise({
1257
+ try: () => loadConfigWithOptions(options),
1258
+ catch: (error) => new ConfigError2({
1259
+ reason: "Failed to load config for pattern matcher",
1260
+ cause: error
1261
+ })
1262
+ }).pipe(
1263
+ Effect.tap(
1264
+ (config) => Effect.sync(() => {
1265
+ initializePatternMatcher(config);
1266
+ })
1267
+ )
1268
+ );
1269
+ }
1270
+ return sdk;
1271
+ });
1272
+ var initializePatternMatchingOnlyEffect = (options = {}) => Effect.gen(function* () {
1273
+ const config = yield* Effect.tryPromise({
1274
+ try: () => loadConfigWithOptions(options),
1275
+ catch: (error) => new ConfigError2({
1276
+ reason: "Failed to load configuration",
1277
+ cause: error
1278
+ })
1279
+ });
1280
+ yield* Effect.sync(() => {
1281
+ initializePatternMatcher(config);
1282
+ logger.log("@atrim/instrumentation: Pattern matching initialized (legacy mode)");
1283
+ logger.log(
1284
+ " Note: NodeSDK is not initialized. Use initializeInstrumentation() for complete setup."
1285
+ );
1286
+ });
1287
+ });
1288
+
1289
+ // src/integrations/standard/span-helpers.ts
1290
+ function setSpanAttributes(span, attributes) {
1291
+ for (const [key, value] of Object.entries(attributes)) {
1292
+ span.setAttribute(key, value);
1293
+ }
1294
+ }
1295
+ function recordException(span, error, context) {
1296
+ span.recordException(error);
1297
+ if (context) {
1298
+ for (const [key, value] of Object.entries(context)) {
1299
+ span.setAttribute(`error.${key}`, value);
1300
+ }
1301
+ }
1302
+ }
1303
+ function markSpanSuccess(span) {
1304
+ span.setStatus({ code: 1 });
1305
+ }
1306
+ function markSpanError(span, message) {
1307
+ if (message !== void 0) {
1308
+ span.setStatus({
1309
+ code: 2,
1310
+ // SpanStatusCode.ERROR = 2
1311
+ message
1312
+ });
1313
+ } else {
1314
+ span.setStatus({
1315
+ code: 2
1316
+ // SpanStatusCode.ERROR = 2
1317
+ });
1318
+ }
1319
+ }
1320
+ function annotateHttpRequest(span, method, url, statusCode) {
1321
+ span.setAttribute("http.method", method);
1322
+ span.setAttribute("http.url", url);
1323
+ if (statusCode !== void 0) {
1324
+ span.setAttribute("http.status_code", statusCode);
1325
+ if (statusCode >= 400) {
1326
+ markSpanError(span, `HTTP ${statusCode}`);
1327
+ } else {
1328
+ markSpanSuccess(span);
1329
+ }
1330
+ }
1331
+ }
1332
+ function annotateDbQuery(span, system, statement, table) {
1333
+ span.setAttribute("db.system", system);
1334
+ span.setAttribute("db.statement", statement);
1335
+ if (table) {
1336
+ span.setAttribute("db.table", table);
1337
+ }
1338
+ }
1339
+ function annotateCacheOperation(span, operation, key, hit) {
1340
+ span.setAttribute("cache.operation", operation);
1341
+ span.setAttribute("cache.key", key);
1342
+ if (hit !== void 0) {
1343
+ span.setAttribute("cache.hit", hit);
1344
+ }
1345
+ }
1346
+
1347
+ // src/core/test-utils.ts
1348
+ function suppressShutdownErrors() {
1349
+ if (!process.env.CI && process.env.NODE_ENV !== "test") {
1350
+ return;
1351
+ }
1352
+ const isHarmlessConnectionError = (error) => {
1353
+ if (!error || typeof error !== "object") {
1354
+ return false;
1355
+ }
1356
+ const nodeError = error;
1357
+ if (nodeError.code === "ECONNREFUSED") {
1358
+ return true;
1359
+ }
1360
+ if (nodeError.errors && Array.isArray(nodeError.errors) && nodeError.errors.length > 0 && nodeError.errors.every((e) => e?.code === "ECONNREFUSED")) {
1361
+ return true;
1362
+ }
1363
+ return false;
1364
+ };
1365
+ process.on("uncaughtException", (error) => {
1366
+ if (isHarmlessConnectionError(error)) {
1367
+ console.log("\u{1F4E4} Export failed (collector stopped) - this is expected in tests");
1368
+ return;
1369
+ }
1370
+ console.error("Uncaught exception:", error);
1371
+ process.exit(1);
1372
+ });
1373
+ process.on("unhandledRejection", (reason) => {
1374
+ if (isHarmlessConnectionError(reason)) {
1375
+ console.log("\u{1F4E4} Export failed (collector stopped) - this is expected in tests");
1376
+ return;
1377
+ }
1378
+ console.error("Unhandled rejection:", reason);
1379
+ process.exit(1);
1380
+ });
1381
+ }
1382
+
1383
+ export { ConfigError2 as ConfigError, ConfigFileError2 as ConfigFileError, ConfigUrlError2 as ConfigUrlError, ConfigValidationError2 as ConfigValidationError, ExportError2 as ExportError, InitializationError2 as InitializationError, PatternMatcher, PatternSpanProcessor, ServiceDetectionError2 as ServiceDetectionError, ShutdownError2 as ShutdownError, annotateCacheOperation, annotateDbQuery, annotateHttpRequest, _resetConfigLoaderCache as clearConfigCache, createOtlpExporter, detectServiceInfoAsync as detectServiceInfo, detectServiceInfo as detectServiceInfoEffect, getOtlpEndpoint, getPatternMatcher, getSdkInstance, getServiceInfoWithFallback, getServiceNameAsync as getServiceName, getServiceName as getServiceNameEffect, getServiceVersionAsync as getServiceVersion, getServiceVersion as getServiceVersionEffect, initializeInstrumentation, initializeInstrumentationEffect, initializePatternMatchingOnly, initializePatternMatchingOnlyEffect, loadConfig, loadConfigFromInline, loadConfigWithOptions, markSpanError, markSpanSuccess, recordException, resetSdk, setSpanAttributes, shouldInstrumentSpan, shutdownSdk, suppressShutdownErrors };
1384
+ //# sourceMappingURL=index.js.map
1385
+ //# sourceMappingURL=index.js.map