@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,14 +1,18 @@
1
- import { Data, Effect, Cache, Duration, Schedule } from 'effect';
1
+ import { Data, Context, Effect, Layer } from 'effect';
2
2
  import { NodeSDK } from '@opentelemetry/sdk-node';
3
3
  import { SimpleSpanProcessor, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
4
4
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
5
5
  import { trace } from '@opentelemetry/api';
6
- import { existsSync, readFileSync } from 'fs';
7
- import { join } from 'path';
6
+ import { FileSystem } from '@effect/platform/FileSystem';
7
+ import * as HttpClient from '@effect/platform/HttpClient';
8
+ import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
8
9
  import { parse } from 'yaml';
9
10
  import { z } from 'zod';
10
11
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
11
12
  import { readFile } from 'fs/promises';
13
+ import { join } from 'path';
14
+ import { NodeContext } from '@effect/platform-node';
15
+ import { FetchHttpClient } from '@effect/platform';
12
16
 
13
17
  var __defProp = Object.defineProperty;
14
18
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -73,233 +77,183 @@ var InstrumentationConfigSchema = z.object({
73
77
  http: HttpFilteringConfigSchema.optional()
74
78
  });
75
79
  (class extends Data.TaggedError("ConfigError") {
80
+ get message() {
81
+ return this.reason;
82
+ }
76
83
  });
77
84
  var ConfigUrlError = class extends Data.TaggedError("ConfigUrlError") {
85
+ get message() {
86
+ return this.reason;
87
+ }
78
88
  };
79
89
  var ConfigValidationError = class extends Data.TaggedError("ConfigValidationError") {
90
+ get message() {
91
+ return this.reason;
92
+ }
80
93
  };
81
94
  var ConfigFileError = class extends Data.TaggedError("ConfigFileError") {
95
+ get message() {
96
+ return this.reason;
97
+ }
82
98
  };
83
99
  (class extends Data.TaggedError("ServiceDetectionError") {
100
+ get message() {
101
+ return this.reason;
102
+ }
84
103
  });
85
104
  (class extends Data.TaggedError("InitializationError") {
105
+ get message() {
106
+ return this.reason;
107
+ }
86
108
  });
87
109
  (class extends Data.TaggedError("ExportError") {
110
+ get message() {
111
+ return this.reason;
112
+ }
88
113
  });
89
114
  (class extends Data.TaggedError("ShutdownError") {
115
+ get message() {
116
+ return this.reason;
117
+ }
90
118
  });
91
119
  var SECURITY_DEFAULTS = {
92
120
  maxConfigSize: 1e6,
93
121
  // 1MB
94
- requestTimeout: 5e3,
95
- allowedProtocols: ["https:"],
96
- // Only HTTPS for remote configs
97
- cacheTimeout: 3e5
98
- // 5 minutes
122
+ requestTimeout: 5e3
123
+ // 5 seconds
99
124
  };
100
- function getDefaultConfig() {
101
- return {
102
- version: "1.0",
103
- instrumentation: {
104
- enabled: true,
105
- logging: "on",
106
- description: "Default instrumentation configuration",
107
- instrument_patterns: [
108
- { pattern: "^app\\.", enabled: true, description: "Application operations" },
109
- { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
110
- { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
111
- ],
112
- ignore_patterns: [
113
- { pattern: "^test\\.", description: "Test utilities" },
114
- { pattern: "^internal\\.", description: "Internal operations" },
115
- { pattern: "^health\\.", description: "Health checks" }
116
- ]
117
- },
118
- effect: {
119
- auto_extract_metadata: true
120
- }
121
- };
122
- }
123
- var validateConfigEffect = (rawConfig) => Effect.try({
124
- try: () => InstrumentationConfigSchema.parse(rawConfig),
125
- catch: (error) => new ConfigValidationError({
126
- reason: "Invalid configuration schema",
127
- cause: error
128
- })
129
- });
130
- var loadConfigFromFileEffect = (filePath) => Effect.gen(function* () {
131
- const fileContents = yield* Effect.try({
132
- try: () => readFileSync(filePath, "utf8"),
133
- catch: (error) => new ConfigFileError({
134
- 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",
135
139
  cause: error
136
140
  })
137
141
  });
138
- 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) {
139
153
  return yield* Effect.fail(
140
154
  new ConfigFileError({
141
155
  reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
142
156
  })
143
157
  );
144
158
  }
145
- let rawConfig;
146
- try {
147
- rawConfig = parse(fileContents);
148
- } catch (error) {
149
- return yield* Effect.fail(
150
- new ConfigValidationError({
151
- reason: "Invalid YAML syntax",
152
- cause: error
153
- })
154
- );
155
- }
156
- return yield* validateConfigEffect(rawConfig);
159
+ return yield* parseYamlContent(content, uri);
157
160
  });
158
- var fetchAndParseConfig = (url) => Effect.gen(function* () {
159
- let urlObj;
160
- try {
161
- urlObj = new URL(url);
162
- } catch (error) {
163
- return yield* Effect.fail(
164
- new ConfigUrlError({
165
- reason: `Invalid URL: ${url}`,
166
- 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"
167
173
  })
168
174
  );
169
- }
170
- if (!SECURITY_DEFAULTS.allowedProtocols.includes(urlObj.protocol)) {
171
- return yield* Effect.fail(
172
- new ConfigUrlError({
173
- 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
+ });
174
187
  })
175
188
  );
176
- }
177
- const response = yield* Effect.tryPromise({
178
- try: () => fetch(url, {
179
- redirect: "follow",
180
- headers: {
181
- Accept: "application/yaml, text/yaml, text/x-yaml"
182
- }
183
- }),
184
- catch: (error) => new ConfigUrlError({
185
- reason: `Failed to load config from URL ${url}`,
186
- cause: error
187
- })
188
- }).pipe(
189
- Effect.timeout(Duration.millis(SECURITY_DEFAULTS.requestTimeout)),
190
- Effect.retry({
191
- times: 3,
192
- schedule: Schedule.exponential(Duration.millis(100))
193
- }),
194
- Effect.catchAll((error) => {
195
- if (error._tag === "TimeoutException") {
196
- return Effect.fail(
197
- new ConfigUrlError({
198
- 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 }
199
225
  })
200
226
  );
201
227
  }
202
- return Effect.fail(error);
203
- })
204
- );
205
- if (!response.ok) {
206
- return yield* Effect.fail(
207
- new ConfigUrlError({
208
- reason: `HTTP ${response.status}: ${response.statusText}`
209
- })
210
- );
211
- }
212
- const contentLength = response.headers.get("content-length");
213
- if (contentLength && parseInt(contentLength) > SECURITY_DEFAULTS.maxConfigSize) {
214
- return yield* Effect.fail(
215
- new ConfigUrlError({
216
- reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
217
- })
218
- );
219
- }
220
- const text = yield* Effect.tryPromise({
221
- try: () => response.text(),
222
- catch: (error) => new ConfigUrlError({
223
- reason: "Failed to read response body",
224
- 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
+ });
225
253
  })
226
254
  });
227
- if (text.length > SECURITY_DEFAULTS.maxConfigSize) {
228
- return yield* Effect.fail(
229
- new ConfigUrlError({
230
- reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
231
- })
232
- );
233
- }
234
- let rawConfig;
235
- try {
236
- rawConfig = parse(text);
237
- } catch (error) {
238
- return yield* Effect.fail(
239
- new ConfigValidationError({
240
- reason: "Invalid YAML syntax",
241
- cause: error
242
- })
243
- );
244
- }
245
- return yield* validateConfigEffect(rawConfig);
246
- });
247
- var makeConfigCache = () => Cache.make({
248
- capacity: 100,
249
- timeToLive: Duration.minutes(5),
250
- lookup: (url) => fetchAndParseConfig(url)
251
- });
252
- var cacheInstance = null;
253
- var getCache = Effect.gen(function* () {
254
- if (!cacheInstance) {
255
- cacheInstance = yield* makeConfigCache();
256
- }
257
- return cacheInstance;
258
- });
259
- var loadConfigFromUrlEffect = (url, cacheTimeout = SECURITY_DEFAULTS.cacheTimeout) => Effect.gen(function* () {
260
- if (cacheTimeout === 0) {
261
- return yield* fetchAndParseConfig(url);
262
- }
263
- const cache = yield* getCache;
264
- return yield* cache.get(url);
265
- });
266
- var loadConfigEffect = (options = {}) => Effect.gen(function* () {
267
- if (options.config) {
268
- return yield* validateConfigEffect(options.config);
269
- }
270
- const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
271
- if (envConfigPath) {
272
- if (envConfigPath.startsWith("http://") || envConfigPath.startsWith("https://")) {
273
- return yield* loadConfigFromUrlEffect(envConfigPath, options.cacheTimeout);
274
- }
275
- return yield* loadConfigFromFileEffect(envConfigPath);
276
- }
277
- if (options.configUrl) {
278
- return yield* loadConfigFromUrlEffect(options.configUrl, options.cacheTimeout);
279
- }
280
- if (options.configPath) {
281
- return yield* loadConfigFromFileEffect(options.configPath);
282
- }
283
- const defaultPath = join(process.cwd(), "instrumentation.yaml");
284
- const exists = yield* Effect.sync(() => existsSync(defaultPath));
285
- if (exists) {
286
- return yield* loadConfigFromFileEffect(defaultPath);
287
- }
288
- return getDefaultConfig();
289
255
  });
