{"_id":"@desek/pi-opentelemetry","_rev":"2-40a8f603e09aa883221beecccb06de72","name":"@desek/pi-opentelemetry","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@desek/pi-opentelemetry","version":"0.1.0","keywords":["pi-package","pi","coding-agent","opentelemetry","otel","otlp","observability","telemetry"],"author":{"name":"desek"},"license":"Apache-2.0","_id":"@desek/pi-opentelemetry@0.1.0","maintainers":[{"name":"desek","email":"daniel@grenemark.se"}],"homepage":"https://github.com/desek/agent-observability/tree/main/packages/pi-opentelemetry#readme","bugs":{"url":"https://github.com/desek/agent-observability/issues"},"pi":{"extensions":["./src/index.ts"]},"dist":{"shasum":"e87d3368789f983c68730e9664d4e5259fd2965c","tarball":"https://registry.npmjs.org/@desek/pi-opentelemetry/-/pi-opentelemetry-0.1.0.tgz","fileCount":13,"integrity":"sha512-BXT2apfOlZsTKDBykVbCS7tsPpGJEqGVW7gPD60qlWBn7m/Em1cQePGNie9jNXxS1BQKYaIiLN9qHaw2ldwNcQ==","signatures":[{"sig":"MEYCIQDuOrVHBkoqMHNxaKscGOYQwVW3+vE/GJf4gczdxfdA0gIhAO7qOz6gXy3S9n5xqZ9PtfpJ89oFbrIV6Wp47Ofi/Lrt","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":117205},"type":"module","engines":{"node":">=22.14.0"},"gitHead":"da5d71e74fcc4df7d9358f6d68e928898a59a48b","scripts":{"test":"node --experimental-strip-types --test 'src/*.test.ts'"},"_npmUser":{"name":"desek","email":"daniel@grenemark.se"},"repository":{"url":"git+https://github.com/desek/agent-observability.git","type":"git","directory":"packages/pi-opentelemetry"},"_npmVersion":"11.12.1","description":"pi coding-agent extension that exports OpenTelemetry metrics, log events, and traces over OTLP gRPC, at parity with Claude Code's built-in telemetry. It installs as a no-op: it emits nothing unless enabled, stays silent when no collector is reachable, and","directories":{},"_nodeVersion":"24.15.0","dependencies":{"@opentelemetry/api-logs":"~0.220.0","@opentelemetry/sdk-logs":"~0.220.0","@opentelemetry/resources":"~2.9.0","@opentelemetry/sdk-metrics":"~2.9.0","@opentelemetry/sdk-trace-base":"~2.9.0","@opentelemetry/exporter-logs-otlp-grpc":"~0.220.0","@opentelemetry/exporter-logs-otlp-http":"~0.220.0","@opentelemetry/exporter-trace-otlp-grpc":"~0.220.0","@opentelemetry/exporter-trace-otlp-http":"~0.220.0","@opentelemetry/exporter-metrics-otlp-grpc":"~0.220.0","@opentelemetry/exporter-metrics-otlp-http":"~0.220.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@earendil-works/pi-coding-agent":"^0.80.3"},"peerDependencies":{"@opentelemetry/api":"^1.9.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-opentelemetry_0.1.0_1785672686087_0.5120877016673631","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@desek/pi-opentelemetry","version":"0.1.1","description":"pi coding-agent extension that exports OpenTelemetry metrics, log events, and traces over OTLP gRPC, at parity with Claude Code's built-in telemetry. It installs as a no-op: it emits nothing unless enabled, stays silent when no collector is reachable, and","license":"Apache-2.0","author":{"name":"desek"},"type":"module","repository":{"type":"git","url":"git+https://github.com/desek/agent-observability.git","directory":"packages/pi-opentelemetry"},"homepage":"https://github.com/desek/agent-observability/tree/main/packages/pi-opentelemetry#readme","bugs":{"url":"https://github.com/desek/agent-observability/issues"},"keywords":["pi-package","pi","coding-agent","opentelemetry","otel","otlp","observability","telemetry"],"engines":{"node":">=22.14.0"},"publishConfig":{"access":"public"},"scripts":{"test":"node --experimental-strip-types --test 'src/*.test.ts'"},"pi":{"extensions":["./src/index.ts"]},"peerDependencies":{"@opentelemetry/api":"^1.9.0"},"dependencies":{"@opentelemetry/api-logs":"~0.220.0","@opentelemetry/exporter-logs-otlp-grpc":"~0.220.0","@opentelemetry/exporter-logs-otlp-http":"~0.220.0","@opentelemetry/exporter-metrics-otlp-grpc":"~0.220.0","@opentelemetry/exporter-metrics-otlp-http":"~0.220.0","@opentelemetry/exporter-trace-otlp-grpc":"~0.220.0","@opentelemetry/exporter-trace-otlp-http":"~0.220.0","@opentelemetry/resources":"~2.9.0","@opentelemetry/sdk-logs":"~0.220.0","@opentelemetry/sdk-metrics":"~2.9.0","@opentelemetry/sdk-trace-base":"~2.9.0"},"devDependencies":{"@earendil-works/pi-coding-agent":"^0.80.3"},"gitHead":"e47ade2141dc703bcd3503cfaf8b726e8f33a105","_id":"@desek/pi-opentelemetry@0.1.1","_nodeVersion":"22.14.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-JV2Nq47N93b56QlSKdB9WCeSn+turReQE5qYecTXgbJhbvKK34/YYIeRYh49ZhYOf0Rf/IScdMyvUoN43dmWZA==","shasum":"d9eae0cf2694012e0ea4d86125b6fd20595bbb5d","tarball":"https://registry.npmjs.org/@desek/pi-opentelemetry/-/pi-opentelemetry-0.1.1.tgz","fileCount":13,"unpackedSize":118053,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@desek%2fpi-opentelemetry@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIG49DkCmI6ahzbexLwCQnl2YKdMhIUi7oPZ9nq1cE5AuAiB5eub/d8Dc8CUmmOAnbyjBc3lXCh2nIuFU3AmBqr2Vkg=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:078f5bfa-038b-4a0d-90b5-68219c968070"}},"directories":{},"maintainers":[{"name":"desek","email":"daniel@grenemark.se"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pi-opentelemetry_0.1.1_1785853450804_0.5990526836795218"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-02T12:11:25.836Z","modified":"2026-08-04T14:24:11.226Z","0.1.0":"2026-08-02T12:11:26.210Z","0.1.1":"2026-08-04T14:24:10.942Z"},"bugs":{"url":"https://github.com/desek/agent-observability/issues"},"author":{"name":"desek"},"license":"Apache-2.0","homepage":"https://github.com/desek/agent-observability/tree/main/packages/pi-opentelemetry#readme","keywords":["pi-package","pi","coding-agent","opentelemetry","otel","otlp","observability","telemetry"],"repository":{"type":"git","url":"git+https://github.com/desek/agent-observability.git","directory":"packages/pi-opentelemetry"},"description":"pi coding-agent extension that exports OpenTelemetry metrics, log events, and traces over OTLP gRPC, at parity with Claude Code's built-in telemetry. It installs as a no-op: it emits nothing unless enabled, stays silent when no collector is reachable, and","maintainers":[{"name":"desek","email":"daniel@grenemark.se"}],"readme":"<!--\n@agents-index: Public README for @desek/pi-opentelemetry, the pi coding-agent\nOpenTelemetry extension. Documents what it emits, how to install and enable it,\nevery configuration variable and its source-verified default, the operational\ncontract, a verification recipe, and a troubleshooting list for a reader who has\nnever seen this repository.\n-->\n\n# @desek/pi-opentelemetry\n\nAn OpenTelemetry extension for the [pi](https://github.com/earendil-works/pi)\ncoding agent. It instruments pi's lifecycle and exports all three OpenTelemetry\nsignals, metrics, log events, and traces, over OTLP to any OpenTelemetry\ncollector, using gRPC by default or HTTP/protobuf when selected. The signal names and attributes match Claude Code's built-in\ntelemetry, with the `claude_code.` prefix replaced by `pi.`, so pi and Claude\nCode stay distinguishable in one backend while remaining comparable.\n\nThe extension is safe to install anywhere. It emits nothing until you enable it,\nit stays silent when no collector is reachable, and a telemetry fault can never\ncrash, block, or slow the agent. See [Operational contract](#operational-contract).\n\n## Install\n\n```bash\npi install npm:@desek/pi-opentelemetry\n```\n\nThis registers the extension with pi. It stays dormant until you enable it.\n\n### Peer dependency\n\nThe extension declares `@opentelemetry/api` as a peer dependency (range\n`^1.9.0`). pi and most host environments already provide it. If your project\npins its own copy of `@opentelemetry/api`, keep it on a compatible version. Two\ndifferent copies of `@opentelemetry/api` in one process break instrumentation\nregistration silently, which looks like a broken collector rather than a\ndependency clash. See [Troubleshooting](#troubleshooting).\n\n## Enable\n\nThe extension is a hard no-op unless one of two conditions is met:\n\n1. `PI_OTEL_ENABLE` is set to a truthy value (the explicit master switch), or\n2. `PI_OTEL_ENABLE` is unset and the extension finds a target: either\n   `OTEL_EXPORTER_OTLP_ENDPOINT` is set, or a local collector answers a health\n   probe. This is the dynamic default. It lets a machine that runs a collector\n   export with no configuration, while a machine that runs no collector stays\n   silent.\n\nThe simplest explicit setup, exporting to a collector on the standard OTLP gRPC\nport:\n\n```bash\nexport PI_OTEL_ENABLE=1\npi -p \"say hi\"\n```\n\nBy default the extension exports to `http://localhost:4317`, the standard OTLP\ngRPC receiver address. A plain OpenTelemetry Collector, or Grafana Alloy with\nits default `otelcol.receiver.otlp`, listens there, so no endpoint configuration\nis needed for the common case.\n\n### If you run the agent-observability edge-proxy stack\n\nThe companion observability stack does not publish the standard OTLP ports. It\nfronts every backend behind a single edge port (default `24317`). A collector\nbehind a non-standard address is not found by the default endpoint or the health\nprobe, so installing the extension alongside that stack exports nothing until\nyou point it at the edge port:\n\n```bash\nexport PI_OTEL_ENABLE=1\nexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:24317\npi -p \"say hi\"\n```\n\nIf you see no telemetry with the stack running, this is the first thing to\ncheck. See [Troubleshooting](#troubleshooting).\n\n## Operational contract\n\nThese four guarantees are what let you trust the extension in every pi session.\nEach is enforced in `src/index.ts` and `src/config.env.ts`.\n\n1. **Off by default.** The extension emits no signal and constructs no exporter\n   unless its master switch `PI_OTEL_ENABLE` is truthy, or the health-gated\n   dynamic default enables it.\n2. **Silent when the collector is absent.** With neither the switch nor an\n   endpoint set, the extension probes the collector and stays silent when the\n   collector does not answer, so installing it on a machine without a stack costs\n   nothing.\n3. **Content is opt-in.** Every content-logging flag defaults to off. Prompts,\n   responses, tool content, and raw request and response bodies are never\n   exported unless you set the matching flag explicitly.\n4. **It never breaks the agent.** An export failure, an unreachable collector,\n   or a malformed configuration is swallowed. No telemetry fault raises into the\n   agent, blocks a turn, or changes agent behaviour.\n\n## Configuration variables\n\nEvery variable the extension reads, with its default and effect. Defaults are\ntaken from `src/config.env.ts` and `src/health.alloy.ts`.\n\n### Switch and endpoint\n\n| Variable | Default | Effect |\n|----------|---------|--------|\n| `PI_OTEL_ENABLE` | unset | Master switch. Truthy enables export, a false value (`0`, `false`, `no`, `off`, empty) disables it, unset defers to the dynamic default. |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | Shared OTLP endpoint for all signals. When set while `PI_OTEL_ENABLE` is unset, it also enables export without a health probe. |\n| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | Shared OTLP protocol for all signals. Supported values are `grpc` (the default) and `http/protobuf`. For `http/protobuf` the endpoint's `/v1/{metrics,logs,traces}` path is appended per signal. Any other value (for example `http/json`) is unsupported: that signal is not exported, an actionable message naming the value and the supported values is written to stderr, and the other signals are unaffected. |\n| `OTEL_EXPORTER_OTLP_HEADERS` | none | Comma-separated `key=value` headers applied to every exporter. |\n\n### Per-signal exporter selection and overrides\n\n| Variable | Default | Effect |\n|----------|---------|--------|\n| `OTEL_METRICS_EXPORTER` / `OTEL_LOGS_EXPORTER` / `OTEL_TRACES_EXPORTER` | `otlp` | `otlp` selects the OTLP exporter for that signal, over the transport its protocol names; `none` disables that signal. Any value other than `none` is treated as `otlp`. |\n| `OTEL_EXPORTER_OTLP_{METRICS,LOGS,TRACES}_ENDPOINT` | shared endpoint | Per-signal endpoint override. |\n| `OTEL_EXPORTER_OTLP_{METRICS,LOGS,TRACES}_PROTOCOL` | shared protocol | Per-signal protocol override, so one signal can go over `http/protobuf` while another stays on `grpc`. Same supported values and unsupported-value handling as `OTEL_EXPORTER_OTLP_PROTOCOL` above. |\n| `OTEL_METRIC_EXPORT_INTERVAL` | SDK default | Metric reader export interval, in milliseconds. |\n| `OTEL_LOGS_EXPORT_INTERVAL` | SDK default | Log processor export interval, in milliseconds. |\n| `OTEL_TRACES_EXPORT_INTERVAL` | SDK default | Span processor export interval, in milliseconds. |\n\n### Resource identity\n\n| Variable | Default | Effect |\n|----------|---------|--------|\n| `OTEL_SERVICE_NAME` | `pi-coding-agent` | `service.name` on the OpenTelemetry Resource. |\n| `OTEL_RESOURCE_ATTRIBUTES` | none | Comma-separated `key=value` Resource attributes. The extension also derives git provenance (`git.org`, `git.repo`, `git.branch`, `git.path`) from the launch directory; values you set here win over the derived ones. |\n\n### Content logging (default off)\n\nEach flag is off unless set to a truthy value. See [Content logging and\nprivacy](#content-logging-and-privacy) for exactly what each records.\n\n| Variable | Records |\n|----------|---------|\n| `OTEL_LOG_USER_PROMPTS` | The full user prompt text on `pi.user_prompt` events and interaction spans. |\n| `OTEL_LOG_ASSISTANT_RESPONSES` | The full assistant response text on `pi.assistant_response` events. |\n| `OTEL_LOG_TOOL_DETAILS` | Tool input and parameters on `pi.tool_result` events. |\n| `OTEL_LOG_TOOL_CONTENT` | Tool input and output as span events on tool spans. |\n| `OTEL_LOG_RAW_API_BODIES` | Raw provider request and response bodies on `pi.api_request_body` and `pi.api_response_body` events. |\n\n### Metric-attribute cardinality (default off)\n\nEach flag is off unless set to a truthy value. Enabling one adds a\nhigh-cardinality attribute to metric series, which increases storage cost in the\nbackend.\n\n| Variable | Adds |\n|----------|------|\n| `OTEL_METRICS_INCLUDE_SESSION_ID` | The session id as a metric attribute. |\n| `OTEL_METRICS_INCLUDE_VERSION` | The pi version as a metric attribute. |\n| `OTEL_METRICS_INCLUDE_ACCOUNT_UUID` | The account uuid as a metric attribute. |\n| `OTEL_METRICS_INCLUDE_ENTRYPOINT` | The entrypoint as a metric attribute. |\n| `OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES` | Resource attributes as metric attributes. |\n\n## Content logging and privacy\n\nEvery content-logging flag is off by default, so out of the box no prompt,\nresponse, tool content, or raw body ever leaves the process. When you enable a\nflag, the content is exported over OTLP to whatever collector you configured and\nis stored wherever that collector sends it. If your collector writes to a local\nlog store such as Loki, that content lands on disk on the machine that runs the\nstore. Enable these flags only when you understand where the content will be\nkept and who can read it.\n\nNon-content fields are always present when export is on: prompt length, response\nlength, model, token counts, cost, durations, and outcome flags. These carry no\nprompt or response text.\n\n## Emitted signal inventory\n\n### Metrics (8 instruments, `pi.` namespace)\n\n| Instrument | Kind | Attributes | Source lifecycle event |\n|------------|------|------------|------------------------|\n| `pi.session.count` | counter | `start_type` | `session_start` |\n| `pi.token.usage` | counter | `type` (`input`/`output`/`cacheRead`/`cacheCreation`), `model` | `message_end` |\n| `pi.cost.usage` | counter (USD) | `model` | `message_end` |\n| `pi.lines_of_code.count` | counter | `type` (`added`/`removed`) | `tool_result` (edit/write) |\n| `pi.code_edit_tool.decision` | counter | `decision`, `tool_name`, `language` | `tool_call`, `tool_result` |\n| `pi.commit.count` | counter | none | `tool_result` (bash `git commit` heuristic) |\n| `pi.pull_request.count` | counter | none | `tool_result` (bash `gh pr create` heuristic) |\n| `pi.active_time.total` | counter (seconds) | none | turn/agent timing |\n\nA Prometheus-compatible backend normalizes the OTel dot-names (dots to\nunderscores, a `_total` suffix on monotonic counters), for example\n`pi_session_count_total`, `pi_token_usage_tokens_total`, `pi_cost_usage_total`.\n\n### Log events (`pi.` namespace)\n\n| Event | Content field (gating flag) | Source lifecycle event |\n|-------|-----------------------------|------------------------|\n| `pi.user_prompt` | `prompt` (`OTEL_LOG_USER_PROMPTS`); `prompt_length` always | `before_agent_start` |\n| `pi.assistant_response` | `response` (`OTEL_LOG_ASSISTANT_RESPONSES`); `model`, `response_length` | `message_end` |\n| `pi.tool_result` | `tool_input`/`tool_parameters` (`OTEL_LOG_TOOL_DETAILS`); `tool_name`, `success`, `duration_ms` | `tool_result` |\n| `pi.api_request` | `model`, `cost_usd`, `duration_ms`, token counts, `response_id` | `message_end` |\n| `pi.api_error` | `model`, `error`, `status_code`, `duration_ms` | `after_provider_response` |\n| `pi.api_refusal` | `model`, `finish_reason` | `message_end` finish reason |\n| `pi.tool_decision` | `tool_name`, `decision`, `source` | `tool_call` |\n| `pi.api_request_body` | `body`/`body_ref` (`OTEL_LOG_RAW_API_BODIES`) | `before_provider_request` |\n| `pi.api_response_body` | `body`/`body_ref` (`OTEL_LOG_RAW_API_BODIES`) | `after_provider_response` |\n| `pi.compaction` | `trigger`, `success`, `pre_tokens`, `post_tokens` | `session_compact` |\n\nAll events carry `service.name = pi-coding-agent`. Content-bearing fields are\nomitted unless their opt-in flag is set.\n\n### Spans (`pi.` namespace)\n\n| Span | Parent | Source lifecycle event |\n|------|--------|------------------------|\n| `pi.interaction` | root (one per user prompt) | `agent_start` to `agent_end` |\n| `pi.llm_request` | `pi.interaction` | provider-request start to `message_end` |\n| `pi.tool` | `pi.interaction` | `tool_execution_start` to `tool_execution_end` |\n| `pi.tool.execution` | `pi.tool` | execution portion of a tool call |\n\nSpans carry `gen_ai.*` and `pi.*` attributes. The interaction prompt text is\ngated by `OTEL_LOG_USER_PROMPTS`; tool input and output span events by\n`OTEL_LOG_TOOL_CONTENT`.\n\n## Verify it is exporting\n\nExport is batched, so allow 15 to 30 seconds after the process exits before\nquerying. The extension force-flushes at the end of every agent loop and again\non session shutdown, so a headless one-shot delivers its signals reliably.\n\nDrive one pi turn with telemetry enabled:\n\n```bash\nexport PI_OTEL_ENABLE=1\n# Set OTEL_EXPORTER_OTLP_ENDPOINT here if your collector is not on localhost:4317.\npi -p \"Print the word telemetry and nothing else.\"\n```\n\nThen query your backend. The examples below assume a Grafana LGTM stack; adjust\nthe base URLs to your own collector's query APIs.\n\nMetrics (Prometheus-compatible API):\n\n```bash\ncurl -sG http://localhost:4317/prometheus/api/v1/query \\\n  --data-urlencode 'query=pi_token_usage_tokens_total' | jq '.data.result'\n```\n\nLog events (Loki):\n\n```bash\ncurl -sG http://localhost:4317/loki/api/v1/query_range \\\n  --data-urlencode 'query={service_name=\"pi-coding-agent\"}' | jq '.data.result | length'\n```\n\nTraces (Tempo):\n\n```bash\ncurl -sG http://localhost:4317/tempo/api/search \\\n  --data-urlencode 'q={ resource.service.name = \"pi-coding-agent\" }' | jq '.traces | length'\n```\n\nA non-empty result for each signal confirms the extension is exporting. If the\nquery ports differ from the OTLP ingest port on your stack, use the query ports\nyour backend documents.\n\n## Troubleshooting\n\nThe extension is installed and enabled but I see nothing.\n\n- **The collector is not on the default endpoint.** The default is\n  `http://localhost:4317`. If your collector listens elsewhere, set\n  `OTEL_EXPORTER_OTLP_ENDPOINT`. This is the most common cause when running the\n  agent-observability edge-proxy stack, which uses a single edge port (default\n  `24317`) rather than the standard OTLP ports. Set\n  `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:24317`.\n- **The dynamic default kept it off.** With `PI_OTEL_ENABLE` unset and no\n  endpoint set, the extension probes a local collector at\n  `http://localhost:12345/-/healthy` (Grafana Alloy's default health route) and\n  stays off when nothing answers. Set `PI_OTEL_ENABLE=1` to force it on\n  regardless of the probe.\n- **A duplicate `@opentelemetry/api` in the process.** If two different copies\n  of `@opentelemetry/api` are resolved in one process, instrumentation registers\n  against one copy and the extension emits from another, so nothing arrives and\n  no error is raised. This looks like a broken stack but is a dependency clash.\n  Deduplicate the package (for example `npm dedupe`, or align your project's\n  pinned version to the peer range `^1.9.0`) so a single copy is loaded.\n- **A signal is disabled.** Check that `OTEL_METRICS_EXPORTER`,\n  `OTEL_LOGS_EXPORTER`, or `OTEL_TRACES_EXPORTER` is not set to `none`.\n- **You are querying too soon.** Export is batched. Wait 15 to 30 seconds after\n  the process exits before querying.\n- **You expected prompt or response text and see none.** Content logging is\n  opt-in. Set the matching flag from\n  [Content logging](#content-logging-and-privacy). The structural fields\n  (lengths, models, counts, costs) are always present, so their presence with\n  absent text means the content flag is simply off.\n\n## License\n\nApache-2.0.\n","readmeFilename":"README.md"}