Observability
Observability
x includes production-ready observability out of the box — structured JSON logging, container health/readiness probes, and pluggable APM error tracing. All of it is optional and configurable via the observability key in x.config.ts.
Configuration overview
import { defineConfig, createSentryReporter } from "@thexjs/core";import * as Sentry from "@sentry/bun";Sentry.init({ dsn: process.env.SENTRY_DSN });export default defineConfig({ // ...pagesDir, apiDir, etc. observability: { logging: true, errorReporter: createSentryReporter(Sentry), health: { checks: { database: () => db.ping(), }, }, },});Structured JSON logging
Every request is logged as one JSON line with timestamp, requestId, route, method, status, and durationMs. This is ready to ingest into Datadog, Grafana Loki, Kibana, or any JSON log pipeline.
{"timestamp":"2026-07-29T12:00:00.000Z","level":"info","message":"request completed","requestId":"abc-123","route":"/api/users","method":"GET","status":200,"durationMs":42}Logging is enabled by default. Disable it with logging: false.
observability: { logging: false,}You can also use the logger export directly in loaders, server functions, and API routes:
import { logger } from "@thexjs/core";export async function loader() { logger.info("fetching users", { userId: 42 }); const users = await getUsers(); logger.info("users fetched", { count: users.length }); return { users };}Health & readiness probes
Two endpoints are served ahead of all routing for container orchestrators:
- /healthz — liveness probe. Returns { status: "ok" } when the Bun process is up and serving.
- /readyz — readiness probe. Runs all configured checks and returns 200 only if every check passes, or 503 otherwise.
observability: { health: { checks: { database: async () => { try { await db.query("SELECT 1"); return true; } catch { return false; } }, redis: () => redis.ping(), }, },}Kubernetes/Docker can poll these endpoints to decide whether to send traffic to a pod or restart it.
APM error tracing
When an uncaught exception occurs during SSR, a server action, or an API handler, x reports it to the configured error reporter. Two reporters are built in:
import { createSentryReporter, createOtelReporter } from "@thexjs/core";// Sentryobservability: { errorReporter: createSentryReporter(Sentry),}// OpenTelemetryobservability: { errorReporter: createOtelReporter(trace.getTracer("x")),}You can also combine multiple reporters, or write your own by implementing the ErrorReporter interface:
import { combineReporters } from "@thexjs/core";const customReporter = { captureException(error, context) { // Send to your own error tracking service fetch("https://errors.example.com", { method: "POST", body: JSON.stringify({ error: String(error), context }), }); },};observability: { errorReporter: combineReporters( createSentryReporter(Sentry), customReporter, ),}If no reporter is configured, errors are logged to the console and the request returns a generic 500. The reporter never blocks the response — if it throws, the error is caught and logged so it can't take down the request.
What's captured
Every error report includes the phase it occurred in:
interface ErrorContext { route?: string; // e.g. "/dashboard" requestId?: string; // matches the log entry for this request phase: "ssr" | "action" | "api" | "loader";}