Skip to content

Enable managed OTLP trace export for Claude Code - #599

Open
tt-le wants to merge 4 commits into
david/AIGTWY-4573-model-pickerfrom
tien/managed-otel-traces
Open

tt-le wants to merge 4 commits into
david/AIGTWY-4573-model-pickerfrom
tien/managed-otel-traces

Conversation

@tt-le

@tt-le tt-le commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

What

When a managed CodingAgentConfig enables tracing for an agent, ucode writes the OpenTelemetry env plus a refreshing otelHeadersHelper into Claude Code's managed settings, so spans export to the AI Gateway OTLP ingest endpoint (/ai-gateway/otel/v1/traces).

Stacked on #597 — base is david/AIGTWY-4573-model-picker, so this diff is just the one tracing commit.

How

  • managed_config: normalize per-agent AgentConfig.tracing_config.enabledotel_tracing_enabled (tri-state), keeping the deprecated workspace-level CodingAgentConfig.tracing only as a back-compat fallback.
  • managed_resolve: managed_otel_tracing_enabled(managed, tool) — per-agent wins, deprecated global falls back. OTEL_TRACING_TOOLS (claude only today) gates a per-agent {tool}_otel_tracing state override — the extension point for other agents once they support OTLP export.
  • claude: render_overlay writes the trace env (CLAUDE_CODE_ENABLE_TELEMETRY, the CLAUDE_CODE_ENHANCED_TELEMETRY_BETA span gate, OTEL_TRACES_EXPORTER=otlp, http/protobuf, the full /v1/traces endpoint via build_otel_traces_endpoint, debounce) plus otelHeadersHelper. No static OTEL_..._HEADERS — the helper supplies a refreshing bearer. Disabling tracing prunes the keys + helper back out.
  • cli/databricks: ucode otel-headers prints the OTLP header map as JSON ({"Authorization": "Bearer <token>"}), minting the same Databricks token as auth-token.

Environment variables written to configure Claude Code

Written into the env block of Claude Code's managed settings, plus one top-level setting.

Variable Value Why
CLAUDE_CODE_ENABLE_TELEMETRY 1 Master telemetry switch. On its own only emits metrics + events.
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA 1 The span/trace gate — without this, telemetry produces no traces.
OTEL_TRACES_EXPORTER otlp Send traces over OTLP. Metrics/logs exporters intentionally left unset.
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL http/protobuf Per-signal protocol (not gRPC — gRPC ignores the headers helper).
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT {workspace}/ai-gateway/otel/v1/traces Per-signal endpoint. Full /v1/traces path — Claude Code does not append it for HTTP.
CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MS 900000 Re-run interval for the headers helper (15 min). Matches the inference apiKeyHelper TTL.

Top-level setting (not an env var):

Setting Value Why
otelHeadersHelper ucode otel-headers --host <ws> [--profile <p>] Command Claude Code re-runs on the debounce interval for fresh export auth, emitting {"Authorization":"Bearer <token>"}.

All six env keys and otelHeadersHelper are registered as ucode-managed, so disabling tracing prunes them from the settings file.

Global vs per-agent (resolved)

