Skip to content
Open
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
28 changes: 28 additions & 0 deletions registry/coder/modules/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,34 @@ module "claude-code" {
>
> The `Content-Type` header doesn't matter—both `text/plain` and `application/json` work fine.
### Telemetry export (OpenTelemetry)
Claude Code can emit OpenTelemetry metrics and events covering token usage, tool calls, session lifecycle, and errors. 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 = "4.9.2"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
telemetry = {
enabled = true
otlp_endpoint = "http://otel-collector.observability:4317"
otlp_protocol = "grpc"
otlp_headers = {
authorization = "Bearer ${var.otel_collector_token}"
}
resource_attributes = {
"service.name" = "claude-code"
"deployment.cluster" = var.cluster_name
}
}
}
```
See the [Claude Code monitoring documentation](https://docs.anthropic.com/en/docs/claude-code/monitoring-usage) for the full list of exported metrics and events.

### Standalone Mode

Run and configure Claude Code as a standalone CLI in your workspace.
Expand Down
34 changes: 34 additions & 0 deletions registry/coder/modules/claude-code/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,40 @@ describe("claude-code", async () => {
expect(coderEnvVars["ANTHROPIC_MODEL"]).toBe(model);
});

test("telemetry-otel", async () => {
const { coderEnvVars } = await setup({
moduleVariables: {
telemetry: JSON.stringify({
enabled: true,
otlp_endpoint: "http://collector:4317",
otlp_headers: { authorization: "Bearer xyz" },
resource_attributes: { "service.name": "claude-code" },
}),
},
});

expect(coderEnvVars["CLAUDE_CODE_ENABLE_TELEMETRY"]).toBe("1");
expect(coderEnvVars["OTEL_EXPORTER_OTLP_ENDPOINT"]).toBe(
"http://collector:4317",
);
expect(coderEnvVars["OTEL_EXPORTER_OTLP_PROTOCOL"]).toBe("http/protobuf");
expect(coderEnvVars["OTEL_EXPORTER_OTLP_HEADERS"]).toBe(
"authorization=Bearer xyz",
);
const attrs = coderEnvVars["OTEL_RESOURCE_ATTRIBUTES"];
expect(attrs).toContain("coder.workspace_id=");
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_RESOURCE_ATTRIBUTES"]).toBeUndefined();
});

test("claude-continue-resume-task-session", async () => {
const { id } = await setup({
moduleVariables: {
Expand Down
57 changes: 57 additions & 0 deletions registry/coder/modules/claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,18 @@ variable "enable_state_persistence" {
default = true
}

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), {})
})
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."
default = {}
}

resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
agent_id = var.agent_id
Expand Down Expand Up @@ -322,6 +334,41 @@ resource "coder_env" "anthropic_base_url" {
value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic"
}

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 != null ? 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 {
# we have to trim the slash because otherwise coder exp mcp will
# set up an invalid claude config
Expand All @@ -334,6 +381,16 @@ locals {
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
claude_api_key = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key

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
},
)

# Required prompts for the module to properly report task status to Coder
report_tasks_system_prompt = <<-EOT
-- Tool Selection --
Expand Down
Loading