Skip to content

Commit 21549bd

Browse files
Merge pull request #48 from DanMaly/signal-controls
2 parents 12591a0 + c8fc0e4 commit 21549bd

11 files changed

Lines changed: 227 additions & 24 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ An [opencode](https://opencode.ai) plugin that exports telemetry via OpenTelemet
1818
- [Headers and resource attributes](#headers-and-resource-attributes)
1919
- [Dynamic headers](#dynamic-headers)
2020
- [Disabling specific metrics](#disabling-specific-metrics)
21+
- [Disabling OTLP logs (`OPENCODE_DISABLE_LOGS`)](#disabling-otlp-logs)
22+
- [Disabling traces (`OPENCODE_DISABLE_TRACES`)](#disabling-traces)
23+
- [SigNoz example](#signoz-example)
2124
- [Datadog example](#datadog-example)
2225
- [Honeycomb example](#honeycomb-example)
2326
- [Claude Code dashboard compatibility](#claude-code-dashboard-compatibility)
@@ -92,6 +95,8 @@ All configuration is via environment variables. Set them in your shell profile (
9295
| `OPENCODE_OTLP_LOGS_INTERVAL` | `5000` | Logs export interval in milliseconds |
9396
| `OPENCODE_METRIC_PREFIX` | `opencode.` | Prefix for all metric names (e.g. set to `claude_code.` for Claude Code dashboard compatibility) |
9497
| `OPENCODE_DISABLE_METRICS` | *(unset)* | Comma-separated list of metric name suffixes to disable (e.g. `cache.count,session.duration`) |
98+
| `OPENCODE_DISABLE_LOGS` | *(unset)* | Set to any non-empty value to suppress all OTLP log events while leaving metrics and traces unchanged |
99+
| `OPENCODE_DISABLE_TRACES` | *(unset)* | Comma-separated list of trace types to disable (`session`, `llm`, `tool`). Use `all`, `*`, `true`, or `1` to disable every trace type |
95100
| `OPENCODE_OTLP_HEADERS` | *(unset)* | Comma-separated `key=value` headers added to all OTLP exports. **Keep out of version control — may contain sensitive auth tokens.** |
96101
| `OPENCODE_OTLP_HEADERS_HELPER` | *(unset)* | Executable script/binary that returns dynamic OTLP headers as JSON after an auth failure. Helper headers override `OPENCODE_OTLP_HEADERS`. |
97102
| `OPENCODE_RESOURCE_ATTRIBUTES` | *(unset)* | Comma-separated `key=value` pairs merged into the OTel resource. Example: `service.version=1.2.3,deployment.environment=production` |
@@ -180,6 +185,33 @@ export OPENCODE_DISABLE_METRICS="cache.count,session.duration,session.token.tota
180185
| `retry.count` | API retry counter — not emitted by Claude Code |
181186
| `message.count` | Completed message counter — not emitted by Claude Code |
182187

188+
### Disabling OTLP logs
189+
190+
Use `OPENCODE_DISABLE_LOGS` to suppress every OTLP log event emitted by the plugin.
191+
192+
```bash
193+
export OPENCODE_DISABLE_LOGS=1
194+
```
195+
196+
This only disables OTLP logs. Metrics and traces continue to be exported unless they are disabled separately.
197+
198+
### Disabling traces
199+
200+
Use `OPENCODE_DISABLE_TRACES` to suppress one or more trace types.
201+
202+
```bash
203+
# Disable one trace type
204+
export OPENCODE_DISABLE_TRACES="tool"
205+
206+
# Disable multiple trace types
207+
export OPENCODE_DISABLE_TRACES="llm,tool"
208+
209+
# Disable every trace type explicitly
210+
export OPENCODE_DISABLE_TRACES="all"
211+
```
212+
213+
Accepted explicit "disable all traces" values are `all`, `*`, `true`, and `1`.
214+
183215
### SigNoz example
184216

185217
```bash

src/config.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import { LEVELS, type Level } from "./types.ts"
33
/** Accepted values for `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`. */
44
export type MetricsTemporality = "cumulative" | "delta" | "lowmemory"
55

6+
/** Valid trace types emitted by the plugin. */
7+
export const TRACE_TYPES = ["session", "llm", "tool"] as const
8+
69
const VALID_TEMPORALITIES: ReadonlySet<MetricsTemporality> = new Set<MetricsTemporality>(["cumulative", "delta", "lowmemory"])
10+
const TRACE_DISABLE_ALL_VALUES = new Set(["all", "*", "true", "1"])
711

812
/** Configuration values resolved from `OPENCODE_*` environment variables. */
913
export type PluginConfig = {
1014
enabled: boolean
15+
logsEnabled: boolean
1116
endpoint: string
1217
protocol: "grpc" | "http/protobuf"
1318
metricsInterval: number
@@ -30,6 +35,25 @@ export function parseEnvInt(key: string, fallback: number): number {
3035
return Number.isSafeInteger(n) ? n : fallback
3136
}
3237

38+
/** Returns `true` when the environment variable is present and non-empty. */
39+
function hasNonEmptyEnv(key: string): boolean {
40+
return !!process.env[key]
41+
}
42+
43+
/** Parses `OPENCODE_DISABLE_TRACES`, expanding explicit global values like `all`. */
44+
function parseDisabledTraces(raw: string | undefined): Set<string> {
45+
const values = (raw ?? "")
46+
.split(",")
47+
.map(s => s.trim().toLowerCase())
48+
.filter(Boolean)
49+
50+
if (values.some(value => TRACE_DISABLE_ALL_VALUES.has(value))) {
51+
return new Set(TRACE_TYPES)
52+
}
53+
54+
return new Set(values)
55+
}
56+
3357
/**
3458
* Reads all `OPENCODE_*` environment variables and returns the resolved plugin config.
3559
* Copies `OPENCODE_OTLP_HEADERS` → `OTEL_EXPORTER_OTLP_HEADERS`,
@@ -68,15 +92,11 @@ export function loadConfig(): PluginConfig {
6892
.filter(Boolean),
6993
)
7094

71-
const disabledTraces = new Set(
72-
(process.env["OPENCODE_DISABLE_TRACES"] ?? "")
73-
.split(",")
74-
.map(s => s.trim())
75-
.filter(Boolean),
76-
)
95+
const disabledTraces = parseDisabledTraces(process.env["OPENCODE_DISABLE_TRACES"])
7796

7897
return {
79-
enabled: !!process.env["OPENCODE_ENABLE_TELEMETRY"],
98+
enabled: hasNonEmptyEnv("OPENCODE_ENABLE_TELEMETRY"),
99+
logsEnabled: !hasNonEmptyEnv("OPENCODE_DISABLE_LOGS"),
80100
endpoint: process.env["OPENCODE_OTLP_ENDPOINT"] ?? "http://localhost:4317",
81101
protocol: protocol === "http/protobuf" ? "http/protobuf" : "grpc",
82102
metricsInterval: parseEnvInt("OPENCODE_OTLP_METRICS_INTERVAL", 60000),

src/handlers/activity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function handleCommandExecuted(e: EventCommandExecuted, ctx: HandlerConte
7777
})
7878
ctx.log("debug", "otel: commit counter incremented", { sessionID: e.properties.sessionID })
7979
}
80-
ctx.logger.emit({
80+
ctx.emitLog({
8181
severityNumber: SeverityNumber.INFO,
8282
severityText: "INFO",
8383
timestamp: Date.now(),

src/handlers/message.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export function handleMessageUpdated(e: EventMessageUpdated, ctx: HandlerContext
139139
}
140140

141141
if (assistant.error) {
142-
ctx.logger.emit({
142+
ctx.emitLog({
143143
severityNumber: SeverityNumber.ERROR,
144144
severityText: "ERROR",
145145
timestamp: assistant.time.created,
@@ -165,7 +165,7 @@ export function handleMessageUpdated(e: EventMessageUpdated, ctx: HandlerContext
165165
})
166166
}
167167

168-
ctx.logger.emit({
168+
ctx.emitLog({
169169
severityNumber: SeverityNumber.INFO,
170170
severityText: "INFO",
171171
timestamp: assistant.time.created,
@@ -226,7 +226,7 @@ export function handleMessagePartUpdated(e: EventMessagePartUpdated, ctx: Handle
226226
agent: subtask.agent,
227227
})
228228
}
229-
ctx.logger.emit({
229+
ctx.emitLog({
230230
severityNumber: SeverityNumber.INFO,
231231
severityText: "INFO",
232232
timestamp: Date.now(),
@@ -358,7 +358,7 @@ export function handleMessagePartUpdated(e: EventMessagePartUpdated, ctx: Handle
358358
? { tool_result_size_bytes: Buffer.byteLength((toolPart.state as { output: string }).output, "utf8") }
359359
: { error: (toolPart.state as { error: string }).error }
360360

361-
ctx.logger.emit({
361+
ctx.emitLog({
362362
severityNumber: success ? SeverityNumber.INFO : SeverityNumber.ERROR,
363363
severityText: success ? "INFO" : "ERROR",
364364
timestamp: start,

src/handlers/permission.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function handlePermissionReplied(e: EventPermissionReplied, ctx: HandlerC
2121
ctx.pendingPermissions.delete(permissionID)
2222
const decision = response === "allow" || response === "allowAlways" ? "accept" : "reject"
2323
ctx.log("debug", "otel: tool_decision emitted", { permissionID, sessionID, decision, source: response, tool_name: pending?.title ?? "unknown" })
24-
ctx.logger.emit({
24+
ctx.emitLog({
2525
severityNumber: SeverityNumber.INFO,
2626
severityText: "INFO",
2727
timestamp: Date.now(),

src/handlers/session.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function handleSessionCreated(e: EventSessionCreated, ctx: HandlerContext
4444
setBoundedMap(ctx.sessionSpans, sessionID, sessionSpan)
4545
}
4646

47-
ctx.logger.emit({
47+
ctx.emitLog({
4848
severityNumber: SeverityNumber.INFO,
4949
severityText: "INFO",
5050
timestamp: createdAt,
@@ -119,7 +119,7 @@ export function handleSessionIdle(e: EventSessionIdle, ctx: HandlerContext) {
119119
ctx.sessionSpans.delete(sessionID)
120120
}
121121

122-
ctx.logger.emit({
122+
ctx.emitLog({
123123
severityNumber: SeverityNumber.INFO,
124124
severityText: "INFO",
125125
timestamp: Date.now(),
@@ -163,7 +163,7 @@ export function handleSessionError(e: EventSessionError, ctx: HandlerContext) {
163163
}
164164
}
165165

166-
ctx.logger.emit({
166+
ctx.emitLog({
167167
severityNumber: SeverityNumber.ERROR,
168168
severityText: "ERROR",
169169
timestamp: Date.now(),

src/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export const OtelPlugin: Plugin = async ({ project, client, directory, worktree
8686

8787
const instruments = createInstruments(config.metricPrefix)
8888
const logger = logs.getLogger("com.opencode")
89+
const emitLog: HandlerContext["emitLog"] = (record) => {
90+
if (!config.logsEnabled) return
91+
logger.emit(record)
92+
}
8993
const tracer = trace.getTracer("com.opencode")
9094
const pendingToolSpans = new Map()
9195
const pendingPermissions = new Map()
@@ -106,9 +110,13 @@ export const OtelPlugin: Plugin = async ({ project, client, directory, worktree
106110
await log("info", "traces disabled", { disabled: [...disabledTraces] })
107111
}
108112

113+
if (!config.logsEnabled) {
114+
await log("info", "OTLP log events disabled")
115+
}
116+
109117
const ctx: HandlerContext = {
110-
logger,
111118
log,
119+
emitLog,
112120
instruments,
113121
commonAttrs,
114122
pendingToolSpans,
@@ -183,7 +191,7 @@ export const OtelPlugin: Plugin = async ({ project, client, directory, worktree
183191
}).filter(Boolean).join("\n")
184192
sessionInputs.set(input.sessionID, promptText)
185193
const promptLength = promptText.length
186-
logger.emit({
194+
emitLog({
187195
severityNumber: SeverityNumber.INFO,
188196
severityText: "INFO",
189197
timestamp: Date.now(),

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Counter, Gauge, Histogram, Span, Tracer } from "@opentelemetry/api"
2-
import type { Logger as OtelLogger } from "@opentelemetry/api-logs"
2+
import type { LogRecord } from "@opentelemetry/api-logs"
33

44
/** Numeric priority map for log levels; higher value = higher severity. */
55
export const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 } as const
@@ -65,8 +65,8 @@ export type SessionTotals = {
6565

6666
/** Shared context threaded through every event handler. */
6767
export type HandlerContext = {
68-
logger: OtelLogger
6968
log: PluginLogger
69+
emitLog: (record: LogRecord) => void
7070
instruments: Instruments
7171
commonAttrs: CommonAttrs
7272
pendingToolSpans: Map<string, PendingToolSpan>

tests/config.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
2-
import { parseEnvInt, loadConfig, resolveHelperPath, resolveLogLevel } from "../src/config.ts"
2+
import { parseEnvInt, loadConfig, resolveHelperPath, resolveLogLevel, TRACE_TYPES } from "../src/config.ts"
33

44
describe("parseEnvInt", () => {
55
test("returns fallback when env var is unset", () => {
@@ -52,6 +52,7 @@ describe("loadConfig", () => {
5252
"OPENCODE_RESOURCE_ATTRIBUTES",
5353
"OPENCODE_OTLP_METRICS_TEMPORALITY",
5454
"OPENCODE_DISABLE_METRICS",
55+
"OPENCODE_DISABLE_LOGS",
5556
"OPENCODE_DISABLE_TRACES",
5657
"OTEL_EXPORTER_OTLP_HEADERS",
5758
"OTEL_RESOURCE_ATTRIBUTES",
@@ -63,6 +64,7 @@ describe("loadConfig", () => {
6364
test("defaults when no env vars set", () => {
6465
const cfg = loadConfig()
6566
expect(cfg.enabled).toBe(false)
67+
expect(cfg.logsEnabled).toBe(true)
6668
expect(cfg.endpoint).toBe("http://localhost:4317")
6769
expect(cfg.protocol).toBe("grpc")
6870
expect(cfg.metricsInterval).toBe(60000)
@@ -74,6 +76,11 @@ describe("loadConfig", () => {
7476
expect(loadConfig().enabled).toBe(true)
7577
})
7678

79+
test("logsEnabled is false when OPENCODE_DISABLE_LOGS is set", () => {
80+
process.env["OPENCODE_DISABLE_LOGS"] = "1"
81+
expect(loadConfig().logsEnabled).toBe(false)
82+
})
83+
7784
test("reads custom endpoint", () => {
7885
process.env["OPENCODE_OTLP_ENDPOINT"] = "http://collector:4317"
7986
expect(loadConfig().endpoint).toBe("http://collector:4317")
@@ -259,6 +266,26 @@ describe("loadConfig", () => {
259266
expect(disabledTraces.has("unknown_type")).toBe(true)
260267
expect(disabledTraces.size).toBe(2)
261268
})
269+
270+
test("disabledTraces expands all to every known trace type", () => {
271+
process.env["OPENCODE_DISABLE_TRACES"] = "all"
272+
expect(loadConfig().disabledTraces).toEqual(new Set(TRACE_TYPES))
273+
})
274+
275+
test("disabledTraces expands wildcard to every known trace type", () => {
276+
process.env["OPENCODE_DISABLE_TRACES"] = "*"
277+
expect(loadConfig().disabledTraces).toEqual(new Set(TRACE_TYPES))
278+
})
279+
280+
test("disabledTraces expands boolean-style values to every known trace type", () => {
281+
process.env["OPENCODE_DISABLE_TRACES"] = "true"
282+
expect(loadConfig().disabledTraces).toEqual(new Set(TRACE_TYPES))
283+
})
284+
285+
test('disabledTraces expands numeric-style value "1" to every known trace type', () => {
286+
process.env["OPENCODE_DISABLE_TRACES"] = "1"
287+
expect(loadConfig().disabledTraces).toEqual(new Set(TRACE_TYPES))
288+
})
262289
})
263290

264291
describe("resolveLogLevel", () => {

0 commit comments

Comments
 (0)