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.
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.
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.
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.
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
| Mode | Enabled by | What it does |
|---|---|---|
| OTLP export | OTEL_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 panel | Default 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
| Variable | Default | Purpose |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | unset | OTLP/HTTP endpoint (for example http://localhost:4318 or https://otlp.grafana.net). Setting it enables export. |
OTEL_EXPORTER_OTLP_HEADERS | unset | Comma-separated key=value pairs, URL-decoded. Useful for auth headers (Authorization=Bearer%20…). |
OTEL_RESOURCE_ATTRIBUTES | unset | Comma-separated key=value pairs merged into every span's resource (URL-decoded). |
NIKCLI_DISABLE_OTEL_LIVE | false | Disable 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.
# 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:
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.
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:
| Attribute | Source |
|---|---|
service.name | Constant: "nikcli". |
service.version | Compile-time NIKCLI_VERSION (or "local" when running from source). |
deployment.environment | Compile-time NIKCLI_CHANNEL (or "local"). |
| User-defined | Parsed 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:
| Dialog | What it shows |
|---|---|
dialog-telemetry-live.tsx | Scrolling, filterable list of captured spans with name, kind, duration, and status code. Updates live as spans complete. |
dialog-opentelemetry.tsx | Configuration 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.tsx | Selects 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:
// 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
| File | What to inspect |
|---|---|
packages/nikcli/src/observability/index.ts | Public Observability namespace: layer, isEnabled, isLiveEnabled, resourceInfo. |
packages/nikcli/src/observability/otlp.ts | OTLP layer built from effect/unstable/observability; reads flags and resource attributes. |
packages/nikcli/src/observability/telemetry-bus.ts | TelemetryRecord bus event and zod schema. |
packages/nikcli/src/effect/runtime.ts | Where the observability layer is merged into the runtime base. |
@nikcli-ai/util/flag | OTLP / OTel environment variable definitions. |
packages/tui/src/component/dialog-telemetry-live.tsx | Live TUI panel. |
packages/tui/src/component/dialog-opentelemetry.tsx | OTLP configuration dialog. |
packages/tui/src/component/dialog-advisor-model.tsx | Advisor model picker (drives advisor spans). |