@atrim/instrument-node 0.4.0 → 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,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;
@@ -84,233 +88,183 @@ var InstrumentationConfigSchema = zod.z.object({
84
88
  http: HttpFilteringConfigSchema.optional()
85
89
  });
86
90
  (class extends effect.Data.TaggedError("ConfigError") {
91
+ get message() {
92
+ return this.reason;
93
+ }
87
94
  });
88
95
  var ConfigUrlError = class extends effect.Data.TaggedError("ConfigUrlError") {
96
+ get message() {
97
+ return this.reason;
98
+ }
89
99
  };
90
100
  var ConfigValidationError = class extends effect.Data.TaggedError("ConfigValidationError") {
101
+ get message() {
102
+ return this.reason;
103
+ }
91
104
  };
92
105
  var ConfigFileError = class extends effect.Data.TaggedError("ConfigFileError") {
106
+ get message() {
107
+ return this.reason;
108
+ }
93
109
  };
94
110
  (class extends effect.Data.TaggedError("ServiceDetectionError") {
111
+ get message() {
112
+ return this.reason;
113
+ }
95
114
  });
96
115
  (class extends effect.Data.TaggedError("InitializationError") {
116
+ get message() {
117
+ return this.reason;
118
+ }
97
119
  });
98
120
  (class extends effect.Data.TaggedError("ExportError") {
121
+ get message() {
122
+ return this.reason;
123
+ }
99
124
  });
100
125
  (class extends effect.Data.TaggedError("ShutdownError") {
126
+ get message() {
127
+ return this.reason;
128
+ }
101
129
  });
102
130
  var SECURITY_DEFAULTS = {
103
131
  maxConfigSize: 1e6,
104
132
  // 1MB
105
- requestTimeout: 5e3,
106
- allowedProtocols: ["https:"],
107
- // Only HTTPS for remote configs
108
- cacheTimeout: 3e5
109
- // 5 minutes
133
+ requestTimeout: 5e3
134
+ // 5 seconds
110
135
  };
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}`,
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",
146
150
  cause: error
147
151
  })
148
152
  });
149
- if (fileContents.length > SECURITY_DEFAULTS.maxConfigSize) {
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) {
150
164
  return yield* effect.Effect.fail(
151
165
  new ConfigFileError({
152
166
  reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
153
167
  })
154
168
  );
155
169
  }
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);
170
+ return yield* parseYamlContent(content, uri);
168
171
  });
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
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"
178
184
  })
179
185
  );
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`
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
+ });
185
198
  })
186
199
  );
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`
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 }
210
236
  })
211
237
  );
212
238
  }
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
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
+ });
236
264
  })
237
265
  });
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
266
  });
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
- }
267
+ var ConfigLoaderLive = effect.Layer.effect(ConfigLoader, makeConfigLoader);
314
268
  var PatternMatcher = class {
315
269
  constructor(config) {
316
270
  __publicField(this, "ignorePatterns", []);
@@ -458,13 +412,88 @@ var Logger = class {
458
412
  }
459
413
  };
460
414
  var logger = new Logger();
415
+ var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
416
+ effect.Layer.provide(effect.Layer.mergeAll(platformNode.NodeContext.layer, platform.FetchHttpClient.layer))
417
+ );
418
+ var cachedLoaderPromise = null;
419
+ function getCachedLoader() {
420
+ if (!cachedLoaderPromise) {
421
+ cachedLoaderPromise = effect.Effect.runPromise(
422
+ effect.Effect.gen(function* () {
423
+ return yield* ConfigLoader;
424
+ }).pipe(effect.Effect.provide(NodeConfigLoaderLive))
425
+ );
426
+ }
427
+ return cachedLoaderPromise;
428
+ }
429
+ async function loadConfig(uri, options) {
430
+ if (options?.cacheTimeout === 0) {
431
+ const program = effect.Effect.gen(function* () {
432
+ const loader2 = yield* ConfigLoader;
433
+ return yield* loader2.loadFromUri(uri);
434
+ });
435
+ return effect.Effect.runPromise(program.pipe(effect.Effect.provide(NodeConfigLoaderLive)));
436
+ }
437
+ const loader = await getCachedLoader();
438
+ return effect.Effect.runPromise(loader.loadFromUri(uri));
439
+ }
440
+ async function loadConfigFromInline(content) {
441
+ const loader = await getCachedLoader();
442
+ return effect.Effect.runPromise(loader.loadFromInline(content));
443
+ }
444
+ function getDefaultConfig() {
445
+ return {
446
+ version: "1.0",
447
+ instrumentation: {
448
+ enabled: true,
449
+ logging: "on",
450
+ description: "Default instrumentation configuration",
451
+ instrument_patterns: [
452
+ { pattern: "^app\\.", enabled: true, description: "Application operations" },
453
+ { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
454
+ { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
455
+ ],
456
+ ignore_patterns: [
457
+ { pattern: "^test\\.", description: "Test utilities" },
458
+ { pattern: "^internal\\.", description: "Internal operations" },
459
+ { pattern: "^health\\.", description: "Health checks" }
460
+ ]
461
+ },
462
+ effect: {
463
+ auto_extract_metadata: true
464
+ }
465
+ };
466
+ }
467
+ async function loadConfigWithOptions(options = {}) {
468
+ const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
469
+ if (options.config) {
470
+ return loadConfigFromInline(options.config);
471
+ }
472
+ const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
473
+ if (envConfigPath) {
474
+ return loadConfig(envConfigPath, loadOptions);
475
+ }
476
+ if (options.configUrl) {
477
+ return loadConfig(options.configUrl, loadOptions);
478
+ }
479
+ if (options.configPath) {
480
+ return loadConfig(options.configPath, loadOptions);
481
+ }
482
+ const { existsSync } = await import('fs');
483
+ const { join } = await import('path');
484
+ const defaultPath = join(process.cwd(), "instrumentation.yaml");
485
+ if (existsSync(defaultPath)) {
486
+ return loadConfig(defaultPath, loadOptions);
487
+ }
488
+ return getDefaultConfig();
489
+ }
461
490
 
462
491
  // src/integrations/effect/effect-tracer.ts
463
492
  function createEffectInstrumentation(options = {}) {
464
493
  return effect.Layer.unwrapEffect(
465
494
  effect.Effect.gen(function* () {
466
495
  const config = yield* effect.Effect.tryPromise({
467
- try: () => loadConfig(options),
496
+ try: () => loadConfigWithOptions(options),
468
497
  catch: (error) => ({
469
498
  _tag: "ConfigError",
470
499
  message: error instanceof Error ? error.message : String(error)