summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-04-18 11:20:29 -0400
committerDax Raad <[email protected]>2026-04-18 11:20:29 -0400
commit078d8a07cf848a7dd8067121d7c21c00d717b9d6 (patch)
treeeaf57667abf5e027739569c974baf4b3645b595d
parent1ee712e549f8e31e38d70abd600ed48010659f8a (diff)
downloadopencode-078d8a07cf848a7dd8067121d7c21c00d717b9d6.tar.gz
opencode-078d8a07cf848a7dd8067121d7c21c00d717b9d6.zip
core: support OTEL_RESOURCE_ATTRIBUTES environment variable for custom telemetry attributes
Users can now pass custom OpenTelemetry resource attributes via the OTEL_RESOURCE_ATTRIBUTES environment variable (comma-separated key=value format). These attributes are automatically included in all telemetry data sent from both the main process and workspace environments, enabling better observability integration with existing monitoring systems that rely on custom resource tags.
-rw-r--r--packages/opencode/src/control-plane/workspace.ts1
-rw-r--r--packages/opencode/src/effect/observability.ts19
-rw-r--r--packages/opencode/test/effect/observability.test.ts45
3 files changed, 64 insertions, 1 deletions
diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts
index e94d6c2c9..eb689df02 100644
--- a/packages/opencode/src/control-plane/workspace.ts
+++ b/packages/opencode/src/control-plane/workspace.ts
@@ -117,6 +117,7 @@ export const create = fn(CreateInput, async (input) => {
OPENCODE_EXPERIMENTAL_WORKSPACES: "true",
OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS,
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+ OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
}
await adaptor.create(config, env)
diff --git a/packages/opencode/src/effect/observability.ts b/packages/opencode/src/effect/observability.ts
index 1c385d60a..fd719fd35 100644
--- a/packages/opencode/src/effect/observability.ts
+++ b/packages/opencode/src/effect/observability.ts
@@ -21,12 +21,29 @@ const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
)
: undefined
-function resource() {
+export function resource(): { serviceName: string, serviceVersion: string, attributes: Record<string, string> } {
const processMetadata = ensureProcessMetadata("main")
+ const attributes: Record<string, string> = (() => {
+ const value = process.env.OTEL_RESOURCE_ATTRIBUTES
+ if (!value) return {}
+ try {
+ return Object.fromEntries(
+ value.split(",").map((entry) => {
+ const index = entry.indexOf("=")
+ if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry")
+ return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))]
+ }),
+ )
+ } catch {
+ return {}
+ }
+ })()
+
return {
serviceName: "opencode",
serviceVersion: InstallationVersion,
attributes: {
+ ...attributes,
"deployment.environment.name": InstallationChannel,
"opencode.client": Flag.OPENCODE_CLIENT,
"opencode.process_role": processMetadata.processRole,
diff --git a/packages/opencode/test/effect/observability.test.ts b/packages/opencode/test/effect/observability.test.ts
new file mode 100644
index 000000000..dd380a2de
--- /dev/null
+++ b/packages/opencode/test/effect/observability.test.ts
@@ -0,0 +1,45 @@
+import { afterEach, describe, expect, test } from "bun:test"
+import { resource } from "../../src/effect/observability"
+
+const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES
+const opencodeClient = process.env.OPENCODE_CLIENT
+
+afterEach(() => {
+ if (otelResourceAttributes === undefined) delete process.env.OTEL_RESOURCE_ATTRIBUTES
+ else process.env.OTEL_RESOURCE_ATTRIBUTES = otelResourceAttributes
+
+ if (opencodeClient === undefined) delete process.env.OPENCODE_CLIENT
+ else process.env.OPENCODE_CLIENT = opencodeClient
+})
+
+describe("resource", () => {
+ test("parses and decodes OTEL resource attributes", () => {
+ process.env.OTEL_RESOURCE_ATTRIBUTES =
+ "service.namespace=anomalyco,team=platform%2Cobservability,label=hello%3Dworld,key%2Fname=value%20here"
+
+ expect(resource().attributes).toMatchObject({
+ "service.namespace": "anomalyco",
+ team: "platform,observability",
+ label: "hello=world",
+ "key/name": "value here",
+ })
+ })
+
+ test("drops OTEL resource attributes when any entry is invalid", () => {
+ process.env.OTEL_RESOURCE_ATTRIBUTES = "service.namespace=anomalyco,broken"
+
+ expect(resource().attributes["service.namespace"]).toBeUndefined()
+ expect(resource().attributes["opencode.client"]).toBeDefined()
+ })
+
+ test("keeps built-in attributes when env values conflict", () => {
+ process.env.OPENCODE_CLIENT = "cli"
+ process.env.OTEL_RESOURCE_ATTRIBUTES = "opencode.client=web,service.instance.id=override,service.namespace=anomalyco"
+
+ expect(resource().attributes).toMatchObject({
+ "opencode.client": "cli",
+ "service.namespace": "anomalyco",
+ })
+ expect(resource().attributes["service.instance.id"]).not.toBe("override")
+ })
+})