UNPKG

agentcrumbs

Version:
251 lines 8.43 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.addSink = addSink; exports.removeSink = removeSink; exports.resetSinks = resetSinks; exports.trail = trail; const node_crypto_1 = require("node:crypto"); const node_fs_1 = require("node:fs"); const noop_js_1 = require("./noop.js"); const env_js_1 = require("./env.js"); const context_js_1 = require("./context.js"); const console_js_1 = require("./sinks/console.js"); const socket_js_1 = require("./sinks/socket.js"); const SESSION_FILE = "/tmp/agentcrumbs.session"; const globalSinks = []; let sinksInitialized = false; function ensureSinks() { if (sinksInitialized) return; sinksInitialized = true; // HTTP sink — fire-and-forget POST to collector. If collector isn't // running, fetch() silently fails. No connection state to manage. const url = (0, env_js_1.getCollectorUrl)(); globalSinks.push(new socket_js_1.HttpSink(url)); // Console sink — always on as fallback. Crumbs show in stderr so // they're visible even without the collector running. const format = (0, env_js_1.getFormat)(); if (format === "json") { globalSinks.push({ write(crumb) { process.stderr.write(JSON.stringify(crumb) + "\n"); }, }); } else { globalSinks.push(new console_js_1.ConsoleSink()); } } function addSink(sink) { globalSinks.push(sink); sinksInitialized = true; } function removeSink(sink) { const idx = globalSinks.indexOf(sink); if (idx !== -1) globalSinks.splice(idx, 1); } /** Reset sinks — for testing */ function resetSinks() { globalSinks.length = 0; sinksInitialized = false; } function emit(crumb) { ensureSinks(); for (const sink of globalSinks) { try { sink.write(crumb); } catch { // Never let a sink error affect the application } } } function getCliSessionId() { try { const content = (0, node_fs_1.readFileSync)(SESSION_FILE, "utf-8").trim(); return content || undefined; } catch { return undefined; } } function createTrailFunction(namespace, parentCtx) { let lastTime = performance.now(); const timers = new Map(); function makeCrumb(msg, type, data, options, overrides) { const now = performance.now(); const dt = now - lastTime; lastTime = now; const asyncCtx = (0, context_js_1.getContext)(); const cliSession = getCliSessionId(); const crumb = { app: (0, env_js_1.getApp)(), ts: new Date().toISOString(), ns: namespace, msg, type, dt: Math.round(dt * 100) / 100, pid: process.pid, ...overrides, }; if (data !== undefined) crumb.data = data; // Merge context: parent -> async -> explicit const mergedCtx = { ...parentCtx, ...asyncCtx?.contextData, }; if (Object.keys(mergedCtx).length > 0) crumb.ctx = mergedCtx; if (!crumb.traceId && asyncCtx?.traceId) crumb.traceId = asyncCtx.traceId; if (!crumb.depth && asyncCtx?.depth) crumb.depth = asyncCtx.depth; // Session: prefer async context session, then CLI session const sid = asyncCtx?.sessionId ?? cliSession; if (sid) crumb.sid = sid; if (options?.tags && options.tags.length > 0) crumb.tags = options.tags; return crumb; } const fn = function trailFn(msg, data, options) { emit(makeCrumb(msg, "crumb", data, options)); }; fn.enabled = true; fn.namespace = namespace; fn.child = (ctx) => { return createTrailFunction(namespace, { ...parentCtx, ...ctx }); }; fn.scope = (name, scopeFn) => { const traceId = (0, node_crypto_1.randomUUID)().slice(0, 8); const asyncCtx = (0, context_js_1.getContext)(); const depth = (asyncCtx?.depth ?? 0) + 1; const newCtx = { namespace, contextData: { ...parentCtx, ...asyncCtx?.contextData }, traceId, depth, sessionId: asyncCtx?.sessionId, }; emit(makeCrumb(name, "scope:enter", undefined, undefined, { traceId, depth })); const startTime = performance.now(); const childTrail = createTrailFunction(namespace, newCtx.contextData); const finish = (error) => { const duration = Math.round((performance.now() - startTime) * 100) / 100; if (error) { const errorData = error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : { value: error }; emit(makeCrumb(name, "scope:error", { ...errorData, duration }, undefined, { traceId, depth, })); } else { emit(makeCrumb(name, "scope:exit", { duration }, undefined, { traceId, depth, })); } }; try { const result = (0, context_js_1.runWithContext)(newCtx, () => scopeFn({ crumb: childTrail, traceId })); if (result instanceof Promise) { return result.then((val) => { finish(); return val; }, (err) => { finish(err); throw err; }); } finish(); return result; } catch (err) { finish(err); throw err; } }; // eslint-disable-next-line @typescript-eslint/no-explicit-any fn.wrap = (name, wrappedFn) => { return ((...args) => { return fn.scope(name, () => wrappedFn(...args)); }); }; fn.time = (label) => { timers.set(label, performance.now()); }; fn.timeEnd = (label, data) => { const start = timers.get(label); if (start === undefined) return; timers.delete(label); const duration = Math.round((performance.now() - start) * 100) / 100; emit(makeCrumb(label, "time", { ...(data ?? {}), duration })); }; fn.snapshot = (label, obj) => { let cloned; try { cloned = structuredClone(obj); } catch { cloned = obj; } emit(makeCrumb(label, "snapshot", cloned)); }; fn.assert = (condition, msg) => { if (!condition) { emit(makeCrumb(msg, "assert", { passed: false })); } }; fn.session = ((name, sessionFn) => { const id = (0, node_crypto_1.randomUUID)().slice(0, 8); const asyncCtx = (0, context_js_1.getContext)(); const sessionCtx = { namespace, contextData: { ...parentCtx, ...asyncCtx?.contextData }, traceId: asyncCtx?.traceId ?? "", depth: asyncCtx?.depth ?? 0, sessionId: id, }; emit(makeCrumb(name, "session:start", undefined, undefined, { sid: id })); const session = { id, name, crumb: (msg, data, options) => { (0, context_js_1.runWithContext)(sessionCtx, () => { emit(makeCrumb(msg, "crumb", data, options, { sid: id })); }); }, end: () => { emit(makeCrumb(name, "session:end", undefined, undefined, { sid: id })); }, }; if (typeof sessionFn === "function") { const result = (0, context_js_1.runWithContext)(sessionCtx, () => sessionFn(session)); if (result instanceof Promise) { return result.then((val) => { session.end(); return val; }, (err) => { session.end(); throw err; }); } session.end(); return result; } return session; }); return fn; } function trail(namespace) { if (!(0, env_js_1.isNamespaceEnabled)(namespace)) { return noop_js_1.NOOP; } return createTrailFunction(namespace); } //# sourceMappingURL=trail.js.map