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

x.config.ts
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.

log output
{"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.

disable logging
observability: {  logging: false,}

You can also use the logger export directly in loaders, server functions, and API routes:

manual logging
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.
health checks
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:

reporters
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:

custom reporter
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:

ErrorContext
interface ErrorContext {  route?: string;          // e.g. "/dashboard"  requestId?: string;      // matches the log entry for this request  phase: "ssr" | "action" | "api" | "loader";}