Skip to content

Commit fb88870

Browse files
committed
Add settings and env surface for opt-in OTEL export
Resolves CL-5175: otel-config.ts parses and validates endpoint, headers, service name, and resource attributes from settings + env; fails closed on invalid config with a stable OTEL_CONFIG_INVALID error. otelConfigForDump exposes header names only so secrets never reach privacy-strict dumps. Docs cover Phoenix, PostHog OTEL, and generic collectors. The actual OTLP transport is a follow-up (CL-5173).
1 parent 73158b2 commit fb88870

7 files changed

Lines changed: 858 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,4 @@ Interchange is the standard library for this repo, consumed as published `@intx/
7171
- `docs/MCP.md` — connecting MCP servers
7272
- `docs/PLUGINS.md` — plugin manifest system and discovery
7373
- `docs/TELEMETRY.md` — what usage telemetry is collected and why
74+
- `docs/PERFTRACE.md` — local PerfTrace and opt-in OTEL export settings

docs/PERFTRACE.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Performance tracing (PerfTrace) and OTEL export
2+
3+
Corbits Code measures session performance with an always-on local tracer
4+
(`src/perf/`). Optional OpenTelemetry export sends the same span tree to **your**
5+
collector. This is separate from product analytics — see `docs/TELEMETRY.md` for
6+
PostHog usage events.
7+
8+
## Local sink (always on)
9+
10+
- In-process ring buffer of phase spans (turn, inference, tools, …)
11+
- Privacy-strict tags: enums, ids, and numbers only — no prompts, paths, tool
12+
args, free-text errors, or credentials
13+
- Future session dumps (CL-5169) use the same allowlist and must never include
14+
OTEL auth headers
15+
16+
Local measurement does not require any settings or env vars.
17+
18+
## OTEL export (opt-in)
19+
20+
Export is **off** until an OTLP endpoint is configured. When enabled, traces go
21+
to the operator-owned backend you point at — not Corbits product analytics.
22+
23+
The settings/env surface is implemented now (`src/perf/otel-config.ts`). The
24+
actual OTLP transport lands in a follow-up (CL-5173). Invalid config fails
25+
closed with a stable error code `OTEL_CONFIG_INVALID` and does not half-enable
26+
export.
27+
28+
### Configuration
29+
30+
**Env vars (preferred for secrets; match OTEL conventions):**
31+
32+
| Variable | Meaning |
33+
|---|---|
34+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP base URL (`http` or `https` only) |
35+
| `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` headers (values may be percent-encoded) |
36+
| `OTEL_SERVICE_NAME` | Resource `service.name` (default: `corbits-code`) |
37+
| `OTEL_RESOURCE_ATTRIBUTES` | Comma-separated `key=value` resource attributes |
38+
39+
**Global settings** (`~/.corbits/settings.json`), optional `otel` block:
40+
41+
```json
42+
{
43+
"otel": {
44+
"enabled": true,
45+
"endpoint": "https://collector.example/v1",
46+
"headers": { "Authorization": "Bearer …" },
47+
"serviceName": "corbits-code",
48+
"resourceAttributes": {
49+
"deployment.environment": "dev"
50+
}
51+
}
52+
}
53+
```
54+
55+
Precedence:
56+
57+
- **endpoint:** env overrides settings
58+
- **headers:** when `OTEL_EXPORTER_OTLP_HEADERS` is set, it fully replaces
59+
settings headers (prefer env so secrets stay out of the settings file)
60+
- **serviceName:** env > settings > `corbits-code`
61+
- **resourceAttributes:** settings merged with env; env wins on key conflict
62+
- **`otel.enabled: false`:** disables export when only settings provide an
63+
endpoint; an explicit env endpoint still enables export
64+
65+
Do not put credentials in the endpoint URL (`https://user:pass@…` is rejected).
66+
Use headers instead.
67+
68+
### Fail closed
69+
70+
Any of the following yields `OTEL_CONFIG_INVALID` and must not start export:
71+
72+
- Endpoint that is not a valid `http`/`https` URL
73+
- Credentials embedded in the endpoint URL
74+
- Headers (settings or env) without an endpoint
75+
- `otel.enabled: true` without an endpoint
76+
- Malformed `key=value` lists for headers or resource attributes
77+
78+
No endpoint and no half-config → export stays disabled (not an error).
79+
80+
### Secrets and dumps
81+
82+
- Header **values** are secrets. Prefer env for them.
83+
- `otelConfigForDump()` exposes only: enabled flag, endpoint, service name,
84+
resource attributes, and header **names** — never values.
85+
- Local privacy-strict dump writers must call `otelConfigForDump` (or omit OTEL
86+
config entirely). Never serialize `OtelExportConfig.headers` into session
87+
artifacts, logs, or crash dumps.
88+
89+
### Targeting common collectors
90+
91+
Examples assume the OTLP HTTP base URL your collector documents. Paths such as
92+
`/v1/traces` are appended by the exporter (CL-5173), not by this settings layer.
93+
94+
#### Arize Phoenix
95+
96+
Local Phoenix typically listens for OTLP HTTP on port 6006:
97+
98+
```bash
99+
export OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:6006"
100+
export OTEL_SERVICE_NAME="corbits-code"
101+
```
102+
103+
Cloud / authenticated Phoenix: set the project endpoint and pass the API key as
104+
a header (exact header name follows Phoenix’s current docs):
105+
106+
```bash
107+
export OTEL_EXPORTER_OTLP_ENDPOINT="https://app.phoenix.arize.com/v1/traces"
108+
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20<phoenix-api-key>"
109+
export OTEL_SERVICE_NAME="corbits-code"
110+
```
111+
112+
#### PostHog OTEL
113+
114+
PostHog can ingest OTLP independently of Corbits product telemetry. Use your
115+
project’s OTEL endpoint and project API key as documented by PostHog:
116+
117+
```bash
118+
export OTEL_EXPORTER_OTLP_ENDPOINT="https://us.i.posthog.com/i/v0/otlp"
119+
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20<phc_…>"
120+
export OTEL_SERVICE_NAME="corbits-code"
121+
```
122+
123+
This does **not** expand the three PostHog product events in `docs/TELEMETRY.md`.
124+
Product analytics opt-out (`CORBITS_TELEMETRY`, `DO_NOT_TRACK`, settings) does
125+
not control OTEL export, and vice versa.
126+
127+
#### Generic OTLP collector (Jaeger, Grafana Alloy, otel-collector, …)
128+
129+
Point at any OTLP-compatible base URL:
130+
131+
```bash
132+
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
133+
export OTEL_SERVICE_NAME="corbits-code"
134+
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=local,service.namespace=dev"
135+
```
136+
137+
Or in settings without secrets:
138+
139+
```json
140+
{
141+
"otel": {
142+
"endpoint": "http://localhost:4318",
143+
"serviceName": "corbits-code",
144+
"resourceAttributes": {
145+
"deployment.environment": "local"
146+
}
147+
}
148+
}
149+
```
150+
151+
Then supply auth only via env when needed.
152+
153+
## Relationship to product telemetry
154+
155+
| Pipe | Purpose | Default | Content |
156+
|---|---|---|---|
157+
| PostHog (`docs/TELEMETRY.md`) | Aggregate product usage | Opt-out | Three allowlisted events |
158+
| Local PerfTrace | Operator/dev attribution | Always on | Privacy-strict phase spans |
159+
| OTEL export | Your APM / Phoenix / collector | Opt-in | Full span tree when enabled |
160+
161+
Do not enlarge the PostHog event schema for performance diagnostics.

