Skip to content

Commit 687cbe1

Browse files
feat: release workflow, OPENCODE_* env var cleanup, and packaging improvements (#1)
- Add .github/workflows/release.yml — publishes to npm on v* tag push - Remove root index.ts re-export; package.json main/module → src/index.ts; add files field - Add OPENCODE_OTLP_HEADERS and OPENCODE_RESOURCE_ATTRIBUTES; loadConfig copies them to OTEL_* before SDK init - Remove parseHeaders (SDK reads OTEL_EXPORTER_OTLP_HEADERS natively) - Fix parseEnvInt to reject partial numeric strings ("1.5", "5000ms") - Add JSDoc to all exported functions, types, and constants - Add regression tests for OTEL_* passthrough behaviour - Add CHANGELOG, README TOC, usage examples, and security note]
2 parents 63ca65a + 3a711a2 commit 687cbe1

19 files changed

Lines changed: 241 additions & 77 deletions

.github/workflows/release.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
release:
10+
name: Publish to npm
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: "22"
19+
registry-url: "https://registry.npmjs.org"
20+
21+
- uses: oven-sh/setup-bun@v2
22+
with:
23+
bun-version: latest
24+
25+
- name: Install dependencies
26+
run: bun install --frozen-lockfile
27+
28+
- name: Typecheck
29+
run: bun run typecheck
30+
31+
- name: Test
32+
run: bun test
33+
34+
- name: Publish to npm
35+
run: npm publish --access public
36+
env:
37+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

AGENTS.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ Always run after making changes:
1010
bun run typecheck
1111
```
1212

13-
There is no separate build step needed for local development. For publishing:
14-
15-
```bash
16-
bun run build
17-
```
13+
There is no build step. TypeScript source files are published directly and loaded natively by Bun.
1814

1915
## Testing
2016

@@ -48,7 +44,7 @@ src/
4844
- **`setBoundedMap`** — always use this instead of `Map.set` for `pendingToolSpans` and `pendingPermissions` to prevent unbounded growth.
4945
- **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`.
5046
- **Shutdown** — OTel providers are flushed via `SIGTERM`/`SIGINT`/`beforeExit`. Do not use `process.on("exit")` for async flushing.
51-
- **All env vars are `OPENCODE_` prefixed**`OPENCODE_ENABLE_TELEMETRY`, `OPENCODE_OTLP_ENDPOINT`, `OPENCODE_OTLP_METRICS_INTERVAL`, `OPENCODE_OTLP_LOGS_INTERVAL`, `OPENCODE_METRIC_PREFIX`. Never use bare `OTEL_*` names for plugin config.
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_OTLP_HEADERS`, `OPENCODE_RESOURCE_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.
5248
- **`OPENCODE_ENABLE_TELEMETRY`** — all OTel instrumentation is gated on this env var. The plugin always loads regardless; only telemetry is disabled when unset.
5349
- **`OPENCODE_METRIC_PREFIX`** — defaults to `opencode.`; set to `claude_code.` for Claude Code dashboard compatibility.
5450

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
---
8+
9+
## [0.1.0] — 2026-03-11
10+
11+
### Added
12+
13+
- **Release workflow**`.github/workflows/release.yml` publishes to npm automatically when a `v*` tag is pushed, gated by typecheck and tests.
14+
- **`OPENCODE_OTLP_HEADERS`** — new env var for comma-separated `key=value` OTLP auth headers (e.g. `x-honeycomb-team=abc,x-tenant=org`). Copied to `OTEL_EXPORTER_OTLP_HEADERS` before the SDK initialises.
15+
- **`OPENCODE_RESOURCE_ATTRIBUTES`** — new env var for comma-separated `key=value` OTel resource attributes (e.g. `service.version=1.2.3,deployment.environment=production`). Copied to `OTEL_RESOURCE_ATTRIBUTES` before the SDK initialises.
16+
- JSDoc on all exported functions, types, and constants.
17+
- Regression tests covering `OTEL_*` passthrough behaviour — pre-existing values are preserved when `OPENCODE_*` vars are unset; `OPENCODE_*` vars overwrite when set.
18+
- README table of contents, usage examples for headers and resource attributes, and a security note advising that `OPENCODE_OTLP_HEADERS` may contain sensitive tokens and should not be committed to version control.
19+
20+
### Changed
21+
22+
- `package.json` `main`/`module` now point directly at `src/index.ts`; root `index.ts` re-export removed.
23+
- `files` field added to `package.json` — published package contains only `src/`, reducing install size.
24+
- All user-facing env vars are now consistently `OPENCODE_`-prefixed. `loadConfig` copies `OPENCODE_OTLP_HEADERS``OTEL_EXPORTER_OTLP_HEADERS` and `OPENCODE_RESOURCE_ATTRIBUTES``OTEL_RESOURCE_ATTRIBUTES` so the OTel SDK picks them up natively.
25+
- `parseEnvInt` now rejects partial numeric strings such as `"1.5"` or `"5000ms"`, returning the fallback instead of silently truncating.
26+
27+
### Removed
28+
29+
- `parseHeaders` removed from `src/otel.ts` — the OTel SDK reads `OTEL_EXPORTER_OTLP_HEADERS` natively once `loadConfig` copies the value across.
30+
- Manual `release:patch` / `release:minor` / `release:major` npm scripts removed in favour of the tag-based CI workflow.

