{"protocol":"clank-doc/1","frameworkVersion":"0.8.0","slug":"observability","title":"Observability","description":"One createObservability instance provides structured logs, W3C trace propagation, metrics, and readiness checks without a runtime package.","group":{"id":"full-stack","title":"Full stack"},"url":"https://docs.clank.run/docs/observability","source":"docs/observability.md","headings":["Logs","Traces","Metrics","Deployment ingress metrics","Readiness"],"tableOfContents":[{"id":"logs","title":"Logs","level":2},{"id":"traces","title":"Traces","level":2},{"id":"metrics","title":"Metrics","level":2},{"id":"deployment-ingress-metrics","title":"Deployment ingress metrics","level":3},{"id":"readiness","title":"Readiness","level":2}],"markdown":"# Observability\n\nOne `createObservability` instance provides structured logs, W3C trace propagation, metrics, and readiness checks without a runtime package.\n\n```ts\nimport {\n  createObservability,\n  createOtlpHttpSpanExporter,\n} from \"@clank.run/framework/observability\";\n\nconst observability = createObservability({\n  serviceName: \"orbit-tasks\",\n  serviceVersion: \"1.0.0\",\n  environment: process.env.NODE_ENV,\n  exporter: process.env.OTLP_TRACES_URL\n    ? createOtlpHttpSpanExporter({\n        url: process.env.OTLP_TRACES_URL,\n        headers: { authorization: `Bearer ${process.env.OTLP_TOKEN}` },\n        serviceName: \"orbit-tasks\",\n      })\n    : undefined,\n});\n\nconst app = createApp()\n  .use(observability.middleware())\n  .get(\"/healthz\", () => observability.health.response());\n```\n\nGenerated apps install the middleware and a database readiness check automatically.\n\n## Logs\n\nLogs 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.\n\nUse child loggers for stable context:\n\n```ts\nconst log = observability.logger.child({ component: \"reminders\" });\nlog.info(\"Reminder queued.\", { taskId });\n```\n\nDo not attach email addresses, request bodies, query strings, session IDs, or unbounded user identifiers as metric labels.\n\n## Traces\n\nIncoming `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.\n\n```ts\nawait observability.tracer.trace(\"reminder.send\", async (span) => {\n  span.setAttribute(\"reminder.channel\", \"email\");\n  await sendReminder();\n});\n```\n\nExceptions become bounded span events and error status. The OTLP/HTTP JSON exporter uses protocol byte encoding, HTTPS, explicit headers, a timeout, and redirect rejection.\n\n## Metrics\n\nThe registry supports counters, gauges, and histograms and renders Prometheus text:\n\n```ts\nconst sent = observability.metrics.counter(\n  \"app_reminders_sent_total\",\n  \"Reminders accepted by the delivery service.\",\n  [\"channel\"],\n);\n\nsent.add(1, { channel: \"email\" });\n```\n\nMetric 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.\n\nExpose `observability.metrics.response()` only on an operator-protected endpoint or private network.\n\n### Deployment ingress metrics\n\nClank 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.\n\nIngress 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](platform-dashboard.md) for exact semantics and retention.\n\n## Readiness\n\nCritical checks determine the HTTP status. Optional dependencies remain visible without taking the service out of rotation:\n\n```ts\nobservability.health.register(\"database\", checkDatabase);\nobservability.health.register(\"email\", checkMail, { critical: false });\n```\n\nChecks run concurrently with individual timeouts. Responses use `no-store` and return `503` when a critical dependency fails.\n"}