@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.
@@ -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;
@@ -62,233 +64,183 @@ var InstrumentationConfigSchema = z.object({
62
64
  http: HttpFilteringConfigSchema.optional()
63
65
  });
64
66
  (class extends Data.TaggedError("ConfigError") {
67
+ get message() {
68
+ return this.reason;
69
+ }
65
70
  });
66
71
  var ConfigUrlError = class extends Data.TaggedError("ConfigUrlError") {
72
+ get message() {
73
+ return this.reason;
74
+ }
67
75
  };
68
76
  var ConfigValidationError = class extends Data.TaggedError("ConfigValidationError") {
77
+ get message() {
78
+ return this.reason;
79
+ }
69
80
  };
70
81
  var ConfigFileError = class extends Data.TaggedError("ConfigFileError") {
82
+ get message() {
83
+ return this.reason;
84
+ }
71
85
  };
72
86
  (class extends Data.TaggedError("ServiceDetectionError") {
87
+ get message() {
88
+ return this.reason;
89
+ }
73
90
  });
74
91
  (class extends Data.TaggedError("InitializationError") {
92
+ get message() {
93
+ return this.reason;
94
+ }
75
95
  });
76
96
  (class extends Data.TaggedError("ExportError") {
97
+ get message() {
98
+ return this.reason;
99
+ }
77
100
  });
78
101
  (class extends Data.TaggedError("ShutdownError") {
102
+ get message() {
103
+ return this.reason;
104
+ }
79
105
  });