CONTRIBUTING.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Point your local opencode config at the repo so changes are picked up immediatel
2020
```json
2121
{
2222
"$schema": "https://opencode.ai/config.json",
23-
"plugin": ["/path/to/opencode-plugin-otel/index.ts"]
23+
"plugin": ["/path/to/opencode-plugin-otel/src/index.ts"]
2424
}
2525
```
2626

@@ -32,7 +32,6 @@ opencode loads TypeScript natively via Bun, so there is no build step required d
3232
|---------|-------------|
3333
| `bun run typecheck` | Type-check all sources without emitting |
3434
| `bun test` | Run the test suite |
35-
| `bun run build` | Compile to `dist/` for publishing |
3635

3736
## Project structure
3837

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
An [opencode](https://opencode.ai) plugin that exports telemetry via OpenTelemetry (OTLP/gRPC), mirroring the same signals as [Claude Code's monitoring](https://code.claude.com/docs/en/monitoring-usage).
44

5+
- [What it instruments](#what-it-instruments)
6+
- [Metrics](#metrics)
7+
- [Log events](#log-events)
8+
- [Installation](#installation)
9+
- [Configuration](#configuration)
10+
- [Quick start](#quick-start)
11+
- [Headers and resource attributes](#headers-and-resource-attributes)
12+
- [Datadog example](#datadog-example)
13+
- [Honeycomb example](#honeycomb-example)
14+
- [Claude Code dashboard compatibility](#claude-code-dashboard-compatibility)
15+
- [Local development](#local-development)
16+
517
## What it instruments
618

719
### Metrics
@@ -45,7 +57,7 @@ Or point directly at a local checkout for development:
4557
```json
4658
{
4759
"$schema": "https://opencode.ai/config.json",
48-
"plugin": ["/path/to/opencode-plugin-otel/index.ts"]
60+
"plugin": ["/path/to/opencode-plugin-otel/src/index.ts"]
4961
}
5062
```
5163

@@ -60,6 +72,20 @@ All configuration is via environment variables. Set them in your shell profile (
6072
| `OPENCODE_OTLP_METRICS_INTERVAL` | `60000` | Metrics export interval in milliseconds |
6173
| `OPENCODE_OTLP_LOGS_INTERVAL` | `5000` | Logs export interval in milliseconds |
6274
| `OPENCODE_METRIC_PREFIX` | `opencode.` | Prefix for all metric names (e.g. set to `claude_code.` for Claude Code dashboard compatibility) |
75+
| `OPENCODE_OTLP_HEADERS` | _(unset)_ | Comma-separated `key=value` headers added to all OTLP exports. Example: `api-key=abc123,x-tenant=my-org`. **Keep out of version control — may contain sensitive auth tokens.** |
76+
| `OPENCODE_RESOURCE_ATTRIBUTES` | _(unset)_ | Comma-separated `key=value` pairs merged into the OTel resource. Example: `service.version=1.2.3,deployment.environment=production` |
77+
78+
### Headers and resource attributes
79+
80+
```bash
81+
# Auth token for a managed collector (e.g. Honeycomb, Grafana Cloud)
82+
export OPENCODE_OTLP_HEADERS="x-honeycomb-team=your-api-key,x-honeycomb-dataset=opencode"
83+
84+
# Tag every metric and log with deployment context
85+
export OPENCODE_RESOURCE_ATTRIBUTES="service.version=1.2.3,deployment.environment=production"
86+
```
87+
88+
> **Security note:** `OPENCODE_OTLP_HEADERS` typically contains auth tokens. Set it in your shell profile (`~/.zshrc`, `~/.bashrc`) or a secrets manager — never commit it to version control or print it in CI logs.
6389
6490
### Quick start
6591

index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
{
22
"name": "opencode-plugin-otel",
3-
"module": "index.ts",
3+
"version": "0.0.1",
4+
"module": "src/index.ts",
5+
"main": "src/index.ts",
46
"type": "module",
7+
"files": [
8+
"src/"
9+
],
510
"devDependencies": {
611
"@types/bun": "latest"
712
},
813
"scripts": {
9-
"release:patch": "npm version patch && npm publish",
10-
"release:minor": "npm version minor && npm publish",
11-
"release:major": "npm version major && npm publish",
1214
"typecheck": "tsc --noEmit"
1315
},
1416
"dependencies": {

src/config.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,53 @@
11
import { LEVELS, type Level } from "./types.ts"
22

3+
/** Configuration values resolved from `OPENCODE_*` environment variables. */
34
export type PluginConfig = {
45
enabled: boolean
56
endpoint: string
67
metricsInterval: number
78
logsInterval: number
89
metricPrefix: string
10+
otlpHeaders: string | undefined
11+
resourceAttributes: string | undefined
912
}
1013

14+
/** Parses a positive integer from an environment variable, returning `fallback` if absent or invalid. */
1115
export function parseEnvInt(key: string, fallback: number): number {
1216
const raw = process.env[key]
1317
if (!raw) return fallback
14-
const n = parseInt(raw, 10)
15-
return Number.isFinite(n) && n > 0 ? n : fallback
18+
if (!/^[1-9]\d*$/.test(raw)) return fallback
19+
const n = Number(raw)
20+
return Number.isSafeInteger(n) ? n : fallback
1621
}
1722

23+
/**
24+
* Reads all `OPENCODE_*` environment variables and returns the resolved plugin config.
25+
* Copies `OPENCODE_OTLP_HEADERS` → `OTEL_EXPORTER_OTLP_HEADERS` and
26+
* `OPENCODE_RESOURCE_ATTRIBUTES` → `OTEL_RESOURCE_ATTRIBUTES` so the OTel SDK
27+
* picks them up automatically when initialised.
28+
*/
1829
export function loadConfig(): PluginConfig {
30+
const otlpHeaders = process.env["OPENCODE_OTLP_HEADERS"]
31+
const resourceAttributes = process.env["OPENCODE_RESOURCE_ATTRIBUTES"]
32+
33+
if (otlpHeaders) process.env["OTEL_EXPORTER_OTLP_HEADERS"] = otlpHeaders
34+
if (resourceAttributes) process.env["OTEL_RESOURCE_ATTRIBUTES"] = resourceAttributes
35+
1936
return {
2037
enabled: !!process.env["OPENCODE_ENABLE_TELEMETRY"],
2138
endpoint: process.env["OPENCODE_OTLP_ENDPOINT"] ?? "http://localhost:4317",
2239
metricsInterval: parseEnvInt("OPENCODE_OTLP_METRICS_INTERVAL", 60000),
2340
logsInterval: parseEnvInt("OPENCODE_OTLP_LOGS_INTERVAL", 5000),
2441
metricPrefix: process.env["OPENCODE_METRIC_PREFIX"] ?? "opencode.",
42+
otlpHeaders,
43+
resourceAttributes,
2544
}
2645
}
2746

47+
/**
48+
* Resolves an opencode log level string to a `Level`.
49+
* Returns `current` unchanged when the input does not match a known level.
50+
*/
2851
export function resolveLogLevel(logLevel: string, current: Level): Level {
2952
const candidate = logLevel.toLowerCase()
3053
if (candidate in LEVELS) return candidate as Level

src/handlers/activity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SeverityNumber } from "@opentelemetry/api-logs"
22
import type { EventSessionDiff, EventCommandExecuted } from "@opencode-ai/sdk"
33
import type { HandlerContext } from "../types.ts"
44

5+
/** Records lines-added and lines-removed metrics for each file in the diff. */
56
export function handleSessionDiff(e: EventSessionDiff, ctx: HandlerContext) {
67
const sessionID = e.properties.sessionID
78
for (const fileDiff of e.properties.diff) {
@@ -24,6 +25,7 @@ export function handleSessionDiff(e: EventSessionDiff, ctx: HandlerContext) {
2425

2526
const GIT_COMMIT_RE = /\bgit\s+commit(?![-\w])/
2627

28+
/** Detects `git commit` invocations in bash tool calls and increments the commit counter and emits a `commit` log event. */
2729
export function handleCommandExecuted(e: EventCommandExecuted, ctx: HandlerContext) {
2830
if (e.properties.name !== "bash") return
2931
if (!GIT_COMMIT_RE.test(e.properties.arguments)) return

src/handlers/message.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { errorSummary } from "../util.ts"
44
import { setBoundedMap } from "../util.ts"
55
import type { HandlerContext } from "../types.ts"
66

7+
/**
8+
* Handles a completed assistant message: increments token and cost counters and emits
9+
* either an `api_request` or `api_error` log event depending on whether the message errored.
10+
*/
711
export function handleMessageUpdated(e: EventMessageUpdated, ctx: HandlerContext) {
812
const msg = e.properties.info
913
if (msg.role !== "assistant") return
@@ -77,6 +81,10 @@ export function handleMessageUpdated(e: EventMessageUpdated, ctx: HandlerContext
7781
})
7882
}
7983

84+
/**
85+
* Tracks tool execution time between `running` and `completed`/`error` part updates,
86+
* records a `tool.duration` histogram measurement, and emits a `tool_result` log event.
87+
*/
8088
export function handleMessagePartUpdated(e: EventMessagePartUpdated, ctx: HandlerContext) {
8189
const part = e.properties.part
8290
if (part.type !== "tool") return

0 commit comments

Comments
 (0)