Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Observability

Capture correlated local evidence, redact it before storage, and export sampled telemetry without coupling handlers to a vendor.

Follow a request across routes, functions, and resources using RELKIT’s correlated logs and traces. Handlers write structured records through context.log; functions, providers, jobs, events, tools, and agents contribute correlated spans without importing a telemetry SDK.

Pre-setup

Start with local logging and bounded capture; neither requires a telemetry account, exporter, new defineEnv fields, or a Docker service. Merge these settings into your existing defineApp configuration, retaining its environment contract:

/** Local telemetry works without a remote exporter or extra environment secrets. */
import { defineApp, defineEnv } from "@relkit/app/config";

export default defineApp({
  env: defineEnv({}),
  telemetry: {
    redaction: { mode: "development-redacted", maxBytes: 65_536 },
    localRetention: { maxRecords: 2_000, maxAgeMs: 3_600_000, maxBytes: 16_777_216 },
  },
});

Run bun run check and bun run dev, then send a request to an existing route to produce correlated logs. For a UI, complete the separate Inspector setup: installation alone does not enable its bridge. Keep your application’s Redis, MinIO, database, and job providers in local Docker.

Remote Sentry/OTLP export is optional. Their integrations have no bundled Docker collector recipe; configure a reachable service and supply its named endpoint/secret bindings only when selecting that exporter. Those binding references are distinct from application context.env fields.

Configure one pipeline

Core logging and bounded capture need no exporter or Inspector installation. When selecting the commerce example’s exporters, install each chosen owner in the backend first. OTLP uses its own packaged implementation; Sentry also selects its native Bun SDK:

From this guide’s application/package root, install only its selected requirements:

bun add @relkit/otlp@0.7.12

Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in dependencies; build/type tooling belongs in devDependencies. Installing an adapter does not supply its optional SDK peers or enable the feature’s configuration.

From this guide’s application/package root, install only its selected requirements:

bun add @relkit/sentry@0.8.1 @sentry/bun@10.71.0

Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in dependencies; build/type tooling belongs in devDependencies. Installing an adapter does not supply its optional SDK peers or enable the feature’s configuration.

Use only the chosen exporter entries/imports in your existing configuration. Keeping both entries below selects both installations; copying telemetry settings never installs their optional packages.

The commerce application combines redacted local capture, bounded retention, export sampling, Sentry, and OTLP:

telemetry: {
  redaction: { mode: "development-redacted", maxBytes: 65_536 },
  localRetention: { maxRecords: 2_000, maxAgeMs: 3_600_000, maxBytes: 16_777_216 },
  exportSampling: { traceRate: 0.25, minimumLogLevel: "info" },
  exporters: {
    errors: sentry({
      dsn: binding.secret("SENTRY_DSN"),
      environment: binding.string("SENTRY_ENVIRONMENT"),
    }),
    traces: otlp({
      endpoint: binding.url("OTLP_ENDPOINT"),
      headers: { authorization: binding.secret("OTLP_AUTHORIZATION") },
      serviceName: "commerce-api",
    }),
  },
},

Sentry and OTLP are independent integrations. Their credentials and endpoints use named binding references, so resolved values do not enter the application graph or Inspector payloads.

Understand capture order

runtime event
→ canonical record
→ capture policy
→ redaction
→ bounded retention and live Inspector stream
→ root trace-consistent sampling
→ independent exporters

Local capture is independent of external sampling. With traceRate: 0.25, the bounded Inspector store can keep a complete redacted local trace even when that trace is not selected for remote export. Spans, logs, and errors with the same trace use the same deterministic trace decision. Log export also follows minimumLogLevel. Configure traceRate: 1 when a Collector performs tail sampling and needs complete failed traces; RELKIT does not keep a second in-process tail-sampling buffer.

localRetention bounds record count, age, and bytes. It is diagnostic evidence, not durable business storage.

Application logging and production telemetry use bounded memory unless a storage capability is explicitly supplied. They do not install or open DuckDB. Normal console output and external exporters remain usable without development history. The default memory store keeps at most 1,024 records and 4 MiB of encoded record payloads; explicit retention settings can lower or change those bounds.

Read development logs

bun run dev prints startup URLs, compilation and reload outcomes, HTTP completions, and application output. The default terminal level is INFO. Use bun run dev -- --verbose for DEBUG events and retained error details, --log-level warn to select an explicit threshold, --no-color for plain text, or --json for newline-delimited records on stderr. An explicit level overrides verbose’s DEBUG selection. NO_COLOR and redirected output also disable color. Terminal filtering does not reduce captured history.

Persistent development history belongs to the separately installed and launched @relkit/inspector tool. Complete its install → bridge enablement → tool startup procedure before using these views. It owns @relkit/observability-local, DuckDB and the Node worker; the backend-only workflow continues to require only Bun.

The tool defaults to .relkit/observability/observability.duckdb under its selected project root. Use the tool’s --history-directory option for an explicit persistent override. One Inspector writer owns a history directory, which is bound to one project. Stopping/restarting the tool or reloading the backend preserves committed history; removing a package-runner cache does not remove project history.

Before collection, an enabled backend bridge holds only a bounded redacted buffer, initially 2,048 records/2 MiB. With no running tool there is no new persisted tool history. Overflow or failed delivery is reported as incomplete capture; collector receipts acknowledge committed session/sequence records and deduplicate replay. Backend capture limits and tool history retention are separate budgets. Exporter delivery remains independent of collector acknowledgments.

The current store uses observability model v2. Moving store ownership preserves existing database schemas, receipt identities, imported segment markers, and source history files. Incompatible development state is not reset automatically: RELKIT reports RELKIT_OBSERVABILITY_STATE_INCOMPATIBLE and preserves the directory for diagnosis. Diagnostic history does not own durable job, checkpoint, provider, or agent application state.

If storage fails, application requests continue. The Logs workspace reports degraded storage and failed or dropped records. Restart the Inspector after fixing the cause; records that failed to commit are not saved.

Redact before storage or export

development-redacted allows bounded invocation and operation input/output capture while replacing protected keys and known secret values. maxBytes is required and prevents oversized content from being retained. Mode off omits protected payload content entirely.

Use synthetic secrets in tests and assert that they do not appear in records, diagnostics, generated artifacts, or exporter payloads. Redaction is a safety boundary, not permission to log credentials.

Diagnose exporter health

Exporter fan-out is isolated. A failed Sentry or OTLP exporter cannot block application work or the other exporter. Failures increment health counters and create redacted local-only diagnostics that are not sent back through the failed exporter.

Start the app and follow one identifiable request:

bun run dev
curl http://localhost:3000/hello\?name=Telemetry \
  -H "x-correlation-id: docs-telemetry"

After the separate Inspector setup, open Requests, select /hello, and follow its trace, logs, graph hash, generation, and activation fingerprint. The trace timeline numbers and connects spans and lifecycle events by their recorded date/time, labels each operation type, and keeps overlapping duration bars visible for concurrent work. Select a span to open the expanded sheet with separate input, output, outcome, and metadata sections alongside its resource attributes, events, links, correlated logs, and safe error details. Event and job continuations remain separate links because they can outlive the HTTP response. If remote telemetry is silent, inspect exporter health, queue depth, failure count, and drop count before assuming application capture failed.

Add application trace detail

RELKIT already creates spans for routes, functions, declared client operations, jobs, events, tools, and agents. Use context.trace only for application work that those boundaries do not describe:

// Dependencies are accessed through the checked execution context.
const unitPrice = await context.trace.span(
  "pricing.calculate",
  { attributes: { "pricing.strategy": "catalog" } },
  () => context.cache.prices.getOrSet({ sku: input.sku }, async () => 1_000),
);
context.trace.event("pricing.calculated", { "pricing.unit_cents": unitPrice });
context.trace.setAttributes({ "pricing.quantity": input.quantity });
Operation What it records
span(name, callback) An internal child span around the callback
span(name, { attributes, kind }, callback) A child span with initial attributes and kind internal or client
event(name, attributes?) A point-in-time event on the active span
setAttributes({ key: value }) Additional attributes on the active span

The callback passed to span runs with a child span as the active span. RELKIT ends it on success or failure and returns or rethrows the callback result unchanged. The default kind is internal; use client only for a custom outbound dependency RELKIT does not already instrument. Nested and parallel callbacks keep the correct parent.

event adds a point-in-time event to the active span. setAttributes adds facts to that same active span, so a call directly in a handler annotates the handler’s invocation span while a call inside span annotates the child. There is no manual start or end API.

Attribute values are limited to strings, finite numbers, and booleans. Names beginning with relkit. and identity keys such as traceId, spanId, and requestId are reserved for the runtime. Recording is bounded; Inspector reports dropped or truncated data instead of implying that omitted telemetry was observed. Do not put secrets, payloads, or high-cardinality values such as random IDs into attributes.

Inspect a trace and its logs

The trace view keeps nesting and overlapping work visible. Open a span to inspect its attributes, events, links, correlated logs, captured redacted input/output, outcome, and safe error details.

RELKIT Inspector trace waterfall showing the nested HTTP request, function, and cache spans

The Logs workspace preserves structured fields and correlation identities. Selecting a record keeps its associated trace beside it so developers and operations teams can move from a symptom to the surrounding execution.

RELKIT Inspector Logs workspace with a selected structured log and its associated trace

AWS hosting routes redacted structured stdout to CloudWatch through the host integration. CloudWatch is host logging, not an application exporter, so it does not belong under telemetry.exporters.

Reference and source