Core Concepts
Understanding the key concepts behind Vision's observability model
Core Concepts
Understanding the key concepts behind Vision's observability model.
Traces
A trace represents the complete journey of a single request through your system.
Request → Middleware → Handler → Database → Response
└─────────── One Trace ──────────┘Each trace contains:
- Trace ID - Unique identifier (UUID)
- Timestamp - When the request started
- Duration - Total request time
- Status - HTTP status code
- Spans - Child operations within the trace
- Metadata - Headers, query params, etc.
Example
// This creates one trace
createModule({ prefix: '/users' })
.get('/:id', async ({ params }) => {
const user = await db.query('SELECT * FROM users WHERE id = ?', [params.id])
return user
})Spans
A span represents a single operation within a trace.
Trace: GET /users/123
├── Span: middleware.cors (2ms)
├── Span: middleware.auth (5ms)
├── Span: handler (50ms)
│ ├── Span: db.query (30ms)
│ └── Span: cache.set (5ms)
└── Total: 62msCreating Custom Spans
Every Vision Server handler receives span in its context:
createModule({ prefix: '/users' })
.get('/', async ({ span }) => {
const users = span(
'db.query',
{ 'db.system': 'postgresql', 'db.statement': 'SELECT * FROM users' },
async () => db.select().from(users).all()
)
return { users }
})Span Attributes
Spans can have custom attributes:
span('operation.name', {
// Common attributes
'service.name': 'my-api',
'service.version': '1.0.0',
// Database spans
'db.system': 'postgresql',
'db.table': 'users',
'db.statement': 'SELECT...',
// HTTP spans
'http.method': 'POST',
'http.url': 'https://api.example.com',
'http.status_code': 200,
// Custom attributes
'user.id': '123',
'feature.flag': true,
}, operation)Context
Vision uses async context to track the current trace across async operations.
// Middleware creates context
app.use('*', visionAdapter())
// Available in handlers
app.get('/users', async (c) => {
const { vision, traceId } = getVisionContext()
// Same context in nested calls
await someAsyncFunction() // Still has traceId!
})How It Works
Vision uses Node.js AsyncLocalStorage to maintain context:
import { AsyncLocalStorage } from 'async_hooks'
const storage = new AsyncLocalStorage()
// Middleware sets context
app.use(async (c, next) => {
const traceId = generateId()
await storage.run({ traceId }, async () => {
await next()
})
})
// Available anywhere in the async chain
function deepNestedFunction() {
const { traceId } = storage.getStore()
console.log(traceId) // Same ID!
}Trace Lifecycle
Trace Lifecycle Steps:
- Request arrives → Vision middleware creates trace
- Context is set →
AsyncLocalStoragestores trace ID - Route handler → Business logic executes
- Spans created → Database, API calls, etc.
- Response sent → Trace is completed
- Stored → Trace saved to dashboard (in-memory or DB)
Best Practices
Do's ✅
- Use meaningful span names -
db.query.usersnotquery - Add relevant attributes - Help debugging later
- Track critical paths - Database, external APIs
- Keep spans focused - One operation per span
Don'ts ❌
- Don't create too many spans - Overhead adds up
- Don't log sensitive data - Passwords, tokens, etc.
- Don't forget error handling - Trace errors too
- Don't block on tracing - Vision is async by default