80
106
  var SECURITY_DEFAULTS = {
81
107
  maxConfigSize: 1e6,
82
108
  // 1MB
83
- requestTimeout: 5e3,
84
- allowedProtocols: ["https:"],
85
- // Only HTTPS for remote configs
86
- cacheTimeout: 3e5
87
- // 5 minutes
109
+ requestTimeout: 5e3
110
+ // 5 seconds
88
111
  };
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}`,
112
+ var ConfigLoader = class extends Context.Tag("ConfigLoader")() {
113
+ };
114
+ var parseYamlContent = (content, uri) => Effect.gen(function* () {
115
+ const parsed = yield* Effect.try({
116
+ try: () => parse(content),
117
+ catch: (error) => new ConfigValidationError({
118
+ reason: uri ? `Failed to parse YAML from ${uri}` : "Failed to parse YAML",
119
+ cause: error
120
+ })
121
+ });
122
+ return yield* Effect.try({
123
+ try: () => InstrumentationConfigSchema.parse(parsed),
124
+ catch: (error) => new ConfigValidationError({
125
+ reason: uri ? `Invalid configuration schema from ${uri}` : "Invalid configuration schema",
124
126
  cause: error
125
127
  })
126
128
  });
127
- if (fileContents.length > SECURITY_DEFAULTS.maxConfigSize) {
129
+ });
130
+ var loadFromFileWithFs = (fs, path, uri) => Effect.gen(function* () {
131
+ const content = yield* fs.readFileString(path).pipe(
132
+ Effect.mapError(
133
+ (error) => new ConfigFileError({
134
+ reason: `Failed to read config file at ${uri}`,
135
+ cause: error
136
+ })
137
+ )
138
+ );
139
+ if (content.length > SECURITY_DEFAULTS.maxConfigSize) {
128
140
  return yield* Effect.fail(
129
141
  new ConfigFileError({
130
142
  reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
131
143
  })
132
144
  );
133
145
  }
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);
146
+ return yield* parseYamlContent(content, uri);
146
147
  });
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
148
+ var loadFromHttpWithClient = (client, url) => Effect.scoped(
149
+ Effect.gen(function* () {
150
+ if (url.startsWith("http://")) {
151
+ return yield* Effect.fail(
152
+ new ConfigUrlError({
153
+ reason: "Insecure protocol: only HTTPS URLs are allowed"
154
+ })
155
+ );
156
+ }
157
+ const request = HttpClientRequest.get(url).pipe(
158
+ HttpClientRequest.setHeaders({
159
+ Accept: "application/yaml, text/yaml, application/x-yaml"
156
160
  })
157
161
  );
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`
162
+ const response = yield* client.execute(request).pipe(
163
+ Effect.timeout(`${SECURITY_DEFAULTS.requestTimeout} millis`),
164
+ Effect.mapError((error) => {
165
+ if (error._tag === "TimeoutException") {
166
+ return new ConfigUrlError({
167
+ reason: `Config fetch timeout after ${SECURITY_DEFAULTS.requestTimeout}ms from ${url}`
168
+ });
169
+ }
170
+ return new ConfigUrlError({
171
+ reason: `Failed to load config from URL: ${url}`,
172
+ cause: error
173
+ });
163
174
  })
164
175
  );
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`
176
+ if (response.status >= 400) {
177
+ return yield* Effect.fail(
178
+ new ConfigUrlError({
179
+ reason: `HTTP ${response.status} from ${url}`
180
+ })
181
+ );
182
+ }
183
+ const text = yield* response.text.pipe(
184
+ Effect.mapError(
185
+ (error) => new ConfigUrlError({
186
+ reason: `Failed to read response body from ${url}`,
187
+ cause: error
188
+ })
189
+ )
190
+ );
191
+ if (text.length > SECURITY_DEFAULTS.maxConfigSize) {
192
+ return yield* Effect.fail(
193
+ new ConfigUrlError({
194
+ reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
195
+ })
196
+ );
197
+ }
198
+ return yield* parseYamlContent(text, url);
199
+ })
200
+ );
201
+ var makeConfigLoader = Effect.gen(function* () {
202
+ const fs = yield* Effect.serviceOption(FileSystem);
203
+ const http = yield* HttpClient.HttpClient;
204
+ const loadFromUriUncached = (uri) => Effect.gen(function* () {
205
+ if (uri.startsWith("file://")) {
206
+ const path = uri.slice(7);
207
+ if (fs._tag === "None") {
208
+ return yield* Effect.fail(
209
+ new ConfigFileError({
210
+ reason: "FileSystem not available (browser environment?)",
211
+ cause: { uri }
188
212
  })
189
213
  );
190
214
  }
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
215
+ return yield* loadFromFileWithFs(fs.value, path, uri);
216
+ }
217
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
218
+ return yield* loadFromHttpWithClient(http, uri);
219
+ }
220
+ if (fs._tag === "Some") {
221
+ return yield* loadFromFileWithFs(fs.value, uri, uri);
222
+ } else {
223
+ return yield* loadFromHttpWithClient(http, uri);
224
+ }
225
+ });
226
+ const loadFromUriCached = yield* Effect.cachedFunction(loadFromUriUncached);
227
+ return ConfigLoader.of({
228
+ loadFromUri: loadFromUriCached,
229
+ loadFromInline: (content) => Effect.gen(function* () {
230
+ if (typeof content === "string") {
231
+ return yield* parseYamlContent(content);
232
+ }
233
+ return yield* Effect.try({
234
+ try: () => InstrumentationConfigSchema.parse(content),
235
+ catch: (error) => new ConfigValidationError({
236
+ reason: "Invalid configuration schema",
237
+ cause: error
238
+ })
239
+ });
214
240
  })
215
241
  });
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
242
  });
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
- }
243
+ var ConfigLoaderLive = Layer.effect(ConfigLoader, makeConfigLoader);
292
244
  var PatternMatcher = class {
293
245
  constructor(config) {
294
246
  __publicField(this, "ignorePatterns", []);
@@ -436,13 +388,88 @@ var Logger = class {
436
388
  }
437
389
  };
438
390
  var logger = new Logger();
391
+ var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
392
+ Layer.provide(Layer.mergeAll(NodeContext.layer, FetchHttpClient.layer))
393
+ );
394
+ var cachedLoaderPromise = null;
395
+ function getCachedLoader() {
396
+ if (!cachedLoaderPromise) {
397
+ cachedLoaderPromise = Effect.runPromise(
398
+ Effect.gen(function* () {
399
+ return yield* ConfigLoader;
400
+ }).pipe(Effect.provide(NodeConfigLoaderLive))
401
+ );
402
+ }
403
+ return cachedLoaderPromise;
404
+ }
405
+ async function loadConfig(uri, options) {
406
+ if (options?.cacheTimeout === 0) {
407
+ const program = Effect.gen(function* () {
408
+ const loader2 = yield* ConfigLoader;
409
+ return yield* loader2.loadFromUri(uri);
410
+ });
411
+ return Effect.runPromise(program.pipe(Effect.provide(NodeConfigLoaderLive)));
412
+ }
413
+ const loader = await getCachedLoader();
414
+ return Effect.runPromise(loader.loadFromUri(uri));
415
+ }
416
+ async function loadConfigFromInline(content) {
417
+ const loader = await getCachedLoader();
418
+ return Effect.runPromise(loader.loadFromInline(content));
419
+ }
420
+ function getDefaultConfig() {
421
+ return {
422
+ version: "1.0",
423
+ instrumentation: {
424
+ enabled: true,
425
+ logging: "on",
426
+ description: "Default instrumentation configuration",
427
+ instrument_patterns: [
428
+ { pattern: "^app\\.", enabled: true, description: "Application operations" },
429
+ { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
430
+ { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
431
+ ],
432
+ ignore_patterns: [
433
+ { pattern: "^test\\.", description: "Test utilities" },
434
+ { pattern: "^internal\\.", description: "Internal operations" },
435
+ { pattern: "^health\\.", description: "Health checks" }
436
+ ]
437
+ },
438
+ effect: {
439
+ auto_extract_metadata: true
440
+ }
441
+ };
442
+ }
443
+ async function loadConfigWithOptions(options = {}) {
444
+ const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
445
+ if (options.config) {
446
+ return loadConfigFromInline(options.config);
447
+ }
448
+ const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
449
+ if (envConfigPath) {
450
+ return loadConfig(envConfigPath, loadOptions);
451
+ }
452
+ if (options.configUrl) {
453
+ return loadConfig(options.configUrl, loadOptions);
454
+ }
455
+ if (options.configPath) {
456
+ return loadConfig(options.configPath, loadOptions);
457
+ }
458
+ const { existsSync } = await import('fs');
459
+ const { join } = await import('path');
460
+ const defaultPath = join(process.cwd(), "instrumentation.yaml");
461
+ if (existsSync(defaultPath)) {
462
+ return loadConfig(defaultPath, loadOptions);
463
+ }
464
+ return getDefaultConfig();
465
+ }
439
466
 
440
467
  // src/integrations/effect/effect-tracer.ts
441
468
  function createEffectInstrumentation(options = {}) {
442
469
  return Layer.unwrapEffect(
443
470
  Effect.gen(function* () {
444
471
  const config = yield* Effect.tryPromise({
445
- try: () => loadConfig(options),
472
+ try: () => loadConfigWithOptions(options),
446
473
  catch: (error) => ({
447
474
  _tag: "ConfigError",
448
475
  message: error instanceof Error ? error.message : String(error)