{"_id":"@ascendenceai/cortena-observability","name":"@ascendenceai/cortena-observability","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@ascendenceai/cortena-observability","version":"0.1.0","description":"Shared OpenTelemetry bootstrap, span attributes, propagation and structured logging for every Cortena service and extension (EXTBP-1, DESIGN-D21)","license":"SEE LICENSE IN LICENSE","repository":{"type":"git","url":"git+https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena.git","directory":"packages/observability"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./bootstrap":{"types":"./dist/bootstrap.d.ts","import":"./dist/bootstrap.js"},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"scripts":{"build":"tsc","dev":"tsc --watch","prepare":"tsc","test":"vitest run"},"dependencies":{"@opentelemetry/api":"^1.9.0","@opentelemetry/api-logs":"^0.213.0","@opentelemetry/core":"^2.6.0","@opentelemetry/exporter-logs-otlp-grpc":"^0.213.0","@opentelemetry/exporter-metrics-otlp-grpc":"^0.213.0","@opentelemetry/exporter-trace-otlp-grpc":"^0.213.0","@opentelemetry/instrumentation-express":"^0.61.0","@opentelemetry/instrumentation-http":"^0.213.0","@opentelemetry/instrumentation-pg":"^0.65.0","@opentelemetry/resources":"^2.6.0","@opentelemetry/sdk-logs":"^0.213.0","@opentelemetry/sdk-metrics":"^2.6.0","@opentelemetry/sdk-node":"^0.213.0","@opentelemetry/sdk-trace-base":"^2.6.0","@opentelemetry/sdk-trace-node":"^2.6.0","@opentelemetry/semantic-conventions":"^1.40.0"},"devDependencies":{"@types/express":"^5.0.2","@types/node":"^22","@types/supertest":"^6.0.2","express":"^5.1","supertest":"^7.1.0","typescript":"^5.9","vitest":"^4.0.18"},"_id":"@ascendenceai/cortena-observability@0.1.0","bugs":{"url":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena/issues"},"homepage":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena#readme","_nodeVersion":"25.6.1","_npmVersion":"11.9.0","dist":{"integrity":"sha512-betRHOB/LiMRVCncrrv/4+1DIAaVkevYLTMBtAdkugOFCvzPKScDvl0j/tmDv7xNRtWgbJQ8i4CGhFW0GIYupw==","shasum":"f087bc362848b065a6e02f2b41ccf3e557c0f021","tarball":"https://registry.npmjs.org/@ascendenceai/cortena-observability/-/cortena-observability-0.1.0.tgz","fileCount":23,"unpackedSize":58285,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD+iiLbl3TBgegBFaCmCJUdNzJG4it8K71F8R0iGgyUXwIhAL3lMfV4BJDOEdLHsLfWQHj5bMlaMV18OKn1HrK/ff31"}]},"_npmUser":{"name":"amit_ascendence","email":"connect@mindmentors.net"},"directories":{},"maintainers":[{"name":"amit_ascendence","email":"connect@mindmentors.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cortena-observability_0.1.0_1788785452147_0.12091780922464279"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-07T12:50:51.915Z","0.1.0":"2026-09-07T12:50:52.285Z","modified":"2026-09-07T12:50:52.545Z"},"maintainers":[{"name":"amit_ascendence","email":"connect@mindmentors.net"}],"description":"Shared OpenTelemetry bootstrap, span attributes, propagation and structured logging for every Cortena service and extension (EXTBP-1, DESIGN-D21)","homepage":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena#readme","repository":{"type":"git","url":"git+https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena.git","directory":"packages/observability"},"bugs":{"url":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena/issues"},"license":"SEE LICENSE IN LICENSE","readme":"# @ascendenceai/cortena-observability\n\nOne OpenTelemetry bootstrap for every Cortena service and extension.\nDESIGN-D21, audit rule P-41, section 15.7 of _How to create a Cortena extension_.\n\nOne agent action is one trace: cortenacore's tool call, the Controller's broker,\nyour extension's route, your MCP tool, the database. A trace that stops at a\nrepository boundary is not a trace, which is why this package lives in the\n`cortena` repo and not in `@cortena-extensions/shared` — cortenacore and the\nController import the same bootstrap as the extensions.\n\n## Install and import\n\n```jsonc\n// package.json\n\"dependencies\": { \"@ascendenceai/cortena-observability\": \"workspace:*\" }\n```\n\n```ts\n// functions/src/index.ts — THE FIRST LINE, before every other import\nimport \"@ascendenceai/cortena-observability/bootstrap\";\n\nimport express from \"express\";\n// …\n```\n\nFirst, and by side effect. Auto-instrumentation has to patch `http`, `express`\nand `pg` before any of them is required. An import placed further down produces\na process with no spans in it — silently: no error, no warning, just an empty\ntrace view three weeks later.\n\n## The env contract\n\nFour variables, and only four. Everything else — sampling, retention, the\nbackend — belongs to the collector (`k8s/helm/otel-collector`, EXTBP-20), so a\nservice speaks OTLP and nothing else and the backend can change without a code\nchange in fourteen repositories. The collector, the Cloud Operations backend,\nthe sampling policy, the IAM it needs and the dashboards to build from these\nsignals are all in [`docs/observability.md`](../../docs/observability.md).\n\n| Variable                      | Meaning                                                                                                                             | Default           |\n| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------- |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | The collector, e.g. `http://otel-collector.cortena-test.svc.cluster.local:4317`. **Unset = telemetry off**, one log line, no spans. | empty             |\n| `OTEL_SERVICE_NAME`           | `service.name`. `ext-<name>` for an extension; `cortena-controller`, `cortenacore`, `cortena-auth` for the platform services.       | `cortena-service` |\n| `CORTENA_SERVICE_VERSION`     | `service.version` — the image tag, i.e. the git SHA from CI.                                                                        | `0.0.0-dev`       |\n| `CORTENA_ENVIRONMENT`         | `deployment.environment.name` — `test`, `prod` or `bcl`.                                                                            | `development`     |\n\nThe charts set all four (`cortena.otelEnv` in `k8s/helm/_shared`). The endpoint\ndefaults to empty everywhere, so nothing changes until the collector deploys.\n\nSampling is **100% at the SDK**, parent-based and always-on. The collector\ndecides what to keep — head 10% in prod with every error and every trace over\n2 s. Sampling in the service instead would mean a redeploy to change a rate, and\na span dropped at the source cannot be recovered by any downstream policy.\n\n## Span attributes\n\n`cortenaSpanAttributes()` is the only way to put Cortena identity on a span. It\nis an allowlist, and it throws in dev and in tests when you leave it.\n\n```ts\nimport { cortenaSpanAttributes, startSpan } from \"@ascendenceai/cortena-observability\";\n\nstartSpan(\n  \"broker.invoke\",\n  cortenaSpanAttributes({\n    orgId,\n    userId,\n    via: \"agent\",\n    capability: \"tasks.task.list\",\n    runId,\n  }),\n  async (span) => {\n    /* … */\n  },\n);\n```\n\n| Key                   | From                                                                                                  |\n| --------------------- | ----------------------------------------------------------------------------------------------------- |\n| `cortena.org_id`      | `orgId`                                                                                               |\n| `cortena.user_id`     | `userId` — the **raw internal id**, not hashed. A hashed id cannot be joined back to the audit trail. |\n| `cortena.actor.via`   | `via` — `chat`, `api`, `agent`, `channel`, …                                                          |\n| `cortena.agent_id`    | `agentId`                                                                                             |\n| `cortena.capability`  | `capability` — the Engram capability id, e.g. `tasks.task.list`                                       |\n| `cortena.run_id`      | `runId`                                                                                               |\n| `cortena.session_key` | `sessionKey`                                                                                          |\n\nRoute spans add `http.route` — the **template**, never the concrete path.\n`/v1/orgs/abc/tasks/123` as a route name gives one time series per task and a\ndashboard nobody can read.\n\n## What never goes on a span or in a log\n\nEmails. Names. Request or response bodies. Tokens, JWTs, API keys, passwords,\ncookies. Anything not in the table above.\n\nThe PII guard enforces the first and last of those: an email-shaped value or a\nkey outside the allowlist throws `PiiGuardError` in dev and in tests, and is\ndropped with one warning in production — a live service is not taken down by an\nobservability assertion, but the value never reaches the exporter either way.\nThe error message deliberately does not echo the offending value; it would\nbecome the leak, in a log line, which is exactly where it must not be.\n\nFor logs, `createLogger()` redacts the same vocabulary recursively, so a nested\n`actor.displayName` is scrubbed as reliably as a top-level `email`.\n\nErrors carry the **field name** that failed, never the payload that carried it\n(§15.6, from the other end).\n\n## Propagation\n\nThe auto-instrumentation injects `traceparent` on ordinary outbound HTTP. The\nbroker relays and the cortenacore tool calls are built by hand and are exactly\nthe hops that must not break the trace:\n\n```ts\nimport { injectTraceparent, withExtractedContext } from \"@ascendenceai/cortena-observability\";\n\n// outbound\nawait fetch(url, { headers: injectTraceparent({ \"content-type\": \"application/json\" }) });\n\n// inbound, on a hand-rolled handler\nwithExtractedContext(req.headers, () => handle(req, res));\n```\n\n`injectTraceparent` returns a new object and is a no-op with no active span, so\nit is safe on every call site regardless of whether telemetry is on.\n\n## Express\n\n```ts\nimport { cortenaExpressMiddleware } from \"@ascendenceai/cortena-observability\";\n\napp.use(cortenaExpressMiddleware());\n```\n\nIt enriches the span the HTTP instrumentation already created; when there is\nnone — telemetry disabled, or a test with a plain tracer provider — it creates\none, so a route is observable either way and a test need not boot the SDK.\n\n## Logging\n\n```ts\nimport { createLogger } from \"@ascendenceai/cortena-observability\";\n\nconst log = createLogger(\"broker\");\nlog.error(\"upstream rejected the write\", { capability, field: \"dueDate\", status: 400 });\n```\n\nStructured JSON, one object per line, with `trace_id` and `span_id` stamped from\nthe active span — the one place it cannot be forgotten.\n\n## Tests\n\n`pnpm --filter @ascendenceai/cortena-observability test` — in-memory span exporter, an\nExpress route, the PII guard, the no-op path and a `traceparent` round trip.\n","readmeFilename":"README.md","_rev":"1-1af851c673da1a41a2de09c561932e94"}