Tracing
Axiom's tracing subsystem records a hierarchical tree of every step that occurs during an event pipeline run or report generation. Each trace is a tree of lightweight trace nodes that reference more detailed records elsewhere in the system, keeping nodes small and fast to query while providing full drill-down capability.
This guide covers the data model, service API, REST endpoints, and how to instrument new pipeline stages with tracing.
Design Principles
- Lightweight breadcrumbs — trace nodes carry a summary, status, and timing, but delegate detail to referenced entities (activity logs, tasks, tool executions, etc.)
- Non-fatal — all trace operations are wrapped in exception handlers. Tracing never interrupts the main pipeline.
- Independent transactions — trace writes use
QuarkusTransaction.requiringNew()so data persists even if the caller's transaction rolls back - Real-time — every trace mutation fires an SSE event for live UI updates
- Correlation by UUID —
TraceEntityuses a UUID primary key that doubles as the correlation identifier threaded through environment variables and API callbacks
Data Model
All trace entities live in core/src/main/java/io/apitomy/axiom/core/entities/.
TraceEntity
Root of a complete execution trace. Extends PanacheEntityBase (not PanacheEntity)
because it uses a UUID primary key instead of the standard auto-increment Long.
Table: trace
| Field | Type | Nullable | Description |
|---|---|---|---|
traceId |
UUID |
No (PK) | Unique trace identifier |
traceType |
String |
No | Trace category: "event-pipeline" or "report-generation" |
status |
String |
No | Current status: "in-progress", "completed", or "failed" |
summary |
String(1024) |
No | Human-readable description (truncated to 1024 chars) |
eventId |
Long |
Yes | Associated event ID (for event pipeline traces) |
projectId |
Long |
Yes | Associated project ID |
reportId |
Long |
Yes | Associated report ID (for report generation traces) |
startedOn |
Instant |
No | Trace start timestamp |
completedOn |
Instant |
Yes | Trace completion timestamp |
TraceNodeEntity
A single step/span within a trace tree. Extends PanacheEntity (auto-increment Long PK).
Table: trace_node
| Field | Type | Nullable | Description |
|---|---|---|---|
id |
Long |
No (PK) | Auto-generated node ID |
traceId |
UUID |
No | Parent trace UUID |
parentNodeId |
Long |
Yes | Parent node ID (null for root nodes) |
nodeType |
String |
No | Step type identifier (see Node Types below) |
status |
String |
No | Current status: "in-progress", "completed", or "failed" |
summary |
String(1024) |
No | Human-readable description |
startedOn |
Instant |
No | Node start timestamp |
completedOn |
Instant |
Yes | Node completion timestamp |
durationMs |
Long |
Yes | Elapsed time in milliseconds (set at completion) |
entityType |
String |
Yes | Type of the referenced detail entity |
entityId |
Long |
Yes | ID of the referenced detail entity |
Node Types
| Node Type | Meaning | Typical Entity Reference |
|---|---|---|
event-ingested |
Event received, processing began | event |
manager-evaluation |
AI Manager was invoked | activity-log |
decision-processed |
A Manager decision was executed | activity-log |
task |
A task was created and assigned | task |
tool-execution |
An MCP tool was invoked | tool-execution |
escalation |
Decision escalated for human review | activity-log |
event-ignored |
Manager decided to ignore the event | activity-log |
report-triggered |
Report generation started | report |
report-ai-invoked |
AI agent launched for report | report |
Entity Types
The entityType field determines which entity table the entityId references:
| Entity Type | Referenced Entity | Detail Content |
|---|---|---|
event |
EventEntity |
Raw event payload |
activity-log |
ActivityLogEntity |
Log entry with type, summary, execution log |
task |
TaskEntity |
Task details — action type, actor, status, output |
tool-execution |
ToolExecutionEntity |
Full JSON input and output |
ai-usage |
AiUsageEntity |
Token counts, cost, model |
report |
ReportEntity |
Report metadata and content |
ToolExecutionEntity
Detailed record of an MCP tool invocation. Stores the full JSON input and output for
debugging. Referenced by trace nodes via entityType="tool-execution".
Table: tool_execution
| Field | Type | Nullable | Description |
|---|---|---|---|
id |
Long |
No (PK) | Auto-generated ID |
traceId |
UUID |
No | Associated trace UUID |
toolName |
String |
No | Name of the MCP tool |
toolInput |
String (TEXT) |
Yes | Tool input as JSON |
toolOutput |
String (TEXT) |
Yes | Tool output as JSON |
status |
String |
No | Execution status: "in-progress", "completed", or "failed" |
durationMs |
Long |
Yes | Tool execution time in milliseconds |
createdOn |
Instant |
No | Creation timestamp |
TraceService API
TraceService is the central service for creating and managing traces. It lives in
the core module so both app and manager can use it without circular dependencies.
Location: core/src/main/java/io/apitomy/axiom/core/tracing/TraceService.java
createTrace()
Creates a new trace and its root node in a single transaction, then fires an SSE event.
public TraceContext createTrace(
String traceType, // e.g. "event-pipeline", "report-generation"
String summary, // human-readable trace description
Long eventId, // associated event ID (nullable)
Long projectId, // associated project ID (nullable)
Long reportId, // associated report ID (nullable)
String rootNodeType, // node type for the root node
String rootNodeSummary, // summary for the root node
String rootEntityType, // entity type for the root node (nullable)
Long rootEntityId // entity ID for the root node (nullable)
)
Returns a TraceContext with the root node already pushed onto the stack.
addNode()
Adds a child node under the current parent (top of the context stack).
public Long addNode(
TraceContext ctx, // current trace context
String nodeType, // node type identifier
String status, // initial status (e.g. "in-progress", "completed")
String summary, // human-readable description
String entityType, // referenced entity type (nullable)
Long entityId // referenced entity ID (nullable)
)
Returns the new node's ID. The parent is determined by ctx.currentParentNodeId().
completeNode()
Marks a node as completed with a final status and calculated duration. Has two overloads:
// Basic completion
public void completeNode(Long nodeId, String status)
// Completion with entity reference (when entity isn't known at creation time)
public void completeNode(Long nodeId, String status,
String entityType, Long entityId)
completeTrace()
Marks the trace itself as completed or failed.
TraceContext
TraceContext is a mutable context object threaded through pipeline and report
processing. It tracks the current position in the trace tree using an internal stack.
Location: core/src/main/java/io/apitomy/axiom/core/tracing/TraceContext.java
| Method | Description |
|---|---|
UUID traceId() |
Returns the trace UUID |
Long currentParentNodeId() |
Returns the top of the stack (current parent) |
void push(Long nodeId) |
Pushes a node onto the stack, making it the current parent |
Long pop() |
Pops the current parent, returning to the previous level |
Push/Pop Pattern
When descending into a child scope, push the new node onto the stack so that any nodes created within that scope become its children. Pop when returning to the parent level.
// Create a parent node
Long parentId = traceService.addNode(traceCtx, "manager-evaluation",
"in-progress", "Evaluating event", null, null);
traceCtx.push(parentId);
try {
// Any nodes created here become children of parentId
traceService.addNode(traceCtx, "decision-processed",
"completed", "Decision: create task", "activity-log", logId);
} finally {
traceCtx.pop();
traceService.completeNode(parentId, "completed");
}
REST API Reference
All trace endpoints are defined in the OpenAPI specification at
common/api/src/main/resources/openapi.json and implemented in
app/src/main/java/io/apitomy/axiom/app/rest/TraceResourceImpl.java.
List Traces
Returns a paginated list of traces with optional filtering.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number (1-indexed) |
limit |
integer | 20 | Page size |
filterTraceType |
string | — | Filter by trace type (e.g. "event-pipeline") |
filterStatus |
string | — | Filter by status (comma-separated, e.g. "in-progress,completed") |
filterEventId |
integer | — | Filter by event ID |
filterProjectId |
integer | — | Filter by project ID |
filterReportId |
integer | — | Filter by report ID |
Response: TraceSearchResults
{
"items": [
{
"traceId": "a1b2c3d4-...",
"traceType": "event-pipeline",
"status": "completed",
"summary": "Processing event #42: issue-created",
"eventId": 42,
"projectId": null,
"reportId": null,
"startedOn": "2026-06-29T10:00:00Z",
"completedOn": "2026-06-29T10:00:05Z"
}
],
"totalCount": 1,
"page": 1,
"limit": 20
}
Convenience: List Traces by Entity
The API also provides convenience endpoints that return traces for a specific event,
project, or report. These return a JSON array of Trace objects (not paginated).
GET /api/v1/events/{eventId}/traces
GET /api/v1/projects/{projectId}/traces
GET /api/v1/reports/{reportId}/traces
| Endpoint | Path Parameter | Description |
|---|---|---|
GET /events/{eventId}/traces |
eventId (integer) |
All traces associated with the given event |
GET /projects/{projectId}/traces |
projectId (integer) |
All traces associated with the given project |
GET /reports/{reportId}/traces |
reportId (integer) |
All traces associated with the given report |
These are equivalent to calling GET /traces with the corresponding filter parameter,
but are more convenient when you already have the entity ID.
Get Trace Detail
Returns a trace and all of its nodes.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
traceId |
string (UUID) | The trace UUID |
Response: TraceDetail
{
"trace": {
"traceId": "a1b2c3d4-...",
"traceType": "event-pipeline",
"status": "completed",
"summary": "Processing event #42: issue-created",
"eventId": 42,
"startedOn": "2026-06-29T10:00:00Z",
"completedOn": "2026-06-29T10:00:05Z"
},
"nodes": [
{
"id": 1,
"traceId": "a1b2c3d4-...",
"parentNodeId": null,
"nodeType": "event-ingested",
"status": "completed",
"summary": "Event received: issue-created",
"startedOn": "2026-06-29T10:00:00Z",
"completedOn": "2026-06-29T10:00:00Z",
"durationMs": 0,
"entityType": "event",
"entityId": 42
},
{
"id": 2,
"traceId": "a1b2c3d4-...",
"parentNodeId": 1,
"nodeType": "manager-evaluation",
"status": "completed",
"summary": "Manager evaluation: issue-created",
"startedOn": "2026-06-29T10:00:00Z",
"completedOn": "2026-06-29T10:00:03Z",
"durationMs": 3000,
"entityType": "activity-log",
"entityId": 15
}
]
}
Get Trace Node Detail
Returns a trace node with its resolved entity detail. The detail object's structure
depends on the node's entityType.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
traceId |
string (UUID) | The trace UUID |
nodeId |
integer | The node ID |
Response: TraceNodeDetail
{
"node": {
"id": 5,
"traceId": "a1b2c3d4-...",
"parentNodeId": 3,
"nodeType": "tool-execution",
"status": "completed",
"summary": "Tool: github_list_issues",
"durationMs": 1200,
"entityType": "tool-execution",
"entityId": 7
},
"detail": {
"toolName": "github_list_issues",
"toolInput": "{\"repo\": \"owner/repo\", \"state\": \"open\"}",
"toolOutput": "[{\"number\": 1, \"title\": \"Bug report\"}]",
"status": "completed",
"durationMs": 1200
}
}
Create Tool Call
Creates a new tool-call trace node and its associated ToolExecutionEntity. Called by
MCP tool wrappers during task or report execution.
Request Body: ToolCallRequest
{
"traceId": "a1b2c3d4-...",
"parentNodeId": 3,
"toolName": "github_list_issues",
"toolInput": "{\"repo\": \"owner/repo\", \"state\": \"open\"}"
}
| Field | Type | Required | Description |
|---|---|---|---|
traceId |
string (UUID) | Yes | The parent trace UUID |
parentNodeId |
integer | No | Parent node ID for tree nesting |
toolName |
string | Yes | Name of the MCP tool being invoked |
toolInput |
string | No | Tool input as JSON |
Response: ToolCallCreated
Complete Tool Call
Completes a tool-call trace node with the execution result.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
nodeId |
integer | The trace node ID returned by the create call |
Request Body: ToolCallCompletion
{
"toolOutput": "[{\"number\": 1, \"title\": \"Bug report\"}]",
"status": "completed",
"durationMs": 1200
}
| Field | Type | Required | Description |
|---|---|---|---|
toolOutput |
string | No | Tool output as JSON |
status |
string | No | Final status ("completed" or "failed") |
durationMs |
integer | No | Execution duration in milliseconds |
Instrumenting New Pipeline Stages
To add tracing to a new pipeline stage or service:
Step 1: Inject TraceService
Step 2: Create a Trace or Add a Node
If your code is the entry point for a new pipeline (like PipelineOrchestrator or
ReportExecutionService), create a new trace:
TraceContext traceCtx = traceService.createTrace(
"my-pipeline",
"Processing something: " + description,
eventId, projectId, reportId,
"my-root-node", "Root: " + description,
"event", eventId);
If your code runs within an existing trace (like ManagerService), receive the
TraceContext as a parameter and add nodes to it:
Long nodeId = traceService.addNode(traceCtx, "my-step", "in-progress",
"Doing something important", null, null);
Step 3: Push/Pop for Child Scopes
If your node will have child nodes, push it onto the stack:
traceCtx.push(nodeId);
try {
// child nodes created here become children of nodeId
doWork(traceCtx);
} finally {
traceCtx.pop();
}
Step 4: Complete Nodes and the Trace
// Complete a node
traceService.completeNode(nodeId, "completed");
// Complete a node and set its entity reference
traceService.completeNode(nodeId, "completed", "activity-log", logEntry.id);
// Complete the trace
traceService.completeTrace(traceCtx.traceId(), "completed");
Step 5: Pass Correlation Data to Subprocesses
If your stage launches a subprocess that should report tool calls back to the trace, inject the trace correlation data as environment variables:
Map<String, String> env = new HashMap<>();
env.put("AXIOM_TRACE_ID", traceCtx.traceId().toString());
env.put("AXIOM_PARENT_NODE_ID", String.valueOf(nodeId));
The subprocess (or its MCP tool wrapper) reads these variables and calls back to
POST /api/v1/traces/tool-calls and PUT /api/v1/traces/tool-calls/{nodeId}.
Non-Fatal Pattern
Always wrap trace operations in try/catch when called from the main pipeline:
Long nodeId = null;
if (traceCtx != null) {
try {
nodeId = traceService.addNode(traceCtx, "my-step", "in-progress",
"Description", null, null);
traceCtx.push(nodeId);
} catch (Exception e) {
LOG.warnf(e, "Failed to add trace node");
}
}
// ... do the actual work ...
if (traceCtx != null && nodeId != null) {
try {
traceCtx.pop();
traceService.completeNode(nodeId, "completed");
} catch (Exception e) {
LOG.warnf(e, "Failed to complete trace node");
}
}
MCP Tool Call Integration
When Axiom generates MCP server configurations for AI agent tasks, it includes the trace
correlation environment variables (AXIOM_TRACE_ID and AXIOM_PARENT_NODE_ID). The
generated MCP tool wrappers use these to register tool calls as trace nodes:
AI Agent starts task
│
├─ Axiom sets AXIOM_TRACE_ID and AXIOM_PARENT_NODE_ID in subprocess env
│
▼
MCP Tool Wrapper executes
│
├─ Reads AXIOM_TRACE_ID and AXIOM_PARENT_NODE_ID from environment
├─ POST /api/v1/traces/tool-calls → receives nodeId
├─ Executes the actual tool
└─ PUT /api/v1/traces/tool-calls/{nodeId} → sends output + status
This flow is implemented in TaskExecutionService (for tasks) and
ReportExecutionService (for reports). Both look up the trace node ID for the current
task or report AI invocation and inject it as AXIOM_PARENT_NODE_ID so tool calls
become children of the correct node in the tree.
If the trace callback fails (network error, missing trace, etc.), the tool execution continues normally — tracing is best-effort and never blocks work.