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