Observability

One createObservability instance provides structured logs, W3C trace propagation, metrics, and readiness checks without a runtime package.

2 min read350 wordsClank 0.7.0

One createObservability instance provides structured logs, W3C trace propagation, metrics, and readiness checks without a runtime package.

ts
import {
  createObservability,
  createOtlpHttpSpanExporter,
} from "clank.run/observability";

const observability = createObservability({
  serviceName: "orbit-tasks",
  serviceVersion: "1.0.0",
  environment: process.env.NODE_ENV,
  exporter: process.env.OTLP_TRACES_URL
    ? createOtlpHttpSpanExporter({
        url: process.env.OTLP_TRACES_URL,
        headers: { authorization: `Bearer ${process.env.OTLP_TOKEN}` },
        serviceName: "orbit-tasks",
      })
    : undefined,
});

const app = createApp()
  .use(observability.middleware())
  .get("/healthz", () => observability.health.response());

Generated apps install the middleware and a database readiness check automatically.

Logs

Logs are one JSON object per event and automatically include service, trace, span, and request IDs. Keys containing password, secret, token, authorization, cookie, or API-key patterns are recursively redacted. Values, nesting, arrays, event counts, and attribute counts are bounded.

Use child loggers for stable context:

ts
const log = observability.logger.child({ component: "reminders" });
log.info("Reminder queued.", { taskId });

Do not attach email addresses, request bodies, query strings, session IDs, or unbounded user identifiers as metric labels.

Traces

Incoming traceparent headers are validated according to W3C Trace Context. Server spans preserve sampled trace IDs, create a new span ID, return traceparent, and attach a safe request ID.

ts
await observability.tracer.trace("reminder.send", async (span) => {
  span.setAttribute("reminder.channel", "email");
  await sendReminder();
});

Exceptions become bounded span events and error status. The OTLP/HTTP JSON exporter uses protocol byte encoding, HTTPS, explicit headers, a timeout, and redirect rejection.

Metrics

The registry supports counters, gauges, and histograms and renders Prometheus text:

ts
const sent = observability.metrics.counter(
  "app_reminders_sent_total",
  "Reminders accepted by the delivery service.",
  ["channel"],
);

sent.add(1, { channel: "email" });

Metric and label names are validated, label sets must match their declaration, histograms use cumulative buckets, and a global series limit prevents accidental memory exhaustion. HTTP middleware normalizes ID-like route segments before using the path as a label.

Expose observability.metrics.response() only on an operator-protected endpoint or private network.

Deployment ingress metrics

Clank Deploy separately records per-project traffic that passes through managed ingress. Minute rows contain status classes, 5xx errors, fixed method counters, duration sum/max, cumulative latency buckets, and bounded byte counts. The dashboard returns bounded 15m, 1h, 24h, 7d, and 30d current/previous series, rates, percentiles, and distributions without storing host, path, IP, user, email, query-string, or user-agent labels.

Ingress latency ends when upstream response headers arrive; it is not streamed-response completion time. Response size is known only when Content-Length is present. See Deployment dashboard, quotas, and domains for exact semantics and retention.

Readiness

Critical checks determine the HTTP status. Optional dependencies remain visible without taking the service out of rotation:

ts
observability.health.register("database", checkDatabase);
observability.health.register("email", checkMail, { critical: false });

Checks run concurrently with individual timeouts. Responses use no-store and return 503 when a critical dependency fails.