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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ All configuration is via environment variables. Set them in your shell profile (
| Variable | Default | Description |
|----------|---------|-------------|
| `OPENCODE_ENABLE_TELEMETRY` | *(unset)* | Set to any non-empty value to enable the plugin |
| `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`. |
| `OPENCODE_OTLP_PROTOCOL` | `grpc` | OTLP transport protocol: `grpc` or `http/protobuf` |
| `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`. |
| `OPENCODE_OTLP_PROTOCOL` | `grpc` | OTLP transport protocol: `grpc`, `http/protobuf`, or `http/json` |
| `OPENCODE_OTLP_METRICS_INTERVAL` | `60000` | Metrics export interval in milliseconds |
| `OPENCODE_OTLP_LOGS_INTERVAL` | `5000` | Logs export interval in milliseconds |
| `OPENCODE_METRIC_PREFIX` | `opencode.` | Prefix for all metric names (e.g. set to `claude_code.` for Claude Code dashboard compatibility) |
Expand All @@ -106,7 +106,7 @@ export OPENCODE_OTLP_PROTOCOL=grpc
opencode
```

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.
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.

### Headers and resource attributes

Expand Down
9 changes: 9 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.213.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.213.0",
"@opentelemetry/exporter-logs-otlp-proto": "^0.213.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.213.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.213.0",
"@opentelemetry/resources": "^2.6.0",
"@opentelemetry/sdk-logs": "^0.213.0",
"@opentelemetry/sdk-metrics": "^2.6.0",
Expand Down
8 changes: 6 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const VALID_TEMPORALITIES: ReadonlySet<MetricsTemporality> = new Set<MetricsTemp
export type PluginConfig = {
enabled: boolean
endpoint: string
protocol: "grpc" | "http/protobuf"
protocol: "grpc" | "http/protobuf" | "http/json"
metricsInterval: number
logsInterval: number
metricPrefix: string
Expand Down Expand Up @@ -78,7 +78,11 @@ export function loadConfig(): PluginConfig {
return {
enabled: !!process.env["OPENCODE_ENABLE_TELEMETRY"],
endpoint: process.env["OPENCODE_OTLP_ENDPOINT"] ?? "http://localhost:4317",
protocol: protocol === "http/protobuf" ? "http/protobuf" : "grpc",
protocol: protocol === "http/protobuf"
? "http/protobuf"
: protocol === "http/json"
? "http/json"
: "grpc",
metricsInterval: parseEnvInt("OPENCODE_OTLP_METRICS_INTERVAL", 60000),
logsInterval: parseEnvInt("OPENCODE_OTLP_LOGS_INTERVAL", 5000),
metricPrefix: process.env["OPENCODE_METRIC_PREFIX"] ?? "opencode.",
Expand Down
23 changes: 16 additions & 7 deletions src/otel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-grpc"
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc"
import { OTLPLogExporter as OTLPHttpLogExporter } from "@opentelemetry/exporter-logs-otlp-http"
import { OTLPLogExporter as OTLPProtoLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"
import { OTLPMetricExporter as OTLPHttpMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"
import { OTLPMetricExporter as OTLPProtoMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"
import { OTLPTraceExporter as OTLPHttpTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
import { OTLPTraceExporter as OTLPProtoTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"
import { resourceFromAttributes } from "@opentelemetry/resources"
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"
import { ATTR_HOST_ARCH } from "@opentelemetry/semantic-conventions/incubating"
Expand Down Expand Up @@ -70,7 +73,7 @@ export function buildHttpSignalUrl(endpoint: string, signal: "traces" | "metrics
*/
export async function setupOtel(
endpoint: string,
protocol: "grpc" | "http/protobuf",
protocol: "grpc" | "http/protobuf" | "http/json",
metricsInterval: number,
logsInterval: number,
version: string,
Expand All @@ -88,14 +91,20 @@ export async function setupOtel(
}
}
const makeMetricExporter = (headers: HeadersMap) => protocol === "http/protobuf"
? new OTLPHttpMetricExporter({ url: buildHttpSignalUrl(endpoint, "metrics"), headers })
: new OTLPMetricExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
? new OTLPProtoMetricExporter({ url: buildHttpSignalUrl(endpoint, "metrics"), headers })
: protocol === "http/json"
? new OTLPHttpMetricExporter({ url: buildHttpSignalUrl(endpoint, "metrics"), headers })
: new OTLPMetricExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
const makeLogExporter = (headers: HeadersMap) => protocol === "http/protobuf"
? new OTLPHttpLogExporter({ url: buildHttpSignalUrl(endpoint, "logs"), headers })
: new OTLPLogExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
? new OTLPProtoLogExporter({ url: buildHttpSignalUrl(endpoint, "logs"), headers })
: protocol === "http/json"
? new OTLPHttpLogExporter({ url: buildHttpSignalUrl(endpoint, "logs"), headers })
: new OTLPLogExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
const makeTraceExporter = (headers: HeadersMap) => protocol === "http/protobuf"
? new OTLPHttpTraceExporter({ url: buildHttpSignalUrl(endpoint, "traces"), headers })
: new OTLPTraceExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
? new OTLPProtoTraceExporter({ url: buildHttpSignalUrl(endpoint, "traces"), headers })
: protocol === "http/json"
? new OTLPHttpTraceExporter({ url: buildHttpSignalUrl(endpoint, "traces"), headers })
: new OTLPTraceExporter({ url: endpoint, metadata: createGrpcMetadata(headers) })
const metricExporter = otlpHeadersHelper
? new RefreshingMetricExporter(makeMetricExporter, dynamicHeaders)
: makeMetricExporter(staticHeaders)
Expand Down
5 changes: 5 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ describe("loadConfig", () => {
expect(loadConfig().protocol).toBe("http/protobuf")
})

test("reads HTTP/json protocol", () => {
process.env["OPENCODE_OTLP_PROTOCOL"] = "http/json"
expect(loadConfig().protocol).toBe("http/json")
})

test("falls back to grpc for unknown protocol", () => {
process.env["OPENCODE_OTLP_PROTOCOL"] = "http"
expect(loadConfig().protocol).toBe("grpc")
Expand Down
66 changes: 65 additions & 1 deletion tests/otel.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
import { describe, test, expect, afterEach } from "bun:test"
import { buildResource } from "../src/otel.ts"
import { OTLPLogExporter as OTLPHttpLogExporter } from "@opentelemetry/exporter-logs-otlp-http"
import { OTLPLogExporter as OTLPProtoLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"
import { OTLPMetricExporter as OTLPHttpMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"
import { OTLPMetricExporter as OTLPProtoMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"
import { OTLPTraceExporter as OTLPHttpTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
import { OTLPTraceExporter as OTLPProtoTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"
import { buildResource, setupOtel, type OtelProviders } from "../src/otel.ts"

let providers: OtelProviders | undefined

function exportersOf(currentProviders: OtelProviders) {
const meterProvider = currentProviders.meterProvider as unknown as {
_sharedState: { metricCollectors: Array<{ _metricReader: { _exporter: unknown } }> }
}
const loggerProvider = currentProviders.loggerProvider as unknown as {
_sharedState: { activeProcessor: { processors: Array<{ _exporter: unknown }> } }
}
const tracerProvider = currentProviders.tracerProvider as unknown as {
_activeSpanProcessor: { _spanProcessors: Array<{ _exporter: unknown }> }
}
const metricCollector = meterProvider._sharedState.metricCollectors[0]
const logProcessor = loggerProvider._sharedState.activeProcessor.processors[0]
const spanProcessor = tracerProvider._activeSpanProcessor._spanProcessors[0]

if (!metricCollector || !logProcessor || !spanProcessor) {
throw new Error("Expected OTEL providers to have active metric/log/trace exporters")
}

return {
metric: metricCollector._metricReader._exporter,
log: logProcessor._exporter,
trace: spanProcessor._exporter,
}
}

describe("buildResource", () => {
const originalEnv = process.env["OTEL_RESOURCE_ATTRIBUTES"]
Expand Down Expand Up @@ -41,3 +74,34 @@ describe("buildResource", () => {
expect(resource.attributes["service.name"]).toBe("my-override")
})
})

describe("setupOtel", () => {
afterEach(async () => {
const current = providers
providers = undefined
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!current) return
await Promise.allSettled([
current.tracerProvider.shutdown(),
current.loggerProvider.shutdown(),
current.meterProvider.shutdown(),
])
})

test("uses protobuf HTTP exporters for http/protobuf", async () => {
providers = await setupOtel("http://collector:4318", "http/protobuf", 60000, 5000, "1.2.3")
const exporters = exportersOf(providers)

expect(exporters.metric).toBeInstanceOf(OTLPProtoMetricExporter)
expect(exporters.log).toBeInstanceOf(OTLPProtoLogExporter)
expect(exporters.trace).toBeInstanceOf(OTLPProtoTraceExporter)
})

test("uses JSON HTTP exporters for http/json", async () => {
providers = await setupOtel("http://collector:4318", "http/json", 60000, 5000, "1.2.3")
const exporters = exportersOf(providers)

expect(exporters.metric).toBeInstanceOf(OTLPHttpMetricExporter)
expect(exporters.log).toBeInstanceOf(OTLPHttpLogExporter)
expect(exporters.trace).toBeInstanceOf(OTLPHttpTraceExporter)
})
})
Loading