@neat.is/core 0.9.1 → 0.9.2-dev.20260822
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-FCO5Z3RW.js → chunk-ERE47MCR.js} +2 -2
- package/dist/{chunk-UUYCTH2E.js → chunk-GDGUY4T6.js} +34 -2
- package/dist/chunk-GDGUY4T6.js.map +1 -0
- package/dist/{chunk-YKQB622D.js → chunk-L4SZIIER.js} +3 -3
- package/dist/{chunk-UN7VFA4H.js → chunk-RQQUI3NQ.js} +29 -3
- package/dist/chunk-RQQUI3NQ.js.map +1 -0
- package/dist/cli.cjs +60 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/index.cjs +60 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +60 -2
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-CO47JRIN.js → otel-grpc-Y5K4WGWO.js} +3 -3
- package/dist/server.cjs +60 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-UN7VFA4H.js.map +0 -1
- package/dist/chunk-UUYCTH2E.js.map +0 -1
- /package/dist/{chunk-FCO5Z3RW.js.map → chunk-ERE47MCR.js.map} +0 -0
- /package/dist/{chunk-YKQB622D.js.map → chunk-L4SZIIER.js.map} +0 -0
- /package/dist/{otel-grpc-CO47JRIN.js.map → otel-grpc-Y5K4WGWO.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseOtlpRequest
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GDGUY4T6.js";
|
|
4
4
|
|
|
5
5
|
// src/otel-grpc.ts
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
@@ -138,4 +138,4 @@ export {
|
|
|
138
138
|
reshapeGrpcRequest,
|
|
139
139
|
startOtelGrpcReceiver
|
|
140
140
|
};
|
|
141
|
-
//# sourceMappingURL=chunk-
|
|
141
|
+
//# sourceMappingURL=chunk-ERE47MCR.js.map
|
|
@@ -99,6 +99,7 @@ function readAuthEnv(env = process.env) {
|
|
|
99
99
|
// src/otel.ts
|
|
100
100
|
import path2 from "path";
|
|
101
101
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
102
|
+
import zlib from "zlib";
|
|
102
103
|
import Fastify from "fastify";
|
|
103
104
|
import protobuf from "protobufjs";
|
|
104
105
|
function extractExceptionFromEvents(events) {
|
|
@@ -352,14 +353,45 @@ async function decodeProtobufBody(buf) {
|
|
|
352
353
|
longs: String,
|
|
353
354
|
enums: Number
|
|
354
355
|
});
|
|
355
|
-
const { reshapeGrpcRequest } = await import("./otel-grpc-
|
|
356
|
+
const { reshapeGrpcRequest } = await import("./otel-grpc-Y5K4WGWO.js");
|
|
356
357
|
return reshapeGrpcRequest(decoded);
|
|
357
358
|
}
|
|
359
|
+
function decompressorForEncoding(encoding) {
|
|
360
|
+
switch (encoding) {
|
|
361
|
+
case "gzip":
|
|
362
|
+
case "x-gzip":
|
|
363
|
+
return zlib.createGunzip();
|
|
364
|
+
case "deflate":
|
|
365
|
+
return zlib.createInflate();
|
|
366
|
+
default:
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
358
370
|
async function buildOtelReceiver(opts) {
|
|
359
371
|
const app = Fastify({
|
|
360
372
|
logger: false,
|
|
361
373
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
362
374
|
});
|
|
375
|
+
app.addHook("preParsing", (req, _reply, payload, done) => {
|
|
376
|
+
const encoding = (req.headers["content-encoding"] ?? "").toString().trim().toLowerCase();
|
|
377
|
+
if (encoding === "" || encoding === "identity") {
|
|
378
|
+
done(null, payload);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const decompressor = decompressorForEncoding(encoding);
|
|
382
|
+
if (!decompressor) {
|
|
383
|
+
done(null, payload);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const tracked = decompressor;
|
|
387
|
+
tracked.receivedEncodedLength = 0;
|
|
388
|
+
payload.on("data", (chunk) => {
|
|
389
|
+
tracked.receivedEncodedLength = (tracked.receivedEncodedLength ?? 0) + chunk.length;
|
|
390
|
+
});
|
|
391
|
+
payload.on("error", (err) => decompressor.destroy(err));
|
|
392
|
+
payload.pipe(decompressor);
|
|
393
|
+
done(null, decompressor);
|
|
394
|
+
});
|
|
363
395
|
const REJECT_WARN_INTERVAL_MS = 6e4;
|
|
364
396
|
let lastRejectWarnAt = 0;
|
|
365
397
|
const warnRejectedOtlp = () => {
|
|
@@ -582,4 +614,4 @@ export {
|
|
|
582
614
|
listenSteppingOtlp,
|
|
583
615
|
logSpanHandler
|
|
584
616
|
};
|
|
585
|
-
//# sourceMappingURL=chunk-
|
|
617
|
+
//# sourceMappingURL=chunk-GDGUY4T6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../node_modules/tsup/assets/esm_shims.js","../src/auth.ts","../src/otel.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","/**\n * Delegated auth at the daemon boundary (ADR-073 §3 + §4).\n *\n * NEAT does not issue, rotate, or distribute the token — that is the deploy\n * platform's job. This module provides the two surfaces the daemon needs:\n *\n * - `assertBindAuthority(host, token)` — fail-loud pre-bind check. When no\n * token is set, the daemon refuses to bind on any non-loopback address.\n * Loopback-only without a token stays unauthenticated (laptop dev path).\n *\n * - `mountBearerAuth(app, opts)` — Fastify `preHandler` that requires\n * `Authorization: Bearer <token>` on every request other than the\n * unauthenticated health/readiness probes. Constant-time comparison.\n *\n * The same shape covers both the REST host and the OTLP receivers; the OTLP\n * side passes a different token (`NEAT_OTEL_TOKEN ?? NEAT_AUTH_TOKEN`) so the\n * two surfaces rotate independently.\n */\n\nimport { timingSafeEqual } from 'node:crypto'\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'\n\n// Hosts that count as loopback for the bind-authority gate. `0.0.0.0` is\n// explicitly not loopback — it binds on every interface, including any\n// public one the operator's box happens to have.\nconst LOOPBACK_HOSTS: ReadonlySet<string> = new Set([\n '127.0.0.1',\n 'localhost',\n '::1',\n '::ffff:127.0.0.1',\n])\n\nexport function isLoopbackHost(host: string | undefined | null): boolean {\n if (!host) return false\n return LOOPBACK_HOSTS.has(host)\n}\n\nexport class BindAuthorityError extends Error {\n constructor(host: string) {\n super(\n `NEAT refuses to bind on a public interface without \\`NEAT_AUTH_TOKEN\\` set (host=\"${host}\"). Set the token or bind to loopback only.`,\n )\n this.name = 'BindAuthorityError'\n }\n}\n\nexport function assertBindAuthority(host: string, token: string | undefined): void {\n if (token && token.length > 0) return\n if (isLoopbackHost(host)) return\n throw new BindAuthorityError(host)\n}\n\nexport interface AuthOptions {\n // Bearer token required on every protected route. Undefined / empty → the\n // middleware is not mounted and the route stays unauthenticated. The\n // loopback-only gate above is what keeps that case from being public.\n token?: string\n // When `true`, trust an upstream reverse proxy and skip the request-side\n // check. The fail-loud bind-authority gate still applies upstream. Wired\n // to `NEAT_AUTH_PROXY=true` in production.\n trustProxy?: boolean\n // ADR-073 §3 amendment — public-read mode for reference deployments.\n // When `true`, GET / HEAD / OPTIONS pass through without a bearer; every\n // other verb still requires the token. OTLP ingest is excluded — that\n // surface stays gated unconditionally (the receiver mounts its own\n // middleware without this flag).\n publicRead?: boolean\n // Extra paths to leave unauthenticated, matched exactly (no suffix\n // matching — see the note on DEFAULT_UNAUTH_PATHS below). Used by tests\n // and by ad-hoc callers that mount their own probes.\n extraUnauthenticatedSuffixes?: ReadonlyArray<string>\n // Diagnostic hook fired whenever a request is rejected with 401 for a\n // missing or invalid bearer. The REST host leaves this unset — a human\n // running `curl` doesn't need a server-side line for their own 401. The\n // OTLP receiver passes it so an app whose telemetry is being dropped for a\n // bad token leaves a signal on the daemon side instead of failing silently.\n // The hook does not change the response body or status; it only observes.\n onReject?: () => void\n}\n\n// Verbs the public-read split treats as reads. Everything else is a write\n// and keeps the bearer requirement.\nconst PUBLIC_READ_METHODS: ReadonlySet<string> = new Set(['GET', 'HEAD', 'OPTIONS'])\n\n// Probes that always stay open. `/health` is dual-mounted under\n// `/projects/:project/` too (registerRoutes mounts it both unprefixed and\n// inside the `/projects/:project` plugin scope) — that's the one project-\n// scoped variant a real route relies on today. ADR-073 §3 names `/healthz`\n// and `/readyz` explicitly as reserved probe paths (no handler registers them\n// yet); `/health` is the existing endpoint the web shell and CI smoke already\n// lean on. `/api/config` is the public-read negotiation endpoint — the web\n// shell hits it before any authed call to learn which mode the daemon is\n// running in.\n//\n// Matching used to be `path === suffix || path.endsWith(suffix)`, which\n// exempts *any* path that merely ends with one of these strings — a future\n// protected route named e.g. `/admin/health` would slip through\n// unauthenticated by accident. Matching is now exact on the root path, plus\n// one explicit pattern for the `/projects/:project/<name>` shape. Nothing\n// else qualifies.\nconst DEFAULT_UNAUTH_PATHS: ReadonlyArray<string> = [\n '/health',\n '/healthz',\n '/readyz',\n '/api/config',\n]\n\n// `/projects/<one segment>/health` (or /healthz, /readyz, /api/config) —\n// exactly one path segment for the project name, nothing before `/projects`\n// and nothing after the probe name. Built from DEFAULT_UNAUTH_PATHS so the\n// two lists can't drift.\nconst PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(\n `^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join('|')})$`,\n)\n\nexport function mountBearerAuth(app: FastifyInstance, opts: AuthOptions): void {\n if (!opts.token || opts.token.length === 0) return\n if (opts.trustProxy) return\n\n const expected = Buffer.from(opts.token, 'utf8')\n const exactUnauthPaths = new Set([\n ...DEFAULT_UNAUTH_PATHS,\n ...(opts.extraUnauthenticatedSuffixes ?? []),\n ])\n const publicRead = opts.publicRead === true\n\n app.addHook('preHandler', (req: FastifyRequest, reply: FastifyReply, done: (err?: Error) => void) => {\n const path = (req.url.split('?')[0] ?? '').replace(/\\/+$/, '')\n if (exactUnauthPaths.has(path) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path)) {\n done()\n return\n }\n\n // Public-read split: GET / HEAD / OPTIONS pass through anonymously, every\n // other verb keeps the bearer check. The token still authorizes writes,\n // and the bind-authority gate above still demands a token for non-loopback\n // binds — public-read enables anonymous reads on top of that, it doesn't\n // replace either invariant.\n if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {\n done()\n return\n }\n\n const header = req.headers.authorization\n if (typeof header !== 'string' || !header.startsWith('Bearer ')) {\n opts.onReject?.()\n void reply.code(401).send({ error: 'unauthorized' })\n return\n }\n const provided = Buffer.from(header.slice('Bearer '.length).trim(), 'utf8')\n if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {\n opts.onReject?.()\n void reply.code(401).send({ error: 'unauthorized' })\n return\n }\n done()\n })\n}\n\n// Read both tokens from the environment in one place so server.ts, daemon.ts,\n// and the OTel receivers all agree on precedence (ADR-073 §4). `publicRead`\n// rides the same shape so callers don't need a second env read.\nexport interface AuthEnv {\n authToken: string | undefined\n otelToken: string | undefined\n trustProxy: boolean\n publicRead: boolean\n}\n\nfunction parseBoolEnv(v: string | undefined): boolean {\n if (!v) return false\n return v === 'true' || v === '1'\n}\n\nexport function readAuthEnv(env: NodeJS.ProcessEnv = process.env): AuthEnv {\n const t = env.NEAT_AUTH_TOKEN\n const ot = env.NEAT_OTEL_TOKEN\n return {\n authToken: t && t.length > 0 ? t : undefined,\n otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : undefined,\n trustProxy: env.NEAT_AUTH_PROXY === 'true',\n publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ),\n }\n}\n","import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport zlib from 'node:zlib'\nimport type { Transform } from 'node:stream'\nimport Fastify, { type FastifyInstance } from 'fastify'\nimport protobuf from 'protobufjs'\nimport { mountBearerAuth } from './auth.js'\n\n// OTLP/HTTP receiver. Listens on /v1/traces and decodes the JSON wire format\n// (collector's `otlphttp` exporter with `encoding: json`). Each span is\n// flattened into a ParsedSpan and handed to the configured handler. The\n// handler is the seam #8 wires its edge mapper into; #7 itself stays decoupled\n// from graph mutation.\n\nexport interface ParsedSpan {\n service: string\n // True when the resource carried a `service.name` attribute. False (or\n // omitted, for legacy producers) routes the span to `service:unidentified`\n // in the resolved project and trips a once-per-session-per-project warning\n // on the ingest side (issue #374). OTel spec requires SDKs to set\n // `service.name`, but customised exporters can omit it — diagnostic\n // visibility beats silent drop. Field is optional so test fixtures that\n // hand-construct ParsedSpan with a known service.name don't need to set\n // a flag they don't care about; the receiver always sets it.\n // See docs/contracts/otlp-routing.md §Fallback when `resource.service.name`\n // is missing.\n resourceServiceNamePresent?: boolean\n traceId: string\n spanId: string\n parentSpanId?: string\n name: string\n kind?: number\n startTimeUnixNano: string\n endTimeUnixNano: string\n // ISO8601 derived from startTimeUnixNano. Production paths (lastObserved on\n // OBSERVED edges) read this so the recorded time reflects when the span fired,\n // not when the receiver received it. Undefined only when startTimeUnixNano is\n // missing or unparseable — handler falls back to wall-clock in that case.\n // See docs/contracts/otel-ingest.md §lastObserved-from-span-time.\n startTimeIso?: string\n // bigint so the 9-digit-nanos arithmetic doesn't lose precision on long traces.\n durationNanos: bigint\n // Deployment environment from `deployment.environment(.name)`. Span attrs\n // win over resource attrs (per-span overrides a resource-wide declaration);\n // the canonical `deployment.environment.name` (OTel SC v1.27+) wins over\n // the compat form `deployment.environment`. Literal `'unknown'` is the\n // honest fallback when no env signal is present anywhere on the span or\n // its resource. See ADR-074 §2 / docs/contracts/env-dimension.md.\n env: string\n attributes: Record<string, AttributeValue>\n // Convenience accessors for the attributes #8 cares about.\n dbSystem?: string\n dbName?: string\n // The collection a `db.system: mongodb` span operated on (ADR-148). The\n // mongodb / mongoose instrumentation sets `db.collection.name` in the stable\n // convention, `db.mongodb.collection` in the older one — read the first, fall\n // back to the second.\n dbCollection?: string\n // The table a SQL span operated on (ADR-152). The SQLAlchemy / dbapi\n // instrumentation emits no table attribute — the table is only inside the\n // `db.statement` SQL text — so this is parsed from it (`tableFromSqlStatement`),\n // conservatively: a single FROM/INTO/UPDATE table, else undefined.\n dbTable?: string\n // The columns that same SQL span TOUCHED (ADR-157 §2), parsed from the\n // `db.statement` alongside the table (`columnsFromSqlStatement`). Empty when\n // the statement recovers no columns (a `SELECT *`, an aggregate, a JOIN /\n // subquery degrade, or a shape the parser doesn't read). Merged onto the\n // `sql-table` node as OBSERVED column attributes in handleSpan.\n dbColumns?: string[]\n // The served route + method off a SERVER span (#576). `http.route` is the\n // templated path the router matched (`/users/{id}`); the method is the stable\n // `http.request.method`, falling back to the legacy `http.method`. handleSpan\n // matches these against the statically-extracted RouteNode to mint the OBSERVED\n // twin of a declared route.\n httpRoute?: string\n httpMethod?: string\n // Messaging semconv (OTel). `messaging.system` names the broker family\n // (kafka, rabbitmq, redis, …); the destination is the topic/queue the span\n // produced to or consumed from — the canonical `messaging.destination.name`\n // (SC v1.24+) with the legacy `messaging.destination` as fallback. handleSpan\n // reads these to mint a PUBLISHES_TO (PRODUCER) or CONSUMES_FROM (CONSUMER)\n // OBSERVED edge to the destination node, fusing with the static extractor's\n // topic node. See docs/contracts/otel-ingest.md §Queue producers and consumers.\n messagingSystem?: string\n messagingDestination?: string\n // GraphQL semconv (OTel). `graphql.operation.name` is the client-supplied\n // operation name the execution span resolved (`GetUser`); `graphql.operation.type`\n // is the operation kind (`query` / `mutation` / `subscription`). handleSpan reads\n // both to mint an OBSERVED `CONTAINS` edge from the serving service to a\n // per-operation node, recovering the operation-level topology that HTTP grain\n // collapses onto `POST /graphql`. See docs/contracts/otel-ingest.md §GraphQL\n // operations.\n graphqlOperationName?: string\n graphqlOperationType?: string\n // gRPC RPC semconv (OTel). `rpc.system` names the RPC framework (`grpc`);\n // `rpc.service` is the fully-qualified proto service (`orders.OrderService`)\n // and `rpc.method` the bare method (`GetOrder`). The serving (SERVER) and\n // calling (CLIENT) sides both carry these. handleSpan reads them off the\n // serving span to mint an OBSERVED `CONTAINS` edge from the serving service to\n // a per-method node, recovering the method-level topology gRPC's service-grain\n // edge collapses — and keyed so the static `.proto` definition fuses onto the\n // same node. See docs/contracts/otel-ingest.md §gRPC methods.\n rpcSystem?: string\n rpcService?: string\n rpcMethod?: string\n // WebSocket channel (OTel HTTP semconv). A WebSocket connection opens with an\n // HTTP upgrade handshake: a SERVER `GET` whose `Upgrade` request header names\n // `websocket`. That single span is the only reliable OBSERVED signal a channel\n // exists — the message frames afterwards ride the socket, not more spans. This\n // field carries the channel path off that upgrade span (`http.route` when the\n // router templated it, else `url.path` / `http.target`), and is set only when\n // the upgrade header is present. handleSpan reads it to mint a\n // WebSocketChannelNode and an OBSERVED `CONNECTS_TO` edge from the serving\n // service to it. See docs/contracts/otel-ingest.md §WebSocket channels.\n websocketChannel?: string\n // 0 = UNSET, 1 = OK, 2 = ERROR per OTLP. We only care that 2 means error.\n statusCode?: number\n errorMessage?: string\n // Pre-extracted from a span event with name=\"exception\". OTLP SDKs record\n // exceptions this way (richer than status.message). handleSpan reads these\n // first, falling back to status.message and span.name. See\n // docs/contracts/otel-ingest.md §exception-data-from-span-events.\n exception?: {\n type?: string\n message?: string\n stacktrace?: string\n }\n}\n\nexport type AttributeValue =\n | string\n | number\n | boolean\n | bigint\n | string[]\n | number[]\n | boolean[]\n | null\n\nexport type SpanHandler = (span: ParsedSpan) => void | Promise<void>\n// Variant that receives the project the URL already resolved to. Used by the\n// project-scoped route mounted at `/projects/:project/v1/traces` (issue #367):\n// the receiver hands the URL-path project to the daemon directly so the\n// daemon never has to guess via `service.name`-against-registry matching.\nexport type ProjectSpanHandler = (project: string, span: ParsedSpan) => void | Promise<void>\n\nexport interface BuildOtelReceiverOptions {\n onSpan: SpanHandler\n // Synchronous handler for spans with statusCode === 2. The receiver awaits\n // it before replying, so a write failure can return 500 → OTel SDK retries.\n // Optional — wiring is expected to plumb appendErrorEvent here when error\n // durability matters; ad-hoc receivers leave it undefined.\n // See docs/contracts/otel-ingest.md §Error events.\n onErrorSpanSync?: (span: ParsedSpan) => Promise<void>\n // Project-scoped variants — used by the `/projects/:project/v1/traces`\n // route. When unset the route is still mounted but falls through to the\n // legacy `onSpan` / `onErrorSpanSync` handlers (the project name is then\n // available to the consumer via the `OTEL_PROJECT_OVERRIDE` attribute on\n // each parsed span; ad-hoc receivers commonly leave both unset).\n onProjectSpan?: ProjectSpanHandler\n onProjectErrorSpanSync?: (project: string, span: ParsedSpan) => Promise<void>\n // #881 — decides whether the `/projects/:project/v1/traces` route serves the\n // named project. When it returns false the route replies 404 instead of a\n // 200 + empty `partialSuccess` that silently drops the batch, so a\n // misconfigured exporter (a wrong or wrong-cased project name in the URL)\n // finds out. Optional: when unset the route accepts any name — the\n // single-project `neat watch` receiver, where the URL name is advisory since\n // there is only one project to land in.\n isProjectRegistered?: (project: string) => boolean\n // #881 — classifies a bare `/v1/traces` batch for routability before reply.\n // The bare route replies 200 as soon as the body is parsed (§Non-blocking\n // ingest) and only routes each span later, off the queue — so without this\n // hook an unroutable span leaves on a 200 + empty `partialSuccess`, which an\n // OTel exporter reads as \"everything accepted\" while the batch was dropped.\n // The hook returns how many spans in the batch belong to no project this\n // daemon hosts (and a human message); the receiver keeps the 200 but reports\n // those as `partialSuccess.rejectedSpans` + `errorMessage` so a misconfigured\n // exporter finds out. Pure and synchronous: it reads in-memory routing state,\n // never mutates the graph or writes the unrouted ledger — the async `onSpan`\n // path still owns the drop and the `errors.ndjson` record. Optional: an\n // ad-hoc or single-consumer receiver leaves it unset and keeps the historical\n // empty-`partialSuccess` reply for every batch.\n classifyBareRoutability?: (spans: ParsedSpan[]) => { rejected: number; message?: string }\n // Fastify body limit. OTLP batches can be large; default is 16 MB.\n bodyLimit?: number\n // ADR-073 §4 — bearer required on `/v1/traces`. Defaults to `NEAT_AUTH_TOKEN`\n // when unset; `NEAT_OTEL_TOKEN` overrides at the call site so the REST and\n // OTLP surfaces rotate on independent schedules.\n authToken?: string\n // Same shape as the REST middleware: skip the request-side check when an\n // upstream reverse proxy already authenticated.\n trustProxy?: boolean\n}\n\ninterface OtlpKeyValue {\n key: string\n value?: OtlpAnyValue\n}\n\ninterface OtlpAnyValue {\n stringValue?: string\n intValue?: string | number\n doubleValue?: number\n boolValue?: boolean\n arrayValue?: { values?: OtlpAnyValue[] }\n // kvlistValue / bytesValue are skipped — neither is on the demo path.\n}\n\ninterface OtlpStatus {\n code?: number\n message?: string\n}\n\ninterface OtlpEvent {\n name?: string\n timeUnixNano?: string\n attributes?: OtlpKeyValue[]\n}\n\ninterface OtlpSpan {\n traceId?: string\n spanId?: string\n parentSpanId?: string\n name?: string\n kind?: number\n startTimeUnixNano?: string\n endTimeUnixNano?: string\n attributes?: OtlpKeyValue[]\n events?: OtlpEvent[]\n status?: OtlpStatus\n}\n\nfunction extractExceptionFromEvents(events: OtlpEvent[] | undefined): ParsedSpan['exception'] {\n if (!events) return undefined\n for (const ev of events) {\n if (ev.name !== 'exception') continue\n const attrs = attrsToRecord(ev.attributes)\n const out: ParsedSpan['exception'] = {}\n const t = attrs['exception.type']\n const m = attrs['exception.message']\n const s = attrs['exception.stacktrace']\n if (typeof t === 'string') out.type = t\n if (typeof m === 'string') out.message = m\n if (typeof s === 'string') out.stacktrace = s\n if (out.type || out.message || out.stacktrace) return out\n }\n return undefined\n}\n\ninterface OtlpScopeSpans {\n spans?: OtlpSpan[]\n}\n\ninterface OtlpResourceSpans {\n resource?: { attributes?: OtlpKeyValue[] }\n scopeSpans?: OtlpScopeSpans[]\n}\n\nexport interface OtlpTracesRequest {\n resourceSpans?: OtlpResourceSpans[]\n}\n\nfunction flattenAttribute(v: OtlpAnyValue | undefined): AttributeValue {\n if (!v) return null\n if (v.stringValue !== undefined) return v.stringValue\n if (v.boolValue !== undefined) return v.boolValue\n if (v.intValue !== undefined) {\n return typeof v.intValue === 'string' ? Number(v.intValue) : v.intValue\n }\n if (v.doubleValue !== undefined) return v.doubleValue\n if (v.arrayValue?.values) {\n return v.arrayValue.values.map((x) => flattenAttribute(x)) as AttributeValue\n }\n return null\n}\n\nfunction attrsToRecord(attrs: OtlpKeyValue[] | undefined): Record<string, AttributeValue> {\n const out: Record<string, AttributeValue> = {}\n if (!attrs) return out\n for (const kv of attrs) {\n if (kv.key) out[kv.key] = flattenAttribute(kv.value)\n }\n return out\n}\n\nfunction durationNanos(start?: string, end?: string): bigint {\n if (!start || !end) return 0n\n try {\n return BigInt(end) - BigInt(start)\n } catch {\n return 0n\n }\n}\n\n// Convert OTLP's startTimeUnixNano (a base-10 string of nanoseconds since the\n// Unix epoch) to ISO8601. Returns undefined when the input is missing, zero,\n// or unparseable, so the caller can fall back to wall-clock without surfacing\n// a fake timestamp on the edge.\nexport function isoFromUnixNano(nanos: string | undefined): string | undefined {\n if (!nanos || nanos === '0') return undefined\n try {\n const ms = Number(BigInt(nanos) / 1_000_000n)\n if (!Number.isFinite(ms)) return undefined\n return new Date(ms).toISOString()\n } catch {\n return undefined\n }\n}\n\n// Resolve `deployment.environment` per ADR-074 §2. The four-step fallback\n// is: span-attr canonical form → span-attr compat form → resource-attr\n// canonical form → resource-attr compat form → literal `'unknown'`. The\n// literal `'unknown'` is the honest sentinel; defaulting to `'production'`\n// or `'development'` would bake an incorrect assumption into every span\n// from a workload that hasn't yet wired its env signal.\nconst ENV_ATTR_CANONICAL = 'deployment.environment.name'\nconst ENV_ATTR_COMPAT = 'deployment.environment'\nconst ENV_FALLBACK = 'unknown'\n\nfunction pickEnv(\n spanAttrs: Record<string, AttributeValue>,\n resourceAttrs: Record<string, AttributeValue>,\n): string {\n for (const attrs of [spanAttrs, resourceAttrs]) {\n for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {\n const v = attrs[key]\n if (typeof v === 'string' && v.length > 0) return v\n }\n }\n return ENV_FALLBACK\n}\n\n// The datastore engine a `db.system` span names, normalized at the parse\n// boundary. The mongoose OTel instrumentation — the one that actually emits\n// per-operation collection spans on real apps — tags its spans\n// `db.system: 'mongoose'`, but mongoose is an ORM over mongodb: the datastore\n// IS mongodb, and `mongoose` on the span is an instrumentation detail. Rewrite\n// it to `mongodb` here so every downstream reader (the collection edge, the\n// database-node engine, ADR-141 fusion) sees one engine and none of them has to\n// know the ORM label. See ADR-150.\nfunction normalizeDbSystem(attrs: Record<string, AttributeValue>): string | undefined {\n // `db.system` (semconv ≤ 1.10) or `db.system.name` (≥ 1.30) — read both so the\n // mongodb guard and every dbSystem consumer see the system on the newer semconv.\n const raw = attrs['db.system'] ?? attrs['db.system.name']\n if (typeof raw !== 'string') return undefined\n return raw === 'mongoose' ? 'mongodb' : raw\n}\n\n// The table a SQL statement targets, for the OBSERVED table-grain edge. The\n// SQLAlchemy / dbapi instrumentation emits no dedicated table attribute (verified\n// against a live app — ADR-152); the table lives only in the `db.statement` SQL.\n// Conservative by design: the single table after FROM / INTO / UPDATE, quote- and\n// schema-stripped. A joined or multi-`FROM` (subquery) statement degrades to\n// `null` — the database-grain edge stands, and the table is never guessed.\nexport function tableFromSqlStatement(sql: string): string | null {\n if (typeof sql !== 'string' || sql.length === 0) return null\n if (/\\bjoin\\b/i.test(sql)) return null\n const froms = sql.match(/\\bfrom\\b/gi)\n if (froms && froms.length > 1) return null // subquery / multi-table — degrade\n const m = /\\b(?:from|into|update)\\s+(?:\"?[\\w$]+\"?\\s*\\.\\s*)?\"?([a-zA-Z_][\\w$]*)\"?/i.exec(sql)\n return m ? m[1]! : null\n}\n\n// The columns a SQL statement TOUCHES, for the OBSERVED column-grain attributes\n// (ADR-157 §2) — the column sibling of `tableFromSqlStatement` above. Production\n// statements are parameterized (values redacted to `$1`), but the column NAMES\n// are still in the text: an INSERT names its column list, an UPDATE its SET\n// targets, a SELECT its projection, and each of those plus a DELETE names the\n// columns in its WHERE predicate. Real ORM SQL is quoted and schema-qualified,\n// and SQLAlchemy projects every column as `<table>.<col> AS <table>_<col>`, so\n// the parse strips quotes and the table/schema qualifier and drops the `AS`\n// alias to recover the REAL column name, never the alias (`orders.id AS\n// orders_id` → `id`). Aggregate / function / `*` projections yield no column.\n// It degrades to `[]` on the same JOIN / multi-`FROM` (subquery) shapes\n// `tableFromSqlStatement` degrades to `null` on — there the columns belong to\n// more than one table and can't be attributed honestly. Returned names are\n// lowercased and de-duplicated; an unparseable or empty statement yields `[]`.\n// Ported verbatim from the proven column-grain spike.\nexport function columnsFromSqlStatement(sql: string): string[] {\n if (typeof sql !== 'string' || sql.length === 0) return []\n const s = sql.replace(/\\s+/g, ' ').trim()\n if (/\\bjoin\\b/i.test(s)) return []\n if ((s.match(/\\bfrom\\b/gi) ?? []).length > 1) return [] // subquery / multi-table — degrade\n\n // One projected/assigned term → its bare column name, or null to skip it (an\n // aggregate / function / wildcard, or a token that isn't a plain identifier).\n const bare = (raw: string): string | null => {\n let t = raw.trim().replace(/\"/g, '')\n if (/[()*]/.test(t)) return null // aggregate / func / wildcard — skip\n t = t.split(/\\s+as\\s+/i)[0]!.trim() // drop \"AS alias\" — keep the real column\n t = t.split('.').pop()! // drop table / schema qualifier\n return /^[a-z_][\\w$]*$/i.test(t) ? t.toLowerCase() : null\n }\n const isCol = (c: string | null): c is string => c !== null\n const cols = (list: string): string[] => [...new Set(list.split(',').map(bare).filter(isCol))]\n const whereCols = (w: string | undefined): string[] =>\n w\n ? [\n ...new Set(\n [\n ...w.matchAll(\n /(?:\"?[\\w$]+\"?\\.)?\"?([a-z_][\\w$]*)\"?\\s*(?:=|<|>|<=|>=|<>|!=|\\bis\\b|\\bin\\b|\\blike\\b)/gi,\n ),\n ]\n .map((match) => match[1]!.toLowerCase())\n .filter((c) => !/^(and|or|not|null)$/i.test(c)),\n ),\n ]\n : []\n\n let m: RegExpExecArray | null\n if ((m = /\\binsert\\s+into\\s+(?:\"?[\\w$]+\"?\\.)?\"?[\\w$]+\"?\\s*\\(([^)]*)\\)/i.exec(s))) {\n return cols(m[1]!)\n }\n if ((m = /\\bupdate\\s+(?:\"?[\\w$]+\"?\\.)?\"?[\\w$]+\"?\\s+set\\s+(.+?)(?:\\bwhere\\b(.+))?$/i.exec(s))) {\n const set = m[1]!\n .split(',')\n .map((p) => bare(p.split('=')[0]!))\n .filter(isCol)\n return [...new Set([...set, ...whereCols(m[2] ?? undefined)])]\n }\n if ((m = /\\bdelete\\s+from\\s+(?:\"?[\\w$]+\"?\\.)?\"?[\\w$]+\"?(?:\\s+where\\b(.+))?$/i.exec(s))) {\n return whereCols(m[1] ?? undefined)\n }\n if ((m = /\\bselect\\s+(.+?)\\s+from\\s+(?:\"?[\\w$]+\"?\\.)?\"?[\\w$]+\"?(?:\\s+where\\b(.+))?$/i.exec(s))) {\n if (m[1]!.trim() === '*') return [] // SELECT * — no column list to attribute\n return [...new Set([...cols(m[1]!), ...whereCols(m[2] ?? undefined)])]\n }\n return []\n}\n\n// The messaging destination (topic / queue / stream) a producer or consumer\n// span names. `messaging.destination.name` is the canonical semconv key\n// (SC v1.24+); `messaging.destination` is the older form some instrumentations\n// still emit. Prefer the canonical one, fall back to the legacy, and treat an\n// empty string as absent so an anonymous destination never keys a node.\nfunction messagingDestinationOf(\n attrs: Record<string, AttributeValue>,\n): string | undefined {\n for (const key of ['messaging.destination.name', 'messaging.destination']) {\n const v = attrs[key]\n if (typeof v === 'string' && v.length > 0) return v\n }\n return undefined\n}\n\n// True when the span's `Upgrade` request header names `websocket`. OTel HTTP\n// instrumentation captures configured request headers as `http.request.header.<key>`\n// with the key lower-cased and the value an array of strings (one per header\n// occurrence); older SDKs may write a bare string. A WebSocket handshake carries\n// `Upgrade: websocket`, so any element equal to `websocket` (case-insensitive)\n// marks the upgrade. Returns false when the header is absent — a plain `GET`\n// route span never mints a channel.\nfunction hasWebsocketUpgradeHeader(attrs: Record<string, AttributeValue>): boolean {\n const v = attrs['http.request.header.upgrade']\n const matches = (s: unknown): boolean =>\n typeof s === 'string' && s.trim().toLowerCase() === 'websocket'\n if (Array.isArray(v)) return v.some(matches)\n return matches(v)\n}\n\n// The channel path an upgrade span opens onto. Prefer the templated route\n// (`http.route`, e.g. `/ws/:room`) so high-cardinality connection paths collapse\n// onto one channel node; fall back to the concrete request path (`url.path`, the\n// SC v1.21+ name, or the legacy `http.target`) with any query string trimmed.\n// Returns undefined when nothing carries a path — the branch then falls through\n// rather than minting a channel keyed on an empty string.\nfunction websocketChannelPathOf(attrs: Record<string, AttributeValue>): string | undefined {\n const route = attrs['http.route']\n if (typeof route === 'string' && route.length > 0) return route\n for (const key of ['url.path', 'http.target']) {\n const v = attrs[key]\n if (typeof v === 'string' && v.length > 0) {\n const q = v.indexOf('?')\n const path = q === -1 ? v : v.slice(0, q)\n if (path.length > 0) return path\n }\n }\n return undefined\n}\n\n// The WebSocket channel this span serves, or undefined. Set only on the HTTP\n// upgrade handshake span — a request carrying `Upgrade: websocket` — so a plain\n// HTTP route span is never mistaken for a channel. See docs/contracts/otel-ingest.md\n// §WebSocket channels.\nfunction websocketChannelOf(attrs: Record<string, AttributeValue>): string | undefined {\n if (!hasWebsocketUpgradeHeader(attrs)) return undefined\n return websocketChannelPathOf(attrs)\n}\n\nexport function parseOtlpRequest(body: OtlpTracesRequest): ParsedSpan[] {\n const out: ParsedSpan[] = []\n for (const rs of body.resourceSpans ?? []) {\n const resourceAttrs = attrsToRecord(rs.resource?.attributes)\n // OTel spec requires SDKs to set `service.name`, but customised exporters\n // can omit it. Missing `service.name` routes to `service:unidentified`\n // in handleSpan + emits a once-per-session-per-project warning so the\n // diagnostic stays visible (issue #374). Silent drop is not an option.\n const rawServiceName = resourceAttrs['service.name']\n const resourceServiceNamePresent =\n typeof rawServiceName === 'string' && rawServiceName.length > 0\n const service = resourceServiceNamePresent\n ? (rawServiceName as string)\n : 'unidentified'\n\n for (const ss of rs.scopeSpans ?? []) {\n for (const span of ss.spans ?? []) {\n const attrs = attrsToRecord(span.attributes)\n // SQL text is `db.statement` (semconv ≤ 1.10) or `db.query.text` (≥ 1.30,\n // the rename the current GORM/pg OTel plugins emit) — read both so a span\n // on the newer semconv still yields a table and columns.\n const dbSqlText =\n typeof attrs['db.statement'] === 'string'\n ? (attrs['db.statement'] as string)\n : typeof attrs['db.query.text'] === 'string'\n ? (attrs['db.query.text'] as string)\n : undefined\n const dbSystemName = normalizeDbSystem(attrs)\n // The ORM's own resolved table, emitted directly as `db.sql.table` (old\n // semconv) or, for a relational system, `db.collection.name` (new semconv).\n // Ground truth — no SELECT*/join/CTE parse degradation. mongodb keeps\n // `db.collection.name` as its collection, never a table.\n const directDbTable =\n typeof attrs['db.sql.table'] === 'string'\n ? (attrs['db.sql.table'] as string)\n : dbSystemName !== 'mongodb' && typeof attrs['db.collection.name'] === 'string'\n ? (attrs['db.collection.name'] as string)\n : undefined\n const parsed: ParsedSpan = {\n service,\n resourceServiceNamePresent,\n traceId: span.traceId ?? '',\n spanId: span.spanId ?? '',\n parentSpanId: span.parentSpanId || undefined,\n name: span.name ?? '',\n kind: span.kind,\n startTimeUnixNano: span.startTimeUnixNano ?? '0',\n endTimeUnixNano: span.endTimeUnixNano ?? '0',\n startTimeIso: isoFromUnixNano(span.startTimeUnixNano),\n durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),\n env: pickEnv(attrs, resourceAttrs),\n attributes: attrs,\n dbSystem: dbSystemName,\n dbName: typeof attrs['db.name'] === 'string' ? (attrs['db.name'] as string) : undefined,\n dbCollection:\n typeof attrs['db.collection.name'] === 'string'\n ? (attrs['db.collection.name'] as string)\n : typeof attrs['db.mongodb.collection'] === 'string'\n ? (attrs['db.mongodb.collection'] as string)\n : undefined,\n dbTable: directDbTable ?? (dbSqlText ? (tableFromSqlStatement(dbSqlText) ?? undefined) : undefined),\n dbColumns: dbSqlText ? columnsFromSqlStatement(dbSqlText) : undefined,\n httpRoute:\n typeof attrs['http.route'] === 'string' ? (attrs['http.route'] as string) : undefined,\n httpMethod:\n typeof attrs['http.request.method'] === 'string'\n ? (attrs['http.request.method'] as string)\n : typeof attrs['http.method'] === 'string'\n ? (attrs['http.method'] as string)\n : undefined,\n messagingSystem:\n typeof attrs['messaging.system'] === 'string'\n ? (attrs['messaging.system'] as string)\n : undefined,\n messagingDestination: messagingDestinationOf(attrs),\n graphqlOperationName:\n typeof attrs['graphql.operation.name'] === 'string' &&\n (attrs['graphql.operation.name'] as string).length > 0\n ? (attrs['graphql.operation.name'] as string)\n : undefined,\n graphqlOperationType:\n typeof attrs['graphql.operation.type'] === 'string' &&\n (attrs['graphql.operation.type'] as string).length > 0\n ? (attrs['graphql.operation.type'] as string)\n : undefined,\n rpcSystem:\n typeof attrs['rpc.system'] === 'string' &&\n (attrs['rpc.system'] as string).length > 0\n ? (attrs['rpc.system'] as string)\n : undefined,\n rpcService:\n typeof attrs['rpc.service'] === 'string' &&\n (attrs['rpc.service'] as string).length > 0\n ? (attrs['rpc.service'] as string)\n : undefined,\n rpcMethod:\n typeof attrs['rpc.method'] === 'string' &&\n (attrs['rpc.method'] as string).length > 0\n ? (attrs['rpc.method'] as string)\n : undefined,\n websocketChannel: websocketChannelOf(attrs),\n statusCode: span.status?.code,\n errorMessage: span.status?.message,\n exception: extractExceptionFromEvents(span.events),\n }\n out.push(parsed)\n }\n }\n }\n return out\n}\n\nexport interface OtelReceiver {\n app: FastifyInstance\n // Resolves once every span enqueued so far has been handed to opts.onSpan.\n // Test seam — production code never awaits this.\n flushPending: () => Promise<void>\n}\n\n// Lazy-loaded protobuf decoder for ExportTraceServiceRequest. The bundled\n// .proto tree at packages/core/proto/ is shared with the gRPC receiver\n// (ADR-020). Cached after first load so successive receiver builds reuse it.\nlet exportTraceServiceRequestType: protobuf.Type | null = null\nlet exportTraceServiceResponseType: protobuf.Type | null = null\n\nfunction loadProtoRoot(): protobuf.Root {\n const here = path.dirname(fileURLToPath(import.meta.url))\n const protoRoot = path.resolve(here, '..', 'proto')\n const root = new protobuf.Root()\n root.resolvePath = (_origin, target) => path.resolve(protoRoot, target)\n root.loadSync(\n 'opentelemetry/proto/collector/trace/v1/trace_service.proto',\n { keepCase: true },\n )\n return root\n}\n\nfunction loadProtobufDecoder(): protobuf.Type {\n if (exportTraceServiceRequestType) return exportTraceServiceRequestType\n const root = loadProtoRoot()\n exportTraceServiceRequestType = root.lookupType(\n 'opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest',\n )\n return exportTraceServiceRequestType\n}\n\nfunction loadProtobufResponseEncoder(): protobuf.Type {\n if (exportTraceServiceResponseType) return exportTraceServiceResponseType\n const root = loadProtoRoot()\n exportTraceServiceResponseType = root.lookupType(\n 'opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse',\n )\n return exportTraceServiceResponseType\n}\n\n// Empty-partial-success response, encoded once and cached — the hot path where\n// every span was accepted. Caching the bytes avoids re-running the protobuf\n// encoder per request.\nlet cachedProtobufResponseBody: Buffer | null = null\n\n// `rejected` omitted / 0 → the cached empty `ExportTraceServiceResponse`\n// (\"everything accepted\"). A positive `rejected` (the #881 unrouted-batch path)\n// encodes a populated `partial_success` — not cached, since the count and\n// message vary per batch. The proto is loaded `keepCase`, so the field names\n// stay snake_case (`partial_success`, `rejected_spans`, `error_message`).\nfunction encodeProtobufResponseBody(rejected?: number, message?: string): Buffer {\n const Type = loadProtobufResponseEncoder()\n if (!rejected) {\n if (cachedProtobufResponseBody) return cachedProtobufResponseBody\n // `partial_success` left unset = empty submessage = \"everything accepted\".\n const msg = Type.create({})\n cachedProtobufResponseBody = Buffer.from(Type.encode(msg).finish())\n return cachedProtobufResponseBody\n }\n const msg = Type.fromObject({\n partial_success: { rejected_spans: rejected, error_message: message ?? '' },\n })\n return Buffer.from(Type.encode(msg).finish())\n}\n\nasync function decodeProtobufBody(buf: Buffer): Promise<OtlpTracesRequest> {\n const Type = loadProtobufDecoder()\n // Decode keeps the proto field names verbatim (keepCase: true), matching the\n // GrpcExportRequest shape that reshapeGrpcRequest already understands.\n // toObject() options mirror the gRPC receiver's proto-loader config\n // (otel-grpc.ts: longs: String, enums: Number, bytes left as Buffers) so\n // both protobuf paths hand reshapeGrpcRequest the identical shape. The old\n // .toJSON() here rendered bytes as base64 strings (bytesToHex returned ''\n // → empty trace/span IDs) and enums as name strings (\"SPAN_KIND_CLIENT\"\n // never matches the numeric mint gate) — every http/protobuf span was\n // accepted and then silently minted nothing (#468).\n // Dynamic import sidesteps the circular module dep with otel-grpc.ts.\n const decoded = Type.toObject(Type.decode(buf), {\n longs: String,\n enums: Number,\n }) as Record<string, unknown>\n const { reshapeGrpcRequest } = await import('./otel-grpc.js')\n return reshapeGrpcRequest(decoded as never)\n}\n\n// The OTLP/HTTP spec allows a request body to be gzip-compressed, and the\n// standard OpenTelemetry Collector's OTLP exporter does so by default\n// (`Content-Encoding: gzip`). The receiver must undo that before the body ever\n// reaches the JSON parser or the protobuf decoder — otherwise a compressed\n// batch is unparseable garbage and every \"bring your own collector\" deployment\n// silently fails to ingest. Returns the zlib transform stream for a supported\n// encoding, or null for `identity` / absent / anything we don't decode (which\n// then flows through untouched, exactly as before). `deflate` rides alongside\n// gzip since it's the same one-liner; `x-gzip` is the legacy alias some proxies\n// still emit. gRPC needs nothing here — @grpc/grpc-js decompresses at the\n// transport layer before `call.request` is ever materialized.\nfunction decompressorForEncoding(encoding: string): Transform | null {\n switch (encoding) {\n case 'gzip':\n case 'x-gzip':\n return zlib.createGunzip()\n case 'deflate':\n return zlib.createInflate()\n default:\n return null\n }\n}\n\nexport async function buildOtelReceiver(\n opts: BuildOtelReceiverOptions,\n): Promise<FastifyInstance & { flushPending: () => Promise<void> }> {\n const app = Fastify({\n logger: false,\n bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024,\n })\n\n // Decompress a `Content-Encoding: gzip` (or `deflate`) body before Fastify's\n // content-type parser runs, so the JSON parser and the protobuf decoder both\n // see plaintext and the rest of the receiver stays oblivious to compression.\n // Streaming (not buffer-then-inflate) so the content-type parser's bodyLimit\n // is enforced on the *decompressed* bytes — a decompression bomb is capped at\n // the same 16 MB ceiling an uncompressed batch is. A truncated or garbage\n // body makes the zlib stream emit `error`, which Fastify turns into a clean\n // 400 (the same shape a bad protobuf body gets); it never throws past the\n // receiver or back-pressures the sender. An uncompressed request carries no\n // `Content-Encoding`, so the hook is a pure pass-through and that path is\n // byte-for-byte unchanged.\n app.addHook('preParsing', (req, _reply, payload, done) => {\n const encoding = (req.headers['content-encoding'] ?? '')\n .toString()\n .trim()\n .toLowerCase()\n if (encoding === '' || encoding === 'identity') {\n done(null, payload)\n return\n }\n const decompressor = decompressorForEncoding(encoding)\n if (!decompressor) {\n // An encoding we don't decode (e.g. `br`). Leave the body untouched — the\n // content-type parser fails it just as it did before, no new behavior.\n done(null, payload)\n return\n }\n // Fastify checks the bytes it reads off the body stream against the\n // `Content-Length` header. After decompression that count is the *plaintext*\n // length, which never matches the compressed `Content-Length` — so Fastify\n // would reject every batch with FST_ERR_CTP_INVALID_CONTENT_LENGTH unless\n // the transformed stream reports how many *encoded* (wire) bytes it\n // consumed. Track the source's byte count and expose it as\n // `receivedEncodedLength`, the property Fastify reads for that comparison\n // (mirrors @fastify/compress).\n const tracked = decompressor as Transform & { receivedEncodedLength?: number }\n tracked.receivedEncodedLength = 0\n payload.on('data', (chunk: Buffer) => {\n tracked.receivedEncodedLength = (tracked.receivedEncodedLength ?? 0) + chunk.length\n })\n // Forward a source read error onto the decompressor so the failure travels\n // the same path a decode error does, rather than surfacing as an unhandled\n // stream error that could dark the daemon.\n payload.on('error', (err) => decompressor.destroy(err))\n payload.pipe(decompressor)\n done(null, decompressor)\n })\n\n // ADR-073 §4 — bearer on `/v1/traces`. `/health` stays unauthenticated via\n // the default suffix list (the CI smoke and supervisors lean on it for\n // liveness probes).\n //\n // A rejected OTLP POST is a silent failure on the sender's side — the app\n // gets a bare 401 and its telemetry vanishes, so the operator sees an empty\n // OBSERVED layer with no clue why. Emit a server-side warning when that\n // happens, rate-limited to one line per interval so a chatty misconfigured\n // exporter (they retry hard) can't flood the log. The plain REST 401 path\n // stays quiet — that surface leaves this hook unset.\n const REJECT_WARN_INTERVAL_MS = 60_000\n let lastRejectWarnAt = 0\n const warnRejectedOtlp = (): void => {\n const now = Date.now()\n if (now - lastRejectWarnAt < REJECT_WARN_INTERVAL_MS) return\n lastRejectWarnAt = now\n console.warn(\n '[neatd] rejecting OTLP spans on /v1/traces — missing or invalid bearer token (set NEAT_OTEL_TOKEN on the instrumented app)',\n )\n }\n mountBearerAuth(app, {\n token: opts.authToken,\n trustProxy: opts.trustProxy,\n onReject: warnRejectedOtlp,\n })\n\n // Non-blocking ingest (ADR-033). The receiver replies 200 OK as soon as the\n // body is parsed; mutation runs through this queue, drained on the next tick.\n // OTel SDK exporters retry on timeout, so blocking ingest produces observable\n // backpressure on the system being observed — ambient observation requires no\n // observable effect.\n const queue: ParsedSpan[] = []\n let draining = false\n let drainPromise: Promise<void> = Promise.resolve()\n\n const drain = async (): Promise<void> => {\n if (draining) return\n draining = true\n try {\n while (queue.length > 0) {\n const span = queue.shift()!\n try {\n await opts.onSpan(span)\n } catch (err) {\n console.warn(`[neat] otel handler error: ${(err as Error).message}`)\n }\n }\n } finally {\n draining = false\n }\n }\n\n const enqueue = (spans: ParsedSpan[]): void => {\n if (spans.length === 0) return\n for (const s of spans) queue.push(s)\n // Schedule on the next tick so the 200 response is on the wire before any\n // mutation runs. Each call gets its own promise so flushPending() can wait\n // on the latest drain cycle.\n drainPromise = drainPromise.then(() => drain())\n }\n\n // Per-project queue is reusable across the project-scoped route — the\n // project name rides in the URL, not on the span. Drain semantics mirror\n // the global queue so flushPending() captures both.\n const projectQueue: Array<{ project: string; span: ParsedSpan }> = []\n let projectDraining = false\n let projectDrainPromise: Promise<void> = Promise.resolve()\n const drainProject = async (): Promise<void> => {\n if (projectDraining) return\n projectDraining = true\n try {\n while (projectQueue.length > 0) {\n const { project, span } = projectQueue.shift()!\n try {\n if (opts.onProjectSpan) {\n await opts.onProjectSpan(project, span)\n } else {\n await opts.onSpan(span)\n }\n } catch (err) {\n console.warn(`[neat] otel handler error: ${(err as Error).message}`)\n }\n }\n } finally {\n projectDraining = false\n }\n }\n const enqueueProject = (project: string, spans: ParsedSpan[]): void => {\n if (spans.length === 0) return\n for (const s of spans) projectQueue.push({ project, span: s })\n projectDrainPromise = projectDrainPromise.then(() => drainProject())\n }\n\n // One-time-per-service-name deprecation warning for spans landing on the\n // bare `/v1/traces` endpoint. Under one daemon per project (ADR-096) that\n // route is the project's own ingest path and needs no migration, so the\n // warning is gated on whether this receiver actually offers project-scoped\n // routing: only a receiver wired with `onProjectSpan` (the multi-project\n // daemon that mounts `/projects/<name>/v1/traces`) has somewhere to migrate\n // an exporter to. A single-project receiver leaves the gate closed and never\n // nags. Still once-per-name so a long-running daemon doesn't flood stderr\n // while an operator migrates.\n const offersProjectRouting = opts.onProjectSpan !== undefined\n const legacyEndpointWarned = new Set<string>()\n function warnLegacyEndpoint(serviceName: string): void {\n if (!offersProjectRouting) return\n if (legacyEndpointWarned.has(serviceName)) return\n legacyEndpointWarned.add(serviceName)\n console.warn(\n `[neatd] received span on the global endpoint; migrate OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to /projects/<name>/v1/traces (service.name=\"${serviceName}\").`,\n )\n }\n\n // Shared body-decode + content-negotiation. Both `/v1/traces` and\n // `/projects/:project/v1/traces` go through this so the protobuf/JSON\n // dispatch stays in one place.\n async function readOtlpBody(req: import('fastify').FastifyRequest): Promise<\n | { ok: true; body: OtlpTracesRequest; flavor: 'json' | 'protobuf' }\n | { ok: false; code: 400 | 415; error: string }\n > {\n const ct = (req.headers['content-type'] ?? '').toString().split(';')[0]!.trim().toLowerCase()\n if (ct === 'application/x-protobuf') {\n try {\n const body = await decodeProtobufBody(req.body as Buffer)\n return { ok: true, body, flavor: 'protobuf' }\n } catch (err) {\n return { ok: false, code: 400, error: `protobuf decode failed: ${(err as Error).message}` }\n }\n }\n if (!ct || ct === 'application/json') {\n return { ok: true, body: (req.body ?? {}) as OtlpTracesRequest, flavor: 'json' }\n }\n return { ok: false, code: 415, error: `unsupported content-type: ${ct}` }\n }\n\n function sendOtlpSuccess(reply: import('fastify').FastifyReply, flavor: 'json' | 'protobuf'): unknown {\n if (flavor === 'protobuf') {\n const buf = encodeProtobufResponseBody()\n return reply\n .code(200)\n .header('content-type', 'application/x-protobuf')\n .send(buf)\n }\n return reply\n .code(200)\n .header('content-type', 'application/json')\n .send({ partialSuccess: {} })\n }\n\n // #881 — a 200 that honestly reports dropped spans. Still a 200 (the OTLP\n // non-blocking discipline holds and the exporter isn't back-pressured), but\n // `partialSuccess.rejectedSpans` + `errorMessage` are populated so an exporter\n // aimed at a service this daemon can't place stops looking healthy. Encoding\n // stays symmetric with the request: protobuf in → protobuf out, JSON in → JSON\n // out (the OTLP spec requires it, per §HTTP receiver supports JSON and\n // protobuf).\n function sendOtlpPartial(\n reply: import('fastify').FastifyReply,\n flavor: 'json' | 'protobuf',\n rejected: number,\n message: string | undefined,\n ): unknown {\n if (flavor === 'protobuf') {\n const buf = encodeProtobufResponseBody(rejected, message)\n return reply\n .code(200)\n .header('content-type', 'application/x-protobuf')\n .send(buf)\n }\n return reply\n .code(200)\n .header('content-type', 'application/json')\n .send({ partialSuccess: { rejectedSpans: rejected, errorMessage: message } })\n }\n\n // Buffer application/x-protobuf bodies as raw bytes; the route handler\n // decodes them via the bundled .proto tree (ADR-020).\n app.addContentTypeParser(\n 'application/x-protobuf',\n { parseAs: 'buffer', bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },\n (_req, body, done) => {\n done(null, body)\n },\n )\n\n app.get('/health', async () => ({ ok: true }))\n\n app.post('/v1/traces', async (req, reply) => {\n // Legacy global endpoint. Spans land here when an OTel exporter hasn't\n // migrated to the project-scoped URL yet (issue #367); the daemon still\n // routes by `service.name` against the registry. Per-service-name\n // deprecation warning fires once so an operator notices without their\n // stderr flooding.\n const result = await readOtlpBody(req)\n if (!result.ok) {\n return reply.code(result.code).send({ error: result.error })\n }\n const spans = parseOtlpRequest(result.body)\n for (const s of spans) warnLegacyEndpoint(s.service)\n if (opts.onErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onErrorSpanSync(span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n }\n enqueue(spans)\n // #881 — the routing decision runs off the queue (above), so by reply time\n // we don't yet know a span will be dropped. Ask the pure classifier now:\n // if any span in the batch belongs to no project this daemon hosts, report\n // it as `partialSuccess.rejectedSpans` rather than a bare \"all accepted\".\n // The async `onSpan` path still drops those spans and writes the unrouted\n // ledger; this only makes the reply honest. Spans stay enqueued regardless,\n // so a routable span in a mixed batch still lands.\n if (opts.classifyBareRoutability) {\n const { rejected, message } = opts.classifyBareRoutability(spans)\n if (rejected > 0) return sendOtlpPartial(reply, result.flavor, rejected, message)\n }\n return sendOtlpSuccess(reply, result.flavor)\n })\n\n // Project-scoped route (issue #367). The URL `:project` carries the routing\n // key, sidestepping the `service.name`-against-registry heuristic the legacy\n // path uses. Spans get dispatched into the named project's ingest path\n // directly; `OTEL_SERVICE_NAME` regains its proper semantic role of naming\n // the ServiceNode inside the one project the URL already picked.\n app.post<{ Params: { project: string } }>('/projects/:project/v1/traces', async (req, reply) => {\n const project = req.params.project\n // #881 — an exporter aimed at a project this receiver doesn't serve gets an\n // honest 404, not a 200 that drops the batch. The bare `/v1/traces` route\n // stays lenient (it routes by service.name); the project-scoped URL is an\n // explicit assertion, so a wrong name is a real error worth surfacing.\n if (opts.isProjectRegistered && !opts.isProjectRegistered(project)) {\n return reply.code(404).send({ error: 'project not found', project })\n }\n const result = await readOtlpBody(req)\n if (!result.ok) {\n return reply.code(result.code).send({ error: result.error })\n }\n const spans = parseOtlpRequest(result.body)\n if (opts.onProjectErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onProjectErrorSpanSync(project, span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n } else if (opts.onErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onErrorSpanSync(span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n }\n enqueueProject(project, spans)\n return sendOtlpSuccess(reply, result.flavor)\n })\n\n // Attach flushPending so tests can wait for the queue without exporting a\n // separate handle. The cast goes through `unknown` because Fastify's typing\n // is parameterised over the raw server type and the simple intersection\n // confuses TS's structural narrowing.\n const decorated = app as unknown as FastifyInstance & { flushPending: () => Promise<void> }\n decorated.flushPending = async () => {\n // Settle both drain chains, then loop until both queues are fully empty\n // (a span enqueued mid-flush would otherwise be missed).\n while (queue.length > 0 || draining || projectQueue.length > 0 || projectDraining) {\n await Promise.all([drainPromise, projectDrainPromise])\n }\n }\n return decorated\n}\n\n// How far the OTLP receiver steps before giving up, and the stride between\n// candidate ports. Matches the orchestrator's triple allocator (8 attempts,\n// stride 1) so the daemon's own bind and the pre-spawn allocation reach the\n// same free port under the same contention.\nconst OTLP_STEP_ATTEMPTS = 8\nconst OTLP_STEP_STRIDE = 1\n\n// Bind an OTLP receiver, stepping to the next free port when the requested one\n// is held (daemon.md §Binding — a held OTLP port steps, it does not crash the\n// daemon; project-daemon §3). The REST port is the daemon's identity and stays\n// fatal on collision, but every OTLP consumer resolves the port dynamically\n// from `daemon.json` `ports.otlp`, so stepping the receiver and recording the\n// port it actually bound keeps the OBSERVED layer alive instead of darking the\n// whole daemon on a foreign collector holding `:4318`. Returns the bound\n// address (host:port); callers read the real port back from it. A requested\n// port of `0` means \"let the kernel pick a free one\" — no collision is\n// possible, so no stepping happens. Non-`EADDRINUSE` failures (permission\n// denied) and an exhausted step window propagate to the caller unchanged.\nexport async function listenSteppingOtlp(\n app: FastifyInstance,\n requestedPort: number,\n host: string,\n): Promise<string> {\n let port = requestedPort\n for (let attempt = 0; ; attempt++) {\n try {\n return await app.listen({ port, host })\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n const canStep =\n requestedPort !== 0 && code === 'EADDRINUSE' && attempt < OTLP_STEP_ATTEMPTS - 1\n if (!canStep) throw err\n console.warn(\n `otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`,\n )\n port += OTLP_STEP_STRIDE\n }\n }\n}\n\nexport function logSpanHandler(span: ParsedSpan): void {\n const parent = span.parentSpanId ? span.parentSpanId.slice(0, 8) : '<root>'\n const status = span.statusCode === 2 ? 'ERROR' : 'OK'\n const db = span.dbSystem ? ` db=${span.dbSystem}/${span.dbName ?? '?'}` : ''\n console.log(\n `otel: ${span.service} ${span.name} parent=${parent} status=${status}${db}`,\n )\n}\n"],"mappings":"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","names":["path","path","fileURLToPath","path","fileURLToPath","msg"]}
|
|
@@ -20,13 +20,13 @@ import {
|
|
|
20
20
|
startStalenessLoop,
|
|
21
21
|
touchLastSeen,
|
|
22
22
|
writeAtomically
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-RQQUI3NQ.js";
|
|
24
24
|
import {
|
|
25
25
|
assertBindAuthority,
|
|
26
26
|
buildOtelReceiver,
|
|
27
27
|
listenSteppingOtlp,
|
|
28
28
|
readAuthEnv
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-GDGUY4T6.js";
|
|
30
30
|
|
|
31
31
|
// src/daemon.ts
|
|
32
32
|
import {
|
|
@@ -891,4 +891,4 @@ export {
|
|
|
891
891
|
resolveHost,
|
|
892
892
|
startDaemon
|
|
893
893
|
};
|
|
894
|
-
//# sourceMappingURL=chunk-
|
|
894
|
+
//# sourceMappingURL=chunk-L4SZIIER.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
mountBearerAuth,
|
|
4
4
|
readAuthEnv,
|
|
5
5
|
tableFromSqlStatement
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-GDGUY4T6.js";
|
|
7
7
|
|
|
8
8
|
// src/graph.ts
|
|
9
9
|
import GraphDefault from "graphology";
|
|
@@ -3831,6 +3831,14 @@ function grpcStatusCodeFromAttrs(attrs) {
|
|
|
3831
3831
|
}
|
|
3832
3832
|
return void 0;
|
|
3833
3833
|
}
|
|
3834
|
+
function spanRecordsError(span) {
|
|
3835
|
+
if (span.statusCode === 2) return true;
|
|
3836
|
+
const grpc = grpcStatusCodeFromAttrs(span.attributes);
|
|
3837
|
+
if (grpc !== void 0 && grpc !== 0) return true;
|
|
3838
|
+
const httpStatus = httpResponseStatusFromAttrs(span.attributes);
|
|
3839
|
+
if (httpStatus !== void 0 && httpStatus >= 500) return true;
|
|
3840
|
+
return false;
|
|
3841
|
+
}
|
|
3834
3842
|
function nonHttpFailureMessageFromAttrs(attrs) {
|
|
3835
3843
|
const grpc = grpcStatusCodeFromAttrs(attrs);
|
|
3836
3844
|
if (grpc !== void 0 && grpc !== 0) {
|
|
@@ -4629,6 +4637,21 @@ async function recordExceptionIncident(ctx, span, ts) {
|
|
|
4629
4637
|
};
|
|
4630
4638
|
await appendErrorEvent(ctx, ev);
|
|
4631
4639
|
}
|
|
4640
|
+
async function recordGrpcFailureIncident(ctx, span, ts) {
|
|
4641
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
4642
|
+
const ev = {
|
|
4643
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
4644
|
+
timestamp: ts,
|
|
4645
|
+
service: span.service,
|
|
4646
|
+
traceId: span.traceId,
|
|
4647
|
+
spanId: span.spanId,
|
|
4648
|
+
errorType: "grpc-failure",
|
|
4649
|
+
errorMessage: incidentMessage(span),
|
|
4650
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
4651
|
+
affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
|
|
4652
|
+
};
|
|
4653
|
+
await appendErrorEvent(ctx, ev);
|
|
4654
|
+
}
|
|
4632
4655
|
async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status) {
|
|
4633
4656
|
const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
|
|
4634
4657
|
if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
|
|
@@ -4699,7 +4722,7 @@ async function handleSpan(ctx, span) {
|
|
|
4699
4722
|
warnUnidentifiedSpan(ctx.project ?? DEFAULT_PROJECT);
|
|
4700
4723
|
}
|
|
4701
4724
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
4702
|
-
const isError = span
|
|
4725
|
+
const isError = spanRecordsError(span);
|
|
4703
4726
|
const durationMs = span.durationNanos > 0n && !spanIsStreaming(span) ? Number(span.durationNanos) / 1e6 : void 0;
|
|
4704
4727
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
4705
4728
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
@@ -4935,10 +4958,13 @@ async function handleSpan(ctx, span) {
|
|
|
4935
4958
|
}
|
|
4936
4959
|
if (span.statusCode !== 2) {
|
|
4937
4960
|
const status = httpResponseStatus(span);
|
|
4961
|
+
const grpcStatus = grpcStatusCodeFromAttrs(span.attributes);
|
|
4938
4962
|
if (span.exception) {
|
|
4939
4963
|
await recordExceptionIncident(ctx, span, ts);
|
|
4940
4964
|
} else if (status !== void 0 && status >= 500) {
|
|
4941
4965
|
await recordFailingResponseIncident(ctx, span, sourceId, ts, status, 1);
|
|
4966
|
+
} else if (grpcStatus !== void 0 && grpcStatus !== 0) {
|
|
4967
|
+
await recordGrpcFailureIncident(ctx, span, ts);
|
|
4942
4968
|
} else if (status !== void 0 && status >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
4943
4969
|
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status);
|
|
4944
4970
|
}
|
|
@@ -20770,4 +20796,4 @@ export {
|
|
|
20770
20796
|
deprovisionConnector,
|
|
20771
20797
|
buildApi
|
|
20772
20798
|
};
|
|
20773
|
-
//# sourceMappingURL=chunk-
|
|
20799
|
+
//# sourceMappingURL=chunk-RQQUI3NQ.js.map
|