Per the proto change (databricks-eng/universe#2604416): tracing is now per-agent via AgentConfig.tracing_config.enabled; CodingAgentConfig.tracing and TracingConfig.table are deprecated. This PR reads per-agent as primary and keeps the deprecated global only as a fallback (per-agent wins, including an explicit per-agent false over a global true).

Validate

  • uv run ruff check . + format clean; focused suites pass. Tests cover per-agent enable/disable, per-agent-over-global precedence, deprecated-global fallback, the OTEL_TRACING_TOOLS gate, render/write/disable-prune, and the builders.
  • End-to-end via UCODE_MANAGED_CONFIG_STUB (per-agent tracing_config.enabled) confirms the full chain writes the correct env + helper.

This pull request and its description were written by Isaac.

david-siqi-liu and others added 3 commits September 13, 2026 23:23
Land the CodingAgentConfig parsing and the version and watermark helpers on their own, ahead of any
change to launch or configure behavior, so the data-shape change is reviewable in isolation. Nothing
here is called from the launch path yet; the stacked launch-apply PR wires it in.

- managed_config: read the extended config shape (enabled_agents with per-agent AgentModels: names /
  model_service_location / provider, default_model, default_alias_models, http_headers; spend_tiers
  with a budget_policy fallback; update_time). Add managed_config_is_newer and managed_update_time, a
  spec_version forward-compat gate (a newer or malformed spec_version keeps the last-known-good
  cache), and the UCODE_MANAGED_CONFIG_STUB local-file read for pre-server testing.
- mcp_servers and skills each parse to a names-or-location selector: an absolute names list of UC
  FQNs, or a unity_catalog_location (a catalog.schema) whose contents are discovered, enforced
  either/or. Both are UC mcp-services / skills only; there is no per-server type model. managed_setup
  serializes and validates the same shape (names are 3-part FQNs, location a 2-part catalog.schema,
  exactly one set), so serialize then normalize round-trips.
- state: add get/set_applied_managed_update_time, the applied-version watermark the launch gate reads.
- managed_export / managed_setup: parse and export updates; spec_version is server-owned on export so
  a supplied config cannot override the envelope value.
- databricks: switch the managed-config update mask to spend_tiers; rename the user-facing "Unity AI
  Gateway" strings to "Unity Gateway".

Co-authored-by: Isaac <no-reply@databricks.com>
With the config parsed (the stacked parse PR), apply the managed configuration to every enabled
agent at launch, but only when it changed since the last apply (an update_time watermark) or on
--refresh, so an unchanged launch never re-writes the OS-managed files or prompts for a password.
Also remove the post-configure probe validation, wire the managed MCP servers and skills, and rename
user-facing strings.

- Gate the launch apply on update_time. refresh_managed_config always fetches; a launch re-applies
  the managed configuration (the only step that writes the OS-managed files and can prompt for a
  password) only when update_time is newer than the applied watermark, or on ug <agent> --refresh,
  and records the new watermark. An unchanged launch skips the apply and never prompts. ug configure
  always applies and records the watermark. The "was updated; re-applying it" note prints only on a
  genuine version change, not on a no-op --refresh. The per-tool launch write is wrapped in
  suppressed_managed_writes() so the apply-all owns the single OS write.
- Apply the managed config's mcp_servers and skills to each enabled agent. Both are UC-services-only
  selectors: register the absolute names (as UC mcp-services) or download the named skills, plus
  everything discovered under a unity_catalog_location (a schema's mcp-services via list_mcp_services,
  or its skills), unioned. Nothing on the launch path turned the workspace's published servers or
  skills into agent registrations before, so they never showed up.
- `ug configure` wires the managed config's MCP servers and skills at configure time (not only at
  launch), and suppresses the interactive MCP/skills/plugins prompt when the managed config already
  defines them (and never installs anything under --dry-run), so a managed workspace is set up
  without asking the developer to configure MCP.
- Stop ug configure from validating agents with a probe message. Remove the post-configure
  validation and the --skip-validate flag: it is not configure's job to test models, the probe also
  reverted a freshly-written config on failure, and the gateway enforces model access regardless.
  The now-unused validate_all_tools / provider_permission_error / resolve_managed_for_tool helpers
  are removed; validate_tool stays for headless and e2e callers.
- Drop the "Using managed config - launching <agent>" banner and the redundant post-panel "Starting
  <agent>" line from the bare-ug launch, so bare ug matches ug <agent>.
- When a config we previously applied is gone (deleted, or the feature turned off), the launch
  does not auto-revert the OS-managed files; it notes that Unity Gateway managed configurations are
  still applied and points the developer to `ug revert`, but only when the config is definitively
  gone (NOT_FOUND or the feature is off), not on a transient fetch failure. Keyed on the applied
  watermark, so a workspace that never had a config still just uses the developer's own settings.
