Observability
Surplus is built to be debuggable. Every request carries a correlation id, every layer emits structured logs and traces, and errors surface a reference you can paste straight into log search. This page explains the moving parts and how to use them.
Request correlation
Everything hangs off a single request id. The client generates an x-request-id for each call; the server accepts it (or mints one), echoes it back on the response, and runs the entire request inside an AsyncLocalStorage scope so every log line and span automatically shares it. The client also sends x-se-client-url and x-se-client-version, so you can tie a request to the exact page and client build that made it.
When an API call fails, the error toast shows a short id prefix and offers a Copy ID action. That id maps directly to server logs and traces — it's the fastest path from "a user reported a bug" to "here's the exact request."
Structured logging
Logging goes through createLogger in @surplus/services. Logs are structured and automatically enriched with the requestId, userId, and route from the request context — you don't thread those through by hand. Logs print to stdout (captured by Railway) and, in staging and production, are also captured by Apitally.
const log = createLogger({ service: 'server' })
log.info('server started', { port, environment })Tracing with OpenTelemetry
The server preloads OpenTelemetry via apps/server/src/instrumentation.ts before anything else runs. It auto-instruments PostgreSQL, Redis, and outbound HTTP (undici). Inbound HTTP/net auto-instrumentation is intentionally disabled — it conflicts with Bun's fetch, and Apitally already covers inbound requests.
Traces are layered so you can see a request from top to bottom:
- ORPC procedure spans —
orpc.{procedure}, wrapping each API call. - Service method spans — every service class is wrapped at construction by
instrumentService, so each method call is a span. - External SDK spans — Stripe and S3 calls are wrapped, and Resend calls use an explicit span.
- Opt-in leaf spans — use
withSpan(name, attributes, fn)inside a complex transaction to mark a specific step.
Spans are exported to Apitally.
Error monitoring
- Client errors — the web client uses Sentry (
@sentry/react), initialized in staging and production with release tagging. It no-ops in local development. Seeapps/client/src/lib/sentry.ts. - Server errors — typed domain errors are mapped to HTTP responses by an interceptor, which attaches the
requestId. For 5xx responses, the underlying exception is surfaced onto the Hono context so Apitally's exception panel shows the real stack trace.
How to debug a problem
- Get the request id from the user (the toast's Copy ID action) or from the failing response's
x-request-idheader. - Search logs and Apitally by that id — Apitally matches by prefix, so the short id works.
- Open the trace to see which layer the time or the error came from: the ORPC span, a service method, or an external call.
- For client-side issues, check Sentry for the captured exception and release.
For reviewers
This page is about debugging. For the security and audit view of monitoring — what's logged, how sensitive fields are masked, and data handling in third-party tools — see Monitoring in the Security & Compliance section.