newX 1.3 — islands to disk, server-mode islands, image proxy

Observability

Observability

x includes production-ready observability out of the box: structured JSON logging, container health/readiness probes, request metrics (Prometheus or OpenTelemetry), 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, the liveness probe. Returns { status: "ok" } when the Bun process is up and serving.
  • /readyz, the 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. The handler itself is exported as createHealthCheckHandler if you want to wire these endpoints into a custom server instead.

Request metrics

x can record production metrics for every request — counts, latency histograms, and error and rate-limit-rejection counters. Two built-in reporters cover the two standard destinations:

  • createInMemoryMetrics() — an in-process registry that serves a /metrics endpoint in Prometheus text format, ready to be scraped by Prometheus/Grafana.
  • createOtlpMetricsReporter(meter) — forwards counters and histograms to an OpenTelemetry meter from your own OTel SDK setup (e.g. an OTLP exporter to Grafana Tempo/Cloud, Datadog, or Honeycomb).
prometheus
import { createInMemoryMetrics } from "@thexjs/core";observability: {  metrics: createInMemoryMetrics(),}// GET /metrics// # TYPE x_http_requests_total counter// x_http_requests_total{method="GET",status="200"} 42
open-telemetry
import { createOtlpMetricsReporter } from "@thexjs/core";import { metrics } from "@opentelemetry/api";observability: {  metrics: createOtlpMetricsReporter(metrics.getMeter("x")),}

The metrics recorded per request are x_http_requests_total (labels method, status), x_http_request_duration_ms (histogram, label method), x_http_errors_total (per phase), and x_rate_limit_rejections_total (per method).

The same pieces are exported for custom wiring: withRequestMetrics(reporter, handler) wraps any fetch handler, and any object implementing the MetricsReporter interface works — including your own exporter that posts to a statsd/Prometheus push gateway.

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.

Low-level APIs

The pieces behind the config are exported directly, so you can swap the wiring for custom logic:

low-level
import {  setErrorReporter,  reportException,  combineReporters,  createSentryReporter,} from "@thexjs/core";setErrorReporter(combineReporters(createSentryReporter(Sentry), customReporter));try {  // ...} catch (error) {  reportException(error, { route: "/dashboard", phase: "loader" });}

setErrorReporter installs a reporter at runtime, reportException fires it with an ErrorContext, and combineReporters fans an exception out to several reporters at once. Reporters may also implement an optional flush() (used to drain buffered events on graceful shutdown).

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";}