Use ctx.tracer in agents and c.var.tracer in routes to create OpenTelemetry spans. Spans help you understand timing, track operations through your system, and debug performance issues.
Basic Span Pattern
Wrap operations in spans to track their duration and status:
import { createAgent } from '@agentuity/runtime';
import { SpanStatusCode } from '@opentelemetry/api';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const agent = createAgent('TracingExample', {
handler: async (ctx, input) => {
return ctx.tracer.startActiveSpan('generate-response', async (span) => {
try {
span.setAttribute('model', 'gpt-5-mini');
span.setAttribute('promptLength', input.prompt.length);
const { text, usage } = await generateText({
model: openai('gpt-5-mini'),
prompt: input.prompt,
});
span.setAttribute('outputLength', text.length);
span.setAttribute('totalTokens', usage.totalTokens);
span.setStatus({ code: SpanStatusCode.OK });
return { response: text };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
span.recordException(error instanceof Error ? error : message);
span.setStatus({ code: SpanStatusCode.ERROR, message });
ctx.logger.error('Generation failed', error);
throw error;
} finally {
span.end();
}
});
},
});startActiveSpan() makes the span active while the callback runs, but it does not close the span. Call span.end() in a finally block, and set status when the outcome is meaningful.
Adding Context with Attributes
Use setAttribute to add searchable metadata to spans:
return ctx.tracer.startActiveSpan('user-lookup', async (span) => {
try {
span.setAttribute('userId', input.userId);
span.setAttribute('source', 'api');
span.setAttribute('cached', false);
const user = await fetchUser(input.userId);
span.setAttribute('userFound', !!user);
span.setAttribute('accountType', user?.type ?? 'unknown');
span.setStatus({ code: SpanStatusCode.OK });
return user;
} finally {
span.end();
}
});Common attributes: IDs, counts, categories, boolean flags. These appear in trace views and can be filtered.
Recording Events
Use addEvent to mark significant moments within a span:
return ctx.tracer.startActiveSpan('data-pipeline', async (span) => {
try {
span.addEvent('pipeline-started', { inputSize: data.length });
const validated = await validateData(data);
span.addEvent('validation-complete', { validRecords: validated.length });
const enriched = await enrichData(validated);
span.addEvent('enrichment-complete', { enrichedFields: 5 });
const stored = await storeData(enriched);
span.addEvent('storage-complete', { recordsStored: stored.count });
span.setStatus({ code: SpanStatusCode.OK });
return { processed: stored.count };
} finally {
span.end();
}
});Events create a timeline within the span, useful for understanding where time is spent.
Nested Spans
Create child spans for multi-step operations:
return ctx.tracer.startActiveSpan('rag-pipeline', async (parentSpan) => {
try {
parentSpan.setAttribute('query', input.query);
// Retrieve relevant context
const context = await ctx.tracer.startActiveSpan('retrieve-context', async (span) => {
try {
span.setAttribute('index', 'knowledge-base');
const results = await ctx.vector.search('docs', { query: input.query, limit: 5 });
span.setAttribute('resultsFound', results.length);
span.setStatus({ code: SpanStatusCode.OK });
return results;
} finally {
span.end();
}
});
// Rerank results
const ranked = await ctx.tracer.startActiveSpan('rerank-results', async (span) => {
try {
span.setAttribute('inputCount', context.length);
const reranked = await rerankByRelevance(context, input.query);
span.setAttribute('outputCount', reranked.length);
span.setStatus({ code: SpanStatusCode.OK });
return reranked;
} finally {
span.end();
}
});
// Generate answer
const answer = await ctx.tracer.startActiveSpan('generate-answer', async (span) => {
try {
span.setAttribute('model', 'gpt-5-mini');
span.setAttribute('contextChunks', ranked.length);
const response = await generateWithContext(input.query, ranked);
span.setAttribute('responseLength', response.length);
span.setStatus({ code: SpanStatusCode.OK });
return response;
} finally {
span.end();
}
});
parentSpan.setStatus({ code: SpanStatusCode.OK });
return { answer, sources: ranked.map((r) => r.id) };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
parentSpan.recordException(error instanceof Error ? error : message);
parentSpan.setStatus({ code: SpanStatusCode.ERROR, message });
throw error;
} finally {
parentSpan.end();
}
});Nested spans create a parent-child hierarchy in trace views, showing how operations relate.
Tracing in Routes
Routes access the tracer via c.var.tracer:
import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import { SpanStatusCode } from '@opentelemetry/api';
import notificationSender from '@agent/notification-sender/agent';
const router = new Hono<Env>();
router.post('/customers/:id/notify', async (c) => {
return c.var.tracer.startActiveSpan('send-notification', async (span) => {
try {
const customerId = c.req.param('id');
span.setAttribute('customerId', customerId);
const body = await c.req.json();
span.setAttribute('notificationType', body.type);
span.setAttribute('channel', body.channel);
const result = await notificationSender.run({
customerId,
...body,
});
span.setAttribute('delivered', result.success);
span.setStatus({ code: SpanStatusCode.OK });
return c.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
span.recordException(error instanceof Error ? error : message);
span.setStatus({ code: SpanStatusCode.ERROR, message });
throw error;
} finally {
span.end();
}
});
});
export default router;Nested Spans in Routes
Use nested spans to track multi-step operations within a route:
import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import { SpanStatusCode } from '@opentelemetry/api';
const router = new Hono<Env>();
router.post('/process', async (c) => {
return c.var.tracer.startActiveSpan('process-request', async (parentSpan) => {
try {
const body = await c.req.json();
parentSpan.setAttribute('inputSize', JSON.stringify(body).length);
// Validate input
const validated = await c.var.tracer.startActiveSpan('validate', async (span) => {
try {
span.setAttribute('fields', Object.keys(body).length);
const result = validateInput(body);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} finally {
span.end();
}
});
// Store in KV
await c.var.tracer.startActiveSpan('store-data', async (span) => {
try {
span.setAttribute('bucket', 'processed');
await c.var.kv.set('processed', validated.id, validated);
span.setStatus({ code: SpanStatusCode.OK });
} finally {
span.end();
}
});
// Search related items
const related = await c.var.tracer.startActiveSpan('search-related', async (span) => {
try {
span.setAttribute('query', validated.category);
const results = await c.var.vector.search('items', {
query: validated.category,
limit: 5,
});
span.setAttribute('resultsFound', results.length);
span.setStatus({ code: SpanStatusCode.OK });
return results;
} finally {
span.end();
}
});
parentSpan.setAttribute('relatedCount', related.length);
parentSpan.setStatus({ code: SpanStatusCode.OK });
return c.json({ processed: validated, related });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
parentSpan.recordException(error instanceof Error ? error : message);
parentSpan.setStatus({ code: SpanStatusCode.ERROR, message });
throw error;
} finally {
parentSpan.end();
}
});
});
export default router;Viewing Traces
View traces for a session using the CLI:
# Get session details including trace timeline
agentuity cloud session get sess_abc123xyzTraces are also visible in the Agentuity Console session timeline, showing the span hierarchy with timing. See Session Logs for related session CLI commands.
When to Use Tracing
| Scenario | Approach |
|---|---|
| Simple operations | Logging is sufficient |
| Multi-step workflows | Create spans for each step |
| Performance debugging | Add spans to identify bottlenecks |
| External API calls | Wrap in spans to track latency |
| Agent-to-agent calls | Spans automatically propagate context |
Best Practices
- Name spans descriptively:
generate-summarynotstep-2 - End every span: Call
span.end()in afinallyblock - Set useful status: Use
SpanStatusCode.OKorSpanStatusCode.ERRORwhen the operation outcome matters - Add relevant attributes: IDs, counts, and categories for filtering
- Use events for milestones: Mark significant points within long operations
- Keep spans focused: One span per logical operation
Next Steps
- Logging: Add searchable context to logs
- Sessions & Debugging: Use session IDs for debugging