API · Observability
Production-readiness in one backend — Prometheus metrics (GET /metrics + a custom counter), Sentry error tracking (inert without a DSN), distributed tracing, and a @voltro/testing unit test (makeTestContext + mockStore, run with voltro test). Zero-infra boot.
The production-readiness slice in one backend: Prometheus metrics, Sentry error tracking, distributed tracing, and a @voltro/testing unit test — so a real service is observable AND covered from day one. Boots zero-infra (store: 'memory'). Template id: api-observability.
Scaffold
voltro create-project acme --api=api-observabilityWhat ships
apps/acme/api/
├── app.config.ts # prometheusPlugin() + sentryPlugin()
├── package.json # + the plugins; devDeps: @voltro/testing, vitest
├── database/schema.ts # a tenant-scoped `notes` table
├── queries/notes.list.query.ts # reactive list (+ .server.ts)
├── mutations/notes.create.mutation.ts # create + a custom metric (+ .server.ts)
└── tests/notes.create.test.ts # makeTestContext + mockStoreMetrics — GET /metrics + your own counters
// app.config.ts
import { prometheusPlugin } from '@voltro/plugin-prometheus'
import { sentryPlugin } from '@voltro/plugin-sentry'
plugins: [ prometheusPlugin(), sentryPlugin() ]The framework already records voltro_rpc_* / voltro_http_* / subscription / cache metrics into Effect's global MetricRegistry; prometheusPlugin() exports them at /metrics. A custom counter shows up there automatically — declare it at module level, bump it in a handler:
// mutations/notes.create.mutation.server.ts
import { Effect, Metric } from 'effect'
import { counter } from '@voltro/runtime'
const notesCreated = counter('app_notes_created_total', 'Notes created.')
Effect.runSync(Metric.increment(notesCreated)) // → /metrics + the DevTools Metrics panelcurl -s http://localhost:4000/metrics | grep -E 'app_notes_created_total|voltro_rpc_requests_total'
# app_notes_created_total 1
# voltro_rpc_requests_total{status="ok",tag="mutation.notes.create"} 1Point Prometheus / Grafana at /metrics; gate a public deploy with prometheusPlugin({ token }) / PROMETHEUS_TOKEN.
Errors — Sentry, inert until you set a DSN
sentryPlugin() reports rpc / REST / workflow / schedule / subscriber / render errors, correlated to the active traceId — and is inert without SENTRY_DSN, so it ships wired. Set the env to turn it on; no code change. The plugin subscribes to the framework's server-error bus once, so every primitive is covered. For the full browser→backend waterfall, add the sentry field to a paired web app's app.config.ts.
Tracing — on by default
voltro dev runs a buffer-only tracer that powers the DevTools Traces panel and stamps fields.traceId on every log line (voltro logs --trace <id>). Set OTEL_EXPORTER_OTLP_ENDPOINT to ship spans to Jaeger / Tempo / Honeycomb — no code change.
Testing — makeTestContext, no DB, no server
// tests/notes.create.test.ts
import { makeTestContext, mockStore } from '@voltro/testing'
import createNote from '../mutations/notes.create.mutation.server'
const ctx = makeTestContext({
subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
store: mockStore({ notes: [] }),
})
const row = await createNote({ title: 'Hello', body: 'World' }, ctx)
expect(row.tenantId).toBe('acme') // tenant() stamped it — the REAL store, in-memoryctx.store is the same mixin-wrapped store production uses, so tenant auto-scoping, soft-delete filtering, and audit auto-fill behave identically — in milliseconds, no docker. ctx.withTenant('t2', (c) => c.store.query(…)) re-scopes the same data to prove cross-tenant isolation. Run it:
voltro testThe package also ships MockClock / MockEmail / mockAi / makeWorkflowRunner and runDialectParity (the @voltro/testing/dialect subpath).
Going to production
| Want… | Do |
|---|---|
| Metrics on a public deploy | prometheusPlugin({ token }) + scrape over TLS |
| Errors in Sentry | set SENTRY_DSN; add sentryPlugin({ traces: true }) for performance traces |
| Spans in your tracer | OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 |
| Vendor-native (Datadog) | add @voltro/plugin-datadog — agentless metrics + correlated logs/traces |
| A CI gate | run voltro test; add runDialectParity for hand-written SQL |
Anti-patterns
- Re-creating the counter per call. Declare
counter(...)at MODULE level — a per-callcounter()muddies the series. - Wiring Sentry per primitive. The plugin covers them all via the server-error bus — you just set the DSN.
- A real DB in unit tests.
makeTestContext+mockStoregive real store behaviour in-memory; save a live database forvoltro e2e.
See also
- Observability · Distributed tracing · Unit testing
- App templates — the full catalogue