@atrim/instrument-node 0.4.0 → 0.5.0-c05e3a1-20251119131235

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,12 @@ 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');
7
+ var FileSystem = require('@effect/platform/FileSystem');
8
+ var HttpClient = require('@effect/platform/HttpClient');
9
+ var HttpClientRequest = require('@effect/platform/HttpClientRequest');
9
10
  var yaml = require('yaml');
10
11
  var zod = require('zod');
12
+ var platformNode = require('@effect/platform-node');
11
13
 
12
14
  function _interopNamespace(e) {
13
15
  if (e && e.__esModule) return e;
@@ -28,6 +30,8 @@ function _interopNamespace(e) {
28
30
  }
29
31
 
30
32
  var Otlp__namespace = /*#__PURE__*/_interopNamespace(Otlp);
33
+ var HttpClient__namespace = /*#__PURE__*/_interopNamespace(HttpClient);
34
+ var HttpClientRequest__namespace = /*#__PURE__*/_interopNamespace(HttpClientRequest);
31
35
 
32
36
  // src/integrations/effect/effect-tracer.ts
33
37
  var __defProp = Object.defineProperty;
@@ -66,7 +70,20 @@ var HttpFilteringConfigSchema = zod.z.object({
66
70
  // Patterns to ignore for incoming HTTP requests (string patterns only in YAML)
67
71
  ignore_incoming_paths: zod.z.array(zod.z.string()).optional(),
68
72
  // Require parent span for outgoing requests (prevents root spans for HTTP calls)
69
- require_parent_for_outgoing_spans: zod.z.boolean().optional()
73
+ require_parent_for_outgoing_spans: zod.z.boolean().optional(),
74
+ // Trace context propagation configuration
75
+ // Controls which cross-origin requests receive W3C Trace Context headers (traceparent, tracestate)
76
+ propagate_trace_context: zod.z.object({
77
+ // Strategy for trace propagation
78
+ // - "all": Propagate to all cross-origin requests (may cause CORS errors)
79
+ // - "none": Never propagate trace headers
80
+ // - "same-origin": Only propagate to same-origin requests (default, safe)
81
+ // - "patterns": Propagate based on include_urls patterns
82
+ strategy: zod.z.enum(["all", "none", "same-origin", "patterns"]).default("same-origin"),
83
+ // URL patterns to include when strategy is "patterns"
84
+ // Supports regex patterns (e.g., "^https://api\\.myapp\\.com")
85
+ include_urls: zod.z.array(zod.z.string()).optional()
86
+ }).optional()
70
87
  });
71
88
  var InstrumentationConfigSchema = zod.z.object({
72
89
  version: zod.z.string(),
@@ -84,233 +101,183 @@ var InstrumentationConfigSchema = zod.z.object({
84
101
  http: HttpFilteringConfigSchema.optional()
85
102
  });
86
103
  (class extends effect.Data.TaggedError("ConfigError") {
104
+ get message() {
105
+ return this.reason;
106
+ }
87
107
  });
88
108
  var ConfigUrlError = class extends effect.Data.TaggedError("ConfigUrlError") {
109
+ get message() {
110
+ return this.reason;
111
+ }
89
112
  };
90
113
  var ConfigValidationError = class extends effect.Data.TaggedError("ConfigValidationError") {
114
+ get message() {
115
+ return this.reason;
116
+ }
91
117
  };
92
118
  var ConfigFileError = class extends effect.Data.TaggedError("ConfigFileError") {
119
+ get message() {
120
+ return this.reason;
121
+ }
93
122
  };
94
123
  (class extends effect.Data.TaggedError("ServiceDetectionError") {
124
+ get message() {
125
+ return this.reason;
126
+ }
95
127
  });
96
128
  (class extends effect.Data.TaggedError("InitializationError") {
129
+ get message() {
130
+ return this.reason;
131
+ }
97
132
  });
98
133
  (class extends effect.Data.TaggedError("ExportError") {
134
+ get message() {
135
+ return this.reason;
136
+ }
99
137
  });
100
138
  (class extends effect.Data.TaggedError("ShutdownError") {
139
+ get message() {
140
+ return this.reason;
141
+ }
101
142
  });
102
143
  var SECURITY_DEFAULTS = {
103
144
  maxConfigSize: 1e6,
104
145
  // 1MB
105
- requestTimeout: 5e3,
106
- allowedProtocols: ["https:"],
107
- // Only HTTPS for remote configs
108
- cacheTimeout: 3e5
109
- // 5 minutes
146
+ requestTimeout: 5e3
147
+ // 5 seconds
110
148
  };
111
- function getDefaultConfig() {
112
- return {
113
- version: "1.0",
114
- instrumentation: {
115
- enabled: true,
116
- logging: "on",
117
- description: "Default instrumentation configuration",
118
- instrument_patterns: [
119
- { pattern: "^app\\.", enabled: true, description: "Application operations" },
120
- { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
121
- { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
122
- ],
123
- ignore_patterns: [
124
- { pattern: "^test\\.", description: "Test utilities" },
125
- { pattern: "^internal\\.", description: "Internal operations" },
126
- { pattern: "^health\\.", description: "Health checks" }
127
- ]
128
- },
129
- effect: {
130
- auto_extract_metadata: true
131
- }
132
- };
133
- }
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}`,
149
+ var ConfigLoader = class extends effect.Context.Tag("ConfigLoader")() {
150
+ };
151
+ var parseYamlContent = (content, uri) => effect.Effect.gen(function* () {
152
+ const parsed = yield* effect.Effect.try({
153
+ try: () => yaml.parse(content),
154
+ catch: (error) => new ConfigValidationError({
155
+ reason: uri ? `Failed to parse YAML from ${uri}` : "Failed to parse YAML",
156
+ cause: error
157
+ })
158
+ });
159
+ return yield* effect.Effect.try({
160
+ try: () => InstrumentationConfigSchema.parse(parsed),
161
+ catch: (error) => new ConfigValidationError({
162
+ reason: uri ? `Invalid configuration schema from ${uri}` : "Invalid configuration schema",
146
163
  cause: error
147
164
  })
148
165
  });
149
- if (fileContents.length > SECURITY_DEFAULTS.maxConfigSize) {
166
+ });
167
+ var loadFromFileWithFs = (fs, path, uri) => effect.Effect.gen(function* () {
168
+ const content = yield* fs.readFileString(path).pipe(
169
+ effect.Effect.mapError(
170
+ (error) => new ConfigFileError({
171
+ reason: `Failed to read config file at ${uri}`,
172
+ cause: error
173
+ })
174
+ )
175
+ );
176
+ if (content.length > SECURITY_DEFAULTS.maxConfigSize) {
150
177
  return yield* effect.Effect.fail(
151
178
  new ConfigFileError({
152
179
  reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
153
180
  })
154
181
  );
155
182
  }
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);
183
+ return yield* parseYamlContent(content, uri);
168
184
  });
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
185
+ var loadFromHttpWithClient = (client, url) => effect.Effect.scoped(
186
+ effect.Effect.gen(function* () {
187
+ if (url.startsWith("http://")) {
188
+ return yield* effect.Effect.fail(
189
+ new ConfigUrlError({
190
+ reason: "Insecure protocol: only HTTPS URLs are allowed"
191
+ })
192
+ );
193
+ }
194
+ const request = HttpClientRequest__namespace.get(url).pipe(
195
+ HttpClientRequest__namespace.setHeaders({
196
+ Accept: "application/yaml, text/yaml, application/x-yaml"
178
197
  })
179
198
  );
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`
199
+ const response = yield* client.execute(request).pipe(
200
+ effect.Effect.timeout(`${SECURITY_DEFAULTS.requestTimeout} millis`),
201
+ effect.Effect.mapError((error) => {
202
+ if (error._tag === "TimeoutException") {
203
+ return new ConfigUrlError({
204
+ reason: `Config fetch timeout after ${SECURITY_DEFAULTS.requestTimeout}ms from ${url}`
205
+ });
206
+ }
207
+ return new ConfigUrlError({
208
+ reason: `Failed to load config from URL: ${url}`,
209
+ cause: error
210
+ });
185
211
  })
186
212
  );
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`
213
+ if (response.status >= 400) {
214
+ return yield* effect.Effect.fail(
215
+ new ConfigUrlError({
216
+ reason: `HTTP ${response.status} from ${url}`
217
+ })
218
+ );
219
+ }
220
+ const text = yield* response.text.pipe(
221
+ effect.Effect.mapError(
222
+ (error) => new ConfigUrlError({
223
+ reason: `Failed to read response body from ${url}`,
224
+ cause: error
225
+ })
226
+ )
227
+ );
228
+ if (text.length > SECURITY_DEFAULTS.maxConfigSize) {
229
+ return yield* effect.Effect.fail(
230
+ new ConfigUrlError({
231
+ reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
232
+ })
233
+ );
234
+ }
235
+ return yield* parseYamlContent(text, url);
236
+ })
237
+ );
238
+ var makeConfigLoader = effect.Effect.gen(function* () {
239
+ const fs = yield* effect.Effect.serviceOption(FileSystem.FileSystem);
240
+ const http = yield* HttpClient__namespace.HttpClient;
241
+ const loadFromUriUncached = (uri) => effect.Effect.gen(function* () {
242
+ if (uri.startsWith("file://")) {
243
+ const path = uri.slice(7);
244
+ if (fs._tag === "None") {
245
+ return yield* effect.Effect.fail(
246
+ new ConfigFileError({
247
+ reason: "FileSystem not available (browser environment?)",
248
+ cause: { uri }
210
249
  })
211
250
  );
212
251
  }
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
252
+ return yield* loadFromFileWithFs(fs.value, path, uri);
253
+ }
254
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
255
+ return yield* loadFromHttpWithClient(http, uri);
256
+ }
257
+ if (fs._tag === "Some") {
258
+ return yield* loadFromFileWithFs(fs.value, uri, uri);
259
+ } else {
260
+ return yield* loadFromHttpWithClient(http, uri);
261
+ }
262
+ });
263
+ const loadFromUriCached = yield* effect.Effect.cachedFunction(loadFromUriUncached);
264
+ return ConfigLoader.of({
265
+ loadFromUri: loadFromUriCached,
266
+ loadFromInline: (content) => effect.Effect.gen(function* () {
267
+ if (typeof content === "string") {
268
+ return yield* parseYamlContent(content);
269
+ }
270
+ return yield* effect.Effect.try({
271
+ try: () => InstrumentationConfigSchema.parse(content),
272
+ catch: (error) => new ConfigValidationError({
273
+ reason: "Invalid configuration schema",
274
+ cause: error
275
+ })
276
+ });
236
277
  })
237
278
  });
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
279
  });
277
- var loadConfigEffect = (options = {}) => effect.Effect.gen(function* () {
278
- if (options.config) {
279
- return yield* validateConfigEffect(options.config);
280
- }
281
- const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
282
- if (envConfigPath) {
283
- if (envConfigPath.startsWith("http://") || envConfigPath.startsWith("https://")) {
284
- return yield* loadConfigFromUrlEffect(envConfigPath, options.cacheTimeout);
285
- }
286
- return yield* loadConfigFromFileEffect(envConfigPath);
287
- }
288
- if (options.configUrl) {
289
- return yield* loadConfigFromUrlEffect(options.configUrl, options.cacheTimeout);
290
- }
291
- if (options.configPath) {
292
- return yield* loadConfigFromFileEffect(options.configPath);
293
- }
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);
298
- }
299
- 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
- }
280
+ var ConfigLoaderLive = effect.Layer.effect(ConfigLoader, makeConfigLoader);
314
281
  var PatternMatcher = class {
315
282
  constructor(config) {
316
283
  __publicField(this, "ignorePatterns", []);
@@ -458,13 +425,88 @@ var Logger = class {
458
425
  }
459
426
  };
460
427
  var logger = new Logger();
428
+ var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
429
+ effect.Layer.provide(effect.Layer.mergeAll(platformNode.NodeContext.layer, platform.FetchHttpClient.layer))
430
+ );
431
+ var cachedLoaderPromise = null;
432
+ function getCachedLoader() {
433
+ if (!cachedLoaderPromise) {
434
+ cachedLoaderPromise = effect.Effect.runPromise(
435
+ effect.Effect.gen(function* () {
436
+ return yield* ConfigLoader;
437
+ }).pipe(effect.Effect.provide(NodeConfigLoaderLive))
438
+ );
439
+ }
440
+ return cachedLoaderPromise;
441
+ }
442
+ async function loadConfig(uri, options) {
443
+ if (options?.cacheTimeout === 0) {
444
+ const program = effect.Effect.gen(function* () {
445
+ const loader2 = yield* ConfigLoader;
446
+ return yield* loader2.loadFromUri(uri);
447
+ });
448
+ return effect.Effect.runPromise(program.pipe(effect.Effect.provide(NodeConfigLoaderLive)));
449
+ }
450
+ const loader = await getCachedLoader();
451
+ return effect.Effect.runPromise(loader.loadFromUri(uri));
452
+ }
453
+ async function loadConfigFromInline(content) {
454
+ const loader = await getCachedLoader();
455
+ return effect.Effect.runPromise(loader.loadFromInline(content));
456
+ }
457
+ function getDefaultConfig() {
458
+ return {
459
+ version: "1.0",
460
+ instrumentation: {
461
+ enabled: true,
462
+ logging: "on",
463
+ description: "Default instrumentation configuration",
464
+ instrument_patterns: [
465
+ { pattern: "^app\\.", enabled: true, description: "Application operations" },
466
+ { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
467
+ { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
468
+ ],
469
+ ignore_patterns: [
470
+ { pattern: "^test\\.", description: "Test utilities" },
471
+ { pattern: "^internal\\.", description: "Internal operations" },
472
+ { pattern: "^health\\.", description: "Health checks" }
473
+ ]
474
+ },
475
+ effect: {
476
+ auto_extract_metadata: true
477
+ }
478
+ };
479
+ }
480
+ async function loadConfigWithOptions(options = {}) {
481
+ const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
482
+ if (options.config) {
483
+ return loadConfigFromInline(options.config);
484
+ }
485
+ const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
486
+ if (envConfigPath) {
487
+ return loadConfig(envConfigPath, loadOptions);
488
+ }
489
+ if (options.configUrl) {
490
+ return loadConfig(options.configUrl, loadOptions);
491
+ }
492
+ if (options.configPath) {
493
+ return loadConfig(options.configPath, loadOptions);
494
+ }
495
+ const { existsSync } = await import('fs');
496
+ const { join } = await import('path');
497
+ const defaultPath = join(process.cwd(), "instrumentation.yaml");
498
+ if (existsSync(defaultPath)) {
499
+ return loadConfig(defaultPath, loadOptions);
500
+ }
501
+ return getDefaultConfig();
502
+ }
461
503
 
462
504
  // src/integrations/effect/effect-tracer.ts
463
505
  function createEffectInstrumentation(options = {}) {
464
506
  return effect.Layer.unwrapEffect(
465
507
  effect.Effect.gen(function* () {
466
508
  const config = yield* effect.Effect.tryPromise({
467
- try: () => loadConfig(options),
509
+ try: () => loadConfigWithOptions(options),
468
510
  catch: (error) => ({
469
511
  _tag: "ConfigError",
470
512
  message: error instanceof Error ? error.message : String(error)