Skip to content

Commit 078d8a0

Browse files
committed
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.
1 parent 1ee712e commit 078d8a0

3 files changed

Lines changed: 64 additions & 1 deletion

File tree

packages/opencode/src/control-plane/workspace.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const create = fn(CreateInput, async (input) => {
117117
OPENCODE_EXPERIMENTAL_WORKSPACES: "true",
118118
OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS,
119119
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
120+
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
120121
}
121122
await adaptor.create(config, env)
122123

packages/opencode/src/effect/observability.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,29 @@ const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
2121
)
2222
: undefined
2323

24-
function resource() {
24+
export function resource(): { serviceName: string, serviceVersion: string, attributes: Record<string, string> } {
2525
const processMetadata = ensureProcessMetadata("main")
26+
const attributes: Record<string, string> = (() => {
27+
const value = process.env.OTEL_RESOURCE_ATTRIBUTES
28+
if (!value) return {}
29+
try {
30+
return Object.fromEntries(
31+
value.split(",").map((entry) => {
32+
const index = entry.indexOf("=")
33+
if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry")
34+
return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))]
35+
}),
36+
)
37+
} catch {
38+
return {}
39+
}
40+
})()
41+
2642
return {
2743
serviceName: "opencode",
2844
serviceVersion: InstallationVersion,
2945
attributes: {
46+
...attributes,
3047
"deployment.environment.name": InstallationChannel,
3148
"opencode.client": Flag.OPENCODE_CLIENT,
3249
"opencode.process_role": processMetadata.processRole,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { afterEach, describe, expect, test } from "bun:test"
2+
import { resource } from "../../src/effect/observability"
3+
4+
const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES
5+
const opencodeClient = process.env.OPENCODE_CLIENT
6+
7+
afterEach(() => {
8+
if (otelResourceAttributes === undefined) delete process.env.OTEL_RESOURCE_ATTRIBUTES
9+
else process.env.OTEL_RESOURCE_ATTRIBUTES = otelResourceAttributes
10+
11+
if (opencodeClient === undefined) delete process.env.OPENCODE_CLIENT
12+
else process.env.OPENCODE_CLIENT = opencodeClient
13+
})
14+
15+
describe("resource", () => {
16+
test("parses and decodes OTEL resource attributes", () => {
17+
process.env.OTEL_RESOURCE_ATTRIBUTES =
18+
"service.namespace=anomalyco,team=platform%2Cobservability,label=hello%3Dworld,key%2Fname=value%20here"
19+
20+
expect(resource().attributes).toMatchObject({
21+
"service.namespace": "anomalyco",
22+
team: "platform,observability",
23+
label: "hello=world",
24+
"key/name": "value here",
25+
})
26+
})
27+
28+
test("drops OTEL resource attributes when any entry is invalid", () => {
29+
process.env.OTEL_RESOURCE_ATTRIBUTES = "service.namespace=anomalyco,broken"
30+
31+
expect(resource().attributes["service.namespace"]).toBeUndefined()
32+
expect(resource().attributes["opencode.client"]).toBeDefined()
33+
})
34+
35+
test("keeps built-in attributes when env values conflict", () => {
36+
process.env.OPENCODE_CLIENT = "cli"
37+
process.env.OTEL_RESOURCE_ATTRIBUTES = "opencode.client=web,service.instance.id=override,service.namespace=anomalyco"
38+
39+
expect(resource().attributes).toMatchObject({
40+
"opencode.client": "cli",
41+
"service.namespace": "anomalyco",
42+
})
43+
expect(resource().attributes["service.instance.id"]).not.toBe("override")
44+
})
45+
})

0 commit comments

Comments
 (0)