Skip to content

Commit dae25f0

Browse files
feat!: migrate to OpenCode V2 (#132)
## Description Migrates the plugin to **OpenCode V2 only**. The `2.x` line default-exports a V2 definition (`id: "devtheops.otel"`, `setup`) and consumes the V2 event stream. The OpenCode V1 implementation remains on the [`v1` branch](https://github.com/DEVtheOPS/opencode-plugin-otel/tree/v1), branched from `v1.5.1`. Closes #128. Supersedes #131's dual V1/V2 approach with a V2-only major release. ### V2 instrumentation - Explicit `session.execution.*`, `session.step.*`, and `session.tool.*` boundaries replace V1 message-part polling for run, LLM, and tool spans. - `session.inbox.enqueued` emits `user_prompt` only after durable admission; synthetic/compaction/move inbox entries are ignored. Queued prompts are retained for the next execution, while steering prompts apply to the current execution. `session.text.ended` restores `output.value` and `llm.output_messages` on LLM spans. - Per-step token, cost, cache, message, and model metrics; `api_request` and `api_error` logs retain `duration_ms` even with LLM traces disabled. Failed LLM spans retain any reported token and cost attributes. Cumulative session totals come from `session.usage.updated` with a per-step fallback. The cumulative baseline persists across executions even if the next turn fails before a usage update; totals are flushed on execution end (also handling idle events). - Durable `session.retry.scheduled` is the single retry-counter source. `permission.asked`/`permission.replied` preserve `tool_decision`; subagent `session.created` events preserve `subtask.count` and `subtask_invoked`. - `commit.count` and `commit` are emitted only for an executed shell tool that reaches `session.tool.success` with `git commit` in its command. Tool spans and duration start at `session.tool.called`, excluding model-side argument streaming. This observes a successful tool call; Git's resulting repository state is not independently verified. - `model.request` injects W3C trace context only for a configured provider and a matching **primary** agent/model request, not title, compaction, or transient generation. It waits for previously queued event processing and, if `step.started` has not arrived, creates a provisional LLM span that the step later adopts. - Optional `OPENCODE_CAPTURE_MODEL_CONTEXT` captures a bounded, text-only primary context preview (two system parts, twelve recent messages, 1,000 characters per entry). Media bytes and structured tool payloads are excluded; it remains off by default and may expose system instructions and earlier user/tool text when enabled. - For explicitly configured trace-propagation providers, the experimental `experimental.ws.handshake` hook injects the matching W3C context. Changing per-step headers may reopen a reused WebSocket; the registration degrades gracefully when the experimental hook is unavailable. - Child subagent run spans nest under an unambiguously identified `subagent` tool span using `session.tool.progress` / terminal metadata containing the child `sessionID`. Links are consumed per child execution so resumed sessions cannot inherit a stale dispatch. Foreground and background tool events were inspected in OpenCode `2.0.18`; invalid `executed: false` calls without a child ID do not create tool spans. Ambiguous or missed correlations fall back to the parent run. ### Multi-location correctness OTel providers and correlation state are shared per process and flushed, never shut down. Enabled plugin instances must have identical telemetry configuration; conflicting collector/credentials/resource/signal options fail setup rather than exporting a different location's data to the first collector. Accepted resource attributes and metric temporality are passed directly into SDK initialization. Resolved `OPENCODE_OTLP_HEADERS` are passed to exporters without copying them to the process's `OTEL_EXPORTER_OTLP_HEADERS`; exporter construction suppresses inherited OTLP header variables so a concurrent rejected setup cannot leak its credentials. `project.id` is resolved from the observed session, not assumed from `ctx.location`. Event subscriptions enqueue subsequent events without awaiting each dispatch; processing is serialized across subscribers before de-duplication so an asynchronous session lookup cannot let `step.ended` overtake `step.started` or leave model-request propagation using an older step. Exporter flushing is scheduled outside the dispatch queue, so slow collectors do not block unrelated model requests; cleanup drains pending events and flushes, including when a subscription iterator throws. Event IDs and message/session correlation sets are bounded. Agent/subagent identity, selected agent, parent session, creation time, and cumulative usage baseline are retained across executions and hydrated with `ctx.session.get` for sessions whose creation event was missed. ### V1 parity limits `lines_of_code.count` and `lines_of_code.total` are not emitted: the V1 `session.diff` event is absent from V2's public event stream. The V2 client has a session diff endpoint, but it is not exposed through the V2 plugin's `ctx.session` API; `ctx.vcs.diff` is repository-scoped and not an equivalent per-session total. The removed `command.executed` event is replaced for commit telemetry by successful tool completion. Per-message/part spans become per-step LLM spans, while completed text output remains attached to those spans. ### Docs and packaging The README and CONTRIBUTING guide use V2's `plugins` object form and a tested `.opencode/plugins/otel/index.ts` development entrypoint. OpenCode `2.0.1` rejects an absolute `.ts` file as a `plugins` package target; directory auto-discovery worked. V1 SDK dependencies were removed, and the official V2 `@opencode/plugin` types are used without a runtime import. The unused `.coderabbit.yaml` was removed. ## Type of change - [x] Breaking change (V2-only package; V1 maintenance stays on `v1`) - [x] Documentation update ## Verification - [x] `bun run lint` - [x] `bun run check:jsdoc-coverage` (81.63%) - [x] `bun run typecheck` - [x] `bun test` (191 pass) - [x] `bun run build` and bundled default-export import - [x] Local OpenCode `2.0.1` smoke test using `.opencode/plugins/`: OTLP HTTP/JSON exported logs, traces, and metrics. Confirmed `user_prompt`, `api_request`, `session.idle`, `opencode.session` → `opencode.llm` nesting, LLM `output.value`, and session/token/cost/model metrics including final session histograms. A run with trace propagation enabled also completed with one correctly nested LLM span; a `delta` temporality run exported counter sums with OTLP aggregation temporality `1` (delta). - [x] OpenCode `2.0.18` foreground subagent OTLP export: parent `opencode.session` → `opencode.tool.subagent` → child `opencode.session`. Captured real foreground and background event sequences; unit tests cover background parenting after the dispatch tool ends and ambiguous-candidate fallback. - [x] OpenCode `2.0.18` opt-in context capture exported a text-only `llm.input_messages` preview with a matching LLM output span, including a foreground subagent trace. WebSocket header injection has unit coverage; a real WebSocket provider route was not available for an end-to-end handshake test. - [x] Added regression tests for conflicting configurations, observed project attribution, bounded sets, durable retries/prompts, cost-only failures, LLM output, subtask logs, and non-executed/failed commits. Two-subscriber delayed-dispatch, buffered subscription events with a concurrent model request, iterator failure with pending dispatch, repeated/resumed subagent, selected agent across executions, delayed model-request, accepted resource-attribute/temporality, queued-next-prompt, steering-prompt, pending-exporter-flush, two-execution cumulative-baseline, actual exporter inherited-header, delayed-tool-input, and API log duration without traces cover the lifecycle review findings. - [x] One-use child dispatch mapping and provisional-span context adoption tests cover the latest review findings. ## Related issues Closes #128 ## Additional context The breaking `feat!` commit is intended to make release-please cut **2.0.0**; `package.json` remains `1.5.1` until its release PR updates it. The [V2 migration guide](https://opencode.ai/v2/docs/build/plugins/migrate-v1) and [plugin API guide](https://opencode.ai/v2/docs/build/plugins) informed the hook selection. WebSocket propagation remains experimental; verify connection reuse and handshake headers with each intended provider route before enabling it broadly.
1 parent a051cda commit dae25f0

36 files changed

Lines changed: 3455 additions & 3720 deletions

‎.coderabbit.yaml‎

Lines changed: 0 additions & 27 deletions
This file was deleted.

‎AGENTS.md‎

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,61 @@ Always run after making changes:
1010
bun run typecheck
1111
```
1212

13-
There is no build step. TypeScript source files are published directly and loaded natively by Bun.
13+
There is no build step during local development. TypeScript source files are loaded natively by Bun; the published package is bundled by `prepack`.
1414

1515
## Testing
1616

1717
```bash
1818
bun test
1919
```
2020

21+
## Target runtime
22+
23+
`main` targets **OpenCode V2** (`>=2`) and exports a `Plugin.define`-style default export
24+
(`id: "devtheops.otel"`, `setup`). The OpenCode V1 plugin lives on the `v1` branch and is
25+
released from the `1.x` line; do not add V1 compatibility shims to `main`.
26+
2127
## Project layout
2228

2329
```text
2430
src/
25-
├── index.ts — Plugin entrypoint
26-
├── types.ts — Shared types
27-
├── config.ts — Env config and log level
31+
├── index.ts — V2 plugin default export (id + setup)
32+
├── plugin.ts — setup(): config, model.request hook, event subscription, dispatch
33+
├── state.ts — per-process shared OTel SDK + tracing state (globalThis)
34+
├── types.ts — Shared types (OpenCodeEvent, HandlerContext, TracingState, etc.)
35+
├── config.ts — Env/option config loading and log level resolution
2836
├── otel.ts — OTel SDK setup and instruments
2937
├── probe.ts — OTLP endpoint TCP probe
30-
├── util.ts — errorSummary, setBoundedMap
38+
├── headers.ts — Dynamic OTLP headers / refreshing exporters
39+
├── trace-context.ts — W3C trace-context inject/extract
40+
├── util.ts — errorSummary, setBoundedMap, context resolution, attrs
3141
└── handlers/
32-
├── session.ts — Session lifecycle events
33-
├── message.ts — LLM message and tool part events
34-
├── permission.ts — Tool permission events
35-
└── activity.ts — File diffs and git commits
42+
├── session.ts — session.created, session.inbox.enqueued, session.execution.*, retry/idle/usage
43+
├── step.ts — session.step.*, session.text.ended (LLM spans + token/cost/cache metrics)
44+
├── tool.ts — session.tool.* (tool spans, subagent correlation, duration, commit detection)
45+
├── permission.ts — permission.asked/replied
46+
└── chat-headers.ts — context preview and model.request / WebSocket trace propagation
3647
```
3748

3849
## Key conventions
3950

4051
- **Bun over Node** — use `bun`, `bun test`, `bun run`. Never use `node`, `npx`, `jest`, or `vitest`.
4152
- **No comments** unless explicitly requested.
4253
- **No `sdk-node`** — the OTel Node SDK meta-package is intentionally excluded; use individual packages.
43-
- **`HandlerContext`** — all event handlers receive a `HandlerContext` (defined in `src/types.ts`). Do not import `client` or OTel globals directly inside handlers; thread them through the context.
44-
- **`setBoundedMap`** — always use this instead of `Map.set` for `pendingToolSpans` and `pendingPermissions` to prevent unbounded growth.
45-
- **Single source of truth for tokens/cost** — token and cost counters are incremented only in `message.updated` (`src/handlers/message.ts`), never in `step-finish`.
46-
- **Shutdown** — OTel providers are flushed via `SIGTERM`/`SIGINT`/`beforeExit`. Do not use `process.on("exit")` for async flushing.
47-
- **All env vars are `OPENCODE_` prefixed** — `OPENCODE_ENABLE_TELEMETRY`, `OPENCODE_OTLP_ENDPOINT`, `OPENCODE_OTLP_METRICS_INTERVAL`, `OPENCODE_OTLP_LOGS_INTERVAL`, `OPENCODE_METRIC_PREFIX`, `OPENCODE_CAPTURE_PROMPT_IN_LOGS`, `OPENCODE_OTLP_HEADERS`, `OPENCODE_RESOURCE_ATTRIBUTES`, `OPENCODE_SPAN_ATTRIBUTES`. Never use bare `OTEL_*` names for plugin config. `loadConfig` copies `OPENCODE_OTLP_HEADERS` → `OTEL_EXPORTER_OTLP_HEADERS` and `OPENCODE_RESOURCE_ATTRIBUTES` → `OTEL_RESOURCE_ATTRIBUTES` before the SDK initializes.
48-
- **`OPENCODE_ENABLE_TELEMETRY`** — all OTel instrumentation is gated on this env var. The plugin always loads regardless; only telemetry is disabled when unset.
54+
- **`@opencode/plugin` is type-only** — import types from it (`import type { Plugin } from "@opencode/plugin"`); the plugin never imports it at runtime, so it stays a dev/optional peer dependency.
55+
- **`HandlerContext`** — all event handlers receive a `HandlerContext` (defined in `src/types.ts`). Do not import OTel globals directly inside handlers; thread them through the context.
56+
- **`setBoundedMap` / `markSeen`** — always use these for correlation maps (`toolMeta`, `stepMeta`, `pendingPrompts`, `pendingPermissions`, `seenEvents`) to prevent unbounded growth.
57+
- **Single source of truth for tokens/cost** — token and cost counters are incremented once per `session.step.ended`/`failed`; session totals come from `session.usage.updated` (cumulative) with a per-step fallback.
58+
- **Event de-duplication** — V2 may deliver the same event to multiple plugin instances; serialize shared dispatch before deduping by `event.id` via `markSeen` so later events cannot overtake an asynchronous session lookup.
59+
- **Session identity** — retain the agent, subagent parent, and creation time across executions, and hydrate missed `session.created` events with `ctx.session.get`.
60+
- **Subagent correlation** — only parent a child run under a dispatch tool span when a child session ID in tool progress/result metadata or one unambiguous live candidate identifies it; otherwise use the parent run.
61+
- **Model context capture** — opt-in only, text parts only, bounded; never serialize full media or structured tool payloads into span attributes.
62+
- **Multi-location configuration** — process-wide exporters require identical telemetry configuration; reject a conflicting setup and derive project attributes from the observed session, not the loading location.
63+
- **Shutdown** — providers are flushed (never shut down) on plugin cleanup and once per process on `beforeExit`. Shutting down the global OTel providers poisons them for the rest of the process.
64+
- **All env vars are `OPENCODE_` prefixed** — `OPENCODE_ENABLE_TELEMETRY`, `OPENCODE_OTLP_ENDPOINT`, `OPENCODE_OTLP_METRICS_INTERVAL`, `OPENCODE_OTLP_LOGS_INTERVAL`, `OPENCODE_METRIC_PREFIX`, `OPENCODE_CAPTURE_PROMPT_IN_LOGS`, `OPENCODE_OTLP_HEADERS`, `OPENCODE_RESOURCE_ATTRIBUTES`, `OPENCODE_SPAN_ATTRIBUTES`. Never use bare `OTEL_*` names for plugin config. Headers are passed directly to exporters; `loadConfig` copies `OPENCODE_RESOURCE_ATTRIBUTES` → `OTEL_RESOURCE_ATTRIBUTES` before the SDK initializes.
65+
- **`OPENCODE_ENABLE_TELEMETRY`** — all OTel instrumentation is gated on this env var (or the `enabled` plugin option). The plugin always loads regardless.
4966
- **`OPENCODE_METRIC_PREFIX`** — defaults to `opencode.`; set to `claude_code.` for Claude Code dashboard compatibility.
50-
- **Plugin options** — `loadConfig` also accepts an `OtelPluginOptions` object passed via opencode's plugin tuple form (`["opencode-plugin-otel", { ... }]`, threaded through `OtelPlugin`'s second argument). Precedence is option → `OPENCODE_*` env → default. Keep option keys 1:1 with `PluginConfig` field names.
67+
- **Plugin options** — read from `ctx.options` during `setup`. Precedence is option → `OPENCODE_*` env → default. Keep option keys 1:1 with `PluginConfig` field names.
5168

5269
## Commit message format
5370

@@ -59,12 +76,5 @@ All commits must follow [Conventional Commits](https://www.conventionalcommits.o
5976

6077
Common types: `feat`, `fix`, `perf`, `refactor`, `test`, `docs`, `ci`, `chore`, `build`.
6178

62-
Use `!` or a `BREAKING CHANGE:` footer for breaking changes.
63-
64-
Examples:
65-
66-
```text
67-
feat(handlers): add support for file.edited event
68-
fix(probe): handle malformed endpoint URL without throwing
69-
chore(deps): bump @opentelemetry/api to 1.10.0
70-
```
79+
Use `!` or a `BREAKING CHANGE:` footer for breaking changes (this repo uses `release-please`, so a
80+
breaking commit bumps the major version automatically).

‎CONTRIBUTING.md‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@ bun install
1515

1616
## Development workflow
1717

18-
Point your local opencode config at the repo so changes are picked up immediately without a build step. In `~/.config/opencode/opencode.json`:
18+
Install dependencies in the checkout with `bun install`. In the project where you run
19+
OpenCode, create `.opencode/plugins/otel/index.ts`:
1920

20-
```json
21-
{
22-
"$schema": "https://opencode.ai/config.json",
23-
"plugin": ["/path/to/opencode-plugin-otel/src/index.ts"]
24-
}
21+
```ts
22+
export { default } from "/path/to/opencode-plugin-otel/src/index.ts"
2523
```
2624

27-
opencode loads TypeScript natively via Bun, so there is no build step required during development.
25+
OpenCode V2 discovers the directory automatically and loads TypeScript via Bun, so
26+
there is no build step during development. A `plugins` entry pointing directly to
27+
an absolute `.ts` file is rejected by OpenCode `2.0.1`.
28+
29+
> **Branching:** `main` targets OpenCode V2. The OpenCode V1 plugin is maintained on the `v1` branch
30+
> (branched from the last `1.x` tag) — open V1 bug/security fixes against `v1`, not `main`.
2831
2932
## Commands
3033

@@ -39,17 +42,20 @@ opencode loads TypeScript natively via Bun, so there is no build step required d
3942

4043
```text
4144
src/
42-
├── index.ts — Plugin entrypoint, wires everything together
45+
├── index.ts — Plugin entrypoint (V2 default export)
46+
├── plugin.ts — setup(): config, hooks, event subscription
47+
├── state.ts — shared OTel SDK + tracing state
4348
├── types.ts — Shared types (Level, HandlerContext, Instruments, etc.)
4449
├── config.ts — Environment config loading and log level resolution
4550
├── otel.ts — OTel SDK setup, resource construction, instrument creation
4651
├── probe.ts — TCP connectivity probe for the OTLP endpoint
4752
├── util.ts — Utility functions (errorSummary, setBoundedMap)
4853
└── handlers/
49-
├── session.ts — session.created / session.idle / session.error
50-
├── message.ts — message.updated / message.part.updated
51-
├── permission.ts — permission.updated / permission.replied
52-
└── activity.ts — session.diff / command.executed
54+
├── session.ts — session.created / session.execution.* / session.status
55+
├── step.ts — session.step.* (LLM spans + token/cost metrics)
56+
├── tool.ts — session.tool.* (tool spans, subagents, duration, commits)
57+
├── permission.ts — permission.asked / permission.replied
58+
└── chat-headers.ts — context preview and model.request / WebSocket propagation
5359
```
5460

5561
## Testing locally with a collector

0 commit comments

Comments
 (0)