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

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