Skip to content

Commit 7b0c2be

Browse files
committed
feat(agent): export cloud run telemetry to PostHog Logs and APM over OTLP
Adds OtelRunTelemetry, a SessionLogWriter sink that ships an allowlisted metadata subset of the session log (run/turn/tool lifecycle, usage, errors; never message content or tool arguments) to PostHog Logs, and, when a traces URL is configured, builds one APM trace per run (task_run root span, a turn span per prompt, a tool_call:<kind> span per tool call) with trace/span ids stamped on log records so Logs and APM cross-link. Resource attributes carry run_id/task_id/team_id/user_id/distinct_id so cloud runs are filterable per user in the Logs UI. Configured via POSTHOG_AGENT_OTEL_LOGS_URL/_TOKEN (+ optional POSTHOG_AGENT_OTEL_TRACES_URL), deliberately not standard OTEL_* names so OTel SDKs in user code running in the sandbox never auto-export into the telemetry project. Telemetry stays off unless the logs pair is set, flushes on session cleanup/terminal errors, and can never break session log persistence (sink failures are isolated). Replaces the unwired otel-log-writer whose default endpoint (/i/v1/agent-logs) does not exist in the ingest service, and removes the dead AgentConfig.otelTransport field. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
1 parent 4d3addd commit 7b0c2be

15 files changed

Lines changed: 1604 additions & 249 deletions

packages/agent/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@ Required environment variables (validated by zod in `src/server/bin.ts`):
166166
- `POSTHOG_PERSONAL_API_KEY` — API key for PostHog requests
167167
- `POSTHOG_PROJECT_ID` — numeric project ID
168168

169+
Optional run telemetry (the logs pair must both be set, otherwise telemetry stays off):
170+
171+
- `POSTHOG_AGENT_OTEL_LOGS_URL` — full OTLP logs URL for run metadata, e.g. `https://us.i.posthog.com/i/v1/logs`
172+
- `POSTHOG_AGENT_OTEL_LOGS_TOKEN` — project API key of the telemetry project
173+
- `POSTHOG_AGENT_OTEL_TRACES_URL` — full OTLP traces URL, e.g. `https://us.i.posthog.com/i/v1/traces`; additionally enables one APM trace per run
174+
175+
When set, `AgentServer` ships an allowlisted metadata subset of the session log (run/turn/tool lifecycle, usage, errors — never message content or tool arguments; see `src/otel-telemetry.ts`) to PostHog Logs, tagged with `service.name=posthog-code-agent` and `run_id`/`task_id`/`team_id`/`user_id`/`distinct_id` resource attributes so cloud runs are filterable per user. With the traces URL set, each run also produces an APM trace (`task_run` root span, a `turn` span per prompt, a `tool_call:<kind>` span per tool call; see `src/otel-trace-builder.ts`), and log records carry the matching trace/span ids so Logs and APM cross-link.
176+
169177
## Agent SDK
170178

171179
The `Agent` class (`src/agent.ts`) is the entrypoint for local/programmatic usage. It handles LLM gateway configuration, log writer setup, and model filtering — then delegates to `createAcpConnection()`.

packages/agent/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@
117117
"node": ">=20.0.0"
118118
},
119119
"devDependencies": {
120-
"@posthog/shared": "workspace:*",
121-
"@posthog/git": "workspace:*",
122120
"@posthog/enricher": "workspace:*",
121+
"@posthog/git": "workspace:*",
122+
"@posthog/shared": "workspace:*",
123123
"@types/bun": "latest",
124124
"@types/tar": "^6.1.13",
125125
"msw": "^2.12.7",
@@ -133,19 +133,22 @@
133133
"@anthropic-ai/claude-agent-sdk": "0.3.197",
134134
"@anthropic-ai/sdk": "0.109.0",
135135
"@hono/node-server": "^1.19.9",
136+
"@modelcontextprotocol/sdk": "1.29.0",
136137
"@openai/codex": "0.140.0",
138+
"@opentelemetry/api": "^1.9.1",
137139
"@opentelemetry/api-logs": "^0.208.0",
138140
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
141+
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
139142
"@opentelemetry/resources": "^2.0.0",
140143
"@opentelemetry/sdk-logs": "^0.208.0",
144+
"@opentelemetry/sdk-trace-base": "^2.8.0",
141145
"@opentelemetry/semantic-conventions": "^1.28.0",
142146
"@types/jsonwebtoken": "^9.0.10",
143147
"commander": "^14.0.2",
144148
"fflate": "^0.8.2",
145149
"hono": "^4.11.7",
146150
"jsonwebtoken": "^9.0.2",
147151
"minimatch": "^10.0.3",
148-
"@modelcontextprotocol/sdk": "1.29.0",
149152
"tar": "^7.5.0",
150153
"uuid": "13.0.0",
151154
"yoga-wasm-web": "^0.3.3",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const MAX_BODY_CHARS = 2000;
2+
// PostHog Logs only facets attribute key/value pairs shorter than 256 chars,
3+
// so free-text attribute values are capped well below that.
4+
const MAX_ATTR_CHARS = 200;
5+
6+
export type AttributeValue = string | number | boolean;
7+
export type Attributes = Record<string, AttributeValue>;
8+
9+
export { MAX_ATTR_CHARS, MAX_BODY_CHARS };
10+
11+
export function truncate(value: string, max: number): string {
12+
return value.length <= max ? value : `${value.slice(0, max)}…`;
13+
}
14+
15+
export function asRecord(value: unknown): Record<string, unknown> | undefined {
16+
return typeof value === "object" && value !== null && !Array.isArray(value)
17+
? (value as Record<string, unknown>)
18+
: undefined;
19+
}
20+
21+
export function strAttr(
22+
attrs: Attributes,
23+
key: string,
24+
value: unknown,
25+
max = MAX_ATTR_CHARS,
26+
): string | undefined {
27+
if (typeof value !== "string" || value.length === 0) return undefined;
28+
const truncated = truncate(value, max);
29+
attrs[key] = truncated;
30+
return truncated;
31+
}
32+
33+
export function numAttr(attrs: Attributes, key: string, value: unknown): void {
34+
if (typeof value === "number" && Number.isFinite(value)) {
35+
attrs[key] = value;
36+
}
37+
}
38+
39+
export function usageAttributes(params: Record<string, unknown>): Attributes {
40+
const attrs: Attributes = {};
41+
const used = asRecord(params.used);
42+
if (used) {
43+
numAttr(attrs, "tokens_input", used.inputTokens);
44+
numAttr(attrs, "tokens_output", used.outputTokens);
45+
numAttr(attrs, "tokens_cached_read", used.cachedReadTokens);
46+
numAttr(attrs, "tokens_cached_write", used.cachedWriteTokens);
47+
}
48+
// Claude sends a plain number; other shapes carry { amount }.
49+
const cost =
50+
typeof params.cost === "number"
51+
? params.cost
52+
: asRecord(params.cost)?.amount;
53+
numAttr(attrs, "cost_usd", cost);
54+
return attrs;
55+
}
56+
57+
/** Timestamp of a stored entry as a Date, falling back to now when invalid. */
58+
export function entryTime(timestamp: string): Date {
59+
const parsed = new Date(timestamp);
60+
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
61+
}

packages/agent/src/otel-log-writer.test.ts

Lines changed: 0 additions & 105 deletions
This file was deleted.

packages/agent/src/otel-log-writer.ts

Lines changed: 0 additions & 94 deletions
This file was deleted.

0 commit comments

Comments
 (0)