Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions .coderabbit.yaml

This file was deleted.

60 changes: 35 additions & 25 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,61 @@ Always run after making changes:
bun run typecheck
```

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

## Testing

```bash
bun test
```

## Target runtime

`main` targets **OpenCode V2** (`>=2`) and exports a `Plugin.define`-style default export
(`id: "devtheops.otel"`, `setup`). The OpenCode V1 plugin lives on the `v1` branch and is
released from the `1.x` line; do not add V1 compatibility shims to `main`.

## Project layout

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

## Key conventions

- **Bun over Node** — use `bun`, `bun test`, `bun run`. Never use `node`, `npx`, `jest`, or `vitest`.
- **No comments** unless explicitly requested.
- **No `sdk-node`** — the OTel Node SDK meta-package is intentionally excluded; use individual packages.
- **`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.
- **`setBoundedMap`** — always use this instead of `Map.set` for `pendingToolSpans` and `pendingPermissions` to prevent unbounded growth.
- **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`.
- **Shutdown** — OTel providers are flushed via `SIGTERM`/`SIGINT`/`beforeExit`. Do not use `process.on("exit")` for async flushing.
- **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.
- **`OPENCODE_ENABLE_TELEMETRY`** — all OTel instrumentation is gated on this env var. The plugin always loads regardless; only telemetry is disabled when unset.
- **`@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.
- **`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.
- **`setBoundedMap` / `markSeen`** — always use these for correlation maps (`toolMeta`, `stepMeta`, `pendingPrompts`, `pendingPermissions`, `seenEvents`) to prevent unbounded growth.
- **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.
- **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.
- **Session identity** — retain the agent, subagent parent, and creation time across executions, and hydrate missed `session.created` events with `ctx.session.get`.
- **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.
- **Model context capture** — opt-in only, text parts only, bounded; never serialize full media or structured tool payloads into span attributes.
- **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.
- **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.
- **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.
- **`OPENCODE_ENABLE_TELEMETRY`** — all OTel instrumentation is gated on this env var (or the `enabled` plugin option). The plugin always loads regardless.
- **`OPENCODE_METRIC_PREFIX`** — defaults to `opencode.`; set to `claude_code.` for Claude Code dashboard compatibility.
- **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.
- **Plugin options** — read from `ctx.options` during `setup`. Precedence is option → `OPENCODE_*` env → default. Keep option keys 1:1 with `PluginConfig` field names.

## Commit message format

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

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

Use `!` or a `BREAKING CHANGE:` footer for breaking changes.

Examples:

```text
feat(handlers): add support for file.edited event
fix(probe): handle malformed endpoint URL without throwing
chore(deps): bump @opentelemetry/api to 1.10.0
```
Use `!` or a `BREAKING CHANGE:` footer for breaking changes (this repo uses `release-please`, so a
breaking commit bumps the major version automatically).
30 changes: 18 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ bun install

## Development workflow

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

```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["/path/to/opencode-plugin-otel/src/index.ts"]
}
```ts
export { default } from "/path/to/opencode-plugin-otel/src/index.ts"
```

opencode loads TypeScript natively via Bun, so there is no build step required during development.
OpenCode V2 discovers the directory automatically and loads TypeScript via Bun, so
there is no build step during development. A `plugins` entry pointing directly to
an absolute `.ts` file is rejected by OpenCode `2.0.1`.

> **Branching:** `main` targets OpenCode V2. The OpenCode V1 plugin is maintained on the `v1` branch
> (branched from the last `1.x` tag) — open V1 bug/security fixes against `v1`, not `main`.

## Commands

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

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

## Testing locally with a collector
Expand Down
Loading
Loading