Observability
Structured logs with LogTape, errors to Sentry, product events to PostHog. All three optional.
Three separate concerns, three separate tools, and none of them required to run the app. A fresh clone logs to the console and sends nothing anywhere.
| Concern | Tool | Off by default |
|---|---|---|
| Structured logs | LogTape | Always on, console sink |
| Error capture | Sentry | Yes, until SENTRY_DSN is set |
| Product analytics | PostHog | Yes, until the keys are set |
Logs
configureLogging() is idempotent. Every entrypoint calls it first, so no app
has to coordinate with another about who initialises logging.
import { getLogger } from "@workspace/logger"
const log = getLogger(["api", "checkout"])
log.info("Created checkout for {userId}", { userId: user.id })Categories are dotted paths and the placeholder syntax is LogTape's, not a
template literal. Writing log.info(`Created checkout for ${user.id}`)
works but throws away the structure: the whole point is that userId arrives
as a field a log pipeline can filter on, rather than baked into a string.
LOG_LEVEL controls verbosity, defaulting to info in production and debug
everywhere else. LogTape's own meta logger is pinned at warning so it stays
out of the way.
Errors
Set SENTRY_DSN and the API and worker report; set NEXT_PUBLIC_SENTRY_DSN
and the browser bundles do too. SENTRY_TRACES_SAMPLE_RATE controls tracing
volume, and SENTRY_RELEASE ties an event to a build.
The worker's error handling is worth reading before you copy it: it reports only when a job has exhausted its retries, not on each attempt. See background jobs for why that guard exists.
Product analytics
NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST turn on the browser
client. The server-side key falls back to the public one when unset, which is
fine for most projects because PostHog's project key is not a secret.
The API flushes PostHog on SIGTERM before the server stops. Without that, the
events from the last few seconds of a deploy are lost, which is exactly the
window where a deploy-related error would have shown up.
There is no /health endpoint. The closest thing today is /api/openapi
answering, which proves the process is up and its routes registered. If you
need a real liveness probe, add one route rather than pointing a monitor at a
document.
What is not wired
OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME are read by the
environment schema, so the variables validate and can be passed through, but
there is no exporter shipped and nothing emits spans. Treat them as a
reservation rather than a feature: if you need traces, wire the SDK yourself
and the configuration surface is already there.