Observability

Logs + errors + traces, all correlated by request-id. Free tiers cover us; upgrade only when scale forces.

Platform
Updated by audit (May 26):
  • For logs, switch from Grafana Loki to Axiom — Axiom free tier is 500 GB ingest/mo with 30-day retention vs Loki's 50 GB. Loki has cardinality footguns at scale.
  • Keep Sentry + OTel; route logs to Axiom, traces/metrics to Grafana Cloud Tempo/Prometheus (still free tier).
  • OTel Node SDK overhead with BatchSpanProcessor: ~0.5-2ms/req, +2-5% CPU, +10-30MB RAM. Use Batch, not Simple.

Recommended stack

ConcernPickCost (early)
Loggingpino v9 → JSON stdout → Grafana Cloud LokiFree (50 GB/mo)
ErrorsSentry (@sentry/nextjs + @sentry/hono)Free (5k errors/mo)
TracingOpenTelemetry + @hono/otel → Grafana Cloud TempoFree (50 GB traces)
MetricsOTel auto-instrumentations + Prometheus → Grafana CloudFree (10k series)
UptimeBetterStack or CronitorFree
Feature flags + product analyticsPostHog CloudFree (<1M events/mo)

Logging — pino

pnpm --filter @spacehub/api add pino pino-pretty pino-http
pnpm --filter @spacehub/workers add pino

// apps/api/src/lib/logger.ts
import pino from "pino";
export const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  transport: process.env.NODE_ENV === "development"
    ? { target: "pino-pretty", options: { colorize: true } }
    : undefined,                                              // JSON in prod
  base: { service: "api", env: process.env.NODE_ENV },
});

Per-request child logger with request-id:

// apps/api/src/middleware/request-logger.ts
import { createMiddleware } from "hono/factory";
import { AsyncLocalStorage } from "node:async_hooks";
import { logger } from "../lib/logger";

export const reqLogStore = new AsyncLocalStorage<pino.Logger>();

export const requestLogger = createMiddleware(async (c, next) => {
  const reqId = c.get("requestId");
  const child = logger.child({ reqId, method: c.req.method, path: c.req.path });
  const start = Date.now();
  await reqLogStore.run(child, next);
  child.info({ ms: Date.now() - start, status: c.res.status }, "request");
});

// anywhere in handler stack: const log = reqLogStore.getStore() ?? logger;

Error tracking — Sentry

@sentry/nextjs v9+ supports Next 15 App Router + Turbopack + onRequestError. @sentry/hono beta in 2026 replaces deprecated community @hono/sentry.

pnpm --filter @spacehub/api add @sentry/node @sentry/hono
pnpm --filter @spacehub/web add @sentry/nextjs

// apps/api/src/lib/sentry.ts
import * as Sentry from "@sentry/node";
import { sentry } from "@sentry/hono";
Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.1, profilesSampleRate: 0.1 });

app.use("*", sentry({ dsn: process.env.SENTRY_DSN! }));

Self-host fallback: GlitchTip (Sentry-protocol compatible) on Coolify if you want everything on one VPS.

Tracing — OpenTelemetry

pnpm --filter @spacehub/api add @opentelemetry/auto-instrumentations-node @hono/otel

// apps/api/src/lib/telemetry.ts (must import BEFORE Hono)
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

new NodeSDK({
  traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }),
  instrumentations: [getNodeAutoInstrumentations({
    "@opentelemetry/instrumentation-fs": { enabled: false },     // noisy
  })],
}).start();

// In Hono:
import { otel } from "@hono/otel";
app.use("*", otel());

Auto-instrumentations cover: http, pg (Drizzle queries via the driver), ioredis, undici (fetch). Drizzle has no first-party OTel integration — wrap db.execute in a thin span helper for query-level naming if needed.

Metrics

OTel auto-instrumentations export to OTLP. Grafana Cloud accepts OTLP natively. Custom metrics via @opentelemetry/api:

import { metrics } from "@opentelemetry/api";
const meter = metrics.getMeter("spacehub");
const billsGenerated = meter.createCounter("bills_generated_total");
billsGenerated.add(1, { ownerId, period });

Uptime + heartbeats

Feature flags + analytics — PostHog

One tool for flags + product analytics + (optional) session replay. Self-host on Coolify if event volume crosses 1M/mo free cap.

pnpm --filter @spacehub/web add posthog-js
pnpm --filter @spacehub/api add posthog-node

Build steps

  1. Phase 0: pino + request-id middleware (already in scaffold).
  2. Sentry init in API + web (Phase 1).
  3. OTel SDK + auto-instrumentations + Grafana Cloud free tier (Phase 1).
  4. BetterStack health monitors + scheduler heartbeat (Phase 1).
  5. PostHog (Phase 5 or whenever first owner-facing feature flag is needed).

Open questions

Sources