# Telemetry & Traces OpenJarvis has two complementary observability systems: **telemetry** for per-inference metrics and **traces** for full interaction-level recording. Together, they provide comprehensive insight into system behavior and power the learning system's routing policy updates. --- ## Telemetry The telemetry system records metrics for every inference call -- latency, token counts, cost, and energy consumption. Data is stored in SQLite and can be queried, exported, and aggregated. ### TelemetryRecord Each inference call produces a `TelemetryRecord` with the following fields: | Field | Type | Description | |----------------------|------------------|------------------------------------------| | `timestamp` | `float` | Unix timestamp of the call | | `model_id` | `str` | Model identifier | | `engine` | `str` | Engine backend used | | `agent` | `str` | Agent used (if any) | | `prompt_tokens` | `int` | Input tokens consumed | | `completion_tokens` | `int` | Output tokens generated | | `total_tokens` | `int` | Total tokens (prompt + completion) | | `latency_seconds` | `float` | Wall-clock inference time | | `ttft` | `float` | Time to first token | | `cost_usd` | `float` | Estimated cost in USD | | `energy_joules` | `float` | Estimated energy consumption | | `power_watts` | `float` | Power draw during inference | | `metadata` | `dict[str, Any]` | Additional metadata | ### TelemetryStore The `TelemetryStore` is an append-only SQLite database that persists telemetry records. It integrates with the event bus to capture records automatically. ```python from openjarvis.telemetry.store import TelemetryStore from openjarvis.core.events import EventBus bus = EventBus() store = TelemetryStore(db_path="~/.openjarvis/telemetry.db") store.subscribe_to_bus(bus) # Records are now captured automatically when TELEMETRY_RECORD events fire. # No manual recording needed -- instrumented_generate() handles this. store.close() ``` The store subscribes to `TELEMETRY_RECORD` events on the event bus. When the `instrumented_generate()` wrapper is used (which happens automatically in both CLI and SDK), telemetry records are published and stored without any manual intervention. ### `instrumented_generate()` This wrapper function calls `engine.generate()` and automatically publishes telemetry events: 1. Publishes `INFERENCE_START` with model and engine info. 2. Calls the engine and measures wall-clock latency. 3. Extracts token usage from the engine response. 4. Creates a `TelemetryRecord` from the measurements. 5. Publishes `INFERENCE_END` and `TELEMETRY_RECORD` events. All CLI commands and SDK methods use this wrapper, so telemetry is recorded transparently. ### TelemetryAggregator The `TelemetryAggregator` provides read-only query and aggregation methods over stored telemetry data. ```python from openjarvis.telemetry.aggregator import TelemetryAggregator agg = TelemetryAggregator(db_path="~/.openjarvis/telemetry.db") # Overall summary summary = agg.summary() print(f"Total calls: {summary.total_calls}") print(f"Total tokens: {summary.total_tokens}") print(f"Total cost: ${summary.total_cost:.6f}") # Per-model breakdown for ms in agg.per_model_stats(): print(f" {ms.model_id}: {ms.call_count} calls, {ms.avg_latency:.3f}s avg") # Per-engine breakdown for es in agg.per_engine_stats(): print(f" {es.engine}: {es.call_count} calls, {es.total_tokens} tokens") # Top models by usage top = agg.top_models(n=5) # Export raw records records = agg.export_records() # Time-range filtering (Unix timestamps) recent = agg.summary(since=1700000000.0) # Clear all records count = agg.clear() print(f"Deleted {count} records") agg.close() ``` #### Aggregation Methods | Method | Returns | Description | |---------------------|--------------------|--------------------------------------------| | `summary()` | `AggregatedStats` | Total calls, tokens, cost, latency + per-model and per-engine breakdowns | | `per_model_stats()` | `list[ModelStats]` | Call count, tokens, latency, cost grouped by model | | `per_engine_stats()`| `list[EngineStats]` | Call count, tokens, latency, cost grouped by engine | | `top_models(n)` | `list[ModelStats]` | Top N models by call count | | `export_records()` | `list[dict]` | All records as plain dictionaries | | `record_count()` | `int` | Total number of stored records | | `clear()` | `int` | Delete all records, return count | All query methods accept optional `since` and `until` parameters (Unix timestamps) for time-range filtering. #### Data Classes **ModelStats:** | Field | Type | Description | |--------------------|---------|--------------------------------| | `model_id` | `str` | Model identifier | | `call_count` | `int` | Total inference calls | | `total_tokens` | `int` | Total tokens processed | | `prompt_tokens` | `int` | Total input tokens | | `completion_tokens`| `int` | Total output tokens | | `total_latency` | `float` | Sum of all latencies | | `avg_latency` | `float` | Average latency per call | | `total_cost` | `float` | Total cost in USD | **EngineStats:** | Field | Type | Description | |-----------------|---------|--------------------------------| | `engine` | `str` | Engine identifier | | `call_count` | `int` | Total inference calls | | `total_tokens` | `int` | Total tokens processed | | `total_latency` | `float` | Sum of all latencies | | `avg_latency` | `float` | Average latency per call | | `total_cost` | `float` | Total cost in USD | **AggregatedStats:** | Field | Type | Description | |-----------------|--------------------|--------------------------------| | `total_calls` | `int` | Total inference calls | | `total_tokens` | `int` | Total tokens across all models | | `total_cost` | `float` | Total cost in USD | | `total_latency` | `float` | Total latency in seconds | | `per_model` | `list[ModelStats]` | Breakdown by model | | `per_engine` | `list[EngineStats]` | Breakdown by engine | ### CLI Commands ```bash # Show aggregated statistics jarvis telemetry stats jarvis telemetry stats -n 5 # Top 5 models only # Export records jarvis telemetry export # JSON to stdout jarvis telemetry export -f csv # CSV to stdout jarvis telemetry export -o data.json # JSON to file jarvis telemetry export -f csv -o metrics.csv # Clear all records jarvis telemetry clear # With confirmation prompt jarvis telemetry clear --yes # Without confirmation ``` --- ## Traces While telemetry captures per-inference metrics, the trace system records **complete interaction sequences** -- the full chain of steps an agent takes to handle a query. Traces are the primary input to the learning system. ### What is a Trace? A `Trace` captures the entire lifecycle of handling a user query: | Field | Type | Description | |--------------------------|--------------------|-----------------------------------------------| | `trace_id` | `str` | Unique identifier (auto-generated) | | `query` | `str` | The original user query | | `agent` | `str` | Agent that handled the query | | `model` | `str` | Model used for inference | | `engine` | `str` | Engine backend used | | `steps` | `list[TraceStep]` | Ordered list of processing steps | | `result` | `str` | Final response content | | `outcome` | `str` or `None` | `"success"`, `"failure"`, or `None` (unknown) | | `feedback` | `float` or `None` | User quality score [0, 1] | | `started_at` | `float` | Unix timestamp when processing began | | `ended_at` | `float` | Unix timestamp when processing ended | | `total_tokens` | `int` | Total tokens across all steps | | `total_latency_seconds` | `float` | Total latency across all steps | | `metadata` | `dict[str, Any]` | Additional metadata | ### Trace vs Telemetry | Aspect | Telemetry | Traces | |----------------|----------------------------------------|----------------------------------------------| | **Scope** | Single inference call | Full interaction (multiple steps) | | **Granularity**| Per-call metrics | Step-by-step sequence | | **Purpose** | Performance monitoring, cost tracking | Learning, routing optimization, debugging | | **Data** | Latency, tokens, cost, energy | Route, retrieve, generate, tool_call, respond | | **Storage** | Flat table of records | Traces table + steps table | ### TraceStep Each step in a trace records a single action the agent took. | Field | Type | Description | |--------------------|------------------|------------------------------------------| | `step_type` | `StepType` | Type of step (see below) | | `timestamp` | `float` | When the step occurred | | `duration_seconds` | `float` | How long the step took | | `input` | `dict[str, Any]` | Input data for the step | | `output` | `dict[str, Any]` | Output data from the step | | `metadata` | `dict[str, Any]` | Additional metadata | ### StepType | Type | Description | Example Input | Example Output | |--------------|--------------------------------------------------|------------------------------|------------------------------------| | `route` | Model/agent selection decision | `{"query_type": "math"}` | `{"model": "qwen3:8b"}` | | `retrieve` | Memory search for context | `{"query": "topic"}` | `{"num_results": 3}` | | `generate` | LLM inference call | `{"model": "qwen3:8b"}` | `{"tokens": 128}` | | `tool_call` | Tool execution | `{"tool": "calculator"}` | `{"success": true}` | | `respond` | Final response to the user | `{}` | `{"content": "...", "turns": 2}` | ### TraceCollector The `TraceCollector` wraps any `BaseAgent` to automatically record a `Trace` for every `run()` call. It subscribes to event bus events during execution and converts them into `TraceStep` objects. ```python from openjarvis.agents.orchestrator import OrchestratorAgent from openjarvis.traces.collector import TraceCollector from openjarvis.traces.store import TraceStore from openjarvis.core.events import EventBus bus = EventBus() store = TraceStore(db_path="./traces.db") agent = OrchestratorAgent(engine, model, tools=tools, bus=bus) collector = TraceCollector(agent, store=store, bus=bus) # The trace is recorded automatically result = collector.run("What is 2+2?") print(result.content) # Trace is now saved to the store and published on the bus ``` **How the collector works:** 1. Subscribes to `INFERENCE_START`, `INFERENCE_END`, `TOOL_CALL_START`, `TOOL_CALL_END`, and `MEMORY_RETRIEVE` events. 2. Executes the wrapped agent's `run()` method. 3. Converts captured events into `TraceStep` objects with timing data. 4. Appends a final `RESPOND` step with the result. 5. Builds a complete `Trace` object and saves it to the `TraceStore`. 6. Publishes a `TRACE_COMPLETE` event on the bus. 7. Unsubscribes from events after the run completes. ### TraceStore The `TraceStore` is an SQLite-backed database for persisting complete traces with their steps. ```python from openjarvis.traces.store import TraceStore store = TraceStore(db_path="./traces.db") # Save a trace store.save(trace) # Get a specific trace trace = store.get("abc123def456") # List traces with filters traces = store.list_traces( agent="orchestrator", model="qwen3:8b", outcome="success", since=1700000000.0, limit=50, ) # Count total traces count = store.count() # Subscribe to event bus for automatic saving store.subscribe_to_bus(bus) store.close() ``` #### Filtering Options | Parameter | Type | Description | |-----------|---------|------------------------------------------| | `agent` | `str` | Filter by agent ID | | `model` | `str` | Filter by model ID | | `outcome` | `str` | Filter by outcome (`"success"`, `"failure"`) | | `since` | `float` | Start of time range (Unix timestamp) | | `until` | `float` | End of time range (Unix timestamp) | | `limit` | `int` | Maximum number of traces to return (default: 100) | ### TraceAnalyzer The `TraceAnalyzer` provides read-only aggregated statistics over stored traces. These statistics are used by the learning system to update routing policies. ```python from openjarvis.traces.analyzer import TraceAnalyzer analyzer = TraceAnalyzer(store=trace_store) # Overall summary summary = analyzer.summary() print(f"Total traces: {summary.total_traces}") print(f"Total steps: {summary.total_steps}") print(f"Avg steps/trace: {summary.avg_steps_per_trace:.1f}") print(f"Avg latency: {summary.avg_latency:.3f}s") print(f"Success rate: {summary.success_rate:.1%}") print(f"Step distribution: {summary.step_type_distribution}") # Per-route statistics (model + agent combinations) for rs in analyzer.per_route_stats(): print(f" {rs.model}/{rs.agent}: {rs.count} traces, " f"{rs.avg_latency:.3f}s avg, {rs.success_rate:.1%} success") # Per-tool statistics for ts in analyzer.per_tool_stats(): print(f" {ts.tool_name}: {ts.call_count} calls, " f"{ts.avg_latency:.3f}s avg, {ts.success_rate:.1%} success") # Find traces matching query characteristics code_traces = analyzer.traces_for_query_type(has_code=True) short_traces = analyzer.traces_for_query_type(max_length=100) # Export traces as plain dicts exported = analyzer.export_traces(limit=500) ``` #### Analysis Methods | Method | Returns | Description | |---------------------------|--------------------|------------------------------------------------------| | `summary()` | `TraceSummary` | Overall statistics: counts, averages, distributions | | `per_route_stats()` | `list[RouteStats]` | Stats grouped by (model, agent) combinations | | `per_tool_stats()` | `list[ToolStats]` | Stats grouped by tool name | | `traces_for_query_type()` | `list[Trace]` | Filter traces by query characteristics | | `export_traces()` | `list[dict]` | Export traces as serializable dictionaries | All analysis methods accept optional `since` and `until` parameters for time-range filtering. #### Data Classes **TraceSummary:** | Field | Type | Description | |--------------------------|-----------------|------------------------------------------| | `total_traces` | `int` | Total number of traces | | `total_steps` | `int` | Total steps across all traces | | `avg_steps_per_trace` | `float` | Average number of steps per trace | | `avg_latency` | `float` | Average total latency per trace | | `avg_tokens` | `float` | Average tokens per trace | | `success_rate` | `float` | Fraction of evaluated traces that succeeded | | `step_type_distribution` | `dict[str, int]`| Count of each step type | **RouteStats:** | Field | Type | Description | |----------------|----------------|------------------------------------------| | `model` | `str` | Model identifier | | `agent` | `str` | Agent identifier | | `count` | `int` | Number of traces for this route | | `avg_latency` | `float` | Average latency for this route | | `avg_tokens` | `float` | Average tokens for this route | | `success_rate` | `float` | Success rate for this route | | `avg_feedback` | `float` or `None` | Average user feedback (if available) | **ToolStats:** | Field | Type | Description | |----------------|---------|------------------------------------------| | `tool_name` | `str` | Tool identifier | | `call_count` | `int` | Number of times the tool was called | | `avg_latency` | `float` | Average execution latency | | `success_rate` | `float` | Fraction of successful executions | --- ## Data Flow The following diagram shows how telemetry and trace data flows through the system: ``` User Query | v Agent.run() --> EventBus --> TraceCollector (captures steps) | | v v Engine.generate() TelemetryStore (captures per-call metrics) | v instrumented_generate() | +---> INFERENCE_START event +---> INFERENCE_END event +---> TELEMETRY_RECORD event | v TraceCollector | +---> Builds Trace with TraceSteps +---> Saves to TraceStore +---> Publishes TRACE_COMPLETE event | v TraceAnalyzer / TelemetryAggregator --> Learning System ``` Both systems operate transparently -- no manual instrumentation is needed when using the CLI or SDK, as they automatically set up the event bus and telemetry store.