From 8f1a0db463fa42f2c78d5ef09e501e0b0f6131cf Mon Sep 17 00:00:00 2001 From: Morgan Westlee Lunt Date: Mon, 27 Apr 2026 23:22:18 +0000 Subject: [PATCH 1/3] feat(claude-code): add telemetry input for OTEL export with workspace attribution Rebased onto post-#861 main. Adds a typed telemetry input that turns on CLAUDE_CODE_ENABLE_TELEMETRY and the standard OTEL_EXPORTER_OTLP_* env vars in one place, and auto-injects coder.workspace_id, coder.workspace_name, coder.workspace_owner, and coder.template_name into OTEL_RESOURCE_ATTRIBUTES so Claude Code spans and metrics can be joined directly with Coder audit logs and exectrace records on workspace_id without per-template wiring. Bumps README examples to 5.1.0. --- registry/coder/modules/claude-code/README.md | 42 ++++++++++--- .../coder/modules/claude-code/main.test.ts | 37 +++++++++++ registry/coder/modules/claude-code/main.tf | 61 +++++++++++++++++++ 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index d3264256f..b10e72bd0 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Install and configure the [Claude Code](https://docs.anthropic.com/en/docs/agent ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id anthropic_api_key = "xxxx-xxxxx-xxxx" } @@ -47,7 +47,7 @@ locals { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = local.claude_workdir anthropic_api_key = "xxxx-xxxxx-xxxx" @@ -78,7 +78,7 @@ resource "coder_app" "claude" { ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_ai_gateway = true @@ -102,7 +102,7 @@ This example shows version pinning, a pre-installed binary path, a custom model, ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -166,7 +166,7 @@ Downstream `coder_script` resources can wait for this module's install pipeline ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" anthropic_api_key = "xxxx-xxxxx-xxxx" @@ -252,7 +252,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -309,7 +309,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" @@ -341,6 +341,34 @@ module "claude-code" { > [!NOTE] > For additional Vertex AI configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Vertex AI documentation](https://docs.claude.com/en/docs/claude-code/google-vertex-ai). +### Telemetry export (OpenTelemetry) + +Claude Code can emit OpenTelemetry metrics and events covering token usage, tool calls, session lifecycle, and errors (see the [monitoring docs](https://docs.anthropic.com/en/docs/claude-code/monitoring-usage)). Set `telemetry.enabled = true` and point `otlp_endpoint` at your OTLP collector. + +The module automatically tags every span and metric with `coder.workspace_id`, `coder.workspace_name`, `coder.workspace_owner`, and `coder.template_name` via `OTEL_RESOURCE_ATTRIBUTES`, so Claude Code telemetry can be joined directly against Coder's [audit logs](https://coder.com/docs/admin/security/audit-logs) and `exectrace` records on `workspace_id`. + +```tf +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "5.1.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + anthropic_api_key = "xxxx-xxxxx-xxxx" + + telemetry = { + enabled = true + otlp_endpoint = "http://otel-collector.observability:4317" + otlp_protocol = "grpc" + otlp_headers = { + authorization = "Bearer ${var.otel_token}" + } + resource_attributes = { + "service.name" = "claude-code" + } + } +} +``` + ## Troubleshooting If you encounter any issues, check the log files in the `~/.coder-modules/coder/claude-code/logs` directory within your workspace for detailed information. diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 6b1367551..bee682d63 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -435,4 +435,41 @@ describe("claude-code", async () => { ]); expect(resp.stdout.trim()).toBe("ABSENT"); }); + + test("telemetry-otel", async () => { + const { coderEnvVars } = await setup({ + moduleVariables: { + telemetry: JSON.stringify({ + enabled: true, + otlp_endpoint: "http://otel-collector:4317", + otlp_protocol: "grpc", + otlp_headers: { authorization: "Bearer test-token" }, + resource_attributes: { "service.name": "claude-code" }, + }), + }, + }); + expect(coderEnvVars["CLAUDE_CODE_ENABLE_TELEMETRY"]).toBe("1"); + expect(coderEnvVars["OTEL_EXPORTER_OTLP_ENDPOINT"]).toBe( + "http://otel-collector:4317", + ); + expect(coderEnvVars["OTEL_EXPORTER_OTLP_PROTOCOL"]).toBe("grpc"); + expect(coderEnvVars["OTEL_EXPORTER_OTLP_HEADERS"]).toBe( + "authorization=Bearer test-token", + ); + const attrs = coderEnvVars["OTEL_RESOURCE_ATTRIBUTES"]; + expect(attrs).toContain("coder.workspace_id="); + expect(attrs).toContain("coder.workspace_name="); + expect(attrs).toContain("coder.workspace_owner="); + expect(attrs).toContain("coder.template_name="); + expect(attrs).toContain("service.name=claude-code"); + }); + + test("telemetry-disabled-by-default", async () => { + const { coderEnvVars } = await setup(); + expect(coderEnvVars["CLAUDE_CODE_ENABLE_TELEMETRY"]).toBeUndefined(); + expect(coderEnvVars["OTEL_EXPORTER_OTLP_ENDPOINT"]).toBeUndefined(); + expect(coderEnvVars["OTEL_EXPORTER_OTLP_PROTOCOL"]).toBeUndefined(); + expect(coderEnvVars["OTEL_EXPORTER_OTLP_HEADERS"]).toBeUndefined(); + expect(coderEnvVars["OTEL_RESOURCE_ATTRIBUTES"]).toBeUndefined(); + }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 346930db9..44ccebc2d 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -118,6 +118,18 @@ variable "enable_ai_gateway" { } } +variable "telemetry" { + type = object({ + enabled = optional(bool, false) + otlp_endpoint = optional(string, "") + otlp_protocol = optional(string, "http/protobuf") + otlp_headers = optional(map(string), {}) + resource_attributes = optional(map(string), {}) + }) + default = {} + description = "Configure Claude Code OpenTelemetry export. When enabled, sets CLAUDE_CODE_ENABLE_TELEMETRY and the standard OTEL_EXPORTER_OTLP_* environment variables. Coder workspace identifiers (coder.workspace_id, coder.workspace_name, coder.workspace_owner, coder.template_name) are automatically appended to OTEL_RESOURCE_ATTRIBUTES so Claude Code telemetry can be joined with Coder audit and exectrace logs." +} + resource "coder_env" "claude_code_oauth_token" { count = var.claude_code_oauth_token != "" ? 1 : 0 agent_id = var.agent_id @@ -163,6 +175,55 @@ resource "coder_env" "anthropic_base_url" { value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" } +locals { + # Always inject Coder workspace identifiers so OTEL data can be joined with + # Coder's audit log / exectrace on workspace_id without per-template wiring. + otel_resource_attributes = merge( + var.telemetry.resource_attributes, + { + "coder.workspace_id" = data.coder_workspace.me.id + "coder.workspace_name" = data.coder_workspace.me.name + "coder.workspace_owner" = data.coder_workspace_owner.me.name + "coder.template_name" = data.coder_workspace.me.template_name + }, + ) +} + +resource "coder_env" "claude_code_enable_telemetry" { + count = var.telemetry.enabled ? 1 : 0 + agent_id = var.agent_id + name = "CLAUDE_CODE_ENABLE_TELEMETRY" + value = "1" +} + +resource "coder_env" "otel_exporter_otlp_endpoint" { + count = var.telemetry.enabled && var.telemetry.otlp_endpoint != "" ? 1 : 0 + agent_id = var.agent_id + name = "OTEL_EXPORTER_OTLP_ENDPOINT" + value = var.telemetry.otlp_endpoint +} + +resource "coder_env" "otel_exporter_otlp_protocol" { + count = var.telemetry.enabled ? 1 : 0 + agent_id = var.agent_id + name = "OTEL_EXPORTER_OTLP_PROTOCOL" + value = var.telemetry.otlp_protocol +} + +resource "coder_env" "otel_exporter_otlp_headers" { + count = var.telemetry.enabled && length(var.telemetry.otlp_headers) > 0 ? 1 : 0 + agent_id = var.agent_id + name = "OTEL_EXPORTER_OTLP_HEADERS" + value = join(",", [for k, v in var.telemetry.otlp_headers : "${k}=${v}"]) +} + +resource "coder_env" "otel_resource_attributes" { + count = var.telemetry.enabled ? 1 : 0 + agent_id = var.agent_id + name = "OTEL_RESOURCE_ATTRIBUTES" + value = join(",", [for k, v in local.otel_resource_attributes : "${k}=${v}"]) +} + locals { workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { From 2083cb523f4469575cc4d23a43cde2c9870b3c8f Mon Sep 17 00:00:00 2001 From: DevCats Date: Tue, 28 Apr 2026 11:32:35 -0500 Subject: [PATCH 2/3] chore: add recommended data fields into telemetry data Co-authored-by: Atif Ali --- registry/coder/modules/claude-code/main.tf | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 44ccebc2d..d23df0e85 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -181,10 +181,13 @@ locals { otel_resource_attributes = merge( var.telemetry.resource_attributes, { - "coder.workspace_id" = data.coder_workspace.me.id - "coder.workspace_name" = data.coder_workspace.me.name - "coder.workspace_owner" = data.coder_workspace_owner.me.name - "coder.template_name" = data.coder_workspace.me.template_name + "coder.workspace_id" = data.coder_workspace.me.id + "coder.workspace_name" = data.coder_workspace.me.name + "coder.template_name" = data.coder_workspace.me.template_name + "coder.template_version" = data.coder_workspace.me.template_version + "coder.access_url" = data.coder_workspace.me.access_url + "coder.workspace_owner_id" = data.coder_workspace_owner.me.id + "coder.workspace_owner_username" = data.coder_workspace_owner.me.name }, ) } From 2da7ab3643d05caddb5c1ff6acd9221c52b55eeb Mon Sep 17 00:00:00 2001 From: DevCats Date: Wed, 29 Apr 2026 15:45:07 +0000 Subject: [PATCH 3/3] fix(claude-code): emit coder.workspace_owner OTEL resource attribute The telemetry-otel test asserts OTEL_RESOURCE_ATTRIBUTES contains 'coder.workspace_owner=', and the variable description and PR body both document that key, but the locals block emitted 'coder.workspace_owner_username' instead, causing the bun test to fail in CI. Rename the username key back to 'coder.workspace_owner' (the data.coder_workspace_owner.me.name value) so the documented contract and the test agree. 'coder.workspace_owner_id' is retained. --- registry/coder/modules/claude-code/main.tf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index d23df0e85..acfe85387 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -181,13 +181,13 @@ locals { otel_resource_attributes = merge( var.telemetry.resource_attributes, { - "coder.workspace_id" = data.coder_workspace.me.id - "coder.workspace_name" = data.coder_workspace.me.name - "coder.template_name" = data.coder_workspace.me.template_name - "coder.template_version" = data.coder_workspace.me.template_version - "coder.access_url" = data.coder_workspace.me.access_url - "coder.workspace_owner_id" = data.coder_workspace_owner.me.id - "coder.workspace_owner_username" = data.coder_workspace_owner.me.name + "coder.workspace_id" = data.coder_workspace.me.id + "coder.workspace_name" = data.coder_workspace.me.name + "coder.workspace_owner" = data.coder_workspace_owner.me.name + "coder.workspace_owner_id" = data.coder_workspace_owner.me.id + "coder.template_name" = data.coder_workspace.me.template_name + "coder.template_version" = data.coder_workspace.me.template_version + "coder.access_url" = data.coder_workspace.me.access_url }, ) }