290
- async function loadConfig(options = {}) {
291
- return Effect.runPromise(
292
- loadConfigEffect(options).pipe(
293
- // Convert typed errors to regular Error with reason message for backward compatibility
294
- Effect.mapError((error) => {
295
- const message = error.reason;
296
- const newError = new Error(message);
297
- newError.cause = error.cause;
298
- return newError;
299
- })
300
- )
301
- );
302
- }
256
+ var ConfigLoaderLive = Layer.effect(ConfigLoader, makeConfigLoader);
303
257
  var PatternMatcher = class {
304
258
  constructor(config) {
305
259
  __publicField2(this, "ignorePatterns", []);
@@ -725,6 +679,84 @@ async function getServiceNameAsync() {
725
679
  async function getServiceVersionAsync() {
726
680
  return Effect.runPromise(getServiceVersion);
727
681
  }
682
+ var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
683
+ Layer.provide(Layer.mergeAll(NodeContext.layer, FetchHttpClient.layer))
684
+ );
685
+ var cachedLoaderPromise = null;
686
+ function getCachedLoader() {
687
+ if (!cachedLoaderPromise) {
688
+ cachedLoaderPromise = Effect.runPromise(
689
+ Effect.gen(function* () {
690
+ return yield* ConfigLoader;
691
+ }).pipe(Effect.provide(NodeConfigLoaderLive))
692
+ );
693
+ }
694
+ return cachedLoaderPromise;
695
+ }
696
+ function _resetConfigLoaderCache() {
697
+ cachedLoaderPromise = null;
698
+ }
699
+ async function loadConfig(uri, options) {
700
+ if (options?.cacheTimeout === 0) {
701
+ const program = Effect.gen(function* () {
702
+ const loader2 = yield* ConfigLoader;
703
+ return yield* loader2.loadFromUri(uri);
704
+ });
705
+ return Effect.runPromise(program.pipe(Effect.provide(NodeConfigLoaderLive)));
706
+ }
707
+ const loader = await getCachedLoader();
708
+ return Effect.runPromise(loader.loadFromUri(uri));
709
+ }
710
+ async function loadConfigFromInline(content) {
711
+ const loader = await getCachedLoader();
712
+ return Effect.runPromise(loader.loadFromInline(content));
713
+ }
714
+ function getDefaultConfig() {
715
+ return {
716
+ version: "1.0",
717
+ instrumentation: {
718
+ enabled: true,
719
+ logging: "on",
720
+ description: "Default instrumentation configuration",
721
+ instrument_patterns: [
722
+ { pattern: "^app\\.", enabled: true, description: "Application operations" },
723
+ { pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
724
+ { pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
725
+ ],
726
+ ignore_patterns: [
727
+ { pattern: "^test\\.", description: "Test utilities" },
728
+ { pattern: "^internal\\.", description: "Internal operations" },
729
+ { pattern: "^health\\.", description: "Health checks" }
730
+ ]
731
+ },
732
+ effect: {
733
+ auto_extract_metadata: true
734
+ }
735
+ };
736
+ }
737
+ async function loadConfigWithOptions(options = {}) {
738
+ const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
739
+ if (options.config) {
740
+ return loadConfigFromInline(options.config);
741
+ }
742
+ const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
743
+ if (envConfigPath) {
744
+ return loadConfig(envConfigPath, loadOptions);
745
+ }
746
+ if (options.configUrl) {
747
+ return loadConfig(options.configUrl, loadOptions);
748
+ }
749
+ if (options.configPath) {
750
+ return loadConfig(options.configPath, loadOptions);
751
+ }
752
+ const { existsSync } = await import('fs');
753
+ const { join: join2 } = await import('path');
754
+ const defaultPath = join2(process.cwd(), "instrumentation.yaml");
755
+ if (existsSync(defaultPath)) {
756
+ return loadConfig(defaultPath, loadOptions);
757
+ }
758
+ return getDefaultConfig();
759
+ }
728
760
 
729
761
  // src/core/sdk-initializer.ts
730
762
  var sdkInstance = null;
@@ -864,7 +896,7 @@ async function initializeSdk(options = {}) {
864
896
  }
865
897
  }
866
898
  async function performInitialization(options) {
867
- const config = await loadConfig(options);
899
+ const config = await loadConfigWithOptions(options);
868
900
  const loggingLevel = config.instrumentation.logging || "on";
869
901
  logger.setLevel(loggingLevel);
870
902
  const alreadyInitialized = isTracingAlreadyInitialized();
@@ -1020,13 +1052,13 @@ function logInitialization(config, serviceName, serviceVersion, options, autoIns
1020
1052
  async function initializeInstrumentation(options = {}) {
1021
1053
  const sdk = await initializeSdk(options);
1022
1054
  if (sdk) {
1023
- const config = await loadConfig(options);
1055
+ const config = await loadConfigWithOptions(options);
1024
1056
  initializePatternMatcher(config);
1025
1057
  }
1026
1058
  return sdk;
1027
1059
  }
1028
1060
  async function initializePatternMatchingOnly(options = {}) {
1029
- const config = await loadConfig(options);
1061
+ const config = await loadConfigWithOptions(options);
1030
1062
  initializePatternMatcher(config);
1031
1063
  logger.log("@atrim/instrumentation: Pattern matching initialized (legacy mode)");
1032
1064
  logger.log(
@@ -1043,7 +1075,7 @@ var initializeInstrumentationEffect = (options = {}) => Effect.gen(function* ()
1043
1075
  });
1044
1076
  if (sdk) {
1045
1077
  yield* Effect.tryPromise({
1046
- try: () => loadConfig(options),
1078
+ try: () => loadConfigWithOptions(options),
1047
1079
  catch: (error) => new ConfigError2({
1048
1080
  reason: "Failed to load config for pattern matcher",
1049
1081
  cause: error
@@ -1060,7 +1092,7 @@ var initializeInstrumentationEffect = (options = {}) => Effect.gen(function* ()
1060
1092
  });
1061
1093
  var initializePatternMatchingOnlyEffect = (options = {}) => Effect.gen(function* () {
1062
1094
  const config = yield* Effect.tryPromise({
1063
- try: () => loadConfig(options),
1095
+ try: () => loadConfigWithOptions(options),
1064
1096
  catch: (error) => new ConfigError2({
1065
1097
  reason: "Failed to load configuration",
1066
1098
  cause: error
@@ -1169,6 +1201,6 @@ function suppressShutdownErrors() {
1169
1201
  });
1170
1202
  }
1171
1203
 
1172
- export { ConfigError2 as ConfigError, ConfigFileError2 as ConfigFileError, ConfigUrlError2 as ConfigUrlError, ConfigValidationError2 as ConfigValidationError, ExportError2 as ExportError, InitializationError2 as InitializationError, PatternMatcher, PatternSpanProcessor, ServiceDetectionError2 as ServiceDetectionError, ShutdownError2 as ShutdownError, annotateCacheOperation, annotateDbQuery, annotateHttpRequest, createOtlpExporter, detectServiceInfoAsync as detectServiceInfo, detectServiceInfo as detectServiceInfoEffect, getOtlpEndpoint, getPatternMatcher, getSdkInstance, getServiceInfoWithFallback, getServiceNameAsync as getServiceName, getServiceName as getServiceNameEffect, getServiceVersionAsync as getServiceVersion, getServiceVersion as getServiceVersionEffect, initializeInstrumentation, initializeInstrumentationEffect, initializePatternMatchingOnly, initializePatternMatchingOnlyEffect, loadConfig, markSpanError, markSpanSuccess, recordException, resetSdk, setSpanAttributes, shouldInstrumentSpan, shutdownSdk, suppressShutdownErrors };
1204
+ export { ConfigError2 as ConfigError, ConfigFileError2 as ConfigFileError, ConfigUrlError2 as ConfigUrlError, ConfigValidationError2 as ConfigValidationError, ExportError2 as ExportError, InitializationError2 as InitializationError, PatternMatcher, PatternSpanProcessor, ServiceDetectionError2 as ServiceDetectionError, ShutdownError2 as ShutdownError, annotateCacheOperation, annotateDbQuery, annotateHttpRequest, _resetConfigLoaderCache as clearConfigCache, createOtlpExporter, detectServiceInfoAsync as detectServiceInfo, detectServiceInfo as detectServiceInfoEffect, getOtlpEndpoint, getPatternMatcher, getSdkInstance, getServiceInfoWithFallback, getServiceNameAsync as getServiceName, getServiceName as getServiceNameEffect, getServiceVersionAsync as getServiceVersion, getServiceVersion as getServiceVersionEffect, initializeInstrumentation, initializeInstrumentationEffect, initializePatternMatchingOnly, initializePatternMatchingOnlyEffect, loadConfig, loadConfigFromInline, loadConfigWithOptions, markSpanError, markSpanSuccess, recordException, resetSdk, setSpanAttributes, shouldInstrumentSpan, shutdownSdk, suppressShutdownErrors };
1173
1205
  //# sourceMappingURL=index.js.map
1174
1206
  //# sourceMappingURL=index.js.map