feat(telemetry): add OpenTelemetry tracing with optional OTLP export to Tempo

This commit is contained in:
Hibryda 2026-03-08 20:34:19 +01:00
parent 3f1638c98b
commit fd9f55faff
9 changed files with 601 additions and 2 deletions

View file

@ -0,0 +1,26 @@
// Telemetry bridge — routes frontend events to Rust tracing via IPC
// No browser OTEL SDK needed (WebKit2GTK incompatible)
import { invoke } from '@tauri-apps/api/core';
type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
/** Emit a structured log event to the Rust tracing layer */
export function telemetryLog(
level: LogLevel,
message: string,
context?: Record<string, unknown>,
): void {
invoke('frontend_log', { level, message, context: context ?? null }).catch(() => {
// Swallow IPC errors — telemetry must never break the app
});
}
/** Convenience wrappers */
export const tel = {
error: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('error', msg, ctx),
warn: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('warn', msg, ctx),
info: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('info', msg, ctx),
debug: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('debug', msg, ctx),
trace: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('trace', msg, ctx),
};

View file

@ -22,6 +22,7 @@ import {
saveAgentMessages,
type AgentMessageRecord,
} from './adapters/groups-bridge';
import { tel } from './adapters/telemetry-bridge';
let unlistenMsg: (() => void) | null = null;
let unlistenExit: (() => void) | null = null;
@ -66,6 +67,7 @@ export async function startAgentDispatcher(): Promise<void> {
switch (msg.type) {
case 'agent_started':
updateAgentStatus(sessionId, 'running');
tel.info('agent_started', { sessionId });
break;
case 'agent_event':
@ -74,11 +76,13 @@ export async function startAgentDispatcher(): Promise<void> {
case 'agent_stopped':
updateAgentStatus(sessionId, 'done');
tel.info('agent_stopped', { sessionId });
notify('success', `Agent ${sessionId.slice(0, 8)} completed`);
break;
case 'agent_error':
updateAgentStatus(sessionId, 'error', msg.message);
tel.error('agent_error', { sessionId, error: msg.message });
notify('error', `Agent error: ${msg.message ?? 'Unknown'}`);
break;
@ -89,6 +93,7 @@ export async function startAgentDispatcher(): Promise<void> {
unlistenExit = await onSidecarExited(async () => {
sidecarAlive = false;
tel.error('sidecar_crashed', { restartAttempts });
// Guard against re-entrant exit handler (double-restart race)
if (restarting) return;
@ -176,6 +181,15 @@ function handleAgentEvent(sessionId: string, event: Record<string, unknown>): vo
numTurns: cost.numTurns,
durationMs: cost.durationMs,
});
tel.info('agent_cost', {
sessionId,
costUsd: cost.totalCostUsd,
inputTokens: cost.inputTokens,
outputTokens: cost.outputTokens,
numTurns: cost.numTurns,
durationMs: cost.durationMs,
isError: cost.isError,
});
if (cost.isError) {
updateAgentStatus(sessionId, 'error', cost.errors?.join('; '));
notify('error', `Agent failed: ${cost.errors?.[0] ?? 'Unknown error'}`);