docs/TELEMETRY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,10 @@ it.
8686
Events are sent to PostHog. PostHog derives an approximate country from the
8787
request IP server-side; the client sends no location data itself. No
8888
self-hosted or third-party analytics beyond PostHog are used.
89+
90+
## Not this document
91+
92+
Local performance tracing and optional OpenTelemetry export to an operator-owned
93+
collector (Phoenix, PostHog OTEL, Jaeger, generic OTLP) are documented in
94+
`docs/PERFTRACE.md`. That pipe is separate: it does not expand these three
95+
events, and product telemetry opt-out does not control OTEL export.

src/config/settings.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ export type Settings = {
123123
installationId?: string;
124124
noticeShown?: boolean;
125125
};
126+
// Opt-in OTEL export (operator-owned collector). Separate from PostHog product
127+
// telemetry. Prefer OTEL_* env vars for secrets; see docs/PERFTRACE.md.
128+
// Local PerfTrace remains always-on regardless of this block.
129+
otel?: {
130+
enabled?: boolean;
131+
endpoint?: string;
132+
headers?: Record<string, string>;
133+
serviceName?: string;
134+
resourceAttributes?: Record<string, string>;
135+
};
126136
};
127137

128138
// Maps the settings shell block to the shape the shell-guard plugin expects.
@@ -384,6 +394,13 @@ const SettingsSchema = type({
384394
"installationId?": "string",
385395
"noticeShown?": "boolean",
386396
}),
397+
"otel?": type({
398+
"enabled?": "boolean",
399+
"endpoint?": "string",
400+
"headers?": "Record<string, string>",
401+
"serviceName?": "string",
402+
"resourceAttributes?": "Record<string, string>",
403+
}),
387404
});
388405

389406
// Per-entry MCP shape without the name key. The "exactly one transport" rule is
@@ -543,6 +560,7 @@ export const GLOBAL_SETTINGS_OPTIONAL_KEYS = [
543560
"shell",
544561
"tools",
545562
"telemetry",
563+
"otel",
546564
] as const satisfies readonly (keyof OptionalSettingsFields)[];
547565

548566
/** Optional local settings keys the load path is required to consider. */
@@ -615,6 +633,7 @@ export async function loadSettings(path: string): Promise<Settings | null> {
615633
shell: s.shell as Settings["shell"] | undefined,
616634
tools: s.tools as Settings["tools"] | undefined,
617635
telemetry: s.telemetry as Settings["telemetry"] | undefined,
636+
otel: s.otel as Settings["otel"] | undefined,
618637
};
619638
return {
620639
providers: s.providers as Settings["providers"],

src/perf/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ export {
1515
type TransportKind,
1616
} from "./sanitize.js";
1717

18+
export {
19+
DEFAULT_OTEL_SERVICE_NAME,
20+
OTEL_CONFIG_INVALID,
21+
OTEL_ENV,
22+
OtelConfigError,
23+
isOtelConfigInvalid,
24+
otelConfigForDump,
25+
parseOtelKeyValueList,
26+
requireOtelExportConfig,
27+
resolveOtelExportConfig,
28+
type DisabledOtelExportConfig,
29+
type EnabledOtelExportConfig,
30+
type OtelConfigResolution,
31+
type OtelExportConfig,
32+
type OtelExportConfigDumpView,
33+
type OtelSettings,
34+
} from "./otel-config.js";
35+
1836
/** Core + adapter phase names. Adapters extend; they do not invent new sinks. */
1937
export const SPAN_NAMES = [
2038
"session",

0 commit comments

Comments
 (0)