- Rename the product string "CLI Managed Configuration" to "managed configuration" (lowercase)
  across user-facing lines, tighten the budget-tier recommendation line, and remove the
  --skip-validate docs.

Co-authored-by: Isaac <no-reply@databricks.com>
Resolve the managed config's per-agent inputs into each agent's own config shape: the model
allow-list or discovery location into Claude's /model picker and Codex's model catalog, and the
custom http_headers into each agent's outbound requests. Stacked on the launch-apply PR (#593).

- managed_resolve: add managed_static_models, managed_model_service_location, and
  managed_custom_headers; extend managed_state_overrides to layer claude_static_models /
  codex_static_models, the per-agent model_service_location, and {tool}_custom_headers into state.
  For codex a discovery location alone does not count as supplying models (no reader for it), so
  discovery still runs; for claude it does.
- claude: a static list becomes availableModels + enforceAvailableModels + a modelPicker (exactly
  the named ids, non-Claude included); a discovery location turns on gateway discovery instead.
  Ownership markers prune only ug-owned picker keys on a static-to-discovery transition, and clear
  only when the OS-managed file was written. Managed http_headers append to ANTHROPIC_CUSTOM_HEADERS,
  with ug's fixed headers winning on conflict; values that would break the header serialization
  (newlines, and commas for Gemini) are dropped.
- codex: a static list is written as a model_catalog_json catalog of full ModelPresets; a fetch
  failure preserves an existing catalog (or falls back to discovery when none exists yet), and a
  successful but empty result falls back to discovery.
  Managed http_headers merge into the provider http_headers, ug's fixed headers winning.
- gemini: managed http_headers merge into GEMINI_CLI_CUSTOM_HEADERS, ug's fixed headers winning.
- README: document the Claude picker, the Codex catalog, and the ucode-models.json managed file.

Co-authored-by: Isaac <no-reply@databricks.com>
@tt-le
tt-le force-pushed the tien/managed-otel-traces branch 2 times, most recently from 6f8810b to f1a6660 Compare September 14, 2026 01:37
When a managed CodingAgentConfig enables tracing for an agent, ucode writes the
OpenTelemetry env plus a refreshing `otelHeadersHelper` into Claude Code's managed
settings so spans export to the AI Gateway OTLP ingest endpoint
(`/ai-gateway/otel/v1/traces`).

- managed_config: normalize per-agent `AgentConfig.tracing_config.enabled` →
  `otel_tracing_enabled` (tri-state), with the deprecated workspace-level
  `CodingAgentConfig.tracing` kept only as a back-compat fallback.
- managed_resolve: `managed_otel_tracing_enabled(managed, tool)` — per-agent wins,
  deprecated global falls back; `OTEL_TRACING_TOOLS` (claude only today) gates a
  per-agent `{tool}_otel_tracing` state override, the extension point for other agents.
- claude: render_overlay writes the trace env (`CLAUDE_CODE_ENABLE_TELEMETRY`, the
  `CLAUDE_CODE_ENHANCED_TELEMETRY_BETA` span gate, `OTEL_TRACES_EXPORTER=otlp`,
  `http/protobuf`, the full `/v1/traces` endpoint via `build_otel_traces_endpoint`,
  debounce) plus `otelHeadersHelper`. No static `OTEL_..._HEADERS`. Disable prunes them.
- cli/databricks: `ucode otel-headers` prints the OTLP header map as JSON, minting the
  same Databricks token as `auth-token`.

Co-authored-by: Isaac <no-reply@databricks.com>
@tt-le
tt-le force-pushed the tien/managed-otel-traces branch from f1a6660 to abe9f24 Compare September 14, 2026 15:03
@david-siqi-liu
david-siqi-liu force-pushed the david/AIGTWY-4573-model-picker branch 3 times, most recently from 6edfdf9 to 23d8bd8 Compare September 14, 2026 19:15
@david-siqi-liu
david-siqi-liu force-pushed the david/AIGTWY-4573-model-picker branch 8 times, most recently from 6b8bb9d to 329ebdd Compare September 15, 2026 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants