@atrim/instrument-node 1.0.0
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.
- package/README.md +30 -0
- package/package.json +130 -0
- package/target/dist/index.cjs +1133 -0
- package/target/dist/index.cjs.map +1 -0
- package/target/dist/index.d.cts +548 -0
- package/target/dist/index.d.ts +548 -0
- package/target/dist/index.js +1095 -0
- package/target/dist/index.js.map +1 -0
- package/target/dist/integrations/effect/index.cjs +705 -0
- package/target/dist/integrations/effect/index.cjs.map +1 -0
- package/target/dist/integrations/effect/index.js +669 -0
- package/target/dist/integrations/effect/index.js.map +1 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var effect = require('effect');
|
|
4
|
+
var Otlp = require('@effect/opentelemetry/Otlp');
|
|
5
|
+
var platform = require('@effect/platform');
|
|
6
|
+
var api = require('@opentelemetry/api');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
var path = require('path');
|
|
9
|
+
var yaml = require('yaml');
|
|
10
|
+
var zod = require('zod');
|
|
11
|
+
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var Otlp__namespace = /*#__PURE__*/_interopNamespace(Otlp);
|
|
31
|
+
|
|
32
|
+
// src/integrations/effect/effect-tracer.ts
|
|
33
|
+
var __defProp = Object.defineProperty;
|
|
34
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
35
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
36
|
+
var PatternConfigSchema = zod.z.object({
|
|
37
|
+
pattern: zod.z.string(),
|
|
38
|
+
enabled: zod.z.boolean().optional(),
|
|
39
|
+
description: zod.z.string().optional()
|
|
40
|
+
});
|
|
41
|
+
var AutoIsolationConfigSchema = zod.z.object({
|
|
42
|
+
// Global enable/disable for auto-isolation
|
|
43
|
+
enabled: zod.z.boolean().default(false),
|
|
44
|
+
// Which operators to auto-isolate
|
|
45
|
+
operators: zod.z.object({
|
|
46
|
+
fiberset_run: zod.z.boolean().default(true),
|
|
47
|
+
effect_fork: zod.z.boolean().default(true),
|
|
48
|
+
effect_fork_daemon: zod.z.boolean().default(true),
|
|
49
|
+
effect_fork_in: zod.z.boolean().default(false)
|
|
50
|
+
}).default({}),
|
|
51
|
+
// Virtual parent tracking configuration
|
|
52
|
+
tracking: zod.z.object({
|
|
53
|
+
use_span_links: zod.z.boolean().default(true),
|
|
54
|
+
use_attributes: zod.z.boolean().default(true),
|
|
55
|
+
capture_logical_parent: zod.z.boolean().default(true)
|
|
56
|
+
}).default({}),
|
|
57
|
+
// Span categorization
|
|
58
|
+
attributes: zod.z.object({
|
|
59
|
+
category: zod.z.string().default("background_task"),
|
|
60
|
+
add_metadata: zod.z.boolean().default(true)
|
|
61
|
+
}).default({})
|
|
62
|
+
});
|
|
63
|
+
var InstrumentationConfigSchema = zod.z.object({
|
|
64
|
+
version: zod.z.string(),
|
|
65
|
+
instrumentation: zod.z.object({
|
|
66
|
+
enabled: zod.z.boolean(),
|
|
67
|
+
description: zod.z.string().optional(),
|
|
68
|
+
logging: zod.z.enum(["on", "off", "minimal"]).optional().default("on"),
|
|
69
|
+
instrument_patterns: zod.z.array(PatternConfigSchema),
|
|
70
|
+
ignore_patterns: zod.z.array(PatternConfigSchema)
|
|
71
|
+
}),
|
|
72
|
+
effect: zod.z.object({
|
|
73
|
+
auto_extract_metadata: zod.z.boolean(),
|
|
74
|
+
auto_isolation: AutoIsolationConfigSchema.optional()
|
|
75
|
+
}).optional()
|
|
76
|
+
});
|
|
77
|
+
(class extends effect.Data.TaggedError("ConfigError") {
|
|
78
|
+
});
|
|
79
|
+
var ConfigUrlError = class extends effect.Data.TaggedError("ConfigUrlError") {
|
|
80
|
+
};
|
|
81
|
+
var ConfigValidationError = class extends effect.Data.TaggedError("ConfigValidationError") {
|
|
82
|
+
};
|
|
83
|
+
var ConfigFileError = class extends effect.Data.TaggedError("ConfigFileError") {
|
|
84
|
+
};
|
|
85
|
+
(class extends effect.Data.TaggedError("ServiceDetectionError") {
|
|
86
|
+
});
|
|
87
|
+
(class extends effect.Data.TaggedError("InitializationError") {
|
|
88
|
+
});
|
|
89
|
+
(class extends effect.Data.TaggedError("ExportError") {
|
|
90
|
+
});
|
|
91
|
+
(class extends effect.Data.TaggedError("ShutdownError") {
|
|
92
|
+
});
|
|
93
|
+
var SECURITY_DEFAULTS = {
|
|
94
|
+
maxConfigSize: 1e6,
|
|
95
|
+
// 1MB
|
|
96
|
+
requestTimeout: 5e3,
|
|
97
|
+
allowedProtocols: ["https:"],
|
|
98
|
+
// Only HTTPS for remote configs
|
|
99
|
+
cacheTimeout: 3e5
|
|
100
|
+
// 5 minutes
|
|
101
|
+
};
|
|
102
|
+
function getDefaultConfig() {
|
|
103
|
+
return {
|
|
104
|
+
version: "1.0",
|
|
105
|
+
instrumentation: {
|
|
106
|
+
enabled: true,
|
|
107
|
+
logging: "on",
|
|
108
|
+
description: "Default instrumentation configuration",
|
|
109
|
+
instrument_patterns: [
|
|
110
|
+
{ pattern: "^app\\.", enabled: true, description: "Application operations" },
|
|
111
|
+
{ pattern: "^http\\.server\\.", enabled: true, description: "HTTP server operations" },
|
|
112
|
+
{ pattern: "^http\\.client\\.", enabled: true, description: "HTTP client operations" }
|
|
113
|
+
],
|
|
114
|
+
ignore_patterns: [
|
|
115
|
+
{ pattern: "^test\\.", description: "Test utilities" },
|
|
116
|
+
{ pattern: "^internal\\.", description: "Internal operations" },
|
|
117
|
+
{ pattern: "^health\\.", description: "Health checks" }
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
effect: {
|
|
121
|
+
auto_extract_metadata: true
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
var validateConfigEffect = (rawConfig) => effect.Effect.try({
|
|
126
|
+
try: () => InstrumentationConfigSchema.parse(rawConfig),
|
|
127
|
+
catch: (error) => new ConfigValidationError({
|
|
128
|
+
reason: "Invalid configuration schema",
|
|
129
|
+
cause: error
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
var loadConfigFromFileEffect = (filePath) => effect.Effect.gen(function* () {
|
|
133
|
+
const fileContents = yield* effect.Effect.try({
|
|
134
|
+
try: () => fs.readFileSync(filePath, "utf8"),
|
|
135
|
+
catch: (error) => new ConfigFileError({
|
|
136
|
+
reason: `Failed to read config file at ${filePath}`,
|
|
137
|
+
cause: error
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
if (fileContents.length > SECURITY_DEFAULTS.maxConfigSize) {
|
|
141
|
+
return yield* effect.Effect.fail(
|
|
142
|
+
new ConfigFileError({
|
|
143
|
+
reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
let rawConfig;
|
|
148
|
+
try {
|
|
149
|
+
rawConfig = yaml.parse(fileContents);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return yield* effect.Effect.fail(
|
|
152
|
+
new ConfigValidationError({
|
|
153
|
+
reason: "Invalid YAML syntax",
|
|
154
|
+
cause: error
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return yield* validateConfigEffect(rawConfig);
|
|
159
|
+
});
|
|
160
|
+
var fetchAndParseConfig = (url) => effect.Effect.gen(function* () {
|
|
161
|
+
let urlObj;
|
|
162
|
+
try {
|
|
163
|
+
urlObj = new URL(url);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return yield* effect.Effect.fail(
|
|
166
|
+
new ConfigUrlError({
|
|
167
|
+
reason: `Invalid URL: ${url}`,
|
|
168
|
+
cause: error
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (!SECURITY_DEFAULTS.allowedProtocols.includes(urlObj.protocol)) {
|
|
173
|
+
return yield* effect.Effect.fail(
|
|
174
|
+
new ConfigUrlError({
|
|
175
|
+
reason: `Insecure protocol ${urlObj.protocol}. Only ${SECURITY_DEFAULTS.allowedProtocols.join(", ")} are allowed`
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const response = yield* effect.Effect.tryPromise({
|
|
180
|
+
try: () => fetch(url, {
|
|
181
|
+
redirect: "follow",
|
|
182
|
+
headers: {
|
|
183
|
+
Accept: "application/yaml, text/yaml, text/x-yaml"
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
catch: (error) => new ConfigUrlError({
|
|
187
|
+
reason: `Failed to load config from URL ${url}`,
|
|
188
|
+
cause: error
|
|
189
|
+
})
|
|
190
|
+
}).pipe(
|
|
191
|
+
effect.Effect.timeout(effect.Duration.millis(SECURITY_DEFAULTS.requestTimeout)),
|
|
192
|
+
effect.Effect.retry({
|
|
193
|
+
times: 3,
|
|
194
|
+
schedule: effect.Schedule.exponential(effect.Duration.millis(100))
|
|
195
|
+
}),
|
|
196
|
+
effect.Effect.catchAll((error) => {
|
|
197
|
+
if (error._tag === "TimeoutException") {
|
|
198
|
+
return effect.Effect.fail(
|
|
199
|
+
new ConfigUrlError({
|
|
200
|
+
reason: `Config fetch timeout after ${SECURITY_DEFAULTS.requestTimeout}ms`
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return effect.Effect.fail(error);
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
return yield* effect.Effect.fail(
|
|
209
|
+
new ConfigUrlError({
|
|
210
|
+
reason: `HTTP ${response.status}: ${response.statusText}`
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const contentLength = response.headers.get("content-length");
|
|
215
|
+
if (contentLength && parseInt(contentLength) > 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
|
+
const text = yield* effect.Effect.tryPromise({
|
|
223
|
+
try: () => response.text(),
|
|
224
|
+
catch: (error) => new ConfigUrlError({
|
|
225
|
+
reason: "Failed to read response body",
|
|
226
|
+
cause: error
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
if (text.length > SECURITY_DEFAULTS.maxConfigSize) {
|
|
230
|
+
return yield* effect.Effect.fail(
|
|
231
|
+
new ConfigUrlError({
|
|
232
|
+
reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
let rawConfig;
|
|
237
|
+
try {
|
|
238
|
+
rawConfig = yaml.parse(text);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return yield* effect.Effect.fail(
|
|
241
|
+
new ConfigValidationError({
|
|
242
|
+
reason: "Invalid YAML syntax",
|
|
243
|
+
cause: error
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return yield* validateConfigEffect(rawConfig);
|
|
248
|
+
});
|
|
249
|
+
var makeConfigCache = () => effect.Cache.make({
|
|
250
|
+
capacity: 100,
|
|
251
|
+
timeToLive: effect.Duration.minutes(5),
|
|
252
|
+
lookup: (url) => fetchAndParseConfig(url)
|
|
253
|
+
});
|
|
254
|
+
var cacheInstance = null;
|
|
255
|
+
var getCache = effect.Effect.gen(function* () {
|
|
256
|
+
if (!cacheInstance) {
|
|
257
|
+
cacheInstance = yield* makeConfigCache();
|
|
258
|
+
}
|
|
259
|
+
return cacheInstance;
|
|
260
|
+
});
|
|
261
|
+
var loadConfigFromUrlEffect = (url, cacheTimeout = SECURITY_DEFAULTS.cacheTimeout) => effect.Effect.gen(function* () {
|
|
262
|
+
if (cacheTimeout === 0) {
|
|
263
|
+
return yield* fetchAndParseConfig(url);
|
|
264
|
+
}
|
|
265
|
+
const cache = yield* getCache;
|
|
266
|
+
return yield* cache.get(url);
|
|
267
|
+
});
|
|
268
|
+
var loadConfigEffect = (options = {}) => effect.Effect.gen(function* () {
|
|
269
|
+
if (options.config) {
|
|
270
|
+
return yield* validateConfigEffect(options.config);
|
|
271
|
+
}
|
|
272
|
+
const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
|
|
273
|
+
if (envConfigPath) {
|
|
274
|
+
if (envConfigPath.startsWith("http://") || envConfigPath.startsWith("https://")) {
|
|
275
|
+
return yield* loadConfigFromUrlEffect(envConfigPath, options.cacheTimeout);
|
|
276
|
+
}
|
|
277
|
+
return yield* loadConfigFromFileEffect(envConfigPath);
|
|
278
|
+
}
|
|
279
|
+
if (options.configUrl) {
|
|
280
|
+
return yield* loadConfigFromUrlEffect(options.configUrl, options.cacheTimeout);
|
|
281
|
+
}
|
|
282
|
+
if (options.configPath) {
|
|
283
|
+
return yield* loadConfigFromFileEffect(options.configPath);
|
|
284
|
+
}
|
|
285
|
+
const defaultPath = path.join(process.cwd(), "instrumentation.yaml");
|
|
286
|
+
const exists = yield* effect.Effect.sync(() => fs.existsSync(defaultPath));
|
|
287
|
+
if (exists) {
|
|
288
|
+
return yield* loadConfigFromFileEffect(defaultPath);
|
|
289
|
+
}
|
|
290
|
+
return getDefaultConfig();
|
|
291
|
+
});
|
|
292
|
+
async function loadConfig(options = {}) {
|
|
293
|
+
return effect.Effect.runPromise(
|
|
294
|
+
loadConfigEffect(options).pipe(
|
|
295
|
+
// Convert typed errors to regular Error with reason message for backward compatibility
|
|
296
|
+
effect.Effect.mapError((error) => {
|
|
297
|
+
const message = error.reason;
|
|
298
|
+
const newError = new Error(message);
|
|
299
|
+
newError.cause = error.cause;
|
|
300
|
+
return newError;
|
|
301
|
+
})
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
var PatternMatcher = class {
|
|
306
|
+
constructor(config) {
|
|
307
|
+
__publicField(this, "ignorePatterns", []);
|
|
308
|
+
__publicField(this, "instrumentPatterns", []);
|
|
309
|
+
__publicField(this, "enabled", true);
|
|
310
|
+
this.enabled = config.instrumentation.enabled;
|
|
311
|
+
this.ignorePatterns = config.instrumentation.ignore_patterns.map((p) => this.compilePattern(p));
|
|
312
|
+
this.instrumentPatterns = config.instrumentation.instrument_patterns.filter((p) => p.enabled !== false).map((p) => this.compilePattern(p));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Compile a pattern configuration into a RegExp
|
|
316
|
+
*/
|
|
317
|
+
compilePattern(pattern) {
|
|
318
|
+
try {
|
|
319
|
+
const compiled = {
|
|
320
|
+
regex: new RegExp(pattern.pattern),
|
|
321
|
+
enabled: pattern.enabled !== false
|
|
322
|
+
};
|
|
323
|
+
if (pattern.description !== void 0) {
|
|
324
|
+
compiled.description = pattern.description;
|
|
325
|
+
}
|
|
326
|
+
return compiled;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Failed to compile pattern "${pattern.pattern}": ${error instanceof Error ? error.message : String(error)}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Check if a span should be instrumented
|
|
335
|
+
*
|
|
336
|
+
* Returns true if the span should be created, false otherwise.
|
|
337
|
+
*
|
|
338
|
+
* Logic:
|
|
339
|
+
* 1. If instrumentation disabled globally, return false
|
|
340
|
+
* 2. Check ignore patterns - if any match, return false
|
|
341
|
+
* 3. Check instrument patterns - if any match, return true
|
|
342
|
+
* 4. Default: return true (fail-open - create span if no patterns match)
|
|
343
|
+
*/
|
|
344
|
+
shouldInstrument(spanName) {
|
|
345
|
+
if (!this.enabled) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
for (const pattern of this.ignorePatterns) {
|
|
349
|
+
if (pattern.regex.test(spanName)) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (const pattern of this.instrumentPatterns) {
|
|
354
|
+
if (pattern.enabled && pattern.regex.test(spanName)) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get statistics about pattern matching (for debugging/monitoring)
|
|
362
|
+
*/
|
|
363
|
+
getStats() {
|
|
364
|
+
return {
|
|
365
|
+
enabled: this.enabled,
|
|
366
|
+
ignorePatternCount: this.ignorePatterns.length,
|
|
367
|
+
instrumentPatternCount: this.instrumentPatterns.filter((p) => p.enabled).length
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
function initializePatternMatcher(config) {
|
|
372
|
+
new PatternMatcher(config);
|
|
373
|
+
}
|
|
374
|
+
var Logger = class {
|
|
375
|
+
constructor() {
|
|
376
|
+
__publicField(this, "level", "on");
|
|
377
|
+
__publicField(this, "hasLoggedMinimal", false);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Set the logging level
|
|
381
|
+
*/
|
|
382
|
+
setLevel(level) {
|
|
383
|
+
this.level = level;
|
|
384
|
+
this.hasLoggedMinimal = false;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get the current logging level
|
|
388
|
+
*/
|
|
389
|
+
getLevel() {
|
|
390
|
+
return this.level;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Log a minimal initialization message (only shown once in minimal mode)
|
|
394
|
+
*/
|
|
395
|
+
minimal(message) {
|
|
396
|
+
if (this.level === "off") {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (this.level === "minimal" && !this.hasLoggedMinimal) {
|
|
400
|
+
console.log(message);
|
|
401
|
+
this.hasLoggedMinimal = true;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (this.level === "on") {
|
|
405
|
+
console.log(message);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Log an informational message
|
|
410
|
+
*/
|
|
411
|
+
log(...args) {
|
|
412
|
+
if (this.level === "on") {
|
|
413
|
+
console.log(...args);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Log a warning message (shown in minimal mode)
|
|
418
|
+
*/
|
|
419
|
+
warn(...args) {
|
|
420
|
+
if (this.level !== "off") {
|
|
421
|
+
console.warn(...args);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Log an error message (shown in minimal mode)
|
|
426
|
+
*/
|
|
427
|
+
error(...args) {
|
|
428
|
+
if (this.level !== "off") {
|
|
429
|
+
console.error(...args);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Check if full logging is enabled
|
|
434
|
+
*/
|
|
435
|
+
isEnabled() {
|
|
436
|
+
return this.level === "on";
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Check if minimal logging is enabled
|
|
440
|
+
*/
|
|
441
|
+
isMinimal() {
|
|
442
|
+
return this.level === "minimal";
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Check if logging is completely disabled
|
|
446
|
+
*/
|
|
447
|
+
isDisabled() {
|
|
448
|
+
return this.level === "off";
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var logger = new Logger();
|
|
452
|
+
|
|
453
|
+
// src/integrations/effect/effect-tracer.ts
|
|
454
|
+
function createEffectInstrumentation(options = {}) {
|
|
455
|
+
return effect.Layer.unwrapEffect(
|
|
456
|
+
effect.Effect.gen(function* () {
|
|
457
|
+
const config = yield* effect.Effect.tryPromise({
|
|
458
|
+
try: () => loadConfig(options),
|
|
459
|
+
catch: (error) => ({
|
|
460
|
+
_tag: "ConfigError",
|
|
461
|
+
message: error instanceof Error ? error.message : String(error)
|
|
462
|
+
})
|
|
463
|
+
});
|
|
464
|
+
yield* effect.Effect.sync(() => {
|
|
465
|
+
const loggingLevel = config.instrumentation.logging || "on";
|
|
466
|
+
logger.setLevel(loggingLevel);
|
|
467
|
+
});
|
|
468
|
+
yield* effect.Effect.sync(() => initializePatternMatcher(config));
|
|
469
|
+
const otlpEndpoint = options.otlpEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
|
|
470
|
+
const serviceName = options.serviceName || process.env.OTEL_SERVICE_NAME || "effect-service";
|
|
471
|
+
const serviceVersion = options.serviceVersion || process.env.npm_package_version || "1.0.0";
|
|
472
|
+
const autoExtractMetadata = options.autoExtractMetadata ?? config.effect?.auto_extract_metadata ?? true;
|
|
473
|
+
const continueExistingTraces = options.continueExistingTraces ?? true;
|
|
474
|
+
logger.log("\u{1F50D} Effect OpenTelemetry instrumentation");
|
|
475
|
+
logger.log(` \u{1F4E1} Endpoint: ${otlpEndpoint}`);
|
|
476
|
+
logger.log(` \u{1F3F7}\uFE0F Service: ${serviceName}`);
|
|
477
|
+
logger.log(` \u2705 Auto metadata extraction: ${autoExtractMetadata}`);
|
|
478
|
+
logger.log(` \u2705 Continue existing traces: ${continueExistingTraces}`);
|
|
479
|
+
const otlpLayer = Otlp__namespace.layer({
|
|
480
|
+
baseUrl: otlpEndpoint,
|
|
481
|
+
resource: {
|
|
482
|
+
serviceName,
|
|
483
|
+
serviceVersion,
|
|
484
|
+
attributes: {
|
|
485
|
+
"platform.component": "effect",
|
|
486
|
+
"effect.auto_metadata": autoExtractMetadata,
|
|
487
|
+
"effect.context_propagation": continueExistingTraces
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
// Bridge Effect context to OpenTelemetry global context
|
|
491
|
+
// This is essential for context propagation to work properly
|
|
492
|
+
tracerContext: (f, span) => {
|
|
493
|
+
if (span._tag !== "Span") {
|
|
494
|
+
return f();
|
|
495
|
+
}
|
|
496
|
+
const spanContext = {
|
|
497
|
+
traceId: span.traceId,
|
|
498
|
+
spanId: span.spanId,
|
|
499
|
+
traceFlags: span.sampled ? api.TraceFlags.SAMPLED : api.TraceFlags.NONE
|
|
500
|
+
};
|
|
501
|
+
const otelSpan = api.trace.wrapSpanContext(spanContext);
|
|
502
|
+
return api.context.with(api.trace.setSpan(api.context.active(), otelSpan), f);
|
|
503
|
+
}
|
|
504
|
+
}).pipe(effect.Layer.provide(platform.FetchHttpClient.layer));
|
|
505
|
+
if (autoExtractMetadata) {
|
|
506
|
+
return otlpLayer;
|
|
507
|
+
}
|
|
508
|
+
return otlpLayer;
|
|
509
|
+
})
|
|
510
|
+
).pipe(effect.Layer.orDie);
|
|
511
|
+
}
|
|
512
|
+
var EffectInstrumentationLive = effect.Effect.sync(() => {
|
|
513
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
|
|
514
|
+
const serviceName = process.env.OTEL_SERVICE_NAME || "effect-service";
|
|
515
|
+
const serviceVersion = process.env.npm_package_version || "1.0.0";
|
|
516
|
+
logger.log("\u{1F50D} Effect OpenTelemetry tracer (Otlp.layer)");
|
|
517
|
+
logger.log(` \u{1F4E1} Endpoint: ${endpoint}`);
|
|
518
|
+
logger.log(` \u{1F3F7}\uFE0F Service: ${serviceName}`);
|
|
519
|
+
return Otlp__namespace.layer({
|
|
520
|
+
baseUrl: endpoint,
|
|
521
|
+
resource: {
|
|
522
|
+
serviceName,
|
|
523
|
+
serviceVersion,
|
|
524
|
+
attributes: {
|
|
525
|
+
"platform.component": "effect"
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
// CRITICAL: Bridge Effect context to OpenTelemetry global context
|
|
529
|
+
// This allows NodeSDK auto-instrumentation to see Effect spans as parent spans
|
|
530
|
+
tracerContext: (f, span) => {
|
|
531
|
+
if (span._tag !== "Span") {
|
|
532
|
+
return f();
|
|
533
|
+
}
|
|
534
|
+
const spanContext = {
|
|
535
|
+
traceId: span.traceId,
|
|
536
|
+
spanId: span.spanId,
|
|
537
|
+
traceFlags: span.sampled ? api.TraceFlags.SAMPLED : api.TraceFlags.NONE
|
|
538
|
+
};
|
|
539
|
+
const otelSpan = api.trace.wrapSpanContext(spanContext);
|
|
540
|
+
return api.context.with(api.trace.setSpan(api.context.active(), otelSpan), f);
|
|
541
|
+
}
|
|
542
|
+
}).pipe(effect.Layer.provide(platform.FetchHttpClient.layer));
|
|
543
|
+
}).pipe(effect.Layer.unwrapEffect);
|
|
544
|
+
|
|
545
|
+
// src/integrations/effect/effect-helpers.ts
|
|
546
|
+
function annotateUser(_userId, _email) {
|
|
547
|
+
}
|
|
548
|
+
function annotateDataSize(_bytes, _count) {
|
|
549
|
+
}
|
|
550
|
+
function annotateBatch(_size, _batchSize) {
|
|
551
|
+
}
|
|
552
|
+
function annotateLLM(_model, _operation, _inputTokens, _outputTokens) {
|
|
553
|
+
}
|
|
554
|
+
function annotateQuery(_query, _database) {
|
|
555
|
+
}
|
|
556
|
+
function annotateHttpRequest(_method, _url, _statusCode) {
|
|
557
|
+
}
|
|
558
|
+
function annotateError(_error, _context) {
|
|
559
|
+
}
|
|
560
|
+
function annotatePriority(_priority) {
|
|
561
|
+
}
|
|
562
|
+
function annotateCache(_operation, _hit) {
|
|
563
|
+
}
|
|
564
|
+
var createLogicalParentLink = (parentSpan, useSpanLinks) => {
|
|
565
|
+
if (!useSpanLinks) {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
return [
|
|
569
|
+
{
|
|
570
|
+
_tag: "SpanLink",
|
|
571
|
+
span: parentSpan,
|
|
572
|
+
attributes: {
|
|
573
|
+
"link.type": "logical_parent",
|
|
574
|
+
"atrim.relationship": "spawned_by",
|
|
575
|
+
description: "Logical parent (isolated to prevent context leakage)"
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
];
|
|
579
|
+
};
|
|
580
|
+
var createLogicalParentAttributes = (parentSpan, useAttributes, category, useSpanLinks, customAttributes) => {
|
|
581
|
+
if (!useAttributes) {
|
|
582
|
+
return customAttributes;
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
// Logical parent tracking (works in ALL tools)
|
|
586
|
+
"atrim.logical_parent.span_id": parentSpan.spanId,
|
|
587
|
+
"atrim.logical_parent.trace_id": parentSpan.traceId,
|
|
588
|
+
"atrim.logical_parent.name": parentSpan._tag === "Span" ? parentSpan.name : "external",
|
|
589
|
+
// Categorization and metadata
|
|
590
|
+
"atrim.fiberset.isolated": true,
|
|
591
|
+
"atrim.span.category": category,
|
|
592
|
+
"atrim.has_logical_parent": true,
|
|
593
|
+
"atrim.isolation.method": useSpanLinks ? "hybrid" : "attributes_only",
|
|
594
|
+
// User-provided attributes
|
|
595
|
+
...customAttributes
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
var runIsolated = (set, effect$1, name, options) => {
|
|
599
|
+
const {
|
|
600
|
+
createRoot = true,
|
|
601
|
+
captureLogicalParent = true,
|
|
602
|
+
useSpanLinks = true,
|
|
603
|
+
useAttributes = true,
|
|
604
|
+
propagateInterruption,
|
|
605
|
+
attributes = {},
|
|
606
|
+
category = "background_task"
|
|
607
|
+
} = options ?? {};
|
|
608
|
+
if (!createRoot && !captureLogicalParent) {
|
|
609
|
+
return effect.FiberSet.run(set, effect$1, { propagateInterruption });
|
|
610
|
+
}
|
|
611
|
+
return effect.Effect.gen(function* () {
|
|
612
|
+
const maybeParent = yield* effect.Effect.serviceOption(effect.Tracer.ParentSpan);
|
|
613
|
+
if (maybeParent._tag === "None" || !captureLogicalParent) {
|
|
614
|
+
const isolated2 = effect$1.pipe(
|
|
615
|
+
effect.Effect.withSpan(name, {
|
|
616
|
+
root: createRoot,
|
|
617
|
+
attributes: {
|
|
618
|
+
"atrim.fiberset.isolated": createRoot,
|
|
619
|
+
"atrim.span.category": category,
|
|
620
|
+
"atrim.has_logical_parent": false,
|
|
621
|
+
...attributes
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
return yield* effect.FiberSet.run(set, isolated2, { propagateInterruption });
|
|
626
|
+
}
|
|
627
|
+
const parent = maybeParent.value;
|
|
628
|
+
const links = createLogicalParentLink(parent, useSpanLinks);
|
|
629
|
+
const spanAttributes = createLogicalParentAttributes(
|
|
630
|
+
parent,
|
|
631
|
+
useAttributes,
|
|
632
|
+
category,
|
|
633
|
+
useSpanLinks,
|
|
634
|
+
attributes
|
|
635
|
+
);
|
|
636
|
+
const isolated = effect$1.pipe(
|
|
637
|
+
effect.Effect.withSpan(name, {
|
|
638
|
+
root: createRoot,
|
|
639
|
+
links: links.length > 0 ? links : void 0,
|
|
640
|
+
attributes: spanAttributes
|
|
641
|
+
})
|
|
642
|
+
);
|
|
643
|
+
return yield* effect.FiberSet.run(set, isolated, { propagateInterruption });
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
var runWithSpan = (set, name, effect, options) => {
|
|
647
|
+
return runIsolated(set, effect, name, {
|
|
648
|
+
createRoot: true,
|
|
649
|
+
captureLogicalParent: true,
|
|
650
|
+
useSpanLinks: true,
|
|
651
|
+
useAttributes: true,
|
|
652
|
+
...options
|
|
653
|
+
});
|
|
654
|
+
};
|
|
655
|
+
var annotateSpawnedTasks = (tasks) => {
|
|
656
|
+
return effect.Effect.annotateCurrentSpan({
|
|
657
|
+
"atrim.fiberset.spawned_count": tasks.length,
|
|
658
|
+
"atrim.fiberset.task_names": tasks.map((t) => t.name).join(","),
|
|
659
|
+
"atrim.has_background_tasks": true,
|
|
660
|
+
"atrim.spawned_tasks": JSON.stringify(
|
|
661
|
+
tasks.map((t) => ({
|
|
662
|
+
name: t.name,
|
|
663
|
+
...t.spanId && { span_id: t.spanId },
|
|
664
|
+
...t.category && { category: t.category }
|
|
665
|
+
}))
|
|
666
|
+
)
|
|
667
|
+
});
|
|
668
|
+
};
|
|
669
|
+
var FiberSet = {
|
|
670
|
+
// Re-export all original FiberSet functions
|
|
671
|
+
make: effect.FiberSet.make,
|
|
672
|
+
add: effect.FiberSet.add,
|
|
673
|
+
unsafeAdd: effect.FiberSet.unsafeAdd,
|
|
674
|
+
run: effect.FiberSet.run,
|
|
675
|
+
clear: effect.FiberSet.clear,
|
|
676
|
+
join: effect.FiberSet.join,
|
|
677
|
+
awaitEmpty: effect.FiberSet.awaitEmpty,
|
|
678
|
+
size: effect.FiberSet.size,
|
|
679
|
+
runtime: effect.FiberSet.runtime,
|
|
680
|
+
runtimePromise: effect.FiberSet.runtimePromise,
|
|
681
|
+
makeRuntime: effect.FiberSet.makeRuntime,
|
|
682
|
+
makeRuntimePromise: effect.FiberSet.makeRuntimePromise,
|
|
683
|
+
isFiberSet: effect.FiberSet.isFiberSet,
|
|
684
|
+
// Add our isolation helpers
|
|
685
|
+
runIsolated,
|
|
686
|
+
runWithSpan
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
exports.EffectInstrumentationLive = EffectInstrumentationLive;
|
|
690
|
+
exports.FiberSet = FiberSet;
|
|
691
|
+
exports.annotateBatch = annotateBatch;
|
|
692
|
+
exports.annotateCache = annotateCache;
|
|
693
|
+
exports.annotateDataSize = annotateDataSize;
|
|
694
|
+
exports.annotateError = annotateError;
|
|
695
|
+
exports.annotateHttpRequest = annotateHttpRequest;
|
|
696
|
+
exports.annotateLLM = annotateLLM;
|
|
697
|
+
exports.annotatePriority = annotatePriority;
|
|
698
|
+
exports.annotateQuery = annotateQuery;
|
|
699
|
+
exports.annotateSpawnedTasks = annotateSpawnedTasks;
|
|
700
|
+
exports.annotateUser = annotateUser;
|
|
701
|
+
exports.createEffectInstrumentation = createEffectInstrumentation;
|
|
702
|
+
exports.runIsolated = runIsolated;
|
|
703
|
+
exports.runWithSpan = runWithSpan;
|
|
704
|
+
//# sourceMappingURL=index.cjs.map
|
|
705
|
+
//# sourceMappingURL=index.cjs.map
|