@atrim/instrument-node 0.4.0-c3ef89c-20251118193817 → 0.4.1

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,7 +4,11 @@ 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 instrumentCore = require('@atrim/instrument-core');
7
+ var FileSystem = require('@effect/platform/FileSystem');
8
+ var HttpClient = require('@effect/platform/HttpClient');
9
+ var HttpClientRequest = require('@effect/platform/HttpClientRequest');
10
+ var yaml = require('yaml');
11
+ var zod = require('zod');
8
12
  var platformNode = require('@effect/platform-node');
9
13
 
10
14
  function _interopNamespace(e) {
@@ -26,9 +30,389 @@ function _interopNamespace(e) {
26
30
  }
27
31
 
28
32
  var Otlp__namespace = /*#__PURE__*/_interopNamespace(Otlp);
33
+ var HttpClient__namespace = /*#__PURE__*/_interopNamespace(HttpClient);
34
+ var HttpClientRequest__namespace = /*#__PURE__*/_interopNamespace(HttpClientRequest);
29
35
 
30
36
  // src/integrations/effect/effect-tracer.ts
31
- var NodeConfigLoaderLive = instrumentCore.ConfigLoaderLive.pipe(
37
+ var __defProp = Object.defineProperty;
38
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
39
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
40
+ var PatternConfigSchema = zod.z.object({
41
+ pattern: zod.z.string(),
42
+ enabled: zod.z.boolean().optional(),
43
+ description: zod.z.string().optional()
44
+ });
45
+ var AutoIsolationConfigSchema = zod.z.object({
46
+ // Global enable/disable for auto-isolation
47
+ enabled: zod.z.boolean().default(false),
48
+ // Which operators to auto-isolate
49
+ operators: zod.z.object({
50
+ fiberset_run: zod.z.boolean().default(true),
51
+ effect_fork: zod.z.boolean().default(true),
52
+ effect_fork_daemon: zod.z.boolean().default(true),
53
+ effect_fork_in: zod.z.boolean().default(false)
54
+ }).default({}),
55
+ // Virtual parent tracking configuration
56
+ tracking: zod.z.object({
57
+ use_span_links: zod.z.boolean().default(true),
58
+ use_attributes: zod.z.boolean().default(true),
59
+ capture_logical_parent: zod.z.boolean().default(true)
60
+ }).default({}),
61
+ // Span categorization
62
+ attributes: zod.z.object({
63
+ category: zod.z.string().default("background_task"),
64
+ add_metadata: zod.z.boolean().default(true)
65
+ }).default({})
66
+ });
67
+ var HttpFilteringConfigSchema = zod.z.object({
68
+ // Patterns to ignore for outgoing HTTP requests (string patterns only in YAML)
69
+ ignore_outgoing_urls: zod.z.array(zod.z.string()).optional(),
70
+ // Patterns to ignore for incoming HTTP requests (string patterns only in YAML)
71
+ ignore_incoming_paths: zod.z.array(zod.z.string()).optional(),
72
+ // Require parent span for outgoing requests (prevents root spans for HTTP calls)
73
+ require_parent_for_outgoing_spans: zod.z.boolean().optional()
74
+ });
75
+ var InstrumentationConfigSchema = zod.z.object({
76
+ version: zod.z.string(),
77
+ instrumentation: zod.z.object({
78
+ enabled: zod.z.boolean(),
79
+ description: zod.z.string().optional(),
80
+ logging: zod.z.enum(["on", "off", "minimal"]).optional().default("on"),
81
+ instrument_patterns: zod.z.array(PatternConfigSchema),
82
+ ignore_patterns: zod.z.array(PatternConfigSchema)
83
+ }),
84
+ effect: zod.z.object({
85
+ auto_extract_metadata: zod.z.boolean(),
86
+ auto_isolation: AutoIsolationConfigSchema.optional()
87
+ }).optional(),
88
+ http: HttpFilteringConfigSchema.optional()
89
+ });
90
+ (class extends effect.Data.TaggedError("ConfigError") {
91
+ get message() {
92
+ return this.reason;
93
+ }
94
+ });
95
+ var ConfigUrlError = class extends effect.Data.TaggedError("ConfigUrlError") {
96
+ get message() {
97
+ return this.reason;
98
+ }
99
+ };
100
+ var ConfigValidationError = class extends effect.Data.TaggedError("ConfigValidationError") {
101
+ get message() {
102
+ return this.reason;
103
+ }
104
+ };
105
+ var ConfigFileError = class extends effect.Data.TaggedError("ConfigFileError") {
106
+ get message() {
107
+ return this.reason;
108
+ }
109
+ };
110
+ (class extends effect.Data.TaggedError("ServiceDetectionError") {
111
+ get message() {
112
+ return this.reason;
113
+ }
114
+ });
115
+ (class extends effect.Data.TaggedError("InitializationError") {
116
+ get message() {
117
+ return this.reason;
118
+ }
119
+ });
120
+ (class extends effect.Data.TaggedError("ExportError") {
121
+ get message() {
122
+ return this.reason;
123
+ }
124
+ });
125
+ (class extends effect.Data.TaggedError("ShutdownError") {
126
+ get message() {
127
+ return this.reason;
128
+ }
129
+ });
130
+ var SECURITY_DEFAULTS = {
131
+ maxConfigSize: 1e6,
132
+ // 1MB
133
+ requestTimeout: 5e3
134
+ // 5 seconds
135
+ };
136
+ var ConfigLoader = class extends effect.Context.Tag("ConfigLoader")() {
137
+ };
138
+ var parseYamlContent = (content, uri) => effect.Effect.gen(function* () {
139
+ const parsed = yield* effect.Effect.try({
140
+ try: () => yaml.parse(content),
141
+ catch: (error) => new ConfigValidationError({
142
+ reason: uri ? `Failed to parse YAML from ${uri}` : "Failed to parse YAML",
143
+ cause: error
144
+ })
145
+ });
146
+ return yield* effect.Effect.try({
147
+ try: () => InstrumentationConfigSchema.parse(parsed),
148
+ catch: (error) => new ConfigValidationError({
149
+ reason: uri ? `Invalid configuration schema from ${uri}` : "Invalid configuration schema",
150
+ cause: error
151
+ })
152
+ });
153
+ });
154
+ var loadFromFileWithFs = (fs, path, uri) => effect.Effect.gen(function* () {
155
+ const content = yield* fs.readFileString(path).pipe(
156
+ effect.Effect.mapError(
157
+ (error) => new ConfigFileError({
158
+ reason: `Failed to read config file at ${uri}`,
159
+ cause: error
160
+ })
161
+ )
162
+ );
163
+ if (content.length > SECURITY_DEFAULTS.maxConfigSize) {
164
+ return yield* effect.Effect.fail(
165
+ new ConfigFileError({
166
+ reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
167
+ })
168
+ );
169
+ }
170
+ return yield* parseYamlContent(content, uri);
171
+ });
172
+ var loadFromHttpWithClient = (client, url) => effect.Effect.scoped(
173
+ effect.Effect.gen(function* () {
174
+ if (url.startsWith("http://")) {
175
+ return yield* effect.Effect.fail(
176
+ new ConfigUrlError({
177
+ reason: "Insecure protocol: only HTTPS URLs are allowed"
178
+ })
179
+ );
180
+ }
181
+ const request = HttpClientRequest__namespace.get(url).pipe(
182
+ HttpClientRequest__namespace.setHeaders({
183
+ Accept: "application/yaml, text/yaml, application/x-yaml"
184
+ })
185
+ );
186
+ const response = yield* client.execute(request).pipe(
187
+ effect.Effect.timeout(`${SECURITY_DEFAULTS.requestTimeout} millis`),
188
+ effect.Effect.mapError((error) => {
189
+ if (error._tag === "TimeoutException") {
190
+ return new ConfigUrlError({
191
+ reason: `Config fetch timeout after ${SECURITY_DEFAULTS.requestTimeout}ms from ${url}`
192
+ });
193
+ }
194
+ return new ConfigUrlError({
195
+ reason: `Failed to load config from URL: ${url}`,
196
+ cause: error
197
+ });
198
+ })
199
+ );
200
+ if (response.status >= 400) {
201
+ return yield* effect.Effect.fail(
202
+ new ConfigUrlError({
203
+ reason: `HTTP ${response.status} from ${url}`
204
+ })
205
+ );
206
+ }
207
+ const text = yield* response.text.pipe(
208
+ effect.Effect.mapError(
209
+ (error) => new ConfigUrlError({
210
+ reason: `Failed to read response body from ${url}`,
211
+ cause: error
212
+ })
213
+ )
214
+ );
215
+ if (text.length > 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
+ return yield* parseYamlContent(text, url);
223
+ })
224
+ );
225
+ var makeConfigLoader = effect.Effect.gen(function* () {
226
+ const fs = yield* effect.Effect.serviceOption(FileSystem.FileSystem);
227
+ const http = yield* HttpClient__namespace.HttpClient;
228
+ const loadFromUriUncached = (uri) => effect.Effect.gen(function* () {
229
+ if (uri.startsWith("file://")) {
230
+ const path = uri.slice(7);
231
+ if (fs._tag === "None") {
232
+ return yield* effect.Effect.fail(
233
+ new ConfigFileError({
234
+ reason: "FileSystem not available (browser environment?)",
235
+ cause: { uri }
236
+ })
237
+ );
238
+ }
239
+ return yield* loadFromFileWithFs(fs.value, path, uri);
240
+ }
241
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
242
+ return yield* loadFromHttpWithClient(http, uri);
243
+ }
244
+ if (fs._tag === "Some") {
245
+ return yield* loadFromFileWithFs(fs.value, uri, uri);
246
+ } else {
247
+ return yield* loadFromHttpWithClient(http, uri);
248
+ }
249
+ });
250
+ const loadFromUriCached = yield* effect.Effect.cachedFunction(loadFromUriUncached);
251
+ return ConfigLoader.of({
252
+ loadFromUri: loadFromUriCached,
253
+ loadFromInline: (content) => effect.Effect.gen(function* () {
254
+ if (typeof content === "string") {
255
+ return yield* parseYamlContent(content);
256
+ }
257
+ return yield* effect.Effect.try({
258
+ try: () => InstrumentationConfigSchema.parse(content),
259
+ catch: (error) => new ConfigValidationError({
260
+ reason: "Invalid configuration schema",
261
+ cause: error
262
+ })
263
+ });
264
+ })
265
+ });
266
+ });
267
+ var ConfigLoaderLive = effect.Layer.effect(ConfigLoader, makeConfigLoader);
268
+ var PatternMatcher = class {
269
+ constructor(config) {
270
+ __publicField(this, "ignorePatterns", []);
271
+ __publicField(this, "instrumentPatterns", []);
272
+ __publicField(this, "enabled", true);
273
+ this.enabled = config.instrumentation.enabled;
274
+ this.ignorePatterns = config.instrumentation.ignore_patterns.map((p) => this.compilePattern(p));
275
+ this.instrumentPatterns = config.instrumentation.instrument_patterns.filter((p) => p.enabled !== false).map((p) => this.compilePattern(p));
276
+ }
277
+ /**
278
+ * Compile a pattern configuration into a RegExp
279
+ */
280
+ compilePattern(pattern) {
281
+ try {
282
+ const compiled = {
283
+ regex: new RegExp(pattern.pattern),
284
+ enabled: pattern.enabled !== false
285
+ };
286
+ if (pattern.description !== void 0) {
287
+ compiled.description = pattern.description;
288
+ }
289
+ return compiled;
290
+ } catch (error) {
291
+ throw new Error(
292
+ `Failed to compile pattern "${pattern.pattern}": ${error instanceof Error ? error.message : String(error)}`
293
+ );
294
+ }
295
+ }
296
+ /**
297
+ * Check if a span should be instrumented
298
+ *
299
+ * Returns true if the span should be created, false otherwise.
300
+ *
301
+ * Logic:
302
+ * 1. If instrumentation disabled globally, return false
303
+ * 2. Check ignore patterns - if any match, return false
304
+ * 3. Check instrument patterns - if any match, return true
305
+ * 4. Default: return true (fail-open - create span if no patterns match)
306
+ */
307
+ shouldInstrument(spanName) {
308
+ if (!this.enabled) {
309
+ return false;
310
+ }
311
+ for (const pattern of this.ignorePatterns) {
312
+ if (pattern.regex.test(spanName)) {
313
+ return false;
314
+ }
315
+ }
316
+ for (const pattern of this.instrumentPatterns) {
317
+ if (pattern.enabled && pattern.regex.test(spanName)) {
318
+ return true;
319
+ }
320
+ }
321
+ return true;
322
+ }
323
+ /**
324
+ * Get statistics about pattern matching (for debugging/monitoring)
325
+ */
326
+ getStats() {
327
+ return {
328
+ enabled: this.enabled,
329
+ ignorePatternCount: this.ignorePatterns.length,
330
+ instrumentPatternCount: this.instrumentPatterns.filter((p) => p.enabled).length
331
+ };
332
+ }
333
+ };
334
+ function initializePatternMatcher(config) {
335
+ new PatternMatcher(config);
336
+ }
337
+ var Logger = class {
338
+ constructor() {
339
+ __publicField(this, "level", "on");
340
+ __publicField(this, "hasLoggedMinimal", false);
341
+ }
342
+ /**
343
+ * Set the logging level
344
+ */
345
+ setLevel(level) {
346
+ this.level = level;
347
+ this.hasLoggedMinimal = false;
348
+ }
349
+ /**
350
+ * Get the current logging level
351
+ */
352
+ getLevel() {
353
+ return this.level;
354
+ }
355
+ /**
356
+ * Log a minimal initialization message (only shown once in minimal mode)
357
+ */
358
+ minimal(message) {
359
+ if (this.level === "off") {
360
+ return;
361
+ }
362
+ if (this.level === "minimal" && !this.hasLoggedMinimal) {
363
+ console.log(message);
364
+ this.hasLoggedMinimal = true;
365
+ return;
366
+ }
367
+ if (this.level === "on") {
368
+ console.log(message);
369
+ }
370
+ }
371
+ /**
372
+ * Log an informational message
373
+ */
374
+ log(...args) {
375
+ if (this.level === "on") {
376
+ console.log(...args);
377
+ }
378
+ }
379
+ /**
380
+ * Log a warning message (shown in minimal mode)
381
+ */
382
+ warn(...args) {
383
+ if (this.level !== "off") {
384
+ console.warn(...args);
385
+ }
386
+ }
387
+ /**
388
+ * Log an error message (shown in minimal mode)
389
+ */
390
+ error(...args) {
391
+ if (this.level !== "off") {
392
+ console.error(...args);
393
+ }
394
+ }
395
+ /**
396
+ * Check if full logging is enabled
397
+ */
398
+ isEnabled() {
399
+ return this.level === "on";
400
+ }
401
+ /**
402
+ * Check if minimal logging is enabled
403
+ */
404
+ isMinimal() {
405
+ return this.level === "minimal";
406
+ }
407
+ /**
408
+ * Check if logging is completely disabled
409
+ */
410
+ isDisabled() {
411
+ return this.level === "off";
412
+ }
413
+ };
414
+ var logger = new Logger();
415
+ var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
32
416
  effect.Layer.provide(effect.Layer.mergeAll(platformNode.NodeContext.layer, platform.FetchHttpClient.layer))
33
417
  );
34
418
  var cachedLoaderPromise = null;
@@ -36,7 +420,7 @@ function getCachedLoader() {
36
420
  if (!cachedLoaderPromise) {
37
421
  cachedLoaderPromise = effect.Effect.runPromise(
38
422
  effect.Effect.gen(function* () {
39
- return yield* instrumentCore.ConfigLoader;
423
+ return yield* ConfigLoader;
40
424
  }).pipe(effect.Effect.provide(NodeConfigLoaderLive))
41
425
  );
42
426
  }
@@ -45,7 +429,7 @@ function getCachedLoader() {
45
429
  async function loadConfig(uri, options) {
46
430
  if (options?.cacheTimeout === 0) {
47
431
  const program = effect.Effect.gen(function* () {
48
- const loader2 = yield* instrumentCore.ConfigLoader;
432
+ const loader2 = yield* ConfigLoader;
49
433
  return yield* loader2.loadFromUri(uri);
50
434
  });
51
435
  return effect.Effect.runPromise(program.pipe(effect.Effect.provide(NodeConfigLoaderLive)));
@@ -117,19 +501,19 @@ function createEffectInstrumentation(options = {}) {
117
501
  });
118
502
  yield* effect.Effect.sync(() => {
119
503
  const loggingLevel = config.instrumentation.logging || "on";
120
- instrumentCore.logger.setLevel(loggingLevel);
504
+ logger.setLevel(loggingLevel);
121
505
  });
122
- yield* effect.Effect.sync(() => instrumentCore.initializePatternMatcher(config));
506
+ yield* effect.Effect.sync(() => initializePatternMatcher(config));
123
507
  const otlpEndpoint = options.otlpEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
124
508
  const serviceName = options.serviceName || process.env.OTEL_SERVICE_NAME || "effect-service";
125
509
  const serviceVersion = options.serviceVersion || process.env.npm_package_version || "1.0.0";
126
510
  const autoExtractMetadata = options.autoExtractMetadata ?? config.effect?.auto_extract_metadata ?? true;
127
511
  const continueExistingTraces = options.continueExistingTraces ?? true;
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}`);
512
+ logger.log("\u{1F50D} Effect OpenTelemetry instrumentation");
513
+ logger.log(` \u{1F4E1} Endpoint: ${otlpEndpoint}`);
514
+ logger.log(` \u{1F3F7}\uFE0F Service: ${serviceName}`);
515
+ logger.log(` \u2705 Auto metadata extraction: ${autoExtractMetadata}`);
516
+ logger.log(` \u2705 Continue existing traces: ${continueExistingTraces}`);
133
517
  const otlpLayer = Otlp__namespace.layer({
134
518
  baseUrl: otlpEndpoint,
135
519
  resource: {
@@ -167,10 +551,10 @@ var EffectInstrumentationLive = effect.Effect.sync(() => {
167
551
  const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
168
552
  const serviceName = process.env.OTEL_SERVICE_NAME || "effect-service";
169
553
  const serviceVersion = process.env.npm_package_version || "1.0.0";
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}`);
554
+ logger.minimal(`@atrim/instrumentation/effect: Effect tracing enabled (${serviceName})`);
555
+ logger.log("\u{1F50D} Effect OpenTelemetry tracer");
556
+ logger.log(` \u{1F4E1} Endpoint: ${endpoint}`);
557
+ logger.log(` \u{1F3F7}\uFE0F Service: ${serviceName}`);
174
558
  return Otlp__namespace.layer({
175
559
  baseUrl: endpoint,
176
560
  resource: {