# 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](https://opentelemetry.io) 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

| 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](https://effect.website) `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.

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
```

$ shell · paste into your terminal

## 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 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

```
export const TelemetryRecord = BusEvent.define(
  "telemetry.record",
  z.object(&lbrace;
    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(),
  &rbrace;),
)
```

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:

| 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:

Effect runtime span example

ts 

1

2

3

4

5

```
// Any service that uses the Effect runtime automatically gets an OTLP-capable tracer.
const program = Effect.gen(function* () &lbrace;
  yield* Effect.logInfo("doing work").pipe(Effect.withSpan("session.run"))
  return yield* someOperation
&rbrace;)
```

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). |

---

Source: https://nikcli-ai.dev/docs/observability
