Aires uses a trace model compatible with W3C Trace Context and OpenTelemetry:
- Trace ID — identifies an entire distributed operation spanning multiple services. All events belonging to the same request share a trace ID.
- Span ID — identifies a single unit of work within a trace (e.g. one HTTP handler, one database query, one RPC call).
- Parent Span ID — links a span to its parent, forming a tree structure.
- Subtrace ID — groups events within a nested sub-operation (e.g. an AI agent’s multi-step task within a larger request).
Use aires.span() to create a span event:
import { aires } from "@aires/sdk"
import { randomUUID } from "crypto"
const traceId = randomUUID()
const spanId = randomUUID()
// Create a span for an HTTP handler
aires.span("POST /api/tasks", {
traceId,
spanId,
category: "http",
attr: {
"http.method": "POST",
"http.path": "/api/tasks",
},
})
Link child spans to parents using parentSpanId:
const traceId = randomUUID()
// Root span: HTTP request
const httpSpanId = randomUUID()
aires.span("POST /api/tasks", {
traceId,
spanId: httpSpanId,
category: "http",
})
// Child span: database query
const dbSpanId = randomUUID()
aires.span("INSERT INTO tasks", {
traceId,
spanId: dbSpanId,
parentSpanId: httpSpanId, // ← links to parent
category: "db",
attr: {
"db.system": "postgres",
"db.operation": "INSERT",
"db.table": "tasks",
},
})
// Child span: cache write
const cacheSpanId = randomUUID()
aires.span("SET task:123", {
traceId,
spanId: cacheSpanId,
parentSpanId: httpSpanId, // ← same parent as db span
category: "cache",
attr: {
"cache.system": "redis",
"cache.operation": "SET",
},
})
This creates a span tree:
POST /api/tasks (httpSpanId)
├── INSERT INTO tasks (dbSpanId)
└── SET task:123 (cacheSpanId)
To propagate trace context across services, pass the traceId and current spanId (as the parent) in your service-to-service calls.
Use the W3C traceparent format or custom headers:
// Service A: outgoing request
const traceId = randomUUID()
const spanId = randomUUID()
aires.span("call billing service", {
traceId,
spanId,
category: "rpc",
})
const response = await fetch("https://billing-service/api/charge", {
method: "POST",
headers: {
"x-trace-id": traceId,
"x-parent-span-id": spanId,
"Content-Type": "application/json",
},
body: JSON.stringify({ amount: 4999 }),
})
// Service B: incoming request
app.post("/api/charge", (req) => {
const traceId = req.headers["x-trace-id"]
const parentSpanId = req.headers["x-parent-span-id"]
const spanId = randomUUID()
aires.span("POST /api/charge", {
traceId,
spanId,
parentSpanId, // ← links to Service A's span
category: "http",
})
// ... handle request
})
For gRPC services, propagate via metadata:
// Client side
const metadata = new grpc.Metadata()
metadata.add("x-trace-id", traceId)
metadata.add("x-parent-span-id", currentSpanId)
client.processTask(request, metadata, (err, response) => {
// ...
})
Subtraces group events within a nested operation that has its own logical scope but belongs to a larger trace. This is particularly useful for AI agent workflows:
const traceId = randomUUID()
const subtraceId = randomUUID()
// Parent request creates the trace
aires.span("POST /api/agent/run", {
traceId,
spanId: randomUUID(),
category: "http",
})
// Agent execution creates a subtrace
aires.info("agent started planning", {
traceId,
subtraceId, // ← groups all agent events
agentId: "agent-planner",
category: "ai",
})
aires.info("agent calling tool: search", {
traceId,
subtraceId, // ← same subtrace
agentId: "agent-planner",
category: "ai",
data: {
tool: { name: "search", args: { query: "quarterly revenue" } },
},
})
aires.info("agent completed", {
traceId,
subtraceId, // ← same subtrace
agentId: "agent-planner",
category: "ai",
})
Query a subtrace:
SELECT timestamp, message, agent_id, category
FROM events
WHERE subtrace_id = 'your-subtrace-id'
ORDER BY timestamp;
To record span duration, log the start and end events with timing attributes:
const traceId = randomUUID()
const spanId = randomUUID()
const start = performance.now()
// ... do work ...
const durationMs = performance.now() - start
aires.span("process-task", {
traceId,
spanId,
attr: {
"duration_ms": durationMs.toFixed(2),
"status": "ok",
},
})
For HTTP spans, use the built-in http.durationMs field:
aires.info("request completed", {
traceId,
spanId,
category: "http",
http: {
method: "POST",
path: "/api/tasks",
status: 201,
durationMs: 47,
},
})
SELECT
timestamp,
severity,
message,
span_id,
parent_span_id,
category,
kind,
http_duration_ms
FROM events
WHERE trace_id = 'your-trace-id'
ORDER BY timestamp;
SELECT
trace_id,
min(timestamp) AS started,
max(timestamp) AS ended,
date_diff('millisecond', min(timestamp), max(timestamp)) AS duration_ms,
count() AS span_count
FROM events
WHERE service = 'my-api'
AND timestamp > now() - INTERVAL 1 HOUR
GROUP BY trace_id
HAVING duration_ms > 1000
ORDER BY duration_ms DESC
LIMIT 20;
SELECT DISTINCT trace_id, min(timestamp) AS first_error
FROM events
WHERE severity = 'error'
AND trace_id != ''
AND timestamp > now() - INTERVAL 1 HOUR
GROUP BY trace_id
ORDER BY first_error DESC;