@atrim/instrument-node 0.1.3-fea6398-20251118005809 → 0.4.0-08fae61-20251118193009

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.
@@ -4,10 +4,8 @@ var effect = require('effect');
4
4
  var Otlp = require('@effect/opentelemetry/Otlp');
5
5
  var platform = require('@effect/platform');
6
6
  var api = require('@opentelemetry/api');
7
- var fs = require('fs');
8
- var path = require('path');
9
- var yaml = require('yaml');
10
- var zod = require('zod');
7
+ var instrumentCore = require('@atrim/instrument-core');
8
+ var platformNode = require('@effect/platform-node');
11
9
 
12
10
  function _interopNamespace(e) {
13
11
  if (e && e.__esModule) return e;
@@ -30,84 +28,35 @@ function _interopNamespace(e) {
30
28
  var Otlp__namespace = /*#__PURE__*/_interopNamespace(Otlp);
31
29
 
32
30
  // src/integrations/effect/effect-tracer.ts
33
- var __defProp = Object.defineProperty;
34
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
35
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
36
- var PatternConfigSchema = zod.z.object({
37
- pattern: zod.z.string(),
38
- enabled: zod.z.boolean().optional(),
39
- description: zod.z.string().optional()
40
- });
41
- var AutoIsolationConfigSchema = zod.z.object({
42
- // Global enable/disable for auto-isolation
43
- enabled: zod.z.boolean().default(false),
44
- // Which operators to auto-isolate
45
- operators: zod.z.object({
46
- fiberset_run: zod.z.boolean().default(true),
47
- effect_fork: zod.z.boolean().default(true),
48
- effect_fork_daemon: zod.z.boolean().default(true),
49
- effect_fork_in: zod.z.boolean().default(false)
50
- }).default({}),
51
- // Virtual parent tracking configuration
52
- tracking: zod.z.object({
53
- use_span_links: zod.z.boolean().default(true),
54
- use_attributes: zod.z.boolean().default(true),
55
- capture_logical_parent: zod.z.boolean().default(true)
56
- }).default({}),
57
- // Span categorization
58
- attributes: zod.z.object({
59
- category: zod.z.string().default("background_task"),
60
- add_metadata: zod.z.boolean().default(true)
61
- }).default({})
62
- });
63
- var HttpFilteringConfigSchema = zod.z.object({
64
- // Patterns to ignore for outgoing HTTP requests (string patterns only in YAML)
65
- ignore_outgoing_urls: zod.z.array(zod.z.string()).optional(),
66
- // Patterns to ignore for incoming HTTP requests (string patterns only in YAML)
67
- ignore_incoming_paths: zod.z.array(zod.z.string()).optional(),
68
- // Require parent span for outgoing requests (prevents root spans for HTTP calls)
69
- require_parent_for_outgoing_spans: zod.z.boolean().optional()
70
- });
71
- var InstrumentationConfigSchema = zod.z.object({
72
- version: zod.z.string(),
73
- instrumentation: zod.z.object({
74
- enabled: zod.z.boolean(),
75
- description: zod.z.string().optional(),
76
- logging: zod.z.enum(["on", "off", "minimal"]).optional().default("on"),
77
- instrument_patterns: zod.z.array(PatternConfigSchema),
78
- ignore_patterns: zod.z.array(PatternConfigSchema)
79
- }),
80
- effect: zod.z.object({
81
- auto_extract_metadata: zod.z.boolean(),
82
- auto_isolation: AutoIsolationConfigSchema.optional()
83
- }).optional(),
84
- http: HttpFilteringConfigSchema.optional()
85
- });
86
- (class extends effect.Data.TaggedError("ConfigError") {
87
- });
88
- var ConfigUrlError = class extends effect.Data.TaggedError("ConfigUrlError") {
89
- };
90
- var ConfigValidationError = class extends effect.Data.TaggedError("ConfigValidationError") {
91
- };
92
- var ConfigFileError = class extends effect.Data.TaggedError("ConfigFileError") {
93
- };
94
- (class extends effect.Data.TaggedError("ServiceDetectionError") {
95
- });
96
- (class extends effect.Data.TaggedError("InitializationError") {
97
- });
98
- (class extends effect.Data.TaggedError("ExportError") {
99
- });
100
- (class extends effect.Data.TaggedError("ShutdownError") {
101
- });
102
- var SECURITY_DEFAULTS = {
103
- maxConfigSize: 1e6,
104
- // 1MB
105
- requestTimeout: 5e3,
106
- allowedProtocols: ["https:"],
107
- // Only HTTPS for remote configs
108
- cacheTimeout: 3e5
109
- // 5 minutes
110
- };
31
+ var NodeConfigLoaderLive = instrumentCore.ConfigLoaderLive.pipe(
32
+ effect.Layer.provide(effect.Layer.mergeAll(platformNode.NodeContext.layer, platform.FetchHttpClient.layer))
33
+ );
34
+ var cachedLoaderPromise = null;
35
+ function getCachedLoader() {
36
+ if (!cachedLoaderPromise) {
37
+ cachedLoaderPromise = effect.Effect.runPromise(
38
+ effect.Effect.gen(function* () {
39
+ return yield* instrumentCore.ConfigLoader;
40
+ }).pipe(effect.Effect.provide(NodeConfigLoaderLive))
41
+ );
42
+ }
43
+ return cachedLoaderPromise;
44
+ }
45
+ async function loadConfig(uri, options) {
46
+ if (options?.cacheTimeout === 0) {
47
+ const program = effect.Effect.gen(function* () {
48
+ const loader2 = yield* instrumentCore.ConfigLoader;
49
+ return yield* loader2.loadFromUri(uri);
50
+ });
51
+ return effect.Effect.runPromise(program.pipe(effect.Effect.provide(NodeConfigLoaderLive)));
52
+ }
53
+ const loader = await getCachedLoader();
54
+ return effect.Effect.runPromise(loader.loadFromUri(uri));
55
+ }
56
+ async function loadConfigFromInline(content) {
57
+ const loader = await getCachedLoader();
58
+ return effect.Effect.runPromise(loader.loadFromInline(content));
59
+ }
111
60
  function getDefaultConfig() {
112
61
  return {
113
62
  version: "1.0",
@@ -131,340 +80,36 @@ function getDefaultConfig() {
131
80
  }
132
81
  };
133
82
  }
134
- var validateConfigEffect = (rawConfig) => effect.Effect.try({
135
- try: () => InstrumentationConfigSchema.parse(rawConfig),
136
- catch: (error) => new ConfigValidationError({
137
- reason: "Invalid configuration schema",
138
- cause: error
139
- })
140
- });
141
- var loadConfigFromFileEffect = (filePath) => effect.Effect.gen(function* () {
142
- const fileContents = yield* effect.Effect.try({
143
- try: () => fs.readFileSync(filePath, "utf8"),
144
- catch: (error) => new ConfigFileError({
145
- reason: `Failed to read config file at ${filePath}`,
146
- cause: error
147
- })
148
- });
149
- if (fileContents.length > SECURITY_DEFAULTS.maxConfigSize) {
150
- return yield* effect.Effect.fail(
151
- new ConfigFileError({
152
- reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
153
- })
154
- );
155
- }
156
- let rawConfig;
157
- try {
158
- rawConfig = yaml.parse(fileContents);
159
- } catch (error) {
160
- return yield* effect.Effect.fail(
161
- new ConfigValidationError({
162
- reason: "Invalid YAML syntax",
163
- cause: error
164
- })
165
- );
166
- }
167
- return yield* validateConfigEffect(rawConfig);
168
- });
169
- var fetchAndParseConfig = (url) => effect.Effect.gen(function* () {
170
- let urlObj;
171
- try {
172
- urlObj = new URL(url);
173
- } catch (error) {
174
- return yield* effect.Effect.fail(
175
- new ConfigUrlError({
176
- reason: `Invalid URL: ${url}`,
177
- cause: error
178
- })
179
- );
180
- }
181
- if (!SECURITY_DEFAULTS.allowedProtocols.includes(urlObj.protocol)) {
182
- return yield* effect.Effect.fail(
183
- new ConfigUrlError({
184
- reason: `Insecure protocol ${urlObj.protocol}. Only ${SECURITY_DEFAULTS.allowedProtocols.join(", ")} are allowed`
185
- })
186
- );
187
- }
188
- const response = yield* effect.Effect.tryPromise({
189
- try: () => fetch(url, {
190
- redirect: "follow",
191
- headers: {
192
- Accept: "application/yaml, text/yaml, text/x-yaml"
193
- }
194
- }),
195
- catch: (error) => new ConfigUrlError({
196
- reason: `Failed to load config from URL ${url}`,
197
- cause: error
198
- })
199
- }).pipe(
200
- effect.Effect.timeout(effect.Duration.millis(SECURITY_DEFAULTS.requestTimeout)),
201
- effect.Effect.retry({
202
- times: 3,
203
- schedule: effect.Schedule.exponential(effect.Duration.millis(100))
204
- }),
205
- effect.Effect.catchAll((error) => {
206
- if (error._tag === "TimeoutException") {
207
- return effect.Effect.fail(
208
- new ConfigUrlError({
209
- reason: `Config fetch timeout after ${SECURITY_DEFAULTS.requestTimeout}ms`
210
- })
211
- );
212
- }
213
- return effect.Effect.fail(error);
214
- })
215
- );
216
- if (!response.ok) {
217
- return yield* effect.Effect.fail(
218
- new ConfigUrlError({
219
- reason: `HTTP ${response.status}: ${response.statusText}`
220
- })
221
- );
222
- }
223
- const contentLength = response.headers.get("content-length");
224
- if (contentLength && parseInt(contentLength) > SECURITY_DEFAULTS.maxConfigSize) {
225
- return yield* effect.Effect.fail(
226
- new ConfigUrlError({
227
- reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
228
- })
229
- );
230
- }
231
- const text = yield* effect.Effect.tryPromise({
232
- try: () => response.text(),
233
- catch: (error) => new ConfigUrlError({
234
- reason: "Failed to read response body",
235
- cause: error
236
- })
237
- });
238
- if (text.length > SECURITY_DEFAULTS.maxConfigSize) {
239
- return yield* effect.Effect.fail(
240
- new ConfigUrlError({
241
- reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
242
- })
243
- );
244
- }
245
- let rawConfig;
246
- try {
247
- rawConfig = yaml.parse(text);
248
- } catch (error) {
249
- return yield* effect.Effect.fail(
250
- new ConfigValidationError({
251
- reason: "Invalid YAML syntax",
252
- cause: error
253
- })
254
- );
255
- }
256
- return yield* validateConfigEffect(rawConfig);
257
- });
258
- var makeConfigCache = () => effect.Cache.make({
259
- capacity: 100,
260
- timeToLive: effect.Duration.minutes(5),
261
- lookup: (url) => fetchAndParseConfig(url)
262
- });
263
- var cacheInstance = null;
264
- var getCache = effect.Effect.gen(function* () {
265
- if (!cacheInstance) {
266
- cacheInstance = yield* makeConfigCache();
267
- }
268
- return cacheInstance;
269
- });
270
- var loadConfigFromUrlEffect = (url, cacheTimeout = SECURITY_DEFAULTS.cacheTimeout) => effect.Effect.gen(function* () {
271
- if (cacheTimeout === 0) {
272
- return yield* fetchAndParseConfig(url);
273
- }
274
- const cache = yield* getCache;
275
- return yield* cache.get(url);
276
- });
277
- var loadConfigEffect = (options = {}) => effect.Effect.gen(function* () {
83
+ async function loadConfigWithOptions(options = {}) {
84
+ const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
278
85
  if (options.config) {
279
- return yield* validateConfigEffect(options.config);
86
+ return loadConfigFromInline(options.config);
280
87
  }
281
88
  const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
282
89
  if (envConfigPath) {
283
- if (envConfigPath.startsWith("http://") || envConfigPath.startsWith("https://")) {
284
- return yield* loadConfigFromUrlEffect(envConfigPath, options.cacheTimeout);
285
- }
286
- return yield* loadConfigFromFileEffect(envConfigPath);
90
+ return loadConfig(envConfigPath, loadOptions);
287
91
  }
288
92
  if (options.configUrl) {
289
- return yield* loadConfigFromUrlEffect(options.configUrl, options.cacheTimeout);
93
+ return loadConfig(options.configUrl, loadOptions);
290
94
  }
291
95
  if (options.configPath) {
292
- return yield* loadConfigFromFileEffect(options.configPath);
96
+ return loadConfig(options.configPath, loadOptions);
293
97
  }
294
- const defaultPath = path.join(process.cwd(), "instrumentation.yaml");
295
- const exists = yield* effect.Effect.sync(() => fs.existsSync(defaultPath));
296
- if (exists) {
297
- return yield* loadConfigFromFileEffect(defaultPath);
98
+ const { existsSync } = await import('fs');
99
+ const { join } = await import('path');
100
+ const defaultPath = join(process.cwd(), "instrumentation.yaml");
101
+ if (existsSync(defaultPath)) {
102
+ return loadConfig(defaultPath, loadOptions);
298
103
  }
299
104
  return getDefaultConfig();
300
- });
301
- async function loadConfig(options = {}) {
302
- return effect.Effect.runPromise(
303
- loadConfigEffect(options).pipe(
304
- // Convert typed errors to regular Error with reason message for backward compatibility
305
- effect.Effect.mapError((error) => {
306
- const message = error.reason;
307
- const newError = new Error(message);
308
- newError.cause = error.cause;
309
- return newError;
310
- })
311
- )
312
- );
313
- }
314
- var PatternMatcher = class {
315
- constructor(config) {
316
- __publicField(this, "ignorePatterns", []);
317
- __publicField(this, "instrumentPatterns", []);
318
- __publicField(this, "enabled", true);
319
- this.enabled = config.instrumentation.enabled;
320
- this.ignorePatterns = config.instrumentation.ignore_patterns.map((p) => this.compilePattern(p));
321
- this.instrumentPatterns = config.instrumentation.instrument_patterns.filter((p) => p.enabled !== false).map((p) => this.compilePattern(p));
322
- }
323
- /**
324
- * Compile a pattern configuration into a RegExp
325
- */
326
- compilePattern(pattern) {
327
- try {
328
- const compiled = {
329
- regex: new RegExp(pattern.pattern),
330
- enabled: pattern.enabled !== false
331
- };
332
- if (pattern.description !== void 0) {
333
- compiled.description = pattern.description;
334
- }
335
- return compiled;
336
- } catch (error) {
337
- throw new Error(
338
- `Failed to compile pattern "${pattern.pattern}": ${error instanceof Error ? error.message : String(error)}`
339
- );
340
- }
341
- }
342
- /**
343
- * Check if a span should be instrumented
344
- *
345
- * Returns true if the span should be created, false otherwise.
346
- *
347
- * Logic:
348
- * 1. If instrumentation disabled globally, return false
349
- * 2. Check ignore patterns - if any match, return false
350
- * 3. Check instrument patterns - if any match, return true
351
- * 4. Default: return true (fail-open - create span if no patterns match)
352
- */
353
- shouldInstrument(spanName) {
354
- if (!this.enabled) {
355
- return false;
356
- }
357
- for (const pattern of this.ignorePatterns) {
358
- if (pattern.regex.test(spanName)) {
359
- return false;
360
- }
361
- }
362
- for (const pattern of this.instrumentPatterns) {
363
- if (pattern.enabled && pattern.regex.test(spanName)) {
364
- return true;
365
- }
366
- }
367
- return true;
368
- }
369
- /**
370
- * Get statistics about pattern matching (for debugging/monitoring)
371
- */
372
- getStats() {
373
- return {
374
- enabled: this.enabled,
375
- ignorePatternCount: this.ignorePatterns.length,
376
- instrumentPatternCount: this.instrumentPatterns.filter((p) => p.enabled).length
377
- };
378
- }
379
- };
380
- function initializePatternMatcher(config) {
381
- new PatternMatcher(config);
382
105
  }
383
- var Logger = class {
384
- constructor() {
385
- __publicField(this, "level", "on");
386
- __publicField(this, "hasLoggedMinimal", false);
387
- }
388
- /**
389
- * Set the logging level
390
- */
391
- setLevel(level) {
392
- this.level = level;
393
- this.hasLoggedMinimal = false;
394
- }
395
- /**
396
- * Get the current logging level
397
- */
398
- getLevel() {
399
- return this.level;
400
- }
401
- /**
402
- * Log a minimal initialization message (only shown once in minimal mode)
403
- */
404
- minimal(message) {
405
- if (this.level === "off") {
406
- return;
407
- }
408
- if (this.level === "minimal" && !this.hasLoggedMinimal) {
409
- console.log(message);
410
- this.hasLoggedMinimal = true;
411
- return;
412
- }
413
- if (this.level === "on") {
414
- console.log(message);
415
- }
416
- }
417
- /**
418
- * Log an informational message
419
- */
420
- log(...args) {
421
- if (this.level === "on") {
422
- console.log(...args);
423
- }
424
- }
425
- /**
426
- * Log a warning message (shown in minimal mode)
427
- */
428
- warn(...args) {
429
- if (this.level !== "off") {
430
- console.warn(...args);
431
- }
432
- }
433
- /**
434
- * Log an error message (shown in minimal mode)
435
- */
436
- error(...args) {
437
- if (this.level !== "off") {
438
- console.error(...args);
439
- }
440
- }
441
- /**
442
- * Check if full logging is enabled
443
- */
444
- isEnabled() {
445
- return this.level === "on";
446
- }
447
- /**
448
- * Check if minimal logging is enabled
449
- */
450
- isMinimal() {
451
- return this.level === "minimal";
452
- }
453
- /**
454
- * Check if logging is completely disabled
455
- */
456
- isDisabled() {
457
- return this.level === "off";
458
- }
459
- };
460
- var logger = new Logger();
461
106
 
462
107
  // src/integrations/effect/effect-tracer.ts
463
108
  function createEffectInstrumentation(options = {}) {
464
109
  return effect.Layer.unwrapEffect(
465
110
  effect.Effect.gen(function* () {
466
111
  const config = yield* effect.Effect.tryPromise({
467
- try: () => loadConfig(options),
112
+ try: () => loadConfigWithOptions(options),
468
113
  catch: (error) => ({
469
114
  _tag: "ConfigError",
470
115
  message: error instanceof Error ? error.message : String(error)
@@ -472,19 +117,19 @@ function createEffectInstrumentation(options = {}) {
472
117
  });
473
118
  yield* effect.Effect.sync(() => {
474
119
  const loggingLevel = config.instrumentation.logging || "on";
475
- logger.setLevel(loggingLevel);
120
+ instrumentCore.logger.setLevel(loggingLevel);
476
121
  });
477
- yield* effect.Effect.sync(() => initializePatternMatcher(config));
122
+ yield* effect.Effect.sync(() => instrumentCore.initializePatternMatcher(config));
478
123
  const otlpEndpoint = options.otlpEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
479
124
  const serviceName = options.serviceName || process.env.OTEL_SERVICE_NAME || "effect-service";
480
125
  const serviceVersion = options.serviceVersion || process.env.npm_package_version || "1.0.0";
481
126
  const autoExtractMetadata = options.autoExtractMetadata ?? config.effect?.auto_extract_metadata ?? true;
482
127
  const continueExistingTraces = options.continueExistingTraces ?? true;
483
- logger.log("\u{1F50D} Effect OpenTelemetry instrumentation");
484
- logger.log(` \u{1F4E1} Endpoint: ${otlpEndpoint}`);
485
- logger.log(` \u{1F3F7}\uFE0F Service: ${serviceName}`);
486
- logger.log(` \u2705 Auto metadata extraction: ${autoExtractMetadata}`);
487
- logger.log(` \u2705 Continue existing traces: ${continueExistingTraces}`);
128
+ instrumentCore.logger.log("\u{1F50D} Effect OpenTelemetry instrumentation");
129
+ instrumentCore.logger.log(` \u{1F4E1} Endpoint: ${otlpEndpoint}`);
130
+ instrumentCore.logger.log(` \u{1F3F7}\uFE0F Service: ${serviceName}`);
131
+ instrumentCore.logger.log(` \u2705 Auto metadata extraction: ${autoExtractMetadata}`);
132
+ instrumentCore.logger.log(` \u2705 Continue existing traces: ${continueExistingTraces}`);
488
133
  const otlpLayer = Otlp__namespace.layer({
489
134
  baseUrl: otlpEndpoint,
490
135
  resource: {
@@ -522,10 +167,10 @@ var EffectInstrumentationLive = effect.Effect.sync(() => {
522
167
  const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
523
168
  const serviceName = process.env.OTEL_SERVICE_NAME || "effect-service";
524
169
  const serviceVersion = process.env.npm_package_version || "1.0.0";
525
- logger.minimal(`@atrim/instrumentation/effect: Effect tracing enabled (${serviceName})`);
526
- logger.log("\u{1F50D} Effect OpenTelemetry tracer");
527
- logger.log(` \u{1F4E1} Endpoint: ${endpoint}`);
528
- logger.log(` \u{1F3F7}\uFE0F Service: ${serviceName}`);
170
+ instrumentCore.logger.minimal(`@atrim/instrumentation/effect: Effect tracing enabled (${serviceName})`);
171
+ instrumentCore.logger.log("\u{1F50D} Effect OpenTelemetry tracer");
172
+ instrumentCore.logger.log(` \u{1F4E1} Endpoint: ${endpoint}`);
173
+ instrumentCore.logger.log(` \u{1F3F7}\uFE0F Service: ${serviceName}`);
529
174
  return Otlp__namespace.layer({
530
175
  baseUrl: endpoint,
531
176
  resource: {