Skip to content

Commit 39dd93d

Browse files
Merge pull request #51 from thomasbonenfant/fix/protobuf-exporter
2 parents 21549bd + bc615ff commit 39dd93d

7 files changed

Lines changed: 107 additions & 13 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ All configuration is via environment variables. Set them in your shell profile (
8989
| Variable | Default | Description |
9090
|----------|---------|-------------|
9191
| `OPENCODE_ENABLE_TELEMETRY` | *(unset)* | Set to any non-empty value to enable the plugin |
92-
| `OPENCODE_OTLP_ENDPOINT` | `http://localhost:4317` | OTLP collector endpoint. For `grpc`, use the collector host/port. For `http/protobuf`, use the base URL and the plugin will append `/v1/traces`, `/v1/metrics`, and `/v1/logs`. |
93-
| `OPENCODE_OTLP_PROTOCOL` | `grpc` | OTLP transport protocol: `grpc` or `http/protobuf` |
92+
| `OPENCODE_OTLP_ENDPOINT` | `http://localhost:4317` | OTLP collector endpoint. For `grpc`, use the collector host/port. For `http/protobuf` and `http/json`, use the base URL and the plugin will append `/v1/traces`, `/v1/metrics`, and `/v1/logs`. |
93+
| `OPENCODE_OTLP_PROTOCOL` | `grpc` | OTLP transport protocol: `grpc`, `http/protobuf`, or `http/json` |
9494
| `OPENCODE_OTLP_METRICS_INTERVAL` | `60000` | Metrics export interval in milliseconds |
9595
| `OPENCODE_OTLP_LOGS_INTERVAL` | `5000` | Logs export interval in milliseconds |
9696
| `OPENCODE_METRIC_PREFIX` | `opencode.` | Prefix for all metric names (e.g. set to `claude_code.` for Claude Code dashboard compatibility) |
@@ -111,7 +111,7 @@ export OPENCODE_OTLP_PROTOCOL=grpc
111111
opencode
112112
```
113113

114-
For `OPENCODE_OTLP_PROTOCOL=http/protobuf`, set `OPENCODE_OTLP_ENDPOINT` to the collector base URL rather than a per-signal path. The plugin expands it to `/v1/traces`, `/v1/metrics`, and `/v1/logs` automatically.
114+
For `OPENCODE_OTLP_PROTOCOL=http/protobuf` or `OPENCODE_OTLP_PROTOCOL=http/json`, set `OPENCODE_OTLP_ENDPOINT` to the collector base URL rather than a per-signal path. The plugin expands it to `/v1/traces`, `/v1/metrics`, and `/v1/logs` automatically.
115115

116116
### Headers and resource attributes
117117

bun.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
"@opentelemetry/api": "^1.9.0",
1111
"@opentelemetry/exporter-logs-otlp-grpc": "^0.213.0",
1212
"@opentelemetry/exporter-logs-otlp-http": "^0.213.0",
13+
"@opentelemetry/exporter-logs-otlp-proto": "^0.213.0",
1314
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0",
1415
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
16+
"@opentelemetry/exporter-metrics-otlp-proto": "^0.213.0",
1517
"@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",
1618
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
19+
"@opentelemetry/exporter-trace-otlp-proto": "^0.213.0",
1720
"@opentelemetry/resources": "^2.6.0",
1821
"@opentelemetry/sdk-logs": "^0.213.0",
1922
"@opentelemetry/sdk-metrics": "^2.6.0",

src/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type PluginConfig = {
1414
enabled: boolean
1515
logsEnabled: boolean
1616
endpoint: string
17-
protocol: "grpc" | "http/protobuf"
17+
protocol: "grpc" | "http/protobuf" | "http/json"
1818
metricsInterval: number
1919
logsInterval: number
2020
metricPrefix: string
@@ -98,7 +98,11 @@ export function loadConfig(): PluginConfig {
9898
enabled: hasNonEmptyEnv("OPENCODE_ENABLE_TELEMETRY"),
9999
logsEnabled: !hasNonEmptyEnv("OPENCODE_DISABLE_LOGS"),
100100
endpoint: process.env["OPENCODE_OTLP_ENDPOINT"] ?? "http://localhost:4317",
101-
protocol: protocol === "http/protobuf" ? "http/protobuf" : "grpc",
101+
protocol: protocol === "http/protobuf"
102+
? "http/protobuf"
103+
: protocol === "http/json"
104+
? "http/json"
105+
: "grpc",
102106
metricsInterval: parseEnvInt("OPENCODE_OTLP_METRICS_INTERVAL", 60000),
103107
logsInterval: parseEnvInt("OPENCODE_OTLP_LOGS_INTERVAL", 5000),
104108
metricPrefix: process.env["OPENCODE_METRIC_PREFIX"] ?? "opencode.",

src/otel.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-grpc"
77
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc"
88
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc"
99
import { OTLPLogExporter as OTLPHttpLogExporter } from "@opentelemetry/exporter-logs-otlp-http"
10+
import { OTLPLogExporter as OTLPProtoLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"
1011
import { OTLPMetricExporter as OTLPHttpMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"
12+
import { OTLPMetricExporter as OTLPProtoMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"
1113
import { OTLPTraceExporter as OTLPHttpTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
14+
import { OTLPTraceExporter as OTLPProtoTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"
1215
import { resourceFromAttributes } from "@opentelemetry/resources"
1316
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"
1417
import { ATTR_HOST_ARCH } from "@opentelemetry/semantic-conventions/incubating"
@@ -70,7 +73,7 @@ export function buildHttpSignalUrl(endpoint: string, signal: "traces" | "metrics
7073
*/
7174
export async function setupOtel(
7275
endpoint: string,
73-
protocol: "grpc" | "http/protobuf",
76+
protocol: "grpc" | "http/protobuf" | "http/json",
7477
metricsInterval: number,
7578
logsInterval: number,
7679
version: string,
@@ -88,14 +91,20 @@ export async function setupOtel(
8891
}
8992
}
9093
const makeMetricExporter = (headers: HeadersMap) => protocol === "http/protobuf"
91-
? new OTLPHttpMetricExporter({ url: buildHttpSignalUrl(endpoint, "metrics"), headers })
92-
: new OTLPMetricExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
94+
? new OTLPProtoMetricExporter({ url: buildHttpSignalUrl(endpoint, "metrics"), headers })
95+
: protocol === "http/json"
96+
? new OTLPHttpMetricExporter({ url: buildHttpSignalUrl(endpoint, "metrics"), headers })
97+
: new OTLPMetricExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
9398
const makeLogExporter = (headers: HeadersMap) => protocol === "http/protobuf"
94-
? new OTLPHttpLogExporter({ url: buildHttpSignalUrl(endpoint, "logs"), headers })
95-
: new OTLPLogExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
99+
? new OTLPProtoLogExporter({ url: buildHttpSignalUrl(endpoint, "logs"), headers })
100+
: protocol === "http/json"
101+
? new OTLPHttpLogExporter({ url: buildHttpSignalUrl(endpoint, "logs"), headers })
102+
: new OTLPLogExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
96103
const makeTraceExporter = (headers: HeadersMap) => protocol === "http/protobuf"
97-
? new OTLPHttpTraceExporter({ url: buildHttpSignalUrl(endpoint, "traces"), headers })
98-
: new OTLPTraceExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
104+
? new OTLPProtoTraceExporter({ url: buildHttpSignalUrl(endpoint, "traces"), headers })
105+
: protocol === "http/json"
106+
? new OTLPHttpTraceExporter({ url: buildHttpSignalUrl(endpoint, "traces"), headers })
107+
: new OTLPTraceExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
99108
const metricExporter = otlpHeadersHelper
100109
? new RefreshingMetricExporter(makeMetricExporter, dynamicHeaders)
101110
: makeMetricExporter(staticHeaders)

tests/config.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ describe("loadConfig", () => {
9191
expect(loadConfig().protocol).toBe("http/protobuf")
9292
})
9393

94+
test("reads HTTP/json protocol", () => {
95+
process.env["OPENCODE_OTLP_PROTOCOL"] = "http/json"
96+
expect(loadConfig().protocol).toBe("http/json")
97+
})
98+
9499
test("falls back to grpc for unknown protocol", () => {
95100
process.env["OPENCODE_OTLP_PROTOCOL"] = "http"
96101
expect(loadConfig().protocol).toBe("grpc")

tests/otel.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
11
import { describe, test, expect, afterEach } from "bun:test"
2-
import { buildResource } from "../src/otel.ts"
2+
import { OTLPLogExporter as OTLPHttpLogExporter } from "@opentelemetry/exporter-logs-otlp-http"
3+
import { OTLPLogExporter as OTLPProtoLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"
4+
import { OTLPMetricExporter as OTLPHttpMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"
5+
import { OTLPMetricExporter as OTLPProtoMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"
6+
import { OTLPTraceExporter as OTLPHttpTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
7+
import { OTLPTraceExporter as OTLPProtoTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"
8+
import { buildResource, setupOtel, type OtelProviders } from "../src/otel.ts"
9+
10+
let providers: OtelProviders | undefined
11+
12+
function exportersOf(currentProviders: OtelProviders) {
13+
const meterProvider = currentProviders.meterProvider as unknown as {
14+
_sharedState: { metricCollectors: Array<{ _metricReader: { _exporter: unknown } }> }
15+
}
16+
const loggerProvider = currentProviders.loggerProvider as unknown as {
17+
_sharedState: { activeProcessor: { processors: Array<{ _exporter: unknown }> } }
18+
}
19+
const tracerProvider = currentProviders.tracerProvider as unknown as {
20+
_activeSpanProcessor: { _spanProcessors: Array<{ _exporter: unknown }> }
21+
}
22+
const metricCollector = meterProvider._sharedState.metricCollectors[0]
23+
const logProcessor = loggerProvider._sharedState.activeProcessor.processors[0]
24+
const spanProcessor = tracerProvider._activeSpanProcessor._spanProcessors[0]
25+
26+
if (!metricCollector || !logProcessor || !spanProcessor) {
27+
throw new Error("Expected OTEL providers to have active metric/log/trace exporters")
28+
}
29+
30+
return {
31+
metric: metricCollector._metricReader._exporter,
32+
log: logProcessor._exporter,
33+
trace: spanProcessor._exporter,
34+
}
35+
}
336

437
describe("buildResource", () => {
538
const originalEnv = process.env["OTEL_RESOURCE_ATTRIBUTES"]
@@ -41,3 +74,34 @@ describe("buildResource", () => {
4174
expect(resource.attributes["service.name"]).toBe("my-override")
4275
})
4376
})
77+
78+
describe("setupOtel", () => {
79+
afterEach(async () => {
80+
const current = providers
81+
providers = undefined
82+
if (!current) return
83+
await Promise.allSettled([
84+
current.tracerProvider.shutdown(),
85+
current.loggerProvider.shutdown(),
86+
current.meterProvider.shutdown(),
87+
])
88+
})
89+
90+
test("uses protobuf HTTP exporters for http/protobuf", async () => {
91+
providers = await setupOtel("http://collector:4318", "http/protobuf", 60000, 5000, "1.2.3")
92+
const exporters = exportersOf(providers)
93+
94+
expect(exporters.metric).toBeInstanceOf(OTLPProtoMetricExporter)
95+
expect(exporters.log).toBeInstanceOf(OTLPProtoLogExporter)
96+
expect(exporters.trace).toBeInstanceOf(OTLPProtoTraceExporter)
97+
})
98+
99+
test("uses JSON HTTP exporters for http/json", async () => {
100+
providers = await setupOtel("http://collector:4318", "http/json", 60000, 5000, "1.2.3")
101+
const exporters = exportersOf(providers)
102+
103+
expect(exporters.metric).toBeInstanceOf(OTLPHttpMetricExporter)
104+
expect(exporters.log).toBeInstanceOf(OTLPHttpLogExporter)
105+
expect(exporters.trace).toBeInstanceOf(OTLPHttpTraceExporter)
106+
})
107+
})

0 commit comments

Comments
 (0)