Skip to content
nikcli
Docs
Documentation

Observability

OpenTelemetry (OTLP) trace export and live in-process telemetry for the TUI panel. Source: packages/nikcli/src/observability/.

Overview

nikcli ships a small observability layer that wires the Effect runtime to OpenTelemetry and publishes a stream of captured spans on the internal bus. The layer is merged into every Effect runtime base (packages/nikcli/src/effect/runtime.ts) so the same spans power the live TUI panel, the OTLP export, and the on-disk log file.

There is no separate nikcli telemetry command. The whole subsystem is configured by environment variables and turned on or off from the TUI.

Backend

OTLP export

When OTEL_EXPORTER_OTLP_ENDPOINT is set, spans and logs are exported to any OTLP/HTTP collector (Grafana, Honeycomb, Jaeger, Datadog, …) using effect/unstable/observability.

In-process

Live panel

On by default, the layer captures spans and publishes them on the telemetry.record bus event. The TUI renders them in the live telemetry dialog. Disable with NIKCLI_DISABLE_OTEL_LIVE=1.

Identity

Resource attributes

Every span carries service.name, service.version, and deployment.environment derived from NIKCLI_VERSION, NIKCLI_CHANNEL, and the OTEL_RESOURCE_ATTRIBUTES env var.

Zero cost

No-op when idle

If neither an OTLP endpoint is set nor the live panel is enabled, the layer is a pass-through and adds no measurable overhead.

Two modes

ModeEnabled byWhat it does
OTLP exportOTEL_EXPORTER_OTLP_ENDPOINT is set to a URL.Spans and logs are POSTed to the OTLP/HTTP endpoint. Headers can be passed through OTEL_EXPORTER_OTLP_HEADERS.
Live panelDefault ON. Disabled with NIKCLI_DISABLE_OTEL_LIVE=1.Each completed span is published on the telemetry.record bus event so the TUI can render it in real time.

Both modes can run at the same time. Disabling both fully deactivates the layer (no spans are even captured).

OTLP export

OTLP export uses the Effect OtlpTracer and OtlpLogger modules. The HTTP client is the standard FetchHttpClient from effect/unstable/http.

Environment variables

VariableDefaultPurpose
OTEL_EXPORTER_OTLP_ENDPOINTunsetOTLP/HTTP endpoint (for example http://localhost:4318 or https://otlp.grafana.net). Setting it enables export.
OTEL_EXPORTER_OTLP_HEADERSunsetComma-separated key=value pairs, URL-decoded. Useful for auth headers (Authorization=Bearer%20…).
OTEL_RESOURCE_ATTRIBUTESunsetComma-separated key=value pairs merged into every span's resource (URL-decoded).
NIKCLI_DISABLE_OTEL_LIVEfalseDisable the live telemetry panel even when OTLP is enabled.

Headers

The header parser is permissive: a header is split on the first = only, so values may contain =. Keys and values are not URL-decoded automatically; the collector's OTEL_EXPORTER_OTLP_HEADERS semantics match the OpenTelemetry spec.

Terminal
bash
# Export to a local Jaeger collector
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 nikcli run "summarize this repo"

# Export to a hosted backend with auth
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.grafana.net \
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20xxx" \
nikcli serve --port 4096

# Disable the live panel but keep OTLP export
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
NIKCLI_DISABLE_OTEL_LIVE=1 \
nikcli serve --port 4096

Live telemetry panel

The live panel is on by default. Each completed span is published on the telemetry.record bus event with the payload defined in packages/nikcli/src/observability/telemetry-bus.ts:

packages/nikcli/src/observability/telemetry-bus.ts
ts
export const TelemetryRecord = BusEvent.define(
  "telemetry.record",
  z.object({
    id: z.string(),
    traceId: z.string(),
    parentId: z.string().optional(),
    name: z.string(),
    kind: z.string(),
    startTime: z.number(),
    durationMs: z.number(),
    statusCode: z.number().optional(),
    statusMessage: z.string().optional(),
    attributes: z.record(z.string(), z.string()).optional(),
  }),
)

The TUI subscribes to this event in packages/tui/src/component/dialog-telemetry-live.tsx and renders a scrolling, searchable list of spans with filter by name, kind, and status.

Cycle-safe

The TelemetryRecord schema is defined in a module that imports only bus-event so the observability layer can be merged into the Effect runtime base without creating an import cycle.

Configuration

All configuration is read from environment variables in @nikcli-ai/util/flag and consumed by packages/nikcli/src/observability/otlp.ts. There is no nikcli.json field for telemetry — the values are deliberately env-only so container images can be reconfigured without rebuilding.

Resource attributes

Every span is decorated with a resource block. The base attributes are:

AttributeSource
service.nameConstant: "nikcli".
service.versionCompile-time NIKCLI_VERSION (or "local" when running from source).
deployment.environmentCompile-time NIKCLI_CHANNEL (or "local").
User-definedParsed from OTEL_RESOURCE_ATTRIBUTES.

Disabling the live panel

Set NIKCLI_DISABLE_OTEL_LIVE=1 to opt out of the in-process span capture. This is useful when you only want the OTLP export and want to skip the bus traffic.

TUI surface

The TUI exposes two dialogs that consume the same span stream:

DialogWhat it shows
dialog-telemetry-live.tsxScrolling, filterable list of captured spans with name, kind, duration, and status code. Updates live as spans complete.
dialog-opentelemetry.tsxConfiguration dialog: shows the resolved OTLP endpoint, the current resource block, and a "Copy as env" button to copy the current configuration to the clipboard.
dialog-advisor-model.tsxSelects the model used by the advisor tool. The advisor emits spans on every invocation, so the choice is visible in the live panel.

The dialogs are registered in packages/tui/src/component/dialog-command.tsx and opened from the command palette (Ctrl+P) or by their keybind.

Effect integration

The observability layer is merged into the Effect runtime base in packages/nikcli/src/effect/runtime.ts. Application code that needs to emit a span can use the standard Effect combinators:

Effect runtime span example
ts
// Any service that uses the Effect runtime automatically gets an OTLP-capable tracer.
const program = Effect.gen(function* () {
  yield* Effect.logInfo("doing work").pipe(Effect.withSpan("session.run"))
  return yield* someOperation
})

The same span is published on the bus (so the TUI sees it) and exported over OTLP (when an endpoint is set). No extra wiring is needed per call site.

Source anchors

FileWhat to inspect
packages/nikcli/src/observability/index.tsPublic Observability namespace: layer, isEnabled, isLiveEnabled, resourceInfo.
packages/nikcli/src/observability/otlp.tsOTLP layer built from effect/unstable/observability; reads flags and resource attributes.
packages/nikcli/src/observability/telemetry-bus.tsTelemetryRecord bus event and zod schema.
packages/nikcli/src/effect/runtime.tsWhere the observability layer is merged into the runtime base.
@nikcli-ai/util/flagOTLP / OTel environment variable definitions.
packages/tui/src/component/dialog-telemetry-live.tsxLive TUI panel.
packages/tui/src/component/dialog-opentelemetry.tsxOTLP configuration dialog.
packages/tui/src/component/dialog-advisor-model.tsxAdvisor model picker (drives advisor spans).