|
6 | 6 | */ |
7 | 7 |
|
8 | 8 | import { metrics, trace } from "@opentelemetry/api"; |
| 9 | +import type { Span } from "@opentelemetry/api"; |
| 10 | +import type { ToolMetadata } from "@stackables/bridge-types"; |
| 11 | +import type { Logger } from "./tree-types.ts"; |
9 | 12 | import { roundMs } from "./tree-utils.ts"; |
10 | 13 |
|
11 | 14 | // ── OTel setup ────────────────────────────────────────────────────────────── |
@@ -45,8 +48,9 @@ export const toolErrorCounter = otelMeter.createCounter("bridge.tool.errors", { |
45 | 48 | description: "Total number of tool invocation errors", |
46 | 49 | }); |
47 | 50 |
|
48 | | -// Re-export SpanStatusCode for callTool usage |
| 51 | +// Re-export SpanStatusCode for internal usage |
49 | 52 | export { SpanStatusCode as SpanStatusCodeEnum } from "@opentelemetry/api"; |
| 53 | +import { SpanStatusCode } from "@opentelemetry/api"; |
50 | 54 |
|
51 | 55 | // ── Trace types ───────────────────────────────────────────────────────────── |
52 | 56 |
|
@@ -96,10 +100,7 @@ export function boundedClone( |
96 | 100 | 0, |
97 | 101 | Number.isFinite(maxStringLength) ? Math.floor(maxStringLength) : 1024, |
98 | 102 | ); |
99 | | - const safeDepth = Math.max( |
100 | | - 0, |
101 | | - Number.isFinite(depth) ? Math.floor(depth) : 5, |
102 | | - ); |
| 103 | + const safeDepth = Math.max(0, Number.isFinite(depth) ? Math.floor(depth) : 5); |
103 | 104 | return _boundedClone(value, safeArrayItems, safeStringLength, safeDepth, 0); |
104 | 105 | } |
105 | 106 |
|
@@ -165,7 +166,11 @@ export class TraceCollector { |
165 | 166 |
|
166 | 167 | constructor( |
167 | 168 | level: "basic" | "full" = "full", |
168 | | - options?: { maxArrayItems?: number; maxStringLength?: number; cloneDepth?: number }, |
| 169 | + options?: { |
| 170 | + maxArrayItems?: number; |
| 171 | + maxStringLength?: number; |
| 172 | + cloneDepth?: number; |
| 173 | + }, |
169 | 174 | ) { |
170 | 175 | this.level = level; |
171 | 176 | this.maxArrayItems = options?.maxArrayItems ?? 100; |
@@ -225,3 +230,93 @@ export class TraceCollector { |
225 | 230 | return t; |
226 | 231 | } |
227 | 232 | } |
| 233 | + |
| 234 | +// ── Tool metadata helpers ──────────────────────────────────────────────────── |
| 235 | + |
| 236 | +/** |
| 237 | + * Resolved logging behaviour derived from a tool's `.bridge` metadata. |
| 238 | + * A fixed shape so call sites never branch on `undefined`. |
| 239 | + */ |
| 240 | +export type EffectiveToolLog = { |
| 241 | + /** Logger level for successful invocations, or `false` to suppress. */ |
| 242 | + execution: false | "debug" | "info"; |
| 243 | + /** Logger level for thrown errors, or `false` to suppress. */ |
| 244 | + errors: false | "warn" | "error"; |
| 245 | +}; |
| 246 | + |
| 247 | +/** Normalised metadata resolved from the optional `.bridge` property. */ |
| 248 | +export type ResolvedToolMeta = { |
| 249 | + /** Emit an OTel span for this call. Default: `true`. */ |
| 250 | + doTrace: boolean; |
| 251 | + log: EffectiveToolLog; |
| 252 | +}; |
| 253 | + |
| 254 | +function resolveToolLog(meta: ToolMetadata | undefined): EffectiveToolLog { |
| 255 | + const log = meta?.log; |
| 256 | + if (log === false) return { execution: false, errors: false }; |
| 257 | + if (log == null) return { execution: false, errors: "error" }; |
| 258 | + if (log === true) return { execution: "info", errors: "error" }; |
| 259 | + return { |
| 260 | + execution: |
| 261 | + log.execution === "info" ? "info" : log.execution ? "debug" : false, |
| 262 | + errors: |
| 263 | + log.errors === false ? false : log.errors === "warn" ? "warn" : "error", |
| 264 | + }; |
| 265 | +} |
| 266 | + |
| 267 | +/** Read and normalise the `.bridge` metadata from a tool function. */ |
| 268 | +export function resolveToolMeta(fn: (...args: any[]) => any): ResolvedToolMeta { |
| 269 | + const bridge = (fn as any).bridge as ToolMetadata | undefined; |
| 270 | + return { doTrace: bridge?.trace !== false, log: resolveToolLog(bridge) }; |
| 271 | +} |
| 272 | + |
| 273 | +/** Log a successful tool invocation. No-ops when `level` is `false`. */ |
| 274 | +export function logToolSuccess( |
| 275 | + logger: Logger | undefined, |
| 276 | + level: EffectiveToolLog["execution"], |
| 277 | + toolName: string, |
| 278 | + fnName: string, |
| 279 | + durationMs: number, |
| 280 | +): void { |
| 281 | + if (!level) return; |
| 282 | + logger?.[level]?.( |
| 283 | + { tool: toolName, fn: fnName, durationMs }, |
| 284 | + "[bridge] tool completed", |
| 285 | + ); |
| 286 | +} |
| 287 | + |
| 288 | +/** Log a tool error. No-ops when `level` is `false`. */ |
| 289 | +export function logToolError( |
| 290 | + logger: Logger | undefined, |
| 291 | + level: EffectiveToolLog["errors"], |
| 292 | + toolName: string, |
| 293 | + fnName: string, |
| 294 | + err: Error, |
| 295 | +): void { |
| 296 | + if (!level) return; |
| 297 | + logger?.[level]?.( |
| 298 | + { tool: toolName, fn: fnName, err: err.message }, |
| 299 | + "[bridge] tool failed", |
| 300 | + ); |
| 301 | +} |
| 302 | + |
| 303 | +/** Record an exception on a span and mark it as errored. */ |
| 304 | +export function recordSpanError(span: Span | undefined, err: Error): void { |
| 305 | + if (!span) return; |
| 306 | + span.recordException(err); |
| 307 | + span.setStatus({ code: SpanStatusCode.ERROR, message: err.message }); |
| 308 | +} |
| 309 | + |
| 310 | +/** |
| 311 | + * Run `fn` inside an OTel span when `doTrace` is true; |
| 312 | + * otherwise call it directly with `undefined` as the span argument. |
| 313 | + */ |
| 314 | +export function withSpan<T>( |
| 315 | + doTrace: boolean, |
| 316 | + name: string, |
| 317 | + attrs: Record<string, string>, |
| 318 | + fn: (span: Span | undefined) => Promise<T>, |
| 319 | +): Promise<T> { |
| 320 | + if (!doTrace) return fn(undefined); |
| 321 | + return otelTracer.startActiveSpan(name, { attributes: attrs }, fn); |
| 322 | +} |
0 commit comments