@semiont/observability 0.5.32 → 0.5.33
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/index.d.ts +12 -5
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +17 -1
- package/dist/node.js +30 -1
- package/dist/node.js.map +1 -1
- package/dist/process-logger.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -219,12 +219,19 @@ declare function recordGitStagingFailure(reason: 'index-lock' | 'other'): void;
|
|
|
219
219
|
/** Register `semiont.process.start_time`. Called by `initObservability*`. */
|
|
220
220
|
declare function registerProcessLifetimeMetrics(): void;
|
|
221
221
|
/**
|
|
222
|
-
* Supply a restart count.
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
222
|
+
* Supply a restart count. A supervisor is POSIX shell and cannot emit OTel, but
|
|
223
|
+
* it keeps a durable event log on the state mount — so the supervised child
|
|
224
|
+
* reads it and reports the count on the supervisor's behalf.
|
|
225
|
+
*
|
|
226
|
+
* **Return `undefined` for "cannot tell", never `0`.** A process running
|
|
227
|
+
* without a supervisor (dev, or a direct `node` invocation) has no restart
|
|
228
|
+
* history to report, and 0 is not that — 0 is the value a HEALTHY supervised
|
|
229
|
+
* process reports, so an unsupervised one publishing 0 is indistinguishable
|
|
230
|
+
* from a supervised one that has never crashed. `undefined` skips the
|
|
231
|
+
* observation entirely and the series simply has no point, which is what
|
|
232
|
+
* "unknown" looks like in a time series.
|
|
226
233
|
*/
|
|
227
|
-
declare function registerRestartCountProvider(provider: () => Promise<number> | number): void;
|
|
234
|
+
declare function registerRestartCountProvider(provider: () => Promise<number | undefined> | number | undefined): void;
|
|
228
235
|
/**
|
|
229
236
|
* Record that this process is dying abnormally, and mark the active span so a
|
|
230
237
|
* trace shows a span that ENDED IN DEATH rather than one that simply never
|
package/dist/index.js
CHANGED
|
@@ -334,7 +334,9 @@ function registerRestartCountProvider(provider) {
|
|
|
334
334
|
description: "Times the supervisor has restarted this service"
|
|
335
335
|
});
|
|
336
336
|
_restartCountGauge.addCallback(async (observer) => {
|
|
337
|
-
if (_restartCountProvider)
|
|
337
|
+
if (!_restartCountProvider) return;
|
|
338
|
+
const count = await _restartCountProvider();
|
|
339
|
+
if (count !== void 0) observer.observe(count);
|
|
338
340
|
});
|
|
339
341
|
}
|
|
340
342
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @semiont/observability — public API.\n *\n * Universal surface (works in Node + browser). For SDK *initialization*,\n * import from `@semiont/observability/node` or `/web` at the process entry\n * point. Everything else uses this module.\n *\n * Tier 2 of `.plans/OBSERVABILITY.md`. The public surface:\n *\n * - `withSpan(name, fn, options?)` — wrap an async block in a span;\n * `options` carries `kind` and `attrs`.\n * - `withActorSpan(actor, channel, fn, extraAttrs?)` — consumer-span\n * wrapper for bus-event handlers, with handler-duration recording.\n * - `injectTraceparent(payload)` / `extractTraceparent(payload)` — W3C\n * trace-context propagation across the SSE channel (the bus payload\n * gets a `_trace?: { traceparent }` sibling to `correlationId`).\n * - `withTraceparent(carrier, fn)` — run `fn` with the incoming\n * traceparent as the parent context.\n * - `getActiveTraceparent()` — read the active span's traceparent for\n * manual propagation (e.g. attaching to a fetch header or SSE field).\n * - `getLogTraceContext()` — active `trace_id` / `span_id` for log-line\n * correlation.\n * - Metric recorders (`recordBusEmit`, `recordHandlerDuration`,\n * `recordJobOutcome`, `recordSubscriberConnect` / `Disconnect`,\n * `recordInferenceUsage`) and gauge providers\n * (`registerJobQueueProvider`, `registerVectorIndexSizeProvider`).\n *\n * No-op when no exporter is configured: `@opentelemetry/api`'s default\n * tracer is a no-op, so `withSpan` is essentially free until\n * `initObservability*()` runs.\n */\n\nimport {\n context,\n isSpanContextValid,\n metrics,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n type Attributes,\n type Counter,\n type Histogram,\n type ObservableGauge,\n type Span,\n type UpDownCounter,\n} from '@opentelemetry/api';\nimport { setBusLogTraceIdProvider } from '@semiont/core';\n\n// Wire `busLog`'s trace-id provider once at module load. When an OTel\n// SDK is initialized (and a span is active when `busLog` fires), the\n// emitted line gets a `trace=<8hex>` suffix that correlates the\n// grep-timeline with the trace UI. No-op when no SDK is active.\nsetBusLogTraceIdProvider(() => {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return ctx.traceId;\n});\n\nconst TRACER_NAME = 'semiont';\n\nconst tracer = () => trace.getTracer(TRACER_NAME);\n\n// ── withSpan ───────────────────────────────────────────────────────────\n\n/**\n * Wrap an async block in a span. The span is started before `fn` runs and\n * ended after it resolves or rejects; exceptions are recorded and the span\n * status is set to ERROR. `kind` defaults to INTERNAL.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T> | T,\n options?: { kind?: SpanKind; attrs?: Attributes },\n): Promise<T> {\n const span = tracer().startSpan(name, {\n kind: options?.kind ?? SpanKind.INTERNAL,\n ...(options?.attrs ? { attributes: options.attrs } : {}),\n });\n try {\n return await context.with(trace.setSpan(context.active(), span), () => fn(span));\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: err instanceof Error ? err.message : String(err),\n });\n throw err;\n } finally {\n span.end();\n }\n}\n\n// ── Traceparent on bus payloads ────────────────────────────────────────\n\nconst TRACE_FIELD = '_trace';\n\n/**\n * Sibling of `correlationId` on bus payloads. Lives on the SSE event body\n * because SSE has no header trailer; the SDK strips it before delivering\n * the payload to subscribers. Additive — payloads without `_trace` parse\n * unchanged.\n */\nexport interface TraceCarrier {\n /** W3C `traceparent` header value (`00-<traceId>-<spanId>-<flags>`). */\n traceparent: string;\n /** W3C `tracestate` header value (vendor-specific extensions). */\n tracestate?: string;\n}\n\n/**\n * Read the active span's W3C traceparent (and tracestate). Returns\n * `undefined` if no span is active.\n */\nexport function getActiveTraceparent(): TraceCarrier | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n const traceparent = carrier['traceparent'];\n if (!traceparent) return undefined;\n return carrier['tracestate']\n ? { traceparent, tracestate: carrier['tracestate'] }\n : { traceparent };\n}\n\n/**\n * Attach the active span's trace-context to a payload object as\n * `_trace`. No-op when no span is active. Returns the same object\n * reference for chaining.\n */\nexport function injectTraceparent<T extends Record<string, unknown>>(payload: T): T {\n const carrier = getActiveTraceparent();\n if (carrier) {\n (payload as Record<string, unknown>)[TRACE_FIELD] = carrier;\n }\n return payload;\n}\n\n/**\n * Strip and return the `_trace` field from a payload. Mutates `payload`.\n * The field is internal plumbing and should not be visible to subscribers.\n */\nexport function extractTraceparent<T extends Record<string, unknown>>(\n payload: T,\n): TraceCarrier | undefined {\n const carrier = (payload as Record<string, unknown>)[TRACE_FIELD] as\n | TraceCarrier\n | undefined;\n if (carrier !== undefined) {\n delete (payload as Record<string, unknown>)[TRACE_FIELD];\n }\n if (!carrier || typeof carrier.traceparent !== 'string') return undefined;\n return carrier;\n}\n\n/**\n * Run `fn` with the given W3C traceparent set as the parent context.\n * Any spans started inside `fn` will be children of the incoming trace.\n * No-op if `carrier` is undefined.\n */\nexport function withTraceparent<T>(\n carrier: TraceCarrier | undefined,\n fn: () => T,\n): T {\n if (!carrier) return fn();\n const carrierObj: Record<string, string> = { traceparent: carrier.traceparent };\n if (carrier.tracestate) carrierObj['tracestate'] = carrier.tracestate;\n const ctx = propagation.extract(context.active(), carrierObj);\n return context.with(ctx, fn);\n}\n\n// ── Actor handler convenience ──────────────────────────────────────────\n\n/**\n * Wrap a bus-event handler in an `actor.<name>:<channel>` consumer span.\n * Used at every `eventBus.get(channel).subscribe(handler)` site inside\n * an actor (Stower, Gatherer, Matcher, Browser, Smelter), to attribute\n * each in-process subscriber's work to a span without scattering manual\n * `withSpan` calls across handler bodies.\n *\n * The span's parent is the active context at the time the handler\n * fires — which is the `bus.dispatch:<channel>` span on the gateway\n * (Subject.next runs synchronously inside the dispatch span), or the\n * `bus.emit:<channel>` span when an actor emits to itself.\n */\nexport async function withActorSpan<T>(\n actor: string,\n channel: string,\n fn: (span: Span) => Promise<T> | T,\n extraAttrs?: Attributes,\n): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(`actor.${actor}:${channel}`, fn, {\n kind: SpanKind.CONSUMER,\n attrs: {\n actor,\n 'bus.channel': channel,\n ...(extraAttrs ?? {}),\n },\n });\n } finally {\n recordHandlerDuration(actor, channel, performance.now() - start);\n }\n}\n\n// ── Log correlation ────────────────────────────────────────────────────\n\n/**\n * Read the active span's `trace_id` / `span_id` for log-line correlation.\n * Tier 3 of `.plans/OBSERVABILITY.md`. Each structured log line gets\n * tagged with these so a log query in CloudWatch / Loki / Datadog can\n * jump to the trace in Tempo / Jaeger / X-Ray.\n *\n * Returns `undefined` if no span is active, or if the active span's\n * context is invalid (uninitialized SDK, no-op tracer).\n */\nexport function getLogTraceContext(): { trace_id: string; span_id: string } | undefined {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return { trace_id: ctx.traceId, span_id: ctx.spanId };\n}\n\n// ── Metrics — Tier 3 ───────────────────────────────────────────────────\n\nconst METER_NAME = 'semiont';\n\nconst meter = () => metrics.getMeter(METER_NAME);\n\nlet _busEmitCounter: Counter | undefined;\nlet _replySuppressedCounter: Counter | undefined;\nlet _resumeGapCounter: Counter | undefined;\nlet _unanswerableCounter: Counter | undefined;\nlet _correlationRegistryGauge: ObservableGauge | undefined;\nlet _correlationRegistryProvider: (() => CorrelationRegistrySnapshot) | undefined;\nlet _handlerDurationHistogram: Histogram | undefined;\nlet _jobOutcomeCounter: Counter | undefined;\nlet _jobDurationHistogram: Histogram | undefined;\nlet _gatherDegradeCounter: Counter | undefined;\nlet _inferenceCallsCounter: Counter | undefined;\nlet _inferenceTokensCounter: Counter | undefined;\nlet _inferenceDurationHistogram: Histogram | undefined;\nlet _sseSubscribers: UpDownCounter | undefined;\nlet _jobQueueGauge: ObservableGauge | undefined;\nlet _jobQueueProvider: (() => Promise<JobQueueSnapshot> | JobQueueSnapshot) | undefined;\nlet _vectorIndexSizeGauge: ObservableGauge | undefined;\nlet _factPumpDepthGauge: ObservableGauge | undefined;\nlet _factPumpDepthProvider: (() => number) | undefined;\nlet _vectorIndexSizeProvider: (() => Promise<number> | number) | undefined;\n\n/** Snapshot of job-queue contents by status. Match `JobQueue.getStats()`. */\nexport interface JobQueueSnapshot {\n pending: number;\n running: number;\n complete: number;\n failed: number;\n cancelled: number;\n}\n\nfunction busEmitCounter(): Counter {\n if (!_busEmitCounter) {\n _busEmitCounter = meter().createCounter('semiont.bus.emit', {\n description: 'Bus emits by channel and scope',\n });\n }\n return _busEmitCounter;\n}\n\nfunction handlerDurationHistogram(): Histogram {\n if (!_handlerDurationHistogram) {\n _handlerDurationHistogram = meter().createHistogram('semiont.handler.duration', {\n description: 'In-process actor handler duration',\n unit: 'ms',\n });\n }\n return _handlerDurationHistogram;\n}\n\nfunction jobOutcomeCounter(): Counter {\n if (!_jobOutcomeCounter) {\n _jobOutcomeCounter = meter().createCounter('semiont.job.outcome', {\n description: 'Worker job completions by type and outcome',\n });\n }\n return _jobOutcomeCounter;\n}\n\nfunction jobDurationHistogram(): Histogram {\n if (!_jobDurationHistogram) {\n _jobDurationHistogram = meter().createHistogram('semiont.job.duration', {\n description: 'Worker job duration by type',\n unit: 'ms',\n });\n }\n return _jobDurationHistogram;\n}\n\nfunction inferenceCallsCounter(): Counter {\n if (!_inferenceCallsCounter) {\n _inferenceCallsCounter = meter().createCounter('semiont.inference.calls', {\n description: 'Inference API calls by provider, model, and outcome',\n });\n }\n return _inferenceCallsCounter;\n}\n\nfunction inferenceTokensCounter(): Counter {\n if (!_inferenceTokensCounter) {\n _inferenceTokensCounter = meter().createCounter('semiont.inference.tokens', {\n description: 'Inference token usage by provider, model, and direction',\n });\n }\n return _inferenceTokensCounter;\n}\n\nfunction inferenceDurationHistogram(): Histogram {\n if (!_inferenceDurationHistogram) {\n _inferenceDurationHistogram = meter().createHistogram('semiont.inference.duration', {\n description: 'Inference call duration by provider, model, and outcome',\n unit: 'ms',\n });\n }\n return _inferenceDurationHistogram;\n}\n\nfunction sseSubscribersCounter(): UpDownCounter {\n if (!_sseSubscribers) {\n _sseSubscribers = meter().createUpDownCounter('semiont.sse.subscribers', {\n description: 'Active SSE subscribers',\n });\n }\n return _sseSubscribers;\n}\n\nfunction replySuppressedCounter(): Counter {\n if (!_replySuppressedCounter) {\n _replySuppressedCounter = meter().createCounter('semiont.bus.reply.suppressed', {\n description: 'Correlated replies withheld from a non-owning subscriber',\n });\n }\n return _replySuppressedCounter;\n}\n\n/**\n * A correlated reply was withheld from a subscriber that does not own its\n * correlationId (CORRELATED-REPLY-ROUTING P5).\n *\n * Counts ONLY that case. A frame with no correlationId is a shape violation\n * (warned, not counted), and a cid nobody claimed is the structural in-process\n * case that fires constantly — counting either would drown the signal this\n * metric exists to show: the fan-out amplification the delivery filter removes.\n */\nexport function recordReplySuppressed(channel: string): void {\n replySuppressedCounter().add(1, { 'bus.channel': channel });\n}\n\nfunction resumeGapCounter(): Counter {\n if (!_resumeGapCounter) {\n _resumeGapCounter = meter().createCounter('semiont.bus.resume_gap', {\n description: 'SSE resumes that degraded to a gap because replay was unavailable',\n });\n }\n return _resumeGapCounter;\n}\n\n/**\n * An SSE resume could not be served and the client was told to fall back to\n * cache. This degradation is CORRECT by design and therefore silent — which is\n * exactly why it needs a number. A rising rate means clients are losing\n * history, and nothing else in the stack says so.\n */\nexport function recordResumeGap(reason: string): void {\n resumeGapCounter().add(1, { 'bus.resume_gap.reason': reason });\n}\n\nfunction unanswerableCounter(): Counter {\n if (!_unanswerableCounter) {\n _unanswerableCounter = meter().createCounter('semiont.bus.unanswerable', {\n description: 'Request emits that reached zero subscribers and were failed at the gateway',\n });\n }\n return _unanswerableCounter;\n}\n\n/**\n * A request-shaped emit reached no subscriber, so the gateway synthesized its\n * mapped failure (ARCHIVIST-STAYS-UP P3). By channel, this is the absence rate\n * of the service that answers it — the difference between \"it went down once\"\n * and \"it is flapping.\"\n */\nexport function recordUnanswerableRequest(channel: string): void {\n unanswerableCounter().add(1, { 'bus.channel': channel });\n}\n\n/** Claims held and reply payloads retained by a gateway's correlation registry. */\nexport interface CorrelationRegistrySnapshot {\n claims: number;\n retainedReplies: number;\n}\n\n/**\n * Register a callback returning the gateway's correlation-registry occupancy.\n *\n * COUNTS, not bytes: retention is count-budgeted today (byte-budgeting is a\n * known limit in CORRELATED-REPLY-ROUTING), so `retainedReplies` is a proxy for\n * heap, not a measure of it. It is still the closest observable to the question\n * two OOM investigations keep asking — a browse result can be 1-2 MB, and up to\n * REPLY_RETENTION_MAX of them are held at once.\n */\nexport function registerCorrelationRegistryProvider(\n provider: () => CorrelationRegistrySnapshot,\n): void {\n _correlationRegistryProvider = provider;\n if (!_correlationRegistryGauge) {\n _correlationRegistryGauge = meter().createObservableGauge('semiont.bus.correlation.size', {\n description: 'Correlation registry occupancy: live claims and retained reply payloads',\n });\n _correlationRegistryGauge.addCallback((observer) => {\n if (!_correlationRegistryProvider) return;\n const snap = _correlationRegistryProvider();\n observer.observe(snap.claims, { 'correlation.kind': 'claims' });\n observer.observe(snap.retainedReplies, { 'correlation.kind': 'retained_replies' });\n });\n }\n}\n\n/** Increment the bus-emit counter. Called at every transport `emit` site. */\nexport function recordBusEmit(channel: string, scope?: string): void {\n busEmitCounter().add(1, {\n 'bus.channel': channel,\n ...(scope ? { 'bus.scope': scope } : {}),\n });\n}\n\n/** Record an in-process actor handler's duration. */\nexport function recordHandlerDuration(actor: string, channel: string, durationMs: number): void {\n handlerDurationHistogram().record(durationMs, {\n actor,\n 'bus.channel': channel,\n });\n}\n\n/** Record a worker job's outcome and duration. */\nexport function recordJobOutcome(jobType: string, outcome: 'completed' | 'failed', durationMs: number): void {\n jobOutcomeCounter().add(1, { 'job.type': jobType, 'job.outcome': outcome });\n jobDurationHistogram().record(durationMs, { 'job.type': jobType, 'job.outcome': outcome });\n}\n\nlet _appendStageHistogram: Histogram | undefined;\nfunction appendStageHistogram(): Histogram {\n if (!_appendStageHistogram) {\n _appendStageHistogram = meter().createHistogram('semiont.record.append.duration', {\n description: 'Time spent in one stage of appending an event to the record, labeled by stage: persist (JSONL write + git), materialize (view rebuild), enrich, publish. The Archivist\\'s core write path.',\n unit: 'ms',\n });\n }\n return _appendStageHistogram;\n}\n\n/**\n * Record one stage of `EventStore.appendEvent` (ARCHIVIST-STAYS-UP P7).\n *\n * The append path is the one operation only the Archivist can perform, and it\n * was entirely dark: reads had `recordHandlerDuration` and the bus had its own\n * counters, while writes had nothing. Stage-labeled because the useful\n * question is never \"was the append slow\" but WHICH PART — and `materialize`\n * in particular does work proportional to a resource's annotation count, so it\n * degrades with history rather than with load.\n */\nexport function recordAppendStage(\n stage: 'persist' | 'materialize' | 'enrich' | 'publish',\n durationMs: number,\n): void {\n appendStageHistogram().record(durationMs, { 'record.stage': stage });\n}\n\nlet _gitCommandHistogram: Histogram | undefined;\nfunction gitCommandHistogram(): Histogram {\n if (!_gitCommandHistogram) {\n _gitCommandHistogram = meter().createHistogram('semiont.git.duration', {\n description: 'Wall time of a git subprocess. Async — this is latency, not event-loop blockage. Staging is deduped, so the `add` count is far below the number of appended events.',\n unit: 'ms',\n });\n }\n return _gitCommandHistogram;\n}\n\n/**\n * Record a git invocation. Read the `add` count against events appended: one\n * per event means deferred staging has stopped deduping.\n */\nexport function recordGitCommand(command: string, durationMs: number): void {\n gitCommandHistogram().record(durationMs, { 'git.command': command });\n}\n\nfunction gatherDegradeCounter(): Counter {\n if (!_gatherDegradeCounter) {\n _gatherDegradeCounter = meter().createCounter('semiont.gather.degraded', {\n description: 'Gathers that degraded because an eventually-consistent projection did not catch up within its read barrier (vectors: absent semanticContext; graph: projection-lag failure). Labeled by projection.',\n });\n }\n return _gatherDegradeCounter;\n}\n\n/**\n * Record a gather degraded by a projection read barrier: `'vectors'` — the\n * Smelter settle barrier timed out (semanticContext shipped absent);\n * `'graph'` — the Weaver applied barrier + poll floor exhausted (projection\n * lag surfaced as a distinct failure). Fleet-alertable counterpart of the\n * `[gather DEGRADED]` L4 breadcrumbs — a rising rate on either label means\n * that pipeline is not keeping up.\n */\nexport function recordGatherDegrade(projection: 'graph' | 'vectors'): void {\n gatherDegradeCounter().add(1, { projection });\n}\n\n/** Increment the SSE subscriber gauge — call on `/bus/subscribe` open. */\nexport function recordSubscriberConnect(): void {\n sseSubscribersCounter().add(1);\n}\n\n/** Decrement on disconnect. Pair with `recordSubscriberConnect`. */\nexport function recordSubscriberDisconnect(): void {\n sseSubscribersCounter().add(-1);\n}\n\n/**\n * Register a callback that returns the current job-queue snapshot.\n * Polled at the SDK's metric-collection interval. The single gauge\n * emits one observation per status (`pending`, `running`, …) tagged\n * with the `job.status` attribute. Idempotent — last registered\n * provider wins.\n */\nexport function registerJobQueueProvider(\n provider: () => Promise<JobQueueSnapshot> | JobQueueSnapshot,\n): void {\n _jobQueueProvider = provider;\n if (!_jobQueueGauge) {\n _jobQueueGauge = meter().createObservableGauge('semiont.job.queue.size', {\n description: 'Job queue size by status',\n });\n _jobQueueGauge.addCallback(async (observer) => {\n if (!_jobQueueProvider) return;\n const snap = await _jobQueueProvider();\n observer.observe(snap.pending, { 'job.status': 'pending' });\n observer.observe(snap.running, { 'job.status': 'running' });\n observer.observe(snap.complete, { 'job.status': 'complete' });\n observer.observe(snap.failed, { 'job.status': 'failed' });\n observer.observe(snap.cancelled, { 'job.status': 'cancelled' });\n });\n }\n}\n\n/**\n * Register a callback that returns the current vector-index size\n * (point count). Async to allow remote queries (Qdrant). Polled at\n * the metric-collection interval.\n */\n/**\n * Register the Archivist's fact-pump backlog — facts appended to the record\n * but not yet republished onto the bus.\n *\n * At rest this is zero. A value that climbs and does not come back means the\n * pump is outrunning its transport, which is the leading hypothesis for the\n * load-correlated heap growth in `bugs/absent-archivist-wedges-browse.md`\n * (ARCHIVIST-STAYS-UP P5). The backlog is deliberately unbounded today, so\n * this number is the only thing standing between \"the pump is behind\" and an\n * OOM whose cause is inferred from RSS after the fact.\n */\nexport function registerFactPumpDepthProvider(provider: () => number): void {\n _factPumpDepthProvider = provider;\n if (!_factPumpDepthGauge) {\n _factPumpDepthGauge = meter().createObservableGauge('semiont.archivist.fact_pump.depth', {\n description: 'Facts appended to the record but not yet published to the bus. Zero at rest; a rising floor means the pump is behind its transport.',\n });\n _factPumpDepthGauge.addCallback((observer) => {\n if (_factPumpDepthProvider) observer.observe(_factPumpDepthProvider());\n });\n }\n}\n\nlet _gitStagingFailureCounter: Counter | undefined;\n\n/**\n * A staging command that could not be run. Staging the index is a CONVENIENCE\n * — the event log is the system of record — so a failure here is degraded\n * service, never a reason to exit. But degraded must be VISIBLE: this counter\n * is what stops \"the index is quietly stale\" from being invisible.\n */\nexport function recordGitStagingFailure(reason: 'index-lock' | 'other'): void {\n if (!_gitStagingFailureCounter) {\n _gitStagingFailureCounter = meter().createCounter('semiont.git.staging.failures', {\n description: 'Staging commands abandoned after retries; the index may be stale',\n });\n }\n _gitStagingFailureCounter.add(1, { reason });\n}\n\n/**\n * Process lifetime telemetry (ARCHIVIST-GIT-STAGER-CRASH).\n *\n * A supervised process that dies and comes back is INVISIBLE in logs unless\n * someone greps for boot lines, and every request in flight when it died looks\n * to its caller like a hang. On 2026-09-08 that cost a multi-hour hunt through\n * search, qdrant, neo4j and the SSE transport for a crash loop that one metric\n * would have named immediately.\n *\n * `start_time` is the diagnostic, not uptime: a CHANGE in it is unambiguous\n * proof of a restart, and uptime is derivable from it.\n */\nconst PROCESS_START_TIME_SECONDS = Math.floor(Date.now() / 1000);\nlet _processStartTimeGauge: ObservableGauge | undefined;\nlet _restartCountGauge: ObservableGauge | undefined;\nlet _restartCountProvider: (() => Promise<number> | number) | undefined;\nlet _abnormalExitCounter: Counter | undefined;\n\n/** Register `semiont.process.start_time`. Called by `initObservability*`. */\nexport function registerProcessLifetimeMetrics(): void {\n if (_processStartTimeGauge) return;\n _processStartTimeGauge = meter().createObservableGauge('semiont.process.start_time', {\n description: 'Unix seconds at which this process started; a change means it restarted',\n unit: 's',\n });\n _processStartTimeGauge.addCallback((observer) => observer.observe(PROCESS_START_TIME_SECONDS));\n}\n\n/**\n * Supply a restart count. The Archivist's supervisor is POSIX shell and cannot\n * emit OTel, but it already keeps a durable event log on the state mount — so\n * the supervised child reads it and reports the count on the supervisor's\n * behalf.\n */\nexport function registerRestartCountProvider(\n provider: () => Promise<number> | number,\n): void {\n _restartCountProvider = provider;\n if (!_restartCountGauge) {\n _restartCountGauge = meter().createObservableGauge('semiont.process.restarts', {\n description: 'Times the supervisor has restarted this service',\n });\n _restartCountGauge.addCallback(async (observer) => {\n if (_restartCountProvider) observer.observe(await _restartCountProvider());\n });\n }\n}\n\n/**\n * Record that this process is dying abnormally, and mark the active span so a\n * trace shows a span that ENDED IN DEATH rather than one that simply never\n * ends. Never swallows: callers re-raise, so Node's own semantics are intact.\n */\nexport function recordAbnormalTermination(reason: string, detail?: string): void {\n if (!_abnormalExitCounter) {\n _abnormalExitCounter = meter().createCounter('semiont.process.abnormal_exit', {\n description: 'Process terminations that were not a clean shutdown',\n });\n }\n _abnormalExitCounter.add(1, { reason });\n const active = trace.getActiveSpan();\n if (active) {\n active.setStatus({ code: SpanStatusCode.ERROR, message: `${reason}: ${detail ?? ''}`.trim() });\n active.setAttribute('semiont.process.abnormal_exit', reason);\n active.end();\n }\n}\n\nexport function registerVectorIndexSizeProvider(\n provider: () => Promise<number> | number,\n): void {\n _vectorIndexSizeProvider = provider;\n if (!_vectorIndexSizeGauge) {\n _vectorIndexSizeGauge = meter().createObservableGauge('semiont.vector.index.size', {\n description: 'Vector store point count',\n });\n _vectorIndexSizeGauge.addCallback(async (observer) => {\n if (_vectorIndexSizeProvider) {\n const value = await _vectorIndexSizeProvider();\n observer.observe(value);\n }\n });\n }\n}\n\n/**\n * Record an inference call. Token counts are optional — providers that\n * don't expose them (or fail before generating) record only call count\n * and duration.\n */\nexport function recordInferenceUsage(opts: {\n provider: string;\n model: string;\n durationMs: number;\n outcome: 'success' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const baseAttrs = {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.outcome': opts.outcome,\n };\n inferenceCallsCounter().add(1, baseAttrs);\n inferenceDurationHistogram().record(opts.durationMs, baseAttrs);\n if (opts.inputTokens != null && opts.inputTokens > 0) {\n inferenceTokensCounter().add(opts.inputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'input',\n });\n }\n if (opts.outputTokens != null && opts.outputTokens > 0) {\n inferenceTokensCounter().add(opts.outputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'output',\n });\n }\n}\n\nlet _detectionCallCounter: Counter | undefined;\nlet _detectionDurationHistogram: Histogram | undefined;\nlet _detectionItemsHistogram: Histogram | undefined;\nlet _detectionTokensHistogram: Histogram | undefined;\n\nfunction detectionCallCounter(): Counter {\n if (!_detectionCallCounter) {\n _detectionCallCounter = meter().createCounter('semiont.detection.calls', {\n description: 'Detection model calls, labeled by motivation, outcome, subdivision depth and whether this was the floor re-roll.',\n });\n }\n return _detectionCallCounter;\n}\n\nfunction detectionDurationHistogram(): Histogram {\n if (!_detectionDurationHistogram) {\n _detectionDurationHistogram = meter().createHistogram('semiont.detection.call.duration', {\n description: 'Wall time of one detection model call, including the attempts that failed and were retried smaller.',\n unit: 'ms',\n });\n }\n return _detectionDurationHistogram;\n}\n\nfunction detectionItemsHistogram(): Histogram {\n if (!_detectionItemsHistogram) {\n _detectionItemsHistogram = meter().createHistogram('semiont.detection.call.items', {\n description: 'Annotations returned by one detection call. Against the input size on the same record, this is yield.',\n });\n }\n return _detectionItemsHistogram;\n}\n\nfunction detectionTokensHistogram(): Histogram {\n if (!_detectionTokensHistogram) {\n _detectionTokensHistogram = meter().createHistogram('semiont.detection.call.tokens', {\n description: \"Provider-reported tokens for one detection call, by direction. Deliberately separate from semiont.inference.tokens: that series is the authoritative total but carries no subdivision depth, and 'what does a depth-2 call cost' is the question every sizing decision asks.\",\n });\n }\n return _detectionTokensHistogram;\n}\n\n/**\n * Record one detection model call (DETECTION-QUALITY-THROUGHPUT P1).\n *\n * The adapters already record provider/model/duration/tokens for every\n * inference call. What they cannot know is the detection shape around it:\n * which motivation asked, how big the piece was, how many annotations came\n * back, how deep subdivision had descended, and whether this was the floor\n * re-roll. Those are the facts that distinguish a healthy call from a\n * expensive descent, and without them a slow detection run is one\n * undifferentiated number.\n *\n * FAILED attempts are recorded too, and that is the point: the calls paid for\n * and thrown away during a descent are exactly the cost later phases exist to\n * avoid, so a record only of successes would hide the thing being optimized.\n *\n * Tokens are the PROVIDER's counts, passed through — never estimated. Absent\n * means the provider did not report them.\n */\nexport function recordDetectionCall(opts: {\n label: string;\n pieceChars: number;\n durationMs: number;\n items: number;\n depth: number;\n reroll: boolean;\n outcome: 'success' | 'truncated' | 'timeout' | 'collapsed' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const attrs = {\n 'detection.label': opts.label,\n 'detection.outcome': opts.outcome,\n 'detection.depth': opts.depth,\n 'detection.reroll': opts.reroll,\n };\n detectionCallCounter().add(1, attrs);\n detectionDurationHistogram().record(opts.durationMs, attrs);\n detectionItemsHistogram().record(opts.items, attrs);\n if (opts.inputTokens !== undefined) {\n detectionTokensHistogram().record(opts.inputTokens, { ...attrs, 'detection.direction': 'input' });\n }\n if (opts.outputTokens !== undefined) {\n detectionTokensHistogram().record(opts.outputTokens, { ...attrs, 'detection.direction': 'output' });\n }\n}\n\nlet _anchorOutcomeCounter: Counter | undefined;\nfunction anchorOutcomeCounter(): Counter {\n if (!_anchorOutcomeCounter) {\n _anchorOutcomeCounter = meter().createCounter('semiont.detection.anchors', {\n description: 'Every annotation anchoring, labeled by the method that resolved it. EVERY outcome is counted, not just the risky ones, because a bare count of degraded anchors has no denominator — the rate is the precision signal.',\n });\n }\n return _anchorOutcomeCounter;\n}\n\n/**\n * Record how one annotation got anchored (DETECTION-QUALITY-THROUGHPUT P5).\n *\n * The selector-vs-source check is already a WRITE-TIME INVARIANT — both\n * `buildTextAnnotation` and `buildPdfAnnotation` throw on a selector that does\n * not match its source — so mechanical correctness is guaranteed rather than\n * sampled, and auditing it would measure a constant.\n *\n * What is genuinely uncertain is which anchoring METHOD got there. An `exact`\n * the model quoted verbatim and that appears once is certain; one resolved by\n * `first-of-many` (several occurrences, no usable context) or `fuzzy-match`\n * picked a plausible occurrence and may have picked wrong. Those were visible\n * only as log warnings — countable by a human reading worker output, which is\n * how 47 of them went unreviewed. As a rate they are the precision number that\n * sits beside the yield numbers.\n */\nexport function recordAnchorOutcome(label: string, method: string): void {\n anchorOutcomeCounter().add(1, { 'detection.label': label, 'anchor.method': method });\n}\n\n// ── Re-exports from @opentelemetry/api ─────────────────────────────────\n\nexport { SpanKind, SpanStatusCode, type Attributes, type Span } from '@opentelemetry/api';\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @semiont/observability — public API.\n *\n * Universal surface (works in Node + browser). For SDK *initialization*,\n * import from `@semiont/observability/node` or `/web` at the process entry\n * point. Everything else uses this module.\n *\n * Tier 2 of `.plans/OBSERVABILITY.md`. The public surface:\n *\n * - `withSpan(name, fn, options?)` — wrap an async block in a span;\n * `options` carries `kind` and `attrs`.\n * - `withActorSpan(actor, channel, fn, extraAttrs?)` — consumer-span\n * wrapper for bus-event handlers, with handler-duration recording.\n * - `injectTraceparent(payload)` / `extractTraceparent(payload)` — W3C\n * trace-context propagation across the SSE channel (the bus payload\n * gets a `_trace?: { traceparent }` sibling to `correlationId`).\n * - `withTraceparent(carrier, fn)` — run `fn` with the incoming\n * traceparent as the parent context.\n * - `getActiveTraceparent()` — read the active span's traceparent for\n * manual propagation (e.g. attaching to a fetch header or SSE field).\n * - `getLogTraceContext()` — active `trace_id` / `span_id` for log-line\n * correlation.\n * - Metric recorders (`recordBusEmit`, `recordHandlerDuration`,\n * `recordJobOutcome`, `recordSubscriberConnect` / `Disconnect`,\n * `recordInferenceUsage`) and gauge providers\n * (`registerJobQueueProvider`, `registerVectorIndexSizeProvider`).\n *\n * No-op when no exporter is configured: `@opentelemetry/api`'s default\n * tracer is a no-op, so `withSpan` is essentially free until\n * `initObservability*()` runs.\n */\n\nimport {\n context,\n isSpanContextValid,\n metrics,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n type Attributes,\n type Counter,\n type Histogram,\n type ObservableGauge,\n type Span,\n type UpDownCounter,\n} from '@opentelemetry/api';\nimport { setBusLogTraceIdProvider } from '@semiont/core';\n\n// Wire `busLog`'s trace-id provider once at module load. When an OTel\n// SDK is initialized (and a span is active when `busLog` fires), the\n// emitted line gets a `trace=<8hex>` suffix that correlates the\n// grep-timeline with the trace UI. No-op when no SDK is active.\nsetBusLogTraceIdProvider(() => {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return ctx.traceId;\n});\n\nconst TRACER_NAME = 'semiont';\n\nconst tracer = () => trace.getTracer(TRACER_NAME);\n\n// ── withSpan ───────────────────────────────────────────────────────────\n\n/**\n * Wrap an async block in a span. The span is started before `fn` runs and\n * ended after it resolves or rejects; exceptions are recorded and the span\n * status is set to ERROR. `kind` defaults to INTERNAL.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T> | T,\n options?: { kind?: SpanKind; attrs?: Attributes },\n): Promise<T> {\n const span = tracer().startSpan(name, {\n kind: options?.kind ?? SpanKind.INTERNAL,\n ...(options?.attrs ? { attributes: options.attrs } : {}),\n });\n try {\n return await context.with(trace.setSpan(context.active(), span), () => fn(span));\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: err instanceof Error ? err.message : String(err),\n });\n throw err;\n } finally {\n span.end();\n }\n}\n\n// ── Traceparent on bus payloads ────────────────────────────────────────\n\nconst TRACE_FIELD = '_trace';\n\n/**\n * Sibling of `correlationId` on bus payloads. Lives on the SSE event body\n * because SSE has no header trailer; the SDK strips it before delivering\n * the payload to subscribers. Additive — payloads without `_trace` parse\n * unchanged.\n */\nexport interface TraceCarrier {\n /** W3C `traceparent` header value (`00-<traceId>-<spanId>-<flags>`). */\n traceparent: string;\n /** W3C `tracestate` header value (vendor-specific extensions). */\n tracestate?: string;\n}\n\n/**\n * Read the active span's W3C traceparent (and tracestate). Returns\n * `undefined` if no span is active.\n */\nexport function getActiveTraceparent(): TraceCarrier | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n const traceparent = carrier['traceparent'];\n if (!traceparent) return undefined;\n return carrier['tracestate']\n ? { traceparent, tracestate: carrier['tracestate'] }\n : { traceparent };\n}\n\n/**\n * Attach the active span's trace-context to a payload object as\n * `_trace`. No-op when no span is active. Returns the same object\n * reference for chaining.\n */\nexport function injectTraceparent<T extends Record<string, unknown>>(payload: T): T {\n const carrier = getActiveTraceparent();\n if (carrier) {\n (payload as Record<string, unknown>)[TRACE_FIELD] = carrier;\n }\n return payload;\n}\n\n/**\n * Strip and return the `_trace` field from a payload. Mutates `payload`.\n * The field is internal plumbing and should not be visible to subscribers.\n */\nexport function extractTraceparent<T extends Record<string, unknown>>(\n payload: T,\n): TraceCarrier | undefined {\n const carrier = (payload as Record<string, unknown>)[TRACE_FIELD] as\n | TraceCarrier\n | undefined;\n if (carrier !== undefined) {\n delete (payload as Record<string, unknown>)[TRACE_FIELD];\n }\n if (!carrier || typeof carrier.traceparent !== 'string') return undefined;\n return carrier;\n}\n\n/**\n * Run `fn` with the given W3C traceparent set as the parent context.\n * Any spans started inside `fn` will be children of the incoming trace.\n * No-op if `carrier` is undefined.\n */\nexport function withTraceparent<T>(\n carrier: TraceCarrier | undefined,\n fn: () => T,\n): T {\n if (!carrier) return fn();\n const carrierObj: Record<string, string> = { traceparent: carrier.traceparent };\n if (carrier.tracestate) carrierObj['tracestate'] = carrier.tracestate;\n const ctx = propagation.extract(context.active(), carrierObj);\n return context.with(ctx, fn);\n}\n\n// ── Actor handler convenience ──────────────────────────────────────────\n\n/**\n * Wrap a bus-event handler in an `actor.<name>:<channel>` consumer span.\n * Used at every `eventBus.get(channel).subscribe(handler)` site inside\n * an actor (Stower, Gatherer, Matcher, Browser, Smelter), to attribute\n * each in-process subscriber's work to a span without scattering manual\n * `withSpan` calls across handler bodies.\n *\n * The span's parent is the active context at the time the handler\n * fires — which is the `bus.dispatch:<channel>` span on the gateway\n * (Subject.next runs synchronously inside the dispatch span), or the\n * `bus.emit:<channel>` span when an actor emits to itself.\n */\nexport async function withActorSpan<T>(\n actor: string,\n channel: string,\n fn: (span: Span) => Promise<T> | T,\n extraAttrs?: Attributes,\n): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(`actor.${actor}:${channel}`, fn, {\n kind: SpanKind.CONSUMER,\n attrs: {\n actor,\n 'bus.channel': channel,\n ...(extraAttrs ?? {}),\n },\n });\n } finally {\n recordHandlerDuration(actor, channel, performance.now() - start);\n }\n}\n\n// ── Log correlation ────────────────────────────────────────────────────\n\n/**\n * Read the active span's `trace_id` / `span_id` for log-line correlation.\n * Tier 3 of `.plans/OBSERVABILITY.md`. Each structured log line gets\n * tagged with these so a log query in CloudWatch / Loki / Datadog can\n * jump to the trace in Tempo / Jaeger / X-Ray.\n *\n * Returns `undefined` if no span is active, or if the active span's\n * context is invalid (uninitialized SDK, no-op tracer).\n */\nexport function getLogTraceContext(): { trace_id: string; span_id: string } | undefined {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return { trace_id: ctx.traceId, span_id: ctx.spanId };\n}\n\n// ── Metrics — Tier 3 ───────────────────────────────────────────────────\n\nconst METER_NAME = 'semiont';\n\nconst meter = () => metrics.getMeter(METER_NAME);\n\nlet _busEmitCounter: Counter | undefined;\nlet _replySuppressedCounter: Counter | undefined;\nlet _resumeGapCounter: Counter | undefined;\nlet _unanswerableCounter: Counter | undefined;\nlet _correlationRegistryGauge: ObservableGauge | undefined;\nlet _correlationRegistryProvider: (() => CorrelationRegistrySnapshot) | undefined;\nlet _handlerDurationHistogram: Histogram | undefined;\nlet _jobOutcomeCounter: Counter | undefined;\nlet _jobDurationHistogram: Histogram | undefined;\nlet _gatherDegradeCounter: Counter | undefined;\nlet _inferenceCallsCounter: Counter | undefined;\nlet _inferenceTokensCounter: Counter | undefined;\nlet _inferenceDurationHistogram: Histogram | undefined;\nlet _sseSubscribers: UpDownCounter | undefined;\nlet _jobQueueGauge: ObservableGauge | undefined;\nlet _jobQueueProvider: (() => Promise<JobQueueSnapshot> | JobQueueSnapshot) | undefined;\nlet _vectorIndexSizeGauge: ObservableGauge | undefined;\nlet _factPumpDepthGauge: ObservableGauge | undefined;\nlet _factPumpDepthProvider: (() => number) | undefined;\nlet _vectorIndexSizeProvider: (() => Promise<number> | number) | undefined;\n\n/** Snapshot of job-queue contents by status. Match `JobQueue.getStats()`. */\nexport interface JobQueueSnapshot {\n pending: number;\n running: number;\n complete: number;\n failed: number;\n cancelled: number;\n}\n\nfunction busEmitCounter(): Counter {\n if (!_busEmitCounter) {\n _busEmitCounter = meter().createCounter('semiont.bus.emit', {\n description: 'Bus emits by channel and scope',\n });\n }\n return _busEmitCounter;\n}\n\nfunction handlerDurationHistogram(): Histogram {\n if (!_handlerDurationHistogram) {\n _handlerDurationHistogram = meter().createHistogram('semiont.handler.duration', {\n description: 'In-process actor handler duration',\n unit: 'ms',\n });\n }\n return _handlerDurationHistogram;\n}\n\nfunction jobOutcomeCounter(): Counter {\n if (!_jobOutcomeCounter) {\n _jobOutcomeCounter = meter().createCounter('semiont.job.outcome', {\n description: 'Worker job completions by type and outcome',\n });\n }\n return _jobOutcomeCounter;\n}\n\nfunction jobDurationHistogram(): Histogram {\n if (!_jobDurationHistogram) {\n _jobDurationHistogram = meter().createHistogram('semiont.job.duration', {\n description: 'Worker job duration by type',\n unit: 'ms',\n });\n }\n return _jobDurationHistogram;\n}\n\nfunction inferenceCallsCounter(): Counter {\n if (!_inferenceCallsCounter) {\n _inferenceCallsCounter = meter().createCounter('semiont.inference.calls', {\n description: 'Inference API calls by provider, model, and outcome',\n });\n }\n return _inferenceCallsCounter;\n}\n\nfunction inferenceTokensCounter(): Counter {\n if (!_inferenceTokensCounter) {\n _inferenceTokensCounter = meter().createCounter('semiont.inference.tokens', {\n description: 'Inference token usage by provider, model, and direction',\n });\n }\n return _inferenceTokensCounter;\n}\n\nfunction inferenceDurationHistogram(): Histogram {\n if (!_inferenceDurationHistogram) {\n _inferenceDurationHistogram = meter().createHistogram('semiont.inference.duration', {\n description: 'Inference call duration by provider, model, and outcome',\n unit: 'ms',\n });\n }\n return _inferenceDurationHistogram;\n}\n\nfunction sseSubscribersCounter(): UpDownCounter {\n if (!_sseSubscribers) {\n _sseSubscribers = meter().createUpDownCounter('semiont.sse.subscribers', {\n description: 'Active SSE subscribers',\n });\n }\n return _sseSubscribers;\n}\n\nfunction replySuppressedCounter(): Counter {\n if (!_replySuppressedCounter) {\n _replySuppressedCounter = meter().createCounter('semiont.bus.reply.suppressed', {\n description: 'Correlated replies withheld from a non-owning subscriber',\n });\n }\n return _replySuppressedCounter;\n}\n\n/**\n * A correlated reply was withheld from a subscriber that does not own its\n * correlationId (CORRELATED-REPLY-ROUTING P5).\n *\n * Counts ONLY that case. A frame with no correlationId is a shape violation\n * (warned, not counted), and a cid nobody claimed is the structural in-process\n * case that fires constantly — counting either would drown the signal this\n * metric exists to show: the fan-out amplification the delivery filter removes.\n */\nexport function recordReplySuppressed(channel: string): void {\n replySuppressedCounter().add(1, { 'bus.channel': channel });\n}\n\nfunction resumeGapCounter(): Counter {\n if (!_resumeGapCounter) {\n _resumeGapCounter = meter().createCounter('semiont.bus.resume_gap', {\n description: 'SSE resumes that degraded to a gap because replay was unavailable',\n });\n }\n return _resumeGapCounter;\n}\n\n/**\n * An SSE resume could not be served and the client was told to fall back to\n * cache. This degradation is CORRECT by design and therefore silent — which is\n * exactly why it needs a number. A rising rate means clients are losing\n * history, and nothing else in the stack says so.\n */\nexport function recordResumeGap(reason: string): void {\n resumeGapCounter().add(1, { 'bus.resume_gap.reason': reason });\n}\n\nfunction unanswerableCounter(): Counter {\n if (!_unanswerableCounter) {\n _unanswerableCounter = meter().createCounter('semiont.bus.unanswerable', {\n description: 'Request emits that reached zero subscribers and were failed at the gateway',\n });\n }\n return _unanswerableCounter;\n}\n\n/**\n * A request-shaped emit reached no subscriber, so the gateway synthesized its\n * mapped failure (ARCHIVIST-STAYS-UP P3). By channel, this is the absence rate\n * of the service that answers it — the difference between \"it went down once\"\n * and \"it is flapping.\"\n */\nexport function recordUnanswerableRequest(channel: string): void {\n unanswerableCounter().add(1, { 'bus.channel': channel });\n}\n\n/** Claims held and reply payloads retained by a gateway's correlation registry. */\nexport interface CorrelationRegistrySnapshot {\n claims: number;\n retainedReplies: number;\n}\n\n/**\n * Register a callback returning the gateway's correlation-registry occupancy.\n *\n * COUNTS, not bytes: retention is count-budgeted today (byte-budgeting is a\n * known limit in CORRELATED-REPLY-ROUTING), so `retainedReplies` is a proxy for\n * heap, not a measure of it. It is still the closest observable to the question\n * two OOM investigations keep asking — a browse result can be 1-2 MB, and up to\n * REPLY_RETENTION_MAX of them are held at once.\n */\nexport function registerCorrelationRegistryProvider(\n provider: () => CorrelationRegistrySnapshot,\n): void {\n _correlationRegistryProvider = provider;\n if (!_correlationRegistryGauge) {\n _correlationRegistryGauge = meter().createObservableGauge('semiont.bus.correlation.size', {\n description: 'Correlation registry occupancy: live claims and retained reply payloads',\n });\n _correlationRegistryGauge.addCallback((observer) => {\n if (!_correlationRegistryProvider) return;\n const snap = _correlationRegistryProvider();\n observer.observe(snap.claims, { 'correlation.kind': 'claims' });\n observer.observe(snap.retainedReplies, { 'correlation.kind': 'retained_replies' });\n });\n }\n}\n\n/** Increment the bus-emit counter. Called at every transport `emit` site. */\nexport function recordBusEmit(channel: string, scope?: string): void {\n busEmitCounter().add(1, {\n 'bus.channel': channel,\n ...(scope ? { 'bus.scope': scope } : {}),\n });\n}\n\n/** Record an in-process actor handler's duration. */\nexport function recordHandlerDuration(actor: string, channel: string, durationMs: number): void {\n handlerDurationHistogram().record(durationMs, {\n actor,\n 'bus.channel': channel,\n });\n}\n\n/** Record a worker job's outcome and duration. */\nexport function recordJobOutcome(jobType: string, outcome: 'completed' | 'failed', durationMs: number): void {\n jobOutcomeCounter().add(1, { 'job.type': jobType, 'job.outcome': outcome });\n jobDurationHistogram().record(durationMs, { 'job.type': jobType, 'job.outcome': outcome });\n}\n\nlet _appendStageHistogram: Histogram | undefined;\nfunction appendStageHistogram(): Histogram {\n if (!_appendStageHistogram) {\n _appendStageHistogram = meter().createHistogram('semiont.record.append.duration', {\n description: 'Time spent in one stage of appending an event to the record, labeled by stage: persist (JSONL write + git), materialize (view rebuild), enrich, publish. The Archivist\\'s core write path.',\n unit: 'ms',\n });\n }\n return _appendStageHistogram;\n}\n\n/**\n * Record one stage of `EventStore.appendEvent` (ARCHIVIST-STAYS-UP P7).\n *\n * The append path is the one operation only the Archivist can perform, and it\n * was entirely dark: reads had `recordHandlerDuration` and the bus had its own\n * counters, while writes had nothing. Stage-labeled because the useful\n * question is never \"was the append slow\" but WHICH PART — and `materialize`\n * in particular does work proportional to a resource's annotation count, so it\n * degrades with history rather than with load.\n */\nexport function recordAppendStage(\n stage: 'persist' | 'materialize' | 'enrich' | 'publish',\n durationMs: number,\n): void {\n appendStageHistogram().record(durationMs, { 'record.stage': stage });\n}\n\nlet _gitCommandHistogram: Histogram | undefined;\nfunction gitCommandHistogram(): Histogram {\n if (!_gitCommandHistogram) {\n _gitCommandHistogram = meter().createHistogram('semiont.git.duration', {\n description: 'Wall time of a git subprocess. Async — this is latency, not event-loop blockage. Staging is deduped, so the `add` count is far below the number of appended events.',\n unit: 'ms',\n });\n }\n return _gitCommandHistogram;\n}\n\n/**\n * Record a git invocation. Read the `add` count against events appended: one\n * per event means deferred staging has stopped deduping.\n */\nexport function recordGitCommand(command: string, durationMs: number): void {\n gitCommandHistogram().record(durationMs, { 'git.command': command });\n}\n\nfunction gatherDegradeCounter(): Counter {\n if (!_gatherDegradeCounter) {\n _gatherDegradeCounter = meter().createCounter('semiont.gather.degraded', {\n description: 'Gathers that degraded because an eventually-consistent projection did not catch up within its read barrier (vectors: absent semanticContext; graph: projection-lag failure). Labeled by projection.',\n });\n }\n return _gatherDegradeCounter;\n}\n\n/**\n * Record a gather degraded by a projection read barrier: `'vectors'` — the\n * Smelter settle barrier timed out (semanticContext shipped absent);\n * `'graph'` — the Weaver applied barrier + poll floor exhausted (projection\n * lag surfaced as a distinct failure). Fleet-alertable counterpart of the\n * `[gather DEGRADED]` L4 breadcrumbs — a rising rate on either label means\n * that pipeline is not keeping up.\n */\nexport function recordGatherDegrade(projection: 'graph' | 'vectors'): void {\n gatherDegradeCounter().add(1, { projection });\n}\n\n/** Increment the SSE subscriber gauge — call on `/bus/subscribe` open. */\nexport function recordSubscriberConnect(): void {\n sseSubscribersCounter().add(1);\n}\n\n/** Decrement on disconnect. Pair with `recordSubscriberConnect`. */\nexport function recordSubscriberDisconnect(): void {\n sseSubscribersCounter().add(-1);\n}\n\n/**\n * Register a callback that returns the current job-queue snapshot.\n * Polled at the SDK's metric-collection interval. The single gauge\n * emits one observation per status (`pending`, `running`, …) tagged\n * with the `job.status` attribute. Idempotent — last registered\n * provider wins.\n */\nexport function registerJobQueueProvider(\n provider: () => Promise<JobQueueSnapshot> | JobQueueSnapshot,\n): void {\n _jobQueueProvider = provider;\n if (!_jobQueueGauge) {\n _jobQueueGauge = meter().createObservableGauge('semiont.job.queue.size', {\n description: 'Job queue size by status',\n });\n _jobQueueGauge.addCallback(async (observer) => {\n if (!_jobQueueProvider) return;\n const snap = await _jobQueueProvider();\n observer.observe(snap.pending, { 'job.status': 'pending' });\n observer.observe(snap.running, { 'job.status': 'running' });\n observer.observe(snap.complete, { 'job.status': 'complete' });\n observer.observe(snap.failed, { 'job.status': 'failed' });\n observer.observe(snap.cancelled, { 'job.status': 'cancelled' });\n });\n }\n}\n\n/**\n * Register a callback that returns the current vector-index size\n * (point count). Async to allow remote queries (Qdrant). Polled at\n * the metric-collection interval.\n */\n/**\n * Register the Archivist's fact-pump backlog — facts appended to the record\n * but not yet republished onto the bus.\n *\n * At rest this is zero. A value that climbs and does not come back means the\n * pump is outrunning its transport, which is the leading hypothesis for the\n * load-correlated heap growth in `bugs/absent-archivist-wedges-browse.md`\n * (ARCHIVIST-STAYS-UP P5). The backlog is deliberately unbounded today, so\n * this number is the only thing standing between \"the pump is behind\" and an\n * OOM whose cause is inferred from RSS after the fact.\n */\nexport function registerFactPumpDepthProvider(provider: () => number): void {\n _factPumpDepthProvider = provider;\n if (!_factPumpDepthGauge) {\n _factPumpDepthGauge = meter().createObservableGauge('semiont.archivist.fact_pump.depth', {\n description: 'Facts appended to the record but not yet published to the bus. Zero at rest; a rising floor means the pump is behind its transport.',\n });\n _factPumpDepthGauge.addCallback((observer) => {\n if (_factPumpDepthProvider) observer.observe(_factPumpDepthProvider());\n });\n }\n}\n\nlet _gitStagingFailureCounter: Counter | undefined;\n\n/**\n * A staging command that could not be run. Staging the index is a CONVENIENCE\n * — the event log is the system of record — so a failure here is degraded\n * service, never a reason to exit. But degraded must be VISIBLE: this counter\n * is what stops \"the index is quietly stale\" from being invisible.\n */\nexport function recordGitStagingFailure(reason: 'index-lock' | 'other'): void {\n if (!_gitStagingFailureCounter) {\n _gitStagingFailureCounter = meter().createCounter('semiont.git.staging.failures', {\n description: 'Staging commands abandoned after retries; the index may be stale',\n });\n }\n _gitStagingFailureCounter.add(1, { reason });\n}\n\n/**\n * Process lifetime telemetry (ARCHIVIST-GIT-STAGER-CRASH).\n *\n * A supervised process that dies and comes back is INVISIBLE in logs unless\n * someone greps for boot lines, and every request in flight when it died looks\n * to its caller like a hang. On 2026-09-08 that cost a multi-hour hunt through\n * search, qdrant, neo4j and the SSE transport for a crash loop that one metric\n * would have named immediately.\n *\n * `start_time` is the diagnostic, not uptime: a CHANGE in it is unambiguous\n * proof of a restart, and uptime is derivable from it.\n */\nconst PROCESS_START_TIME_SECONDS = Math.floor(Date.now() / 1000);\nlet _processStartTimeGauge: ObservableGauge | undefined;\nlet _restartCountGauge: ObservableGauge | undefined;\nlet _restartCountProvider: (() => Promise<number | undefined> | number | undefined) | undefined;\nlet _abnormalExitCounter: Counter | undefined;\n\n/** Register `semiont.process.start_time`. Called by `initObservability*`. */\nexport function registerProcessLifetimeMetrics(): void {\n if (_processStartTimeGauge) return;\n _processStartTimeGauge = meter().createObservableGauge('semiont.process.start_time', {\n description: 'Unix seconds at which this process started; a change means it restarted',\n unit: 's',\n });\n _processStartTimeGauge.addCallback((observer) => observer.observe(PROCESS_START_TIME_SECONDS));\n}\n\n/**\n * Supply a restart count. A supervisor is POSIX shell and cannot emit OTel, but\n * it keeps a durable event log on the state mount — so the supervised child\n * reads it and reports the count on the supervisor's behalf.\n *\n * **Return `undefined` for \"cannot tell\", never `0`.** A process running\n * without a supervisor (dev, or a direct `node` invocation) has no restart\n * history to report, and 0 is not that — 0 is the value a HEALTHY supervised\n * process reports, so an unsupervised one publishing 0 is indistinguishable\n * from a supervised one that has never crashed. `undefined` skips the\n * observation entirely and the series simply has no point, which is what\n * \"unknown\" looks like in a time series.\n */\nexport function registerRestartCountProvider(\n provider: () => Promise<number | undefined> | number | undefined,\n): void {\n _restartCountProvider = provider;\n if (!_restartCountGauge) {\n _restartCountGauge = meter().createObservableGauge('semiont.process.restarts', {\n description: 'Times the supervisor has restarted this service',\n });\n _restartCountGauge.addCallback(async (observer) => {\n if (!_restartCountProvider) return;\n const count = await _restartCountProvider();\n // Not `if (count)` — 0 is a real, meaningful reading.\n if (count !== undefined) observer.observe(count);\n });\n }\n}\n\n/**\n * Record that this process is dying abnormally, and mark the active span so a\n * trace shows a span that ENDED IN DEATH rather than one that simply never\n * ends. Never swallows: callers re-raise, so Node's own semantics are intact.\n */\nexport function recordAbnormalTermination(reason: string, detail?: string): void {\n if (!_abnormalExitCounter) {\n _abnormalExitCounter = meter().createCounter('semiont.process.abnormal_exit', {\n description: 'Process terminations that were not a clean shutdown',\n });\n }\n _abnormalExitCounter.add(1, { reason });\n const active = trace.getActiveSpan();\n if (active) {\n active.setStatus({ code: SpanStatusCode.ERROR, message: `${reason}: ${detail ?? ''}`.trim() });\n active.setAttribute('semiont.process.abnormal_exit', reason);\n active.end();\n }\n}\n\nexport function registerVectorIndexSizeProvider(\n provider: () => Promise<number> | number,\n): void {\n _vectorIndexSizeProvider = provider;\n if (!_vectorIndexSizeGauge) {\n _vectorIndexSizeGauge = meter().createObservableGauge('semiont.vector.index.size', {\n description: 'Vector store point count',\n });\n _vectorIndexSizeGauge.addCallback(async (observer) => {\n if (_vectorIndexSizeProvider) {\n const value = await _vectorIndexSizeProvider();\n observer.observe(value);\n }\n });\n }\n}\n\n/**\n * Record an inference call. Token counts are optional — providers that\n * don't expose them (or fail before generating) record only call count\n * and duration.\n */\nexport function recordInferenceUsage(opts: {\n provider: string;\n model: string;\n durationMs: number;\n outcome: 'success' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const baseAttrs = {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.outcome': opts.outcome,\n };\n inferenceCallsCounter().add(1, baseAttrs);\n inferenceDurationHistogram().record(opts.durationMs, baseAttrs);\n if (opts.inputTokens != null && opts.inputTokens > 0) {\n inferenceTokensCounter().add(opts.inputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'input',\n });\n }\n if (opts.outputTokens != null && opts.outputTokens > 0) {\n inferenceTokensCounter().add(opts.outputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'output',\n });\n }\n}\n\nlet _detectionCallCounter: Counter | undefined;\nlet _detectionDurationHistogram: Histogram | undefined;\nlet _detectionItemsHistogram: Histogram | undefined;\nlet _detectionTokensHistogram: Histogram | undefined;\n\nfunction detectionCallCounter(): Counter {\n if (!_detectionCallCounter) {\n _detectionCallCounter = meter().createCounter('semiont.detection.calls', {\n description: 'Detection model calls, labeled by motivation, outcome, subdivision depth and whether this was the floor re-roll.',\n });\n }\n return _detectionCallCounter;\n}\n\nfunction detectionDurationHistogram(): Histogram {\n if (!_detectionDurationHistogram) {\n _detectionDurationHistogram = meter().createHistogram('semiont.detection.call.duration', {\n description: 'Wall time of one detection model call, including the attempts that failed and were retried smaller.',\n unit: 'ms',\n });\n }\n return _detectionDurationHistogram;\n}\n\nfunction detectionItemsHistogram(): Histogram {\n if (!_detectionItemsHistogram) {\n _detectionItemsHistogram = meter().createHistogram('semiont.detection.call.items', {\n description: 'Annotations returned by one detection call. Against the input size on the same record, this is yield.',\n });\n }\n return _detectionItemsHistogram;\n}\n\nfunction detectionTokensHistogram(): Histogram {\n if (!_detectionTokensHistogram) {\n _detectionTokensHistogram = meter().createHistogram('semiont.detection.call.tokens', {\n description: \"Provider-reported tokens for one detection call, by direction. Deliberately separate from semiont.inference.tokens: that series is the authoritative total but carries no subdivision depth, and 'what does a depth-2 call cost' is the question every sizing decision asks.\",\n });\n }\n return _detectionTokensHistogram;\n}\n\n/**\n * Record one detection model call (DETECTION-QUALITY-THROUGHPUT P1).\n *\n * The adapters already record provider/model/duration/tokens for every\n * inference call. What they cannot know is the detection shape around it:\n * which motivation asked, how big the piece was, how many annotations came\n * back, how deep subdivision had descended, and whether this was the floor\n * re-roll. Those are the facts that distinguish a healthy call from a\n * expensive descent, and without them a slow detection run is one\n * undifferentiated number.\n *\n * FAILED attempts are recorded too, and that is the point: the calls paid for\n * and thrown away during a descent are exactly the cost later phases exist to\n * avoid, so a record only of successes would hide the thing being optimized.\n *\n * Tokens are the PROVIDER's counts, passed through — never estimated. Absent\n * means the provider did not report them.\n */\nexport function recordDetectionCall(opts: {\n label: string;\n pieceChars: number;\n durationMs: number;\n items: number;\n depth: number;\n reroll: boolean;\n outcome: 'success' | 'truncated' | 'timeout' | 'collapsed' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const attrs = {\n 'detection.label': opts.label,\n 'detection.outcome': opts.outcome,\n 'detection.depth': opts.depth,\n 'detection.reroll': opts.reroll,\n };\n detectionCallCounter().add(1, attrs);\n detectionDurationHistogram().record(opts.durationMs, attrs);\n detectionItemsHistogram().record(opts.items, attrs);\n if (opts.inputTokens !== undefined) {\n detectionTokensHistogram().record(opts.inputTokens, { ...attrs, 'detection.direction': 'input' });\n }\n if (opts.outputTokens !== undefined) {\n detectionTokensHistogram().record(opts.outputTokens, { ...attrs, 'detection.direction': 'output' });\n }\n}\n\nlet _anchorOutcomeCounter: Counter | undefined;\nfunction anchorOutcomeCounter(): Counter {\n if (!_anchorOutcomeCounter) {\n _anchorOutcomeCounter = meter().createCounter('semiont.detection.anchors', {\n description: 'Every annotation anchoring, labeled by the method that resolved it. EVERY outcome is counted, not just the risky ones, because a bare count of degraded anchors has no denominator — the rate is the precision signal.',\n });\n }\n return _anchorOutcomeCounter;\n}\n\n/**\n * Record how one annotation got anchored (DETECTION-QUALITY-THROUGHPUT P5).\n *\n * The selector-vs-source check is already a WRITE-TIME INVARIANT — both\n * `buildTextAnnotation` and `buildPdfAnnotation` throw on a selector that does\n * not match its source — so mechanical correctness is guaranteed rather than\n * sampled, and auditing it would measure a constant.\n *\n * What is genuinely uncertain is which anchoring METHOD got there. An `exact`\n * the model quoted verbatim and that appears once is certain; one resolved by\n * `first-of-many` (several occurrences, no usable context) or `fuzzy-match`\n * picked a plausible occurrence and may have picked wrong. Those were visible\n * only as log warnings — countable by a human reading worker output, which is\n * how 47 of them went unreviewed. As a rate they are the precision number that\n * sits beside the yield numbers.\n */\nexport function recordAnchorOutcome(label: string, method: string): void {\n anchorOutcomeCounter().add(1, { 'detection.label': label, 'anchor.method': method });\n}\n\n// ── Re-exports from @opentelemetry/api ─────────────────────────────────\n\nexport { SpanKind, SpanStatusCode, type Attributes, type Span } from '@opentelemetry/api';\n"]}
|
package/dist/node.d.ts
CHANGED
|
@@ -48,8 +48,24 @@ declare function metricsExporterKind(endpoint: string | undefined, requested: st
|
|
|
48
48
|
* the same `OTEL_EXPORTER_OTLP_ENDPOINT` as traces.
|
|
49
49
|
*/
|
|
50
50
|
declare function initObservabilityNode(config: NodeObservabilityConfig): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Report the supervisor's restart count for this service
|
|
53
|
+
* (GATEWAY-SUPERVISION F3). Call once, after `initObservabilityNode`.
|
|
54
|
+
*
|
|
55
|
+
* `scripts/container/supervise.sh` is POSIX shell and cannot emit OTel, but it
|
|
56
|
+
* writes one `starting <name>` line per life to a durable event log and
|
|
57
|
+
* exports that log's path and the service name to the child. This reads them
|
|
58
|
+
* back. A crash LOOP is otherwise invisible to everything but `container logs`.
|
|
59
|
+
*
|
|
60
|
+
* Both halves of the contract — the path and the marker — come from the
|
|
61
|
+
* environment, so nothing here restates what the shell already decided. This
|
|
62
|
+
* lives in `node.ts` rather than beside `registerRestartCountProvider` in
|
|
63
|
+
* `index.ts` because it reads a file: `index.ts` imports no node builtins, and
|
|
64
|
+
* `./web` depends on it staying that way.
|
|
65
|
+
*/
|
|
66
|
+
declare function registerSupervisorRestartCount(): void;
|
|
51
67
|
/** Force-flush + shutdown both SDKs. Test cleanup, not production. */
|
|
52
68
|
declare function shutdownObservabilityNode(): Promise<void>;
|
|
53
69
|
|
|
54
|
-
export { initObservabilityNode, metricsExporterKind, shutdownObservabilityNode };
|
|
70
|
+
export { initObservabilityNode, metricsExporterKind, registerSupervisorRestartCount, shutdownObservabilityNode };
|
|
55
71
|
export type { NodeObservabilityConfig };
|
package/dist/node.js
CHANGED
|
@@ -23,6 +23,8 @@ var METER_NAME = "semiont";
|
|
|
23
23
|
var meter = () => metrics.getMeter(METER_NAME);
|
|
24
24
|
var PROCESS_START_TIME_SECONDS = Math.floor(Date.now() / 1e3);
|
|
25
25
|
var _processStartTimeGauge;
|
|
26
|
+
var _restartCountGauge;
|
|
27
|
+
var _restartCountProvider;
|
|
26
28
|
var _abnormalExitCounter;
|
|
27
29
|
function registerProcessLifetimeMetrics() {
|
|
28
30
|
if (_processStartTimeGauge) return;
|
|
@@ -32,6 +34,19 @@ function registerProcessLifetimeMetrics() {
|
|
|
32
34
|
});
|
|
33
35
|
_processStartTimeGauge.addCallback((observer) => observer.observe(PROCESS_START_TIME_SECONDS));
|
|
34
36
|
}
|
|
37
|
+
function registerRestartCountProvider(provider) {
|
|
38
|
+
_restartCountProvider = provider;
|
|
39
|
+
if (!_restartCountGauge) {
|
|
40
|
+
_restartCountGauge = meter().createObservableGauge("semiont.process.restarts", {
|
|
41
|
+
description: "Times the supervisor has restarted this service"
|
|
42
|
+
});
|
|
43
|
+
_restartCountGauge.addCallback(async (observer) => {
|
|
44
|
+
if (!_restartCountProvider) return;
|
|
45
|
+
const count = await _restartCountProvider();
|
|
46
|
+
if (count !== void 0) observer.observe(count);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
35
50
|
function recordAbnormalTermination(reason, detail) {
|
|
36
51
|
if (!_abnormalExitCounter) {
|
|
37
52
|
_abnormalExitCounter = meter().createCounter("semiont.process.abnormal_exit", {
|
|
@@ -151,6 +166,20 @@ function initObservabilityNode(config) {
|
|
|
151
166
|
process.on("uncaughtException", fatal("uncaughtException"));
|
|
152
167
|
return true;
|
|
153
168
|
}
|
|
169
|
+
function registerSupervisorRestartCount() {
|
|
170
|
+
const events = process.env["SUPERVISE_EVENTS"];
|
|
171
|
+
const name = process.env["SUPERVISE_NAME"];
|
|
172
|
+
if (!events || !name) return;
|
|
173
|
+
registerRestartCountProvider(async () => {
|
|
174
|
+
try {
|
|
175
|
+
const { readFile } = await import('fs/promises');
|
|
176
|
+
const log = await readFile(events, "utf-8");
|
|
177
|
+
return Math.max(0, log.split("\n").filter((l) => l.includes(`starting ${name}`)).length - 1);
|
|
178
|
+
} catch {
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
154
183
|
async function shutdownObservabilityNode() {
|
|
155
184
|
await Promise.all([
|
|
156
185
|
tracerProviderInstance?.shutdown(),
|
|
@@ -160,6 +189,6 @@ async function shutdownObservabilityNode() {
|
|
|
160
189
|
meterProviderInstance = void 0;
|
|
161
190
|
}
|
|
162
191
|
|
|
163
|
-
export { initObservabilityNode, metricsExporterKind, shutdownObservabilityNode };
|
|
192
|
+
export { initObservabilityNode, metricsExporterKind, registerSupervisorRestartCount, shutdownObservabilityNode };
|
|
164
193
|
//# sourceMappingURL=node.js.map
|
|
165
194
|
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/runtime-stats.ts","../src/node.ts"],"names":["context","trace","propagation","metrics"],"mappings":"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","file":"node.js","sourcesContent":["/**\n * @semiont/observability — public API.\n *\n * Universal surface (works in Node + browser). For SDK *initialization*,\n * import from `@semiont/observability/node` or `/web` at the process entry\n * point. Everything else uses this module.\n *\n * Tier 2 of `.plans/OBSERVABILITY.md`. The public surface:\n *\n * - `withSpan(name, fn, options?)` — wrap an async block in a span;\n * `options` carries `kind` and `attrs`.\n * - `withActorSpan(actor, channel, fn, extraAttrs?)` — consumer-span\n * wrapper for bus-event handlers, with handler-duration recording.\n * - `injectTraceparent(payload)` / `extractTraceparent(payload)` — W3C\n * trace-context propagation across the SSE channel (the bus payload\n * gets a `_trace?: { traceparent }` sibling to `correlationId`).\n * - `withTraceparent(carrier, fn)` — run `fn` with the incoming\n * traceparent as the parent context.\n * - `getActiveTraceparent()` — read the active span's traceparent for\n * manual propagation (e.g. attaching to a fetch header or SSE field).\n * - `getLogTraceContext()` — active `trace_id` / `span_id` for log-line\n * correlation.\n * - Metric recorders (`recordBusEmit`, `recordHandlerDuration`,\n * `recordJobOutcome`, `recordSubscriberConnect` / `Disconnect`,\n * `recordInferenceUsage`) and gauge providers\n * (`registerJobQueueProvider`, `registerVectorIndexSizeProvider`).\n *\n * No-op when no exporter is configured: `@opentelemetry/api`'s default\n * tracer is a no-op, so `withSpan` is essentially free until\n * `initObservability*()` runs.\n */\n\nimport {\n context,\n isSpanContextValid,\n metrics,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n type Attributes,\n type Counter,\n type Histogram,\n type ObservableGauge,\n type Span,\n type UpDownCounter,\n} from '@opentelemetry/api';\nimport { setBusLogTraceIdProvider } from '@semiont/core';\n\n// Wire `busLog`'s trace-id provider once at module load. When an OTel\n// SDK is initialized (and a span is active when `busLog` fires), the\n// emitted line gets a `trace=<8hex>` suffix that correlates the\n// grep-timeline with the trace UI. No-op when no SDK is active.\nsetBusLogTraceIdProvider(() => {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return ctx.traceId;\n});\n\nconst TRACER_NAME = 'semiont';\n\nconst tracer = () => trace.getTracer(TRACER_NAME);\n\n// ── withSpan ───────────────────────────────────────────────────────────\n\n/**\n * Wrap an async block in a span. The span is started before `fn` runs and\n * ended after it resolves or rejects; exceptions are recorded and the span\n * status is set to ERROR. `kind` defaults to INTERNAL.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T> | T,\n options?: { kind?: SpanKind; attrs?: Attributes },\n): Promise<T> {\n const span = tracer().startSpan(name, {\n kind: options?.kind ?? SpanKind.INTERNAL,\n ...(options?.attrs ? { attributes: options.attrs } : {}),\n });\n try {\n return await context.with(trace.setSpan(context.active(), span), () => fn(span));\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: err instanceof Error ? err.message : String(err),\n });\n throw err;\n } finally {\n span.end();\n }\n}\n\n// ── Traceparent on bus payloads ────────────────────────────────────────\n\nconst TRACE_FIELD = '_trace';\n\n/**\n * Sibling of `correlationId` on bus payloads. Lives on the SSE event body\n * because SSE has no header trailer; the SDK strips it before delivering\n * the payload to subscribers. Additive — payloads without `_trace` parse\n * unchanged.\n */\nexport interface TraceCarrier {\n /** W3C `traceparent` header value (`00-<traceId>-<spanId>-<flags>`). */\n traceparent: string;\n /** W3C `tracestate` header value (vendor-specific extensions). */\n tracestate?: string;\n}\n\n/**\n * Read the active span's W3C traceparent (and tracestate). Returns\n * `undefined` if no span is active.\n */\nexport function getActiveTraceparent(): TraceCarrier | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n const traceparent = carrier['traceparent'];\n if (!traceparent) return undefined;\n return carrier['tracestate']\n ? { traceparent, tracestate: carrier['tracestate'] }\n : { traceparent };\n}\n\n/**\n * Attach the active span's trace-context to a payload object as\n * `_trace`. No-op when no span is active. Returns the same object\n * reference for chaining.\n */\nexport function injectTraceparent<T extends Record<string, unknown>>(payload: T): T {\n const carrier = getActiveTraceparent();\n if (carrier) {\n (payload as Record<string, unknown>)[TRACE_FIELD] = carrier;\n }\n return payload;\n}\n\n/**\n * Strip and return the `_trace` field from a payload. Mutates `payload`.\n * The field is internal plumbing and should not be visible to subscribers.\n */\nexport function extractTraceparent<T extends Record<string, unknown>>(\n payload: T,\n): TraceCarrier | undefined {\n const carrier = (payload as Record<string, unknown>)[TRACE_FIELD] as\n | TraceCarrier\n | undefined;\n if (carrier !== undefined) {\n delete (payload as Record<string, unknown>)[TRACE_FIELD];\n }\n if (!carrier || typeof carrier.traceparent !== 'string') return undefined;\n return carrier;\n}\n\n/**\n * Run `fn` with the given W3C traceparent set as the parent context.\n * Any spans started inside `fn` will be children of the incoming trace.\n * No-op if `carrier` is undefined.\n */\nexport function withTraceparent<T>(\n carrier: TraceCarrier | undefined,\n fn: () => T,\n): T {\n if (!carrier) return fn();\n const carrierObj: Record<string, string> = { traceparent: carrier.traceparent };\n if (carrier.tracestate) carrierObj['tracestate'] = carrier.tracestate;\n const ctx = propagation.extract(context.active(), carrierObj);\n return context.with(ctx, fn);\n}\n\n// ── Actor handler convenience ──────────────────────────────────────────\n\n/**\n * Wrap a bus-event handler in an `actor.<name>:<channel>` consumer span.\n * Used at every `eventBus.get(channel).subscribe(handler)` site inside\n * an actor (Stower, Gatherer, Matcher, Browser, Smelter), to attribute\n * each in-process subscriber's work to a span without scattering manual\n * `withSpan` calls across handler bodies.\n *\n * The span's parent is the active context at the time the handler\n * fires — which is the `bus.dispatch:<channel>` span on the gateway\n * (Subject.next runs synchronously inside the dispatch span), or the\n * `bus.emit:<channel>` span when an actor emits to itself.\n */\nexport async function withActorSpan<T>(\n actor: string,\n channel: string,\n fn: (span: Span) => Promise<T> | T,\n extraAttrs?: Attributes,\n): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(`actor.${actor}:${channel}`, fn, {\n kind: SpanKind.CONSUMER,\n attrs: {\n actor,\n 'bus.channel': channel,\n ...(extraAttrs ?? {}),\n },\n });\n } finally {\n recordHandlerDuration(actor, channel, performance.now() - start);\n }\n}\n\n// ── Log correlation ────────────────────────────────────────────────────\n\n/**\n * Read the active span's `trace_id` / `span_id` for log-line correlation.\n * Tier 3 of `.plans/OBSERVABILITY.md`. Each structured log line gets\n * tagged with these so a log query in CloudWatch / Loki / Datadog can\n * jump to the trace in Tempo / Jaeger / X-Ray.\n *\n * Returns `undefined` if no span is active, or if the active span's\n * context is invalid (uninitialized SDK, no-op tracer).\n */\nexport function getLogTraceContext(): { trace_id: string; span_id: string } | undefined {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return { trace_id: ctx.traceId, span_id: ctx.spanId };\n}\n\n// ── Metrics — Tier 3 ───────────────────────────────────────────────────\n\nconst METER_NAME = 'semiont';\n\nconst meter = () => metrics.getMeter(METER_NAME);\n\nlet _busEmitCounter: Counter | undefined;\nlet _replySuppressedCounter: Counter | undefined;\nlet _resumeGapCounter: Counter | undefined;\nlet _unanswerableCounter: Counter | undefined;\nlet _correlationRegistryGauge: ObservableGauge | undefined;\nlet _correlationRegistryProvider: (() => CorrelationRegistrySnapshot) | undefined;\nlet _handlerDurationHistogram: Histogram | undefined;\nlet _jobOutcomeCounter: Counter | undefined;\nlet _jobDurationHistogram: Histogram | undefined;\nlet _gatherDegradeCounter: Counter | undefined;\nlet _inferenceCallsCounter: Counter | undefined;\nlet _inferenceTokensCounter: Counter | undefined;\nlet _inferenceDurationHistogram: Histogram | undefined;\nlet _sseSubscribers: UpDownCounter | undefined;\nlet _jobQueueGauge: ObservableGauge | undefined;\nlet _jobQueueProvider: (() => Promise<JobQueueSnapshot> | JobQueueSnapshot) | undefined;\nlet _vectorIndexSizeGauge: ObservableGauge | undefined;\nlet _factPumpDepthGauge: ObservableGauge | undefined;\nlet _factPumpDepthProvider: (() => number) | undefined;\nlet _vectorIndexSizeProvider: (() => Promise<number> | number) | undefined;\n\n/** Snapshot of job-queue contents by status. Match `JobQueue.getStats()`. */\nexport interface JobQueueSnapshot {\n pending: number;\n running: number;\n complete: number;\n failed: number;\n cancelled: number;\n}\n\nfunction busEmitCounter(): Counter {\n if (!_busEmitCounter) {\n _busEmitCounter = meter().createCounter('semiont.bus.emit', {\n description: 'Bus emits by channel and scope',\n });\n }\n return _busEmitCounter;\n}\n\nfunction handlerDurationHistogram(): Histogram {\n if (!_handlerDurationHistogram) {\n _handlerDurationHistogram = meter().createHistogram('semiont.handler.duration', {\n description: 'In-process actor handler duration',\n unit: 'ms',\n });\n }\n return _handlerDurationHistogram;\n}\n\nfunction jobOutcomeCounter(): Counter {\n if (!_jobOutcomeCounter) {\n _jobOutcomeCounter = meter().createCounter('semiont.job.outcome', {\n description: 'Worker job completions by type and outcome',\n });\n }\n return _jobOutcomeCounter;\n}\n\nfunction jobDurationHistogram(): Histogram {\n if (!_jobDurationHistogram) {\n _jobDurationHistogram = meter().createHistogram('semiont.job.duration', {\n description: 'Worker job duration by type',\n unit: 'ms',\n });\n }\n return _jobDurationHistogram;\n}\n\nfunction inferenceCallsCounter(): Counter {\n if (!_inferenceCallsCounter) {\n _inferenceCallsCounter = meter().createCounter('semiont.inference.calls', {\n description: 'Inference API calls by provider, model, and outcome',\n });\n }\n return _inferenceCallsCounter;\n}\n\nfunction inferenceTokensCounter(): Counter {\n if (!_inferenceTokensCounter) {\n _inferenceTokensCounter = meter().createCounter('semiont.inference.tokens', {\n description: 'Inference token usage by provider, model, and direction',\n });\n }\n return _inferenceTokensCounter;\n}\n\nfunction inferenceDurationHistogram(): Histogram {\n if (!_inferenceDurationHistogram) {\n _inferenceDurationHistogram = meter().createHistogram('semiont.inference.duration', {\n description: 'Inference call duration by provider, model, and outcome',\n unit: 'ms',\n });\n }\n return _inferenceDurationHistogram;\n}\n\nfunction sseSubscribersCounter(): UpDownCounter {\n if (!_sseSubscribers) {\n _sseSubscribers = meter().createUpDownCounter('semiont.sse.subscribers', {\n description: 'Active SSE subscribers',\n });\n }\n return _sseSubscribers;\n}\n\nfunction replySuppressedCounter(): Counter {\n if (!_replySuppressedCounter) {\n _replySuppressedCounter = meter().createCounter('semiont.bus.reply.suppressed', {\n description: 'Correlated replies withheld from a non-owning subscriber',\n });\n }\n return _replySuppressedCounter;\n}\n\n/**\n * A correlated reply was withheld from a subscriber that does not own its\n * correlationId (CORRELATED-REPLY-ROUTING P5).\n *\n * Counts ONLY that case. A frame with no correlationId is a shape violation\n * (warned, not counted), and a cid nobody claimed is the structural in-process\n * case that fires constantly — counting either would drown the signal this\n * metric exists to show: the fan-out amplification the delivery filter removes.\n */\nexport function recordReplySuppressed(channel: string): void {\n replySuppressedCounter().add(1, { 'bus.channel': channel });\n}\n\nfunction resumeGapCounter(): Counter {\n if (!_resumeGapCounter) {\n _resumeGapCounter = meter().createCounter('semiont.bus.resume_gap', {\n description: 'SSE resumes that degraded to a gap because replay was unavailable',\n });\n }\n return _resumeGapCounter;\n}\n\n/**\n * An SSE resume could not be served and the client was told to fall back to\n * cache. This degradation is CORRECT by design and therefore silent — which is\n * exactly why it needs a number. A rising rate means clients are losing\n * history, and nothing else in the stack says so.\n */\nexport function recordResumeGap(reason: string): void {\n resumeGapCounter().add(1, { 'bus.resume_gap.reason': reason });\n}\n\nfunction unanswerableCounter(): Counter {\n if (!_unanswerableCounter) {\n _unanswerableCounter = meter().createCounter('semiont.bus.unanswerable', {\n description: 'Request emits that reached zero subscribers and were failed at the gateway',\n });\n }\n return _unanswerableCounter;\n}\n\n/**\n * A request-shaped emit reached no subscriber, so the gateway synthesized its\n * mapped failure (ARCHIVIST-STAYS-UP P3). By channel, this is the absence rate\n * of the service that answers it — the difference between \"it went down once\"\n * and \"it is flapping.\"\n */\nexport function recordUnanswerableRequest(channel: string): void {\n unanswerableCounter().add(1, { 'bus.channel': channel });\n}\n\n/** Claims held and reply payloads retained by a gateway's correlation registry. */\nexport interface CorrelationRegistrySnapshot {\n claims: number;\n retainedReplies: number;\n}\n\n/**\n * Register a callback returning the gateway's correlation-registry occupancy.\n *\n * COUNTS, not bytes: retention is count-budgeted today (byte-budgeting is a\n * known limit in CORRELATED-REPLY-ROUTING), so `retainedReplies` is a proxy for\n * heap, not a measure of it. It is still the closest observable to the question\n * two OOM investigations keep asking — a browse result can be 1-2 MB, and up to\n * REPLY_RETENTION_MAX of them are held at once.\n */\nexport function registerCorrelationRegistryProvider(\n provider: () => CorrelationRegistrySnapshot,\n): void {\n _correlationRegistryProvider = provider;\n if (!_correlationRegistryGauge) {\n _correlationRegistryGauge = meter().createObservableGauge('semiont.bus.correlation.size', {\n description: 'Correlation registry occupancy: live claims and retained reply payloads',\n });\n _correlationRegistryGauge.addCallback((observer) => {\n if (!_correlationRegistryProvider) return;\n const snap = _correlationRegistryProvider();\n observer.observe(snap.claims, { 'correlation.kind': 'claims' });\n observer.observe(snap.retainedReplies, { 'correlation.kind': 'retained_replies' });\n });\n }\n}\n\n/** Increment the bus-emit counter. Called at every transport `emit` site. */\nexport function recordBusEmit(channel: string, scope?: string): void {\n busEmitCounter().add(1, {\n 'bus.channel': channel,\n ...(scope ? { 'bus.scope': scope } : {}),\n });\n}\n\n/** Record an in-process actor handler's duration. */\nexport function recordHandlerDuration(actor: string, channel: string, durationMs: number): void {\n handlerDurationHistogram().record(durationMs, {\n actor,\n 'bus.channel': channel,\n });\n}\n\n/** Record a worker job's outcome and duration. */\nexport function recordJobOutcome(jobType: string, outcome: 'completed' | 'failed', durationMs: number): void {\n jobOutcomeCounter().add(1, { 'job.type': jobType, 'job.outcome': outcome });\n jobDurationHistogram().record(durationMs, { 'job.type': jobType, 'job.outcome': outcome });\n}\n\nlet _appendStageHistogram: Histogram | undefined;\nfunction appendStageHistogram(): Histogram {\n if (!_appendStageHistogram) {\n _appendStageHistogram = meter().createHistogram('semiont.record.append.duration', {\n description: 'Time spent in one stage of appending an event to the record, labeled by stage: persist (JSONL write + git), materialize (view rebuild), enrich, publish. The Archivist\\'s core write path.',\n unit: 'ms',\n });\n }\n return _appendStageHistogram;\n}\n\n/**\n * Record one stage of `EventStore.appendEvent` (ARCHIVIST-STAYS-UP P7).\n *\n * The append path is the one operation only the Archivist can perform, and it\n * was entirely dark: reads had `recordHandlerDuration` and the bus had its own\n * counters, while writes had nothing. Stage-labeled because the useful\n * question is never \"was the append slow\" but WHICH PART — and `materialize`\n * in particular does work proportional to a resource's annotation count, so it\n * degrades with history rather than with load.\n */\nexport function recordAppendStage(\n stage: 'persist' | 'materialize' | 'enrich' | 'publish',\n durationMs: number,\n): void {\n appendStageHistogram().record(durationMs, { 'record.stage': stage });\n}\n\nlet _gitCommandHistogram: Histogram | undefined;\nfunction gitCommandHistogram(): Histogram {\n if (!_gitCommandHistogram) {\n _gitCommandHistogram = meter().createHistogram('semiont.git.duration', {\n description: 'Wall time of a git subprocess. Async — this is latency, not event-loop blockage. Staging is deduped, so the `add` count is far below the number of appended events.',\n unit: 'ms',\n });\n }\n return _gitCommandHistogram;\n}\n\n/**\n * Record a git invocation. Read the `add` count against events appended: one\n * per event means deferred staging has stopped deduping.\n */\nexport function recordGitCommand(command: string, durationMs: number): void {\n gitCommandHistogram().record(durationMs, { 'git.command': command });\n}\n\nfunction gatherDegradeCounter(): Counter {\n if (!_gatherDegradeCounter) {\n _gatherDegradeCounter = meter().createCounter('semiont.gather.degraded', {\n description: 'Gathers that degraded because an eventually-consistent projection did not catch up within its read barrier (vectors: absent semanticContext; graph: projection-lag failure). Labeled by projection.',\n });\n }\n return _gatherDegradeCounter;\n}\n\n/**\n * Record a gather degraded by a projection read barrier: `'vectors'` — the\n * Smelter settle barrier timed out (semanticContext shipped absent);\n * `'graph'` — the Weaver applied barrier + poll floor exhausted (projection\n * lag surfaced as a distinct failure). Fleet-alertable counterpart of the\n * `[gather DEGRADED]` L4 breadcrumbs — a rising rate on either label means\n * that pipeline is not keeping up.\n */\nexport function recordGatherDegrade(projection: 'graph' | 'vectors'): void {\n gatherDegradeCounter().add(1, { projection });\n}\n\n/** Increment the SSE subscriber gauge — call on `/bus/subscribe` open. */\nexport function recordSubscriberConnect(): void {\n sseSubscribersCounter().add(1);\n}\n\n/** Decrement on disconnect. Pair with `recordSubscriberConnect`. */\nexport function recordSubscriberDisconnect(): void {\n sseSubscribersCounter().add(-1);\n}\n\n/**\n * Register a callback that returns the current job-queue snapshot.\n * Polled at the SDK's metric-collection interval. The single gauge\n * emits one observation per status (`pending`, `running`, …) tagged\n * with the `job.status` attribute. Idempotent — last registered\n * provider wins.\n */\nexport function registerJobQueueProvider(\n provider: () => Promise<JobQueueSnapshot> | JobQueueSnapshot,\n): void {\n _jobQueueProvider = provider;\n if (!_jobQueueGauge) {\n _jobQueueGauge = meter().createObservableGauge('semiont.job.queue.size', {\n description: 'Job queue size by status',\n });\n _jobQueueGauge.addCallback(async (observer) => {\n if (!_jobQueueProvider) return;\n const snap = await _jobQueueProvider();\n observer.observe(snap.pending, { 'job.status': 'pending' });\n observer.observe(snap.running, { 'job.status': 'running' });\n observer.observe(snap.complete, { 'job.status': 'complete' });\n observer.observe(snap.failed, { 'job.status': 'failed' });\n observer.observe(snap.cancelled, { 'job.status': 'cancelled' });\n });\n }\n}\n\n/**\n * Register a callback that returns the current vector-index size\n * (point count). Async to allow remote queries (Qdrant). Polled at\n * the metric-collection interval.\n */\n/**\n * Register the Archivist's fact-pump backlog — facts appended to the record\n * but not yet republished onto the bus.\n *\n * At rest this is zero. A value that climbs and does not come back means the\n * pump is outrunning its transport, which is the leading hypothesis for the\n * load-correlated heap growth in `bugs/absent-archivist-wedges-browse.md`\n * (ARCHIVIST-STAYS-UP P5). The backlog is deliberately unbounded today, so\n * this number is the only thing standing between \"the pump is behind\" and an\n * OOM whose cause is inferred from RSS after the fact.\n */\nexport function registerFactPumpDepthProvider(provider: () => number): void {\n _factPumpDepthProvider = provider;\n if (!_factPumpDepthGauge) {\n _factPumpDepthGauge = meter().createObservableGauge('semiont.archivist.fact_pump.depth', {\n description: 'Facts appended to the record but not yet published to the bus. Zero at rest; a rising floor means the pump is behind its transport.',\n });\n _factPumpDepthGauge.addCallback((observer) => {\n if (_factPumpDepthProvider) observer.observe(_factPumpDepthProvider());\n });\n }\n}\n\nlet _gitStagingFailureCounter: Counter | undefined;\n\n/**\n * A staging command that could not be run. Staging the index is a CONVENIENCE\n * — the event log is the system of record — so a failure here is degraded\n * service, never a reason to exit. But degraded must be VISIBLE: this counter\n * is what stops \"the index is quietly stale\" from being invisible.\n */\nexport function recordGitStagingFailure(reason: 'index-lock' | 'other'): void {\n if (!_gitStagingFailureCounter) {\n _gitStagingFailureCounter = meter().createCounter('semiont.git.staging.failures', {\n description: 'Staging commands abandoned after retries; the index may be stale',\n });\n }\n _gitStagingFailureCounter.add(1, { reason });\n}\n\n/**\n * Process lifetime telemetry (ARCHIVIST-GIT-STAGER-CRASH).\n *\n * A supervised process that dies and comes back is INVISIBLE in logs unless\n * someone greps for boot lines, and every request in flight when it died looks\n * to its caller like a hang. On 2026-09-08 that cost a multi-hour hunt through\n * search, qdrant, neo4j and the SSE transport for a crash loop that one metric\n * would have named immediately.\n *\n * `start_time` is the diagnostic, not uptime: a CHANGE in it is unambiguous\n * proof of a restart, and uptime is derivable from it.\n */\nconst PROCESS_START_TIME_SECONDS = Math.floor(Date.now() / 1000);\nlet _processStartTimeGauge: ObservableGauge | undefined;\nlet _restartCountGauge: ObservableGauge | undefined;\nlet _restartCountProvider: (() => Promise<number> | number) | undefined;\nlet _abnormalExitCounter: Counter | undefined;\n\n/** Register `semiont.process.start_time`. Called by `initObservability*`. */\nexport function registerProcessLifetimeMetrics(): void {\n if (_processStartTimeGauge) return;\n _processStartTimeGauge = meter().createObservableGauge('semiont.process.start_time', {\n description: 'Unix seconds at which this process started; a change means it restarted',\n unit: 's',\n });\n _processStartTimeGauge.addCallback((observer) => observer.observe(PROCESS_START_TIME_SECONDS));\n}\n\n/**\n * Supply a restart count. The Archivist's supervisor is POSIX shell and cannot\n * emit OTel, but it already keeps a durable event log on the state mount — so\n * the supervised child reads it and reports the count on the supervisor's\n * behalf.\n */\nexport function registerRestartCountProvider(\n provider: () => Promise<number> | number,\n): void {\n _restartCountProvider = provider;\n if (!_restartCountGauge) {\n _restartCountGauge = meter().createObservableGauge('semiont.process.restarts', {\n description: 'Times the supervisor has restarted this service',\n });\n _restartCountGauge.addCallback(async (observer) => {\n if (_restartCountProvider) observer.observe(await _restartCountProvider());\n });\n }\n}\n\n/**\n * Record that this process is dying abnormally, and mark the active span so a\n * trace shows a span that ENDED IN DEATH rather than one that simply never\n * ends. Never swallows: callers re-raise, so Node's own semantics are intact.\n */\nexport function recordAbnormalTermination(reason: string, detail?: string): void {\n if (!_abnormalExitCounter) {\n _abnormalExitCounter = meter().createCounter('semiont.process.abnormal_exit', {\n description: 'Process terminations that were not a clean shutdown',\n });\n }\n _abnormalExitCounter.add(1, { reason });\n const active = trace.getActiveSpan();\n if (active) {\n active.setStatus({ code: SpanStatusCode.ERROR, message: `${reason}: ${detail ?? ''}`.trim() });\n active.setAttribute('semiont.process.abnormal_exit', reason);\n active.end();\n }\n}\n\nexport function registerVectorIndexSizeProvider(\n provider: () => Promise<number> | number,\n): void {\n _vectorIndexSizeProvider = provider;\n if (!_vectorIndexSizeGauge) {\n _vectorIndexSizeGauge = meter().createObservableGauge('semiont.vector.index.size', {\n description: 'Vector store point count',\n });\n _vectorIndexSizeGauge.addCallback(async (observer) => {\n if (_vectorIndexSizeProvider) {\n const value = await _vectorIndexSizeProvider();\n observer.observe(value);\n }\n });\n }\n}\n\n/**\n * Record an inference call. Token counts are optional — providers that\n * don't expose them (or fail before generating) record only call count\n * and duration.\n */\nexport function recordInferenceUsage(opts: {\n provider: string;\n model: string;\n durationMs: number;\n outcome: 'success' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const baseAttrs = {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.outcome': opts.outcome,\n };\n inferenceCallsCounter().add(1, baseAttrs);\n inferenceDurationHistogram().record(opts.durationMs, baseAttrs);\n if (opts.inputTokens != null && opts.inputTokens > 0) {\n inferenceTokensCounter().add(opts.inputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'input',\n });\n }\n if (opts.outputTokens != null && opts.outputTokens > 0) {\n inferenceTokensCounter().add(opts.outputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'output',\n });\n }\n}\n\nlet _detectionCallCounter: Counter | undefined;\nlet _detectionDurationHistogram: Histogram | undefined;\nlet _detectionItemsHistogram: Histogram | undefined;\nlet _detectionTokensHistogram: Histogram | undefined;\n\nfunction detectionCallCounter(): Counter {\n if (!_detectionCallCounter) {\n _detectionCallCounter = meter().createCounter('semiont.detection.calls', {\n description: 'Detection model calls, labeled by motivation, outcome, subdivision depth and whether this was the floor re-roll.',\n });\n }\n return _detectionCallCounter;\n}\n\nfunction detectionDurationHistogram(): Histogram {\n if (!_detectionDurationHistogram) {\n _detectionDurationHistogram = meter().createHistogram('semiont.detection.call.duration', {\n description: 'Wall time of one detection model call, including the attempts that failed and were retried smaller.',\n unit: 'ms',\n });\n }\n return _detectionDurationHistogram;\n}\n\nfunction detectionItemsHistogram(): Histogram {\n if (!_detectionItemsHistogram) {\n _detectionItemsHistogram = meter().createHistogram('semiont.detection.call.items', {\n description: 'Annotations returned by one detection call. Against the input size on the same record, this is yield.',\n });\n }\n return _detectionItemsHistogram;\n}\n\nfunction detectionTokensHistogram(): Histogram {\n if (!_detectionTokensHistogram) {\n _detectionTokensHistogram = meter().createHistogram('semiont.detection.call.tokens', {\n description: \"Provider-reported tokens for one detection call, by direction. Deliberately separate from semiont.inference.tokens: that series is the authoritative total but carries no subdivision depth, and 'what does a depth-2 call cost' is the question every sizing decision asks.\",\n });\n }\n return _detectionTokensHistogram;\n}\n\n/**\n * Record one detection model call (DETECTION-QUALITY-THROUGHPUT P1).\n *\n * The adapters already record provider/model/duration/tokens for every\n * inference call. What they cannot know is the detection shape around it:\n * which motivation asked, how big the piece was, how many annotations came\n * back, how deep subdivision had descended, and whether this was the floor\n * re-roll. Those are the facts that distinguish a healthy call from a\n * expensive descent, and without them a slow detection run is one\n * undifferentiated number.\n *\n * FAILED attempts are recorded too, and that is the point: the calls paid for\n * and thrown away during a descent are exactly the cost later phases exist to\n * avoid, so a record only of successes would hide the thing being optimized.\n *\n * Tokens are the PROVIDER's counts, passed through — never estimated. Absent\n * means the provider did not report them.\n */\nexport function recordDetectionCall(opts: {\n label: string;\n pieceChars: number;\n durationMs: number;\n items: number;\n depth: number;\n reroll: boolean;\n outcome: 'success' | 'truncated' | 'timeout' | 'collapsed' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const attrs = {\n 'detection.label': opts.label,\n 'detection.outcome': opts.outcome,\n 'detection.depth': opts.depth,\n 'detection.reroll': opts.reroll,\n };\n detectionCallCounter().add(1, attrs);\n detectionDurationHistogram().record(opts.durationMs, attrs);\n detectionItemsHistogram().record(opts.items, attrs);\n if (opts.inputTokens !== undefined) {\n detectionTokensHistogram().record(opts.inputTokens, { ...attrs, 'detection.direction': 'input' });\n }\n if (opts.outputTokens !== undefined) {\n detectionTokensHistogram().record(opts.outputTokens, { ...attrs, 'detection.direction': 'output' });\n }\n}\n\nlet _anchorOutcomeCounter: Counter | undefined;\nfunction anchorOutcomeCounter(): Counter {\n if (!_anchorOutcomeCounter) {\n _anchorOutcomeCounter = meter().createCounter('semiont.detection.anchors', {\n description: 'Every annotation anchoring, labeled by the method that resolved it. EVERY outcome is counted, not just the risky ones, because a bare count of degraded anchors has no denominator — the rate is the precision signal.',\n });\n }\n return _anchorOutcomeCounter;\n}\n\n/**\n * Record how one annotation got anchored (DETECTION-QUALITY-THROUGHPUT P5).\n *\n * The selector-vs-source check is already a WRITE-TIME INVARIANT — both\n * `buildTextAnnotation` and `buildPdfAnnotation` throw on a selector that does\n * not match its source — so mechanical correctness is guaranteed rather than\n * sampled, and auditing it would measure a constant.\n *\n * What is genuinely uncertain is which anchoring METHOD got there. An `exact`\n * the model quoted verbatim and that appears once is certain; one resolved by\n * `first-of-many` (several occurrences, no usable context) or `fuzzy-match`\n * picked a plausible occurrence and may have picked wrong. Those were visible\n * only as log warnings — countable by a human reading worker output, which is\n * how 47 of them went unreviewed. As a rate they are the precision number that\n * sits beside the yield numbers.\n */\nexport function recordAnchorOutcome(label: string, method: string): void {\n anchorOutcomeCounter().add(1, { 'detection.label': label, 'anchor.method': method });\n}\n\n// ── Re-exports from @opentelemetry/api ─────────────────────────────────\n\nexport { SpanKind, SpanStatusCode, type Attributes, type Span } from '@opentelemetry/api';\n","/**\n * Process-runtime readings (ARCHIVIST-STAYS-UP P4).\n *\n * Node-only, and deliberately a plain function rather than a gauge callback:\n * the numbers are the thing worth testing, and a callback registered inside\n * an SDK is awkward to assert against.\n *\n * **Why `heapLimit` is the field that matters.** The Archivist died at\n * ~1016 MB inside a 2048 MB container (`bugs/absent-archivist-wedges-browse.md`)\n * — not because it exhausted the container, but because it hit V8's OWN\n * default old-space ceiling, which is derived from visible memory and lands\n * well under it. `heapUsed` alone cannot express \"how close to death is\n * this\"; only the pair can. It is also how a configured\n * `--max-old-space-size` is verified to have taken effect, rather than\n * assumed from the fact that someone set an env var.\n */\n\nimport { getHeapStatistics } from 'node:v8';\n\nexport interface HeapStats {\n /** Live heap in use. */\n heapUsed: number;\n /** Heap V8 has currently reserved. */\n heapTotal: number;\n /** The ceiling V8 will die at — NOT the container's limit. */\n heapLimit: number;\n /** Resident set: everything, including buffers outside the JS heap. */\n rss: number;\n}\n\nexport function heapStats(): HeapStats {\n const mem = process.memoryUsage();\n return {\n heapUsed: mem.heapUsed,\n heapTotal: mem.heapTotal,\n heapLimit: getHeapStatistics().heap_size_limit,\n rss: mem.rss,\n };\n}\n","/**\n * Node SDK initialization. Call once at the process entry point\n * (gateway `index.ts`, `worker-main.ts`, `smelter-main.ts`).\n *\n * Configuration is via standard `OTEL_*` env vars:\n * - `OTEL_SERVICE_NAME` — service identity (e.g. `semiont-gateway`)\n * - `OTEL_EXPORTER_OTLP_ENDPOINT` — collector endpoint (HTTP)\n * - `OTEL_TRACES_SAMPLER` — sampler (default: `parentbased_always_on`)\n * - `OTEL_TRACES_SAMPLER_ARG` — sampler ratio (default: `1.0`)\n * - `OTEL_CONSOLE_EXPORTER=true` — dev-only: emit spans + metrics to stderr\n * - `OTEL_SDK_DISABLED=true` — skip initialization entirely\n *\n * **Off-by-default invariant**: with neither `OTEL_EXPORTER_OTLP_ENDPOINT`\n * nor `OTEL_CONSOLE_EXPORTER=true` set, this function is a no-op and the\n * `@opentelemetry/api` no-op tracer takes over. This avoids accidentally\n * flooding production stderr (and CloudWatch) when an operator deploys\n * without configuring an exporter.\n *\n * Implementation note: this module wires `BasicTracerProvider` and\n * `MeterProvider` (both from the stable `@opentelemetry/sdk-trace-base`\n * 2.x line) directly, plus `AsyncLocalStorageContextManager` for Node\n * async-context propagation. We deliberately avoid `@opentelemetry/sdk-node`\n * because its `0.x` experimental versions cross-depend on older 2.0.x\n * SDKs, forcing npm to nest duplicate copies of the stable packages and\n * blowing up bundles for every consumer.\n */\n\nimport { recordAbnormalTermination, registerProcessLifetimeMetrics } from './index.js';\nimport { monitorEventLoopDelay } from 'node:perf_hooks';\nimport { heapStats } from './runtime-stats';\nimport { context, metrics, propagation, trace } from '@opentelemetry/api';\nimport { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';\nimport { W3CTraceContextPropagator } from '@opentelemetry/core';\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport { resourceFromAttributes } from '@opentelemetry/resources';\nimport {\n ConsoleMetricExporter,\n MeterProvider,\n PeriodicExportingMetricReader,\n} from '@opentelemetry/sdk-metrics';\nimport {\n BasicTracerProvider,\n BatchSpanProcessor,\n ConsoleSpanExporter,\n} from '@opentelemetry/sdk-trace-base';\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from '@opentelemetry/semantic-conventions';\n\nexport interface NodeObservabilityConfig {\n /** Service identity (e.g. `semiont-gateway`). Overridden by `OTEL_SERVICE_NAME`. */\n serviceName: string;\n /** Service version. Defaults to `0.0.0` if omitted. */\n serviceVersion?: string;\n}\n\nlet tracerProviderInstance: BasicTracerProvider | undefined;\nlet meterProviderInstance: MeterProvider | undefined;\n\n/**\n * Default metric export interval. 30s mirrors the SDK default and gives\n * operators enough granularity without flooding the collector.\n */\nconst DEFAULT_METRIC_EXPORT_INTERVAL_MS = 30_000;\n\n/**\n * Which exporter metrics use. `console` wins even with an OTLP endpoint set —\n * the readout for a bare process or CI with no collector; traces unaffected.\n * Any other value (incl. unrecognised) leaves the endpoint to decide, so an\n * unknown value cannot silently disable metrics. Exported and pure so tests\n * assert the choice without mocking the exporter modules.\n */\nexport function metricsExporterKind(\n endpoint: string | undefined,\n requested: string | undefined,\n): 'otlp' | 'console' {\n if (requested === 'console') return 'console';\n return endpoint ? 'otlp' : 'console';\n}\n\n/**\n * Initialize OTel for the current process. Wires up both tracing and\n * metrics. Idempotent — calling twice is a no-op. Returns `true` if the\n * SDK started, `false` if disabled, no exporter is configured, or\n * already initialized.\n *\n * Metrics export at `OTEL_METRIC_EXPORT_INTERVAL` ms (default 30s) to\n * the same `OTEL_EXPORTER_OTLP_ENDPOINT` as traces.\n */\nexport function initObservabilityNode(config: NodeObservabilityConfig): boolean {\n if (tracerProviderInstance) return false;\n if (process.env['OTEL_SDK_DISABLED'] === 'true') return false;\n\n const endpoint = process.env['OTEL_EXPORTER_OTLP_ENDPOINT'];\n const useConsole = process.env['OTEL_CONSOLE_EXPORTER'] === 'true';\n\n // No exporter configured = no SDK init. The `@opentelemetry/api`\n // no-op tracer takes over; `withSpan` still runs `fn` but emits\n // nothing, `getActiveSpan()` returns a sentinel. Avoids flooding\n // production stderr when no collector endpoint is set.\n if (!endpoint && !useConsole) return false;\n\n const resource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: process.env['OTEL_SERVICE_NAME'] ?? config.serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? '0.0.0',\n });\n\n // Trace SDK\n const traceExporter = endpoint ? new OTLPTraceExporter() : new ConsoleSpanExporter();\n tracerProviderInstance = new BasicTracerProvider({\n resource,\n spanProcessors: [new BatchSpanProcessor(traceExporter)],\n });\n\n // Async-context propagation across `await` boundaries — equivalent\n // to what NodeSDK installs internally, but pinned to the stable 2.x\n // cohort.\n context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());\n trace.setGlobalTracerProvider(tracerProviderInstance);\n\n // W3C trace-context propagator. Without this, `propagation.inject`\n // and `propagation.extract` walk an empty propagator chain and\n // silently do nothing — which means traceparent never makes it onto\n // outgoing HTTP headers or SSE `_trace` payloads, and cross-service\n // traces stay disconnected. NodeSDK registers this for you; bare\n // BasicTracerProvider does not.\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n\n // Traces follow the endpoint; metrics additionally honour OTEL_METRICS_EXPORTER.\n const metricExporter =\n metricsExporterKind(endpoint, process.env['OTEL_METRICS_EXPORTER']) === 'console'\n ? new ConsoleMetricExporter()\n : new OTLPMetricExporter();\n const intervalRaw = process.env['OTEL_METRIC_EXPORT_INTERVAL'];\n const exportIntervalMillis = intervalRaw\n ? Number.parseInt(intervalRaw, 10)\n : DEFAULT_METRIC_EXPORT_INTERVAL_MS;\n\n meterProviderInstance = new MeterProvider({\n resource,\n readers: [\n new PeriodicExportingMetricReader({\n exporter: metricExporter,\n exportIntervalMillis: Number.isFinite(exportIntervalMillis) && exportIntervalMillis > 0\n ? exportIntervalMillis\n : DEFAULT_METRIC_EXPORT_INTERVAL_MS,\n }),\n ],\n });\n metrics.setGlobalMeterProvider(meterProviderInstance);\n\n // The cause-AGNOSTIC detector for a blocked process: it rises whether the\n // cause is a large JSON parse, GC, or a sync subprocess. Cheap — libuv keeps\n // the histogram; we read percentiles and reset once per export interval.\n const loopDelay = monitorEventLoopDelay({ resolution: 10 });\n loopDelay.enable();\n const runtimeMeter = meterProviderInstance.getMeter('semiont-runtime');\n runtimeMeter\n .createObservableGauge('semiont.runtime.event_loop.lag', {\n description: 'Event-loop delay percentiles over the last export interval. Time the process could not serve anything.',\n unit: 'ms',\n })\n .addCallback((observer) => {\n observer.observe(loopDelay.mean / 1e6, { 'lag.stat': 'mean' });\n observer.observe(loopDelay.percentile(99) / 1e6, { 'lag.stat': 'p99' });\n observer.observe(loopDelay.max / 1e6, { 'lag.stat': 'max' });\n loopDelay.reset();\n });\n\n // Heap (ARCHIVIST-STAYS-UP P4), on the SAME registration as lag rather than\n // a second mechanism — they are read together when diagnosing a process\n // that stopped answering, and splitting them would mean two things to wire.\n //\n // `limit` is the field that repays the effort: the Archivist died at\n // ~1016 MB inside a 2048 MB container because V8's own default ceiling sits\n // well under the container's. `used` alone cannot say how close to death a\n // process is, and `limit` is what a configured --max-old-space-size changes\n // — so this is also how that setting is VERIFIED rather than assumed.\n runtimeMeter\n .createObservableGauge('semiont.runtime.heap', {\n description: \"Process memory by kind. `limit` is V8's own ceiling, which is what the process dies at — not the container's allocation.\",\n unit: 'By',\n })\n .addCallback((observer) => {\n const s = heapStats();\n observer.observe(s.heapUsed, { 'heap.stat': 'used' });\n observer.observe(s.heapTotal, { 'heap.stat': 'total' });\n observer.observe(s.heapLimit, { 'heap.stat': 'limit' });\n observer.observe(s.rss, { 'heap.stat': 'rss' });\n });\n\n // Flush traces + metrics on shutdown so nothing is lost on SIGTERM/SIGINT.\n const shutdown = () => {\n Promise.all([\n tracerProviderInstance?.shutdown().catch(() => {}),\n meterProviderInstance?.shutdown().catch(() => {}),\n ]).finally(() => {\n tracerProviderInstance = undefined;\n meterProviderInstance = undefined;\n });\n };\n process.once('SIGTERM', shutdown);\n process.once('SIGINT', shutdown);\n\n registerProcessLifetimeMetrics();\n\n // A fatal path must leave a RECORD, not just a stack trace on stdout.\n //\n // Semantics are deliberately unchanged: Node treats an unhandled rejection\n // and an uncaught exception as fatal, and so do we. Registering a listener\n // would normally SUPPRESS that, which is why each handler re-raises after\n // recording — swallowing here would convert a loud crash into a silent\n // wedged process, which is strictly worse than the bug that motivated this.\n const fatal = (reason: string) => (err: unknown) => {\n const detail = err instanceof Error ? err.message : String(err);\n try {\n recordAbnormalTermination(reason, detail);\n } catch {\n // Telemetry must never be the reason a crash report is lost.\n }\n // Best-effort export before the process goes; bounded so a dead collector\n // cannot hold a dying process open.\n const flushed = Promise.all([\n tracerProviderInstance?.forceFlush().catch(() => {}),\n meterProviderInstance?.forceFlush().catch(() => {}),\n ]);\n const bounded = new Promise((resolve) => setTimeout(resolve, 2_000).unref?.());\n void Promise.race([flushed, bounded]).finally(() => {\n console.error(`[fatal] ${reason}: ${detail}`);\n process.exit(1);\n });\n };\n process.on('unhandledRejection', fatal('unhandledRejection'));\n process.on('uncaughtException', fatal('uncaughtException'));\n\n return true;\n}\n\n/** Force-flush + shutdown both SDKs. Test cleanup, not production. */\nexport async function shutdownObservabilityNode(): Promise<void> {\n await Promise.all([\n tracerProviderInstance?.shutdown(),\n meterProviderInstance?.shutdown(),\n ]);\n tracerProviderInstance = undefined;\n meterProviderInstance = undefined;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/runtime-stats.ts","../src/node.ts"],"names":["context","trace","propagation","metrics"],"mappings":"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","file":"node.js","sourcesContent":["/**\n * @semiont/observability — public API.\n *\n * Universal surface (works in Node + browser). For SDK *initialization*,\n * import from `@semiont/observability/node` or `/web` at the process entry\n * point. Everything else uses this module.\n *\n * Tier 2 of `.plans/OBSERVABILITY.md`. The public surface:\n *\n * - `withSpan(name, fn, options?)` — wrap an async block in a span;\n * `options` carries `kind` and `attrs`.\n * - `withActorSpan(actor, channel, fn, extraAttrs?)` — consumer-span\n * wrapper for bus-event handlers, with handler-duration recording.\n * - `injectTraceparent(payload)` / `extractTraceparent(payload)` — W3C\n * trace-context propagation across the SSE channel (the bus payload\n * gets a `_trace?: { traceparent }` sibling to `correlationId`).\n * - `withTraceparent(carrier, fn)` — run `fn` with the incoming\n * traceparent as the parent context.\n * - `getActiveTraceparent()` — read the active span's traceparent for\n * manual propagation (e.g. attaching to a fetch header or SSE field).\n * - `getLogTraceContext()` — active `trace_id` / `span_id` for log-line\n * correlation.\n * - Metric recorders (`recordBusEmit`, `recordHandlerDuration`,\n * `recordJobOutcome`, `recordSubscriberConnect` / `Disconnect`,\n * `recordInferenceUsage`) and gauge providers\n * (`registerJobQueueProvider`, `registerVectorIndexSizeProvider`).\n *\n * No-op when no exporter is configured: `@opentelemetry/api`'s default\n * tracer is a no-op, so `withSpan` is essentially free until\n * `initObservability*()` runs.\n */\n\nimport {\n context,\n isSpanContextValid,\n metrics,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n type Attributes,\n type Counter,\n type Histogram,\n type ObservableGauge,\n type Span,\n type UpDownCounter,\n} from '@opentelemetry/api';\nimport { setBusLogTraceIdProvider } from '@semiont/core';\n\n// Wire `busLog`'s trace-id provider once at module load. When an OTel\n// SDK is initialized (and a span is active when `busLog` fires), the\n// emitted line gets a `trace=<8hex>` suffix that correlates the\n// grep-timeline with the trace UI. No-op when no SDK is active.\nsetBusLogTraceIdProvider(() => {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return ctx.traceId;\n});\n\nconst TRACER_NAME = 'semiont';\n\nconst tracer = () => trace.getTracer(TRACER_NAME);\n\n// ── withSpan ───────────────────────────────────────────────────────────\n\n/**\n * Wrap an async block in a span. The span is started before `fn` runs and\n * ended after it resolves or rejects; exceptions are recorded and the span\n * status is set to ERROR. `kind` defaults to INTERNAL.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T> | T,\n options?: { kind?: SpanKind; attrs?: Attributes },\n): Promise<T> {\n const span = tracer().startSpan(name, {\n kind: options?.kind ?? SpanKind.INTERNAL,\n ...(options?.attrs ? { attributes: options.attrs } : {}),\n });\n try {\n return await context.with(trace.setSpan(context.active(), span), () => fn(span));\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: err instanceof Error ? err.message : String(err),\n });\n throw err;\n } finally {\n span.end();\n }\n}\n\n// ── Traceparent on bus payloads ────────────────────────────────────────\n\nconst TRACE_FIELD = '_trace';\n\n/**\n * Sibling of `correlationId` on bus payloads. Lives on the SSE event body\n * because SSE has no header trailer; the SDK strips it before delivering\n * the payload to subscribers. Additive — payloads without `_trace` parse\n * unchanged.\n */\nexport interface TraceCarrier {\n /** W3C `traceparent` header value (`00-<traceId>-<spanId>-<flags>`). */\n traceparent: string;\n /** W3C `tracestate` header value (vendor-specific extensions). */\n tracestate?: string;\n}\n\n/**\n * Read the active span's W3C traceparent (and tracestate). Returns\n * `undefined` if no span is active.\n */\nexport function getActiveTraceparent(): TraceCarrier | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n const traceparent = carrier['traceparent'];\n if (!traceparent) return undefined;\n return carrier['tracestate']\n ? { traceparent, tracestate: carrier['tracestate'] }\n : { traceparent };\n}\n\n/**\n * Attach the active span's trace-context to a payload object as\n * `_trace`. No-op when no span is active. Returns the same object\n * reference for chaining.\n */\nexport function injectTraceparent<T extends Record<string, unknown>>(payload: T): T {\n const carrier = getActiveTraceparent();\n if (carrier) {\n (payload as Record<string, unknown>)[TRACE_FIELD] = carrier;\n }\n return payload;\n}\n\n/**\n * Strip and return the `_trace` field from a payload. Mutates `payload`.\n * The field is internal plumbing and should not be visible to subscribers.\n */\nexport function extractTraceparent<T extends Record<string, unknown>>(\n payload: T,\n): TraceCarrier | undefined {\n const carrier = (payload as Record<string, unknown>)[TRACE_FIELD] as\n | TraceCarrier\n | undefined;\n if (carrier !== undefined) {\n delete (payload as Record<string, unknown>)[TRACE_FIELD];\n }\n if (!carrier || typeof carrier.traceparent !== 'string') return undefined;\n return carrier;\n}\n\n/**\n * Run `fn` with the given W3C traceparent set as the parent context.\n * Any spans started inside `fn` will be children of the incoming trace.\n * No-op if `carrier` is undefined.\n */\nexport function withTraceparent<T>(\n carrier: TraceCarrier | undefined,\n fn: () => T,\n): T {\n if (!carrier) return fn();\n const carrierObj: Record<string, string> = { traceparent: carrier.traceparent };\n if (carrier.tracestate) carrierObj['tracestate'] = carrier.tracestate;\n const ctx = propagation.extract(context.active(), carrierObj);\n return context.with(ctx, fn);\n}\n\n// ── Actor handler convenience ──────────────────────────────────────────\n\n/**\n * Wrap a bus-event handler in an `actor.<name>:<channel>` consumer span.\n * Used at every `eventBus.get(channel).subscribe(handler)` site inside\n * an actor (Stower, Gatherer, Matcher, Browser, Smelter), to attribute\n * each in-process subscriber's work to a span without scattering manual\n * `withSpan` calls across handler bodies.\n *\n * The span's parent is the active context at the time the handler\n * fires — which is the `bus.dispatch:<channel>` span on the gateway\n * (Subject.next runs synchronously inside the dispatch span), or the\n * `bus.emit:<channel>` span when an actor emits to itself.\n */\nexport async function withActorSpan<T>(\n actor: string,\n channel: string,\n fn: (span: Span) => Promise<T> | T,\n extraAttrs?: Attributes,\n): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(`actor.${actor}:${channel}`, fn, {\n kind: SpanKind.CONSUMER,\n attrs: {\n actor,\n 'bus.channel': channel,\n ...(extraAttrs ?? {}),\n },\n });\n } finally {\n recordHandlerDuration(actor, channel, performance.now() - start);\n }\n}\n\n// ── Log correlation ────────────────────────────────────────────────────\n\n/**\n * Read the active span's `trace_id` / `span_id` for log-line correlation.\n * Tier 3 of `.plans/OBSERVABILITY.md`. Each structured log line gets\n * tagged with these so a log query in CloudWatch / Loki / Datadog can\n * jump to the trace in Tempo / Jaeger / X-Ray.\n *\n * Returns `undefined` if no span is active, or if the active span's\n * context is invalid (uninitialized SDK, no-op tracer).\n */\nexport function getLogTraceContext(): { trace_id: string; span_id: string } | undefined {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return { trace_id: ctx.traceId, span_id: ctx.spanId };\n}\n\n// ── Metrics — Tier 3 ───────────────────────────────────────────────────\n\nconst METER_NAME = 'semiont';\n\nconst meter = () => metrics.getMeter(METER_NAME);\n\nlet _busEmitCounter: Counter | undefined;\nlet _replySuppressedCounter: Counter | undefined;\nlet _resumeGapCounter: Counter | undefined;\nlet _unanswerableCounter: Counter | undefined;\nlet _correlationRegistryGauge: ObservableGauge | undefined;\nlet _correlationRegistryProvider: (() => CorrelationRegistrySnapshot) | undefined;\nlet _handlerDurationHistogram: Histogram | undefined;\nlet _jobOutcomeCounter: Counter | undefined;\nlet _jobDurationHistogram: Histogram | undefined;\nlet _gatherDegradeCounter: Counter | undefined;\nlet _inferenceCallsCounter: Counter | undefined;\nlet _inferenceTokensCounter: Counter | undefined;\nlet _inferenceDurationHistogram: Histogram | undefined;\nlet _sseSubscribers: UpDownCounter | undefined;\nlet _jobQueueGauge: ObservableGauge | undefined;\nlet _jobQueueProvider: (() => Promise<JobQueueSnapshot> | JobQueueSnapshot) | undefined;\nlet _vectorIndexSizeGauge: ObservableGauge | undefined;\nlet _factPumpDepthGauge: ObservableGauge | undefined;\nlet _factPumpDepthProvider: (() => number) | undefined;\nlet _vectorIndexSizeProvider: (() => Promise<number> | number) | undefined;\n\n/** Snapshot of job-queue contents by status. Match `JobQueue.getStats()`. */\nexport interface JobQueueSnapshot {\n pending: number;\n running: number;\n complete: number;\n failed: number;\n cancelled: number;\n}\n\nfunction busEmitCounter(): Counter {\n if (!_busEmitCounter) {\n _busEmitCounter = meter().createCounter('semiont.bus.emit', {\n description: 'Bus emits by channel and scope',\n });\n }\n return _busEmitCounter;\n}\n\nfunction handlerDurationHistogram(): Histogram {\n if (!_handlerDurationHistogram) {\n _handlerDurationHistogram = meter().createHistogram('semiont.handler.duration', {\n description: 'In-process actor handler duration',\n unit: 'ms',\n });\n }\n return _handlerDurationHistogram;\n}\n\nfunction jobOutcomeCounter(): Counter {\n if (!_jobOutcomeCounter) {\n _jobOutcomeCounter = meter().createCounter('semiont.job.outcome', {\n description: 'Worker job completions by type and outcome',\n });\n }\n return _jobOutcomeCounter;\n}\n\nfunction jobDurationHistogram(): Histogram {\n if (!_jobDurationHistogram) {\n _jobDurationHistogram = meter().createHistogram('semiont.job.duration', {\n description: 'Worker job duration by type',\n unit: 'ms',\n });\n }\n return _jobDurationHistogram;\n}\n\nfunction inferenceCallsCounter(): Counter {\n if (!_inferenceCallsCounter) {\n _inferenceCallsCounter = meter().createCounter('semiont.inference.calls', {\n description: 'Inference API calls by provider, model, and outcome',\n });\n }\n return _inferenceCallsCounter;\n}\n\nfunction inferenceTokensCounter(): Counter {\n if (!_inferenceTokensCounter) {\n _inferenceTokensCounter = meter().createCounter('semiont.inference.tokens', {\n description: 'Inference token usage by provider, model, and direction',\n });\n }\n return _inferenceTokensCounter;\n}\n\nfunction inferenceDurationHistogram(): Histogram {\n if (!_inferenceDurationHistogram) {\n _inferenceDurationHistogram = meter().createHistogram('semiont.inference.duration', {\n description: 'Inference call duration by provider, model, and outcome',\n unit: 'ms',\n });\n }\n return _inferenceDurationHistogram;\n}\n\nfunction sseSubscribersCounter(): UpDownCounter {\n if (!_sseSubscribers) {\n _sseSubscribers = meter().createUpDownCounter('semiont.sse.subscribers', {\n description: 'Active SSE subscribers',\n });\n }\n return _sseSubscribers;\n}\n\nfunction replySuppressedCounter(): Counter {\n if (!_replySuppressedCounter) {\n _replySuppressedCounter = meter().createCounter('semiont.bus.reply.suppressed', {\n description: 'Correlated replies withheld from a non-owning subscriber',\n });\n }\n return _replySuppressedCounter;\n}\n\n/**\n * A correlated reply was withheld from a subscriber that does not own its\n * correlationId (CORRELATED-REPLY-ROUTING P5).\n *\n * Counts ONLY that case. A frame with no correlationId is a shape violation\n * (warned, not counted), and a cid nobody claimed is the structural in-process\n * case that fires constantly — counting either would drown the signal this\n * metric exists to show: the fan-out amplification the delivery filter removes.\n */\nexport function recordReplySuppressed(channel: string): void {\n replySuppressedCounter().add(1, { 'bus.channel': channel });\n}\n\nfunction resumeGapCounter(): Counter {\n if (!_resumeGapCounter) {\n _resumeGapCounter = meter().createCounter('semiont.bus.resume_gap', {\n description: 'SSE resumes that degraded to a gap because replay was unavailable',\n });\n }\n return _resumeGapCounter;\n}\n\n/**\n * An SSE resume could not be served and the client was told to fall back to\n * cache. This degradation is CORRECT by design and therefore silent — which is\n * exactly why it needs a number. A rising rate means clients are losing\n * history, and nothing else in the stack says so.\n */\nexport function recordResumeGap(reason: string): void {\n resumeGapCounter().add(1, { 'bus.resume_gap.reason': reason });\n}\n\nfunction unanswerableCounter(): Counter {\n if (!_unanswerableCounter) {\n _unanswerableCounter = meter().createCounter('semiont.bus.unanswerable', {\n description: 'Request emits that reached zero subscribers and were failed at the gateway',\n });\n }\n return _unanswerableCounter;\n}\n\n/**\n * A request-shaped emit reached no subscriber, so the gateway synthesized its\n * mapped failure (ARCHIVIST-STAYS-UP P3). By channel, this is the absence rate\n * of the service that answers it — the difference between \"it went down once\"\n * and \"it is flapping.\"\n */\nexport function recordUnanswerableRequest(channel: string): void {\n unanswerableCounter().add(1, { 'bus.channel': channel });\n}\n\n/** Claims held and reply payloads retained by a gateway's correlation registry. */\nexport interface CorrelationRegistrySnapshot {\n claims: number;\n retainedReplies: number;\n}\n\n/**\n * Register a callback returning the gateway's correlation-registry occupancy.\n *\n * COUNTS, not bytes: retention is count-budgeted today (byte-budgeting is a\n * known limit in CORRELATED-REPLY-ROUTING), so `retainedReplies` is a proxy for\n * heap, not a measure of it. It is still the closest observable to the question\n * two OOM investigations keep asking — a browse result can be 1-2 MB, and up to\n * REPLY_RETENTION_MAX of them are held at once.\n */\nexport function registerCorrelationRegistryProvider(\n provider: () => CorrelationRegistrySnapshot,\n): void {\n _correlationRegistryProvider = provider;\n if (!_correlationRegistryGauge) {\n _correlationRegistryGauge = meter().createObservableGauge('semiont.bus.correlation.size', {\n description: 'Correlation registry occupancy: live claims and retained reply payloads',\n });\n _correlationRegistryGauge.addCallback((observer) => {\n if (!_correlationRegistryProvider) return;\n const snap = _correlationRegistryProvider();\n observer.observe(snap.claims, { 'correlation.kind': 'claims' });\n observer.observe(snap.retainedReplies, { 'correlation.kind': 'retained_replies' });\n });\n }\n}\n\n/** Increment the bus-emit counter. Called at every transport `emit` site. */\nexport function recordBusEmit(channel: string, scope?: string): void {\n busEmitCounter().add(1, {\n 'bus.channel': channel,\n ...(scope ? { 'bus.scope': scope } : {}),\n });\n}\n\n/** Record an in-process actor handler's duration. */\nexport function recordHandlerDuration(actor: string, channel: string, durationMs: number): void {\n handlerDurationHistogram().record(durationMs, {\n actor,\n 'bus.channel': channel,\n });\n}\n\n/** Record a worker job's outcome and duration. */\nexport function recordJobOutcome(jobType: string, outcome: 'completed' | 'failed', durationMs: number): void {\n jobOutcomeCounter().add(1, { 'job.type': jobType, 'job.outcome': outcome });\n jobDurationHistogram().record(durationMs, { 'job.type': jobType, 'job.outcome': outcome });\n}\n\nlet _appendStageHistogram: Histogram | undefined;\nfunction appendStageHistogram(): Histogram {\n if (!_appendStageHistogram) {\n _appendStageHistogram = meter().createHistogram('semiont.record.append.duration', {\n description: 'Time spent in one stage of appending an event to the record, labeled by stage: persist (JSONL write + git), materialize (view rebuild), enrich, publish. The Archivist\\'s core write path.',\n unit: 'ms',\n });\n }\n return _appendStageHistogram;\n}\n\n/**\n * Record one stage of `EventStore.appendEvent` (ARCHIVIST-STAYS-UP P7).\n *\n * The append path is the one operation only the Archivist can perform, and it\n * was entirely dark: reads had `recordHandlerDuration` and the bus had its own\n * counters, while writes had nothing. Stage-labeled because the useful\n * question is never \"was the append slow\" but WHICH PART — and `materialize`\n * in particular does work proportional to a resource's annotation count, so it\n * degrades with history rather than with load.\n */\nexport function recordAppendStage(\n stage: 'persist' | 'materialize' | 'enrich' | 'publish',\n durationMs: number,\n): void {\n appendStageHistogram().record(durationMs, { 'record.stage': stage });\n}\n\nlet _gitCommandHistogram: Histogram | undefined;\nfunction gitCommandHistogram(): Histogram {\n if (!_gitCommandHistogram) {\n _gitCommandHistogram = meter().createHistogram('semiont.git.duration', {\n description: 'Wall time of a git subprocess. Async — this is latency, not event-loop blockage. Staging is deduped, so the `add` count is far below the number of appended events.',\n unit: 'ms',\n });\n }\n return _gitCommandHistogram;\n}\n\n/**\n * Record a git invocation. Read the `add` count against events appended: one\n * per event means deferred staging has stopped deduping.\n */\nexport function recordGitCommand(command: string, durationMs: number): void {\n gitCommandHistogram().record(durationMs, { 'git.command': command });\n}\n\nfunction gatherDegradeCounter(): Counter {\n if (!_gatherDegradeCounter) {\n _gatherDegradeCounter = meter().createCounter('semiont.gather.degraded', {\n description: 'Gathers that degraded because an eventually-consistent projection did not catch up within its read barrier (vectors: absent semanticContext; graph: projection-lag failure). Labeled by projection.',\n });\n }\n return _gatherDegradeCounter;\n}\n\n/**\n * Record a gather degraded by a projection read barrier: `'vectors'` — the\n * Smelter settle barrier timed out (semanticContext shipped absent);\n * `'graph'` — the Weaver applied barrier + poll floor exhausted (projection\n * lag surfaced as a distinct failure). Fleet-alertable counterpart of the\n * `[gather DEGRADED]` L4 breadcrumbs — a rising rate on either label means\n * that pipeline is not keeping up.\n */\nexport function recordGatherDegrade(projection: 'graph' | 'vectors'): void {\n gatherDegradeCounter().add(1, { projection });\n}\n\n/** Increment the SSE subscriber gauge — call on `/bus/subscribe` open. */\nexport function recordSubscriberConnect(): void {\n sseSubscribersCounter().add(1);\n}\n\n/** Decrement on disconnect. Pair with `recordSubscriberConnect`. */\nexport function recordSubscriberDisconnect(): void {\n sseSubscribersCounter().add(-1);\n}\n\n/**\n * Register a callback that returns the current job-queue snapshot.\n * Polled at the SDK's metric-collection interval. The single gauge\n * emits one observation per status (`pending`, `running`, …) tagged\n * with the `job.status` attribute. Idempotent — last registered\n * provider wins.\n */\nexport function registerJobQueueProvider(\n provider: () => Promise<JobQueueSnapshot> | JobQueueSnapshot,\n): void {\n _jobQueueProvider = provider;\n if (!_jobQueueGauge) {\n _jobQueueGauge = meter().createObservableGauge('semiont.job.queue.size', {\n description: 'Job queue size by status',\n });\n _jobQueueGauge.addCallback(async (observer) => {\n if (!_jobQueueProvider) return;\n const snap = await _jobQueueProvider();\n observer.observe(snap.pending, { 'job.status': 'pending' });\n observer.observe(snap.running, { 'job.status': 'running' });\n observer.observe(snap.complete, { 'job.status': 'complete' });\n observer.observe(snap.failed, { 'job.status': 'failed' });\n observer.observe(snap.cancelled, { 'job.status': 'cancelled' });\n });\n }\n}\n\n/**\n * Register a callback that returns the current vector-index size\n * (point count). Async to allow remote queries (Qdrant). Polled at\n * the metric-collection interval.\n */\n/**\n * Register the Archivist's fact-pump backlog — facts appended to the record\n * but not yet republished onto the bus.\n *\n * At rest this is zero. A value that climbs and does not come back means the\n * pump is outrunning its transport, which is the leading hypothesis for the\n * load-correlated heap growth in `bugs/absent-archivist-wedges-browse.md`\n * (ARCHIVIST-STAYS-UP P5). The backlog is deliberately unbounded today, so\n * this number is the only thing standing between \"the pump is behind\" and an\n * OOM whose cause is inferred from RSS after the fact.\n */\nexport function registerFactPumpDepthProvider(provider: () => number): void {\n _factPumpDepthProvider = provider;\n if (!_factPumpDepthGauge) {\n _factPumpDepthGauge = meter().createObservableGauge('semiont.archivist.fact_pump.depth', {\n description: 'Facts appended to the record but not yet published to the bus. Zero at rest; a rising floor means the pump is behind its transport.',\n });\n _factPumpDepthGauge.addCallback((observer) => {\n if (_factPumpDepthProvider) observer.observe(_factPumpDepthProvider());\n });\n }\n}\n\nlet _gitStagingFailureCounter: Counter | undefined;\n\n/**\n * A staging command that could not be run. Staging the index is a CONVENIENCE\n * — the event log is the system of record — so a failure here is degraded\n * service, never a reason to exit. But degraded must be VISIBLE: this counter\n * is what stops \"the index is quietly stale\" from being invisible.\n */\nexport function recordGitStagingFailure(reason: 'index-lock' | 'other'): void {\n if (!_gitStagingFailureCounter) {\n _gitStagingFailureCounter = meter().createCounter('semiont.git.staging.failures', {\n description: 'Staging commands abandoned after retries; the index may be stale',\n });\n }\n _gitStagingFailureCounter.add(1, { reason });\n}\n\n/**\n * Process lifetime telemetry (ARCHIVIST-GIT-STAGER-CRASH).\n *\n * A supervised process that dies and comes back is INVISIBLE in logs unless\n * someone greps for boot lines, and every request in flight when it died looks\n * to its caller like a hang. On 2026-09-08 that cost a multi-hour hunt through\n * search, qdrant, neo4j and the SSE transport for a crash loop that one metric\n * would have named immediately.\n *\n * `start_time` is the diagnostic, not uptime: a CHANGE in it is unambiguous\n * proof of a restart, and uptime is derivable from it.\n */\nconst PROCESS_START_TIME_SECONDS = Math.floor(Date.now() / 1000);\nlet _processStartTimeGauge: ObservableGauge | undefined;\nlet _restartCountGauge: ObservableGauge | undefined;\nlet _restartCountProvider: (() => Promise<number | undefined> | number | undefined) | undefined;\nlet _abnormalExitCounter: Counter | undefined;\n\n/** Register `semiont.process.start_time`. Called by `initObservability*`. */\nexport function registerProcessLifetimeMetrics(): void {\n if (_processStartTimeGauge) return;\n _processStartTimeGauge = meter().createObservableGauge('semiont.process.start_time', {\n description: 'Unix seconds at which this process started; a change means it restarted',\n unit: 's',\n });\n _processStartTimeGauge.addCallback((observer) => observer.observe(PROCESS_START_TIME_SECONDS));\n}\n\n/**\n * Supply a restart count. A supervisor is POSIX shell and cannot emit OTel, but\n * it keeps a durable event log on the state mount — so the supervised child\n * reads it and reports the count on the supervisor's behalf.\n *\n * **Return `undefined` for \"cannot tell\", never `0`.** A process running\n * without a supervisor (dev, or a direct `node` invocation) has no restart\n * history to report, and 0 is not that — 0 is the value a HEALTHY supervised\n * process reports, so an unsupervised one publishing 0 is indistinguishable\n * from a supervised one that has never crashed. `undefined` skips the\n * observation entirely and the series simply has no point, which is what\n * \"unknown\" looks like in a time series.\n */\nexport function registerRestartCountProvider(\n provider: () => Promise<number | undefined> | number | undefined,\n): void {\n _restartCountProvider = provider;\n if (!_restartCountGauge) {\n _restartCountGauge = meter().createObservableGauge('semiont.process.restarts', {\n description: 'Times the supervisor has restarted this service',\n });\n _restartCountGauge.addCallback(async (observer) => {\n if (!_restartCountProvider) return;\n const count = await _restartCountProvider();\n // Not `if (count)` — 0 is a real, meaningful reading.\n if (count !== undefined) observer.observe(count);\n });\n }\n}\n\n/**\n * Record that this process is dying abnormally, and mark the active span so a\n * trace shows a span that ENDED IN DEATH rather than one that simply never\n * ends. Never swallows: callers re-raise, so Node's own semantics are intact.\n */\nexport function recordAbnormalTermination(reason: string, detail?: string): void {\n if (!_abnormalExitCounter) {\n _abnormalExitCounter = meter().createCounter('semiont.process.abnormal_exit', {\n description: 'Process terminations that were not a clean shutdown',\n });\n }\n _abnormalExitCounter.add(1, { reason });\n const active = trace.getActiveSpan();\n if (active) {\n active.setStatus({ code: SpanStatusCode.ERROR, message: `${reason}: ${detail ?? ''}`.trim() });\n active.setAttribute('semiont.process.abnormal_exit', reason);\n active.end();\n }\n}\n\nexport function registerVectorIndexSizeProvider(\n provider: () => Promise<number> | number,\n): void {\n _vectorIndexSizeProvider = provider;\n if (!_vectorIndexSizeGauge) {\n _vectorIndexSizeGauge = meter().createObservableGauge('semiont.vector.index.size', {\n description: 'Vector store point count',\n });\n _vectorIndexSizeGauge.addCallback(async (observer) => {\n if (_vectorIndexSizeProvider) {\n const value = await _vectorIndexSizeProvider();\n observer.observe(value);\n }\n });\n }\n}\n\n/**\n * Record an inference call. Token counts are optional — providers that\n * don't expose them (or fail before generating) record only call count\n * and duration.\n */\nexport function recordInferenceUsage(opts: {\n provider: string;\n model: string;\n durationMs: number;\n outcome: 'success' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const baseAttrs = {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.outcome': opts.outcome,\n };\n inferenceCallsCounter().add(1, baseAttrs);\n inferenceDurationHistogram().record(opts.durationMs, baseAttrs);\n if (opts.inputTokens != null && opts.inputTokens > 0) {\n inferenceTokensCounter().add(opts.inputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'input',\n });\n }\n if (opts.outputTokens != null && opts.outputTokens > 0) {\n inferenceTokensCounter().add(opts.outputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'output',\n });\n }\n}\n\nlet _detectionCallCounter: Counter | undefined;\nlet _detectionDurationHistogram: Histogram | undefined;\nlet _detectionItemsHistogram: Histogram | undefined;\nlet _detectionTokensHistogram: Histogram | undefined;\n\nfunction detectionCallCounter(): Counter {\n if (!_detectionCallCounter) {\n _detectionCallCounter = meter().createCounter('semiont.detection.calls', {\n description: 'Detection model calls, labeled by motivation, outcome, subdivision depth and whether this was the floor re-roll.',\n });\n }\n return _detectionCallCounter;\n}\n\nfunction detectionDurationHistogram(): Histogram {\n if (!_detectionDurationHistogram) {\n _detectionDurationHistogram = meter().createHistogram('semiont.detection.call.duration', {\n description: 'Wall time of one detection model call, including the attempts that failed and were retried smaller.',\n unit: 'ms',\n });\n }\n return _detectionDurationHistogram;\n}\n\nfunction detectionItemsHistogram(): Histogram {\n if (!_detectionItemsHistogram) {\n _detectionItemsHistogram = meter().createHistogram('semiont.detection.call.items', {\n description: 'Annotations returned by one detection call. Against the input size on the same record, this is yield.',\n });\n }\n return _detectionItemsHistogram;\n}\n\nfunction detectionTokensHistogram(): Histogram {\n if (!_detectionTokensHistogram) {\n _detectionTokensHistogram = meter().createHistogram('semiont.detection.call.tokens', {\n description: \"Provider-reported tokens for one detection call, by direction. Deliberately separate from semiont.inference.tokens: that series is the authoritative total but carries no subdivision depth, and 'what does a depth-2 call cost' is the question every sizing decision asks.\",\n });\n }\n return _detectionTokensHistogram;\n}\n\n/**\n * Record one detection model call (DETECTION-QUALITY-THROUGHPUT P1).\n *\n * The adapters already record provider/model/duration/tokens for every\n * inference call. What they cannot know is the detection shape around it:\n * which motivation asked, how big the piece was, how many annotations came\n * back, how deep subdivision had descended, and whether this was the floor\n * re-roll. Those are the facts that distinguish a healthy call from a\n * expensive descent, and without them a slow detection run is one\n * undifferentiated number.\n *\n * FAILED attempts are recorded too, and that is the point: the calls paid for\n * and thrown away during a descent are exactly the cost later phases exist to\n * avoid, so a record only of successes would hide the thing being optimized.\n *\n * Tokens are the PROVIDER's counts, passed through — never estimated. Absent\n * means the provider did not report them.\n */\nexport function recordDetectionCall(opts: {\n label: string;\n pieceChars: number;\n durationMs: number;\n items: number;\n depth: number;\n reroll: boolean;\n outcome: 'success' | 'truncated' | 'timeout' | 'collapsed' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const attrs = {\n 'detection.label': opts.label,\n 'detection.outcome': opts.outcome,\n 'detection.depth': opts.depth,\n 'detection.reroll': opts.reroll,\n };\n detectionCallCounter().add(1, attrs);\n detectionDurationHistogram().record(opts.durationMs, attrs);\n detectionItemsHistogram().record(opts.items, attrs);\n if (opts.inputTokens !== undefined) {\n detectionTokensHistogram().record(opts.inputTokens, { ...attrs, 'detection.direction': 'input' });\n }\n if (opts.outputTokens !== undefined) {\n detectionTokensHistogram().record(opts.outputTokens, { ...attrs, 'detection.direction': 'output' });\n }\n}\n\nlet _anchorOutcomeCounter: Counter | undefined;\nfunction anchorOutcomeCounter(): Counter {\n if (!_anchorOutcomeCounter) {\n _anchorOutcomeCounter = meter().createCounter('semiont.detection.anchors', {\n description: 'Every annotation anchoring, labeled by the method that resolved it. EVERY outcome is counted, not just the risky ones, because a bare count of degraded anchors has no denominator — the rate is the precision signal.',\n });\n }\n return _anchorOutcomeCounter;\n}\n\n/**\n * Record how one annotation got anchored (DETECTION-QUALITY-THROUGHPUT P5).\n *\n * The selector-vs-source check is already a WRITE-TIME INVARIANT — both\n * `buildTextAnnotation` and `buildPdfAnnotation` throw on a selector that does\n * not match its source — so mechanical correctness is guaranteed rather than\n * sampled, and auditing it would measure a constant.\n *\n * What is genuinely uncertain is which anchoring METHOD got there. An `exact`\n * the model quoted verbatim and that appears once is certain; one resolved by\n * `first-of-many` (several occurrences, no usable context) or `fuzzy-match`\n * picked a plausible occurrence and may have picked wrong. Those were visible\n * only as log warnings — countable by a human reading worker output, which is\n * how 47 of them went unreviewed. As a rate they are the precision number that\n * sits beside the yield numbers.\n */\nexport function recordAnchorOutcome(label: string, method: string): void {\n anchorOutcomeCounter().add(1, { 'detection.label': label, 'anchor.method': method });\n}\n\n// ── Re-exports from @opentelemetry/api ─────────────────────────────────\n\nexport { SpanKind, SpanStatusCode, type Attributes, type Span } from '@opentelemetry/api';\n","/**\n * Process-runtime readings (ARCHIVIST-STAYS-UP P4).\n *\n * Node-only, and deliberately a plain function rather than a gauge callback:\n * the numbers are the thing worth testing, and a callback registered inside\n * an SDK is awkward to assert against.\n *\n * **Why `heapLimit` is the field that matters.** The Archivist died at\n * ~1016 MB inside a 2048 MB container (`bugs/absent-archivist-wedges-browse.md`)\n * — not because it exhausted the container, but because it hit V8's OWN\n * default old-space ceiling, which is derived from visible memory and lands\n * well under it. `heapUsed` alone cannot express \"how close to death is\n * this\"; only the pair can. It is also how a configured\n * `--max-old-space-size` is verified to have taken effect, rather than\n * assumed from the fact that someone set an env var.\n */\n\nimport { getHeapStatistics } from 'node:v8';\n\nexport interface HeapStats {\n /** Live heap in use. */\n heapUsed: number;\n /** Heap V8 has currently reserved. */\n heapTotal: number;\n /** The ceiling V8 will die at — NOT the container's limit. */\n heapLimit: number;\n /** Resident set: everything, including buffers outside the JS heap. */\n rss: number;\n}\n\nexport function heapStats(): HeapStats {\n const mem = process.memoryUsage();\n return {\n heapUsed: mem.heapUsed,\n heapTotal: mem.heapTotal,\n heapLimit: getHeapStatistics().heap_size_limit,\n rss: mem.rss,\n };\n}\n","/**\n * Node SDK initialization. Call once at the process entry point\n * (gateway `index.ts`, `worker-main.ts`, `smelter-main.ts`).\n *\n * Configuration is via standard `OTEL_*` env vars:\n * - `OTEL_SERVICE_NAME` — service identity (e.g. `semiont-gateway`)\n * - `OTEL_EXPORTER_OTLP_ENDPOINT` — collector endpoint (HTTP)\n * - `OTEL_TRACES_SAMPLER` — sampler (default: `parentbased_always_on`)\n * - `OTEL_TRACES_SAMPLER_ARG` — sampler ratio (default: `1.0`)\n * - `OTEL_CONSOLE_EXPORTER=true` — dev-only: emit spans + metrics to stderr\n * - `OTEL_SDK_DISABLED=true` — skip initialization entirely\n *\n * **Off-by-default invariant**: with neither `OTEL_EXPORTER_OTLP_ENDPOINT`\n * nor `OTEL_CONSOLE_EXPORTER=true` set, this function is a no-op and the\n * `@opentelemetry/api` no-op tracer takes over. This avoids accidentally\n * flooding production stderr (and CloudWatch) when an operator deploys\n * without configuring an exporter.\n *\n * Implementation note: this module wires `BasicTracerProvider` and\n * `MeterProvider` (both from the stable `@opentelemetry/sdk-trace-base`\n * 2.x line) directly, plus `AsyncLocalStorageContextManager` for Node\n * async-context propagation. We deliberately avoid `@opentelemetry/sdk-node`\n * because its `0.x` experimental versions cross-depend on older 2.0.x\n * SDKs, forcing npm to nest duplicate copies of the stable packages and\n * blowing up bundles for every consumer.\n */\n\nimport { recordAbnormalTermination, registerProcessLifetimeMetrics, registerRestartCountProvider } from './index.js';\nimport { monitorEventLoopDelay } from 'node:perf_hooks';\nimport { heapStats } from './runtime-stats';\nimport { context, metrics, propagation, trace } from '@opentelemetry/api';\nimport { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';\nimport { W3CTraceContextPropagator } from '@opentelemetry/core';\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport { resourceFromAttributes } from '@opentelemetry/resources';\nimport {\n ConsoleMetricExporter,\n MeterProvider,\n PeriodicExportingMetricReader,\n} from '@opentelemetry/sdk-metrics';\nimport {\n BasicTracerProvider,\n BatchSpanProcessor,\n ConsoleSpanExporter,\n} from '@opentelemetry/sdk-trace-base';\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from '@opentelemetry/semantic-conventions';\n\nexport interface NodeObservabilityConfig {\n /** Service identity (e.g. `semiont-gateway`). Overridden by `OTEL_SERVICE_NAME`. */\n serviceName: string;\n /** Service version. Defaults to `0.0.0` if omitted. */\n serviceVersion?: string;\n}\n\nlet tracerProviderInstance: BasicTracerProvider | undefined;\nlet meterProviderInstance: MeterProvider | undefined;\n\n/**\n * Default metric export interval. 30s mirrors the SDK default and gives\n * operators enough granularity without flooding the collector.\n */\nconst DEFAULT_METRIC_EXPORT_INTERVAL_MS = 30_000;\n\n/**\n * Which exporter metrics use. `console` wins even with an OTLP endpoint set —\n * the readout for a bare process or CI with no collector; traces unaffected.\n * Any other value (incl. unrecognised) leaves the endpoint to decide, so an\n * unknown value cannot silently disable metrics. Exported and pure so tests\n * assert the choice without mocking the exporter modules.\n */\nexport function metricsExporterKind(\n endpoint: string | undefined,\n requested: string | undefined,\n): 'otlp' | 'console' {\n if (requested === 'console') return 'console';\n return endpoint ? 'otlp' : 'console';\n}\n\n/**\n * Initialize OTel for the current process. Wires up both tracing and\n * metrics. Idempotent — calling twice is a no-op. Returns `true` if the\n * SDK started, `false` if disabled, no exporter is configured, or\n * already initialized.\n *\n * Metrics export at `OTEL_METRIC_EXPORT_INTERVAL` ms (default 30s) to\n * the same `OTEL_EXPORTER_OTLP_ENDPOINT` as traces.\n */\nexport function initObservabilityNode(config: NodeObservabilityConfig): boolean {\n if (tracerProviderInstance) return false;\n if (process.env['OTEL_SDK_DISABLED'] === 'true') return false;\n\n const endpoint = process.env['OTEL_EXPORTER_OTLP_ENDPOINT'];\n const useConsole = process.env['OTEL_CONSOLE_EXPORTER'] === 'true';\n\n // No exporter configured = no SDK init. The `@opentelemetry/api`\n // no-op tracer takes over; `withSpan` still runs `fn` but emits\n // nothing, `getActiveSpan()` returns a sentinel. Avoids flooding\n // production stderr when no collector endpoint is set.\n if (!endpoint && !useConsole) return false;\n\n const resource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: process.env['OTEL_SERVICE_NAME'] ?? config.serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? '0.0.0',\n });\n\n // Trace SDK\n const traceExporter = endpoint ? new OTLPTraceExporter() : new ConsoleSpanExporter();\n tracerProviderInstance = new BasicTracerProvider({\n resource,\n spanProcessors: [new BatchSpanProcessor(traceExporter)],\n });\n\n // Async-context propagation across `await` boundaries — equivalent\n // to what NodeSDK installs internally, but pinned to the stable 2.x\n // cohort.\n context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());\n trace.setGlobalTracerProvider(tracerProviderInstance);\n\n // W3C trace-context propagator. Without this, `propagation.inject`\n // and `propagation.extract` walk an empty propagator chain and\n // silently do nothing — which means traceparent never makes it onto\n // outgoing HTTP headers or SSE `_trace` payloads, and cross-service\n // traces stay disconnected. NodeSDK registers this for you; bare\n // BasicTracerProvider does not.\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n\n // Traces follow the endpoint; metrics additionally honour OTEL_METRICS_EXPORTER.\n const metricExporter =\n metricsExporterKind(endpoint, process.env['OTEL_METRICS_EXPORTER']) === 'console'\n ? new ConsoleMetricExporter()\n : new OTLPMetricExporter();\n const intervalRaw = process.env['OTEL_METRIC_EXPORT_INTERVAL'];\n const exportIntervalMillis = intervalRaw\n ? Number.parseInt(intervalRaw, 10)\n : DEFAULT_METRIC_EXPORT_INTERVAL_MS;\n\n meterProviderInstance = new MeterProvider({\n resource,\n readers: [\n new PeriodicExportingMetricReader({\n exporter: metricExporter,\n exportIntervalMillis: Number.isFinite(exportIntervalMillis) && exportIntervalMillis > 0\n ? exportIntervalMillis\n : DEFAULT_METRIC_EXPORT_INTERVAL_MS,\n }),\n ],\n });\n metrics.setGlobalMeterProvider(meterProviderInstance);\n\n // The cause-AGNOSTIC detector for a blocked process: it rises whether the\n // cause is a large JSON parse, GC, or a sync subprocess. Cheap — libuv keeps\n // the histogram; we read percentiles and reset once per export interval.\n const loopDelay = monitorEventLoopDelay({ resolution: 10 });\n loopDelay.enable();\n const runtimeMeter = meterProviderInstance.getMeter('semiont-runtime');\n runtimeMeter\n .createObservableGauge('semiont.runtime.event_loop.lag', {\n description: 'Event-loop delay percentiles over the last export interval. Time the process could not serve anything.',\n unit: 'ms',\n })\n .addCallback((observer) => {\n observer.observe(loopDelay.mean / 1e6, { 'lag.stat': 'mean' });\n observer.observe(loopDelay.percentile(99) / 1e6, { 'lag.stat': 'p99' });\n observer.observe(loopDelay.max / 1e6, { 'lag.stat': 'max' });\n loopDelay.reset();\n });\n\n // Heap (ARCHIVIST-STAYS-UP P4), on the SAME registration as lag rather than\n // a second mechanism — they are read together when diagnosing a process\n // that stopped answering, and splitting them would mean two things to wire.\n //\n // `limit` is the field that repays the effort: the Archivist died at\n // ~1016 MB inside a 2048 MB container because V8's own default ceiling sits\n // well under the container's. `used` alone cannot say how close to death a\n // process is, and `limit` is what a configured --max-old-space-size changes\n // — so this is also how that setting is VERIFIED rather than assumed.\n runtimeMeter\n .createObservableGauge('semiont.runtime.heap', {\n description: \"Process memory by kind. `limit` is V8's own ceiling, which is what the process dies at — not the container's allocation.\",\n unit: 'By',\n })\n .addCallback((observer) => {\n const s = heapStats();\n observer.observe(s.heapUsed, { 'heap.stat': 'used' });\n observer.observe(s.heapTotal, { 'heap.stat': 'total' });\n observer.observe(s.heapLimit, { 'heap.stat': 'limit' });\n observer.observe(s.rss, { 'heap.stat': 'rss' });\n });\n\n // Flush traces + metrics on shutdown so nothing is lost on SIGTERM/SIGINT.\n const shutdown = () => {\n Promise.all([\n tracerProviderInstance?.shutdown().catch(() => {}),\n meterProviderInstance?.shutdown().catch(() => {}),\n ]).finally(() => {\n tracerProviderInstance = undefined;\n meterProviderInstance = undefined;\n });\n };\n process.once('SIGTERM', shutdown);\n process.once('SIGINT', shutdown);\n\n registerProcessLifetimeMetrics();\n\n // A fatal path must leave a RECORD, not just a stack trace on stdout.\n //\n // Semantics are deliberately unchanged: Node treats an unhandled rejection\n // and an uncaught exception as fatal, and so do we. Registering a listener\n // would normally SUPPRESS that, which is why each handler re-raises after\n // recording — swallowing here would convert a loud crash into a silent\n // wedged process, which is strictly worse than the bug that motivated this.\n const fatal = (reason: string) => (err: unknown) => {\n const detail = err instanceof Error ? err.message : String(err);\n try {\n recordAbnormalTermination(reason, detail);\n } catch {\n // Telemetry must never be the reason a crash report is lost.\n }\n // Best-effort export before the process goes; bounded so a dead collector\n // cannot hold a dying process open.\n const flushed = Promise.all([\n tracerProviderInstance?.forceFlush().catch(() => {}),\n meterProviderInstance?.forceFlush().catch(() => {}),\n ]);\n const bounded = new Promise((resolve) => setTimeout(resolve, 2_000).unref?.());\n void Promise.race([flushed, bounded]).finally(() => {\n console.error(`[fatal] ${reason}: ${detail}`);\n process.exit(1);\n });\n };\n process.on('unhandledRejection', fatal('unhandledRejection'));\n process.on('uncaughtException', fatal('uncaughtException'));\n\n return true;\n}\n\n/**\n * Report the supervisor's restart count for this service\n * (GATEWAY-SUPERVISION F3). Call once, after `initObservabilityNode`.\n *\n * `scripts/container/supervise.sh` is POSIX shell and cannot emit OTel, but it\n * writes one `starting <name>` line per life to a durable event log and\n * exports that log's path and the service name to the child. This reads them\n * back. A crash LOOP is otherwise invisible to everything but `container logs`.\n *\n * Both halves of the contract — the path and the marker — come from the\n * environment, so nothing here restates what the shell already decided. This\n * lives in `node.ts` rather than beside `registerRestartCountProvider` in\n * `index.ts` because it reads a file: `index.ts` imports no node builtins, and\n * `./web` depends on it staying that way.\n */\nexport function registerSupervisorRestartCount(): void {\n const events = process.env['SUPERVISE_EVENTS'];\n const name = process.env['SUPERVISE_NAME'];\n // Unsupervised (dev, or a direct `node dist/index.js`). Register no provider\n // at all, so the gauge is never created and the series simply does not exist\n // — as opposed to existing and reading 0, which is what a healthy supervised\n // process reports.\n if (!events || !name) return;\n registerRestartCountProvider(async () => {\n try {\n const { readFile } = await import('node:fs/promises');\n const log = await readFile(events, 'utf-8');\n // Lives, not restarts: the first start is life 1, so a healthy process\n // reports 0 and any non-zero value is a real restart.\n return Math.max(0, log.split('\\n').filter((l) => l.includes(`starting ${name}`)).length - 1);\n } catch {\n // The log went unreadable mid-life. Report nothing, never 0.\n return undefined;\n }\n });\n}\n\n/** Force-flush + shutdown both SDKs. Test cleanup, not production. */\nexport async function shutdownObservabilityNode(): Promise<void> {\n await Promise.all([\n tracerProviderInstance?.shutdown(),\n meterProviderInstance?.shutdown(),\n ]);\n tracerProviderInstance = undefined;\n meterProviderInstance = undefined;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/process-logger.ts"],"names":["trace"],"mappings":";;;;;AAqDA,wBAAA,CAAyB,MAAM;AAC7B,EAAA,MAAM,IAAA,GAAO,MAAM,aAAA,EAAc;AACjC,EAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,EAAA,MAAM,GAAA,GAAM,KAAK,WAAA,EAAY;AAC7B,EAAA,IAAI,CAAC,kBAAA,CAAmB,GAAG,CAAA,EAAG,OAAO,MAAA;AACrC,EAAA,OAAO,GAAA,CAAI,OAAA;AACb,CAAC,CAAA;AA+JM,SAAS,kBAAA,GAAwE;AACtF,EAAA,MAAM,IAAA,GAAO,MAAM,aAAA,EAAc;AACjC,EAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,EAAA,MAAM,GAAA,GAAM,KAAK,WAAA,EAAY;AAC7B,EAAA,IAAI,CAAC,kBAAA,CAAmB,GAAG,CAAA,EAAG,OAAO,MAAA;AACrC,EAAA,OAAO,EAAE,QAAA,EAAU,GAAA,CAAI,OAAA,EAAS,OAAA,EAAS,IAAI,MAAA,EAAO;AACtD;;;AC1MA,IAAM,kBAAA,GAAqB,OAAA,CAAQ,MAAA,CAAO,CAAC,IAAA,KAAS;AAClD,EAAA,MAAMA,SAAQ,kBAAA,EAAmB;AACjC,EAAA,IAAIA,MAAAA,EAAO;AACT,IAAA,IAAA,CAAK,WAAWA,MAAAA,CAAM,QAAA;AACtB,IAAA,IAAA,CAAK,UAAUA,MAAAA,CAAM,OAAA;AAAA,EACvB;AACA,EAAA,OAAO,IAAA;AACT,CAAC,CAAA,EAAE;AAEI,SAAS,oBAAoB,SAAA,EAA2B;AAC7D,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,SAAA,IAAa,MAAA;AACvC,EAAA,MAAM,SAAS,OAAA,CAAQ,GAAA,CAAI,UAAA,KAAe,QAAA,GACtC,QAAQ,MAAA,CAAO,OAAA;AAAA,IACb,QAAQ,MAAA,CAAO,SAAA,CAAU,EAAE,MAAA,EAAQ,uBAAuB,CAAA;AAAA,IAC1D,QAAQ,MAAA,CAAO,MAAA,CAAO,EAAE,KAAA,EAAO,MAAM,CAAA;AAAA,IACrC,kBAAA;AAAA,IACA,OAAA,CAAQ,MAAA,CAAO,MAAA,CAAO,CAAC,EAAE,KAAA,EAAO,GAAA,EAAK,OAAA,EAAS,SAAA,EAAW,GAAG,IAAA,EAAK,KAAM;AACrE,MAAA,MAAM,OAAA,GAAU,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,CAAE,MAAA,GAAS,CAAA,GAAI,CAAA,CAAA,EAAI,IAAA,CAAK,SAAA,CAAU,IAAI,CAAC,CAAA,CAAA,GAAK,EAAA;AAC5E,MAAA,OAAO,CAAA,EAAG,SAAS,CAAA,EAAA,EAAK,GAAA,CAAI,WAAA,EAAa,CAAA,GAAA,EAAM,SAAS,CAAA,EAAA,EAAK,OAAO,CAAA,EAAG,OAAO,CAAA,CAAA;AAAA,IAChF,CAAC;AAAA,GACH,GACA,QAAQ,MAAA,CAAO,OAAA;AAAA,IACb,OAAA,CAAQ,OAAO,SAAA,EAAU;AAAA,IACzB,QAAQ,MAAA,CAAO,MAAA,CAAO,EAAE,KAAA,EAAO,MAAM,CAAA;AAAA,IACrC,kBAAA;AAAA,IACA,OAAA,CAAQ,OAAO,IAAA;AAAK,GACtB;AAEJ,EAAA,MAAM,MAAA,GAAS,QAAQ,YAAA,CAAa;AAAA,IAClC,KAAA;AAAA,IACA,WAAA,EAAa,EAAE,SAAA,EAAU;AAAA,IACzB,MAAA;AAAA,IACA,YAAY,CAAC,IAAI,OAAA,CAAQ,UAAA,CAAW,SAAS;AAAA,GAC9C,CAAA;AAED,EAAA,OAAO,MAAA;AACT","file":"process-logger.js","sourcesContent":["/**\n * @semiont/observability — public API.\n *\n * Universal surface (works in Node + browser). For SDK *initialization*,\n * import from `@semiont/observability/node` or `/web` at the process entry\n * point. Everything else uses this module.\n *\n * Tier 2 of `.plans/OBSERVABILITY.md`. The public surface:\n *\n * - `withSpan(name, fn, options?)` — wrap an async block in a span;\n * `options` carries `kind` and `attrs`.\n * - `withActorSpan(actor, channel, fn, extraAttrs?)` — consumer-span\n * wrapper for bus-event handlers, with handler-duration recording.\n * - `injectTraceparent(payload)` / `extractTraceparent(payload)` — W3C\n * trace-context propagation across the SSE channel (the bus payload\n * gets a `_trace?: { traceparent }` sibling to `correlationId`).\n * - `withTraceparent(carrier, fn)` — run `fn` with the incoming\n * traceparent as the parent context.\n * - `getActiveTraceparent()` — read the active span's traceparent for\n * manual propagation (e.g. attaching to a fetch header or SSE field).\n * - `getLogTraceContext()` — active `trace_id` / `span_id` for log-line\n * correlation.\n * - Metric recorders (`recordBusEmit`, `recordHandlerDuration`,\n * `recordJobOutcome`, `recordSubscriberConnect` / `Disconnect`,\n * `recordInferenceUsage`) and gauge providers\n * (`registerJobQueueProvider`, `registerVectorIndexSizeProvider`).\n *\n * No-op when no exporter is configured: `@opentelemetry/api`'s default\n * tracer is a no-op, so `withSpan` is essentially free until\n * `initObservability*()` runs.\n */\n\nimport {\n context,\n isSpanContextValid,\n metrics,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n type Attributes,\n type Counter,\n type Histogram,\n type ObservableGauge,\n type Span,\n type UpDownCounter,\n} from '@opentelemetry/api';\nimport { setBusLogTraceIdProvider } from '@semiont/core';\n\n// Wire `busLog`'s trace-id provider once at module load. When an OTel\n// SDK is initialized (and a span is active when `busLog` fires), the\n// emitted line gets a `trace=<8hex>` suffix that correlates the\n// grep-timeline with the trace UI. No-op when no SDK is active.\nsetBusLogTraceIdProvider(() => {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return ctx.traceId;\n});\n\nconst TRACER_NAME = 'semiont';\n\nconst tracer = () => trace.getTracer(TRACER_NAME);\n\n// ── withSpan ───────────────────────────────────────────────────────────\n\n/**\n * Wrap an async block in a span. The span is started before `fn` runs and\n * ended after it resolves or rejects; exceptions are recorded and the span\n * status is set to ERROR. `kind` defaults to INTERNAL.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T> | T,\n options?: { kind?: SpanKind; attrs?: Attributes },\n): Promise<T> {\n const span = tracer().startSpan(name, {\n kind: options?.kind ?? SpanKind.INTERNAL,\n ...(options?.attrs ? { attributes: options.attrs } : {}),\n });\n try {\n return await context.with(trace.setSpan(context.active(), span), () => fn(span));\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: err instanceof Error ? err.message : String(err),\n });\n throw err;\n } finally {\n span.end();\n }\n}\n\n// ── Traceparent on bus payloads ────────────────────────────────────────\n\nconst TRACE_FIELD = '_trace';\n\n/**\n * Sibling of `correlationId` on bus payloads. Lives on the SSE event body\n * because SSE has no header trailer; the SDK strips it before delivering\n * the payload to subscribers. Additive — payloads without `_trace` parse\n * unchanged.\n */\nexport interface TraceCarrier {\n /** W3C `traceparent` header value (`00-<traceId>-<spanId>-<flags>`). */\n traceparent: string;\n /** W3C `tracestate` header value (vendor-specific extensions). */\n tracestate?: string;\n}\n\n/**\n * Read the active span's W3C traceparent (and tracestate). Returns\n * `undefined` if no span is active.\n */\nexport function getActiveTraceparent(): TraceCarrier | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n const traceparent = carrier['traceparent'];\n if (!traceparent) return undefined;\n return carrier['tracestate']\n ? { traceparent, tracestate: carrier['tracestate'] }\n : { traceparent };\n}\n\n/**\n * Attach the active span's trace-context to a payload object as\n * `_trace`. No-op when no span is active. Returns the same object\n * reference for chaining.\n */\nexport function injectTraceparent<T extends Record<string, unknown>>(payload: T): T {\n const carrier = getActiveTraceparent();\n if (carrier) {\n (payload as Record<string, unknown>)[TRACE_FIELD] = carrier;\n }\n return payload;\n}\n\n/**\n * Strip and return the `_trace` field from a payload. Mutates `payload`.\n * The field is internal plumbing and should not be visible to subscribers.\n */\nexport function extractTraceparent<T extends Record<string, unknown>>(\n payload: T,\n): TraceCarrier | undefined {\n const carrier = (payload as Record<string, unknown>)[TRACE_FIELD] as\n | TraceCarrier\n | undefined;\n if (carrier !== undefined) {\n delete (payload as Record<string, unknown>)[TRACE_FIELD];\n }\n if (!carrier || typeof carrier.traceparent !== 'string') return undefined;\n return carrier;\n}\n\n/**\n * Run `fn` with the given W3C traceparent set as the parent context.\n * Any spans started inside `fn` will be children of the incoming trace.\n * No-op if `carrier` is undefined.\n */\nexport function withTraceparent<T>(\n carrier: TraceCarrier | undefined,\n fn: () => T,\n): T {\n if (!carrier) return fn();\n const carrierObj: Record<string, string> = { traceparent: carrier.traceparent };\n if (carrier.tracestate) carrierObj['tracestate'] = carrier.tracestate;\n const ctx = propagation.extract(context.active(), carrierObj);\n return context.with(ctx, fn);\n}\n\n// ── Actor handler convenience ──────────────────────────────────────────\n\n/**\n * Wrap a bus-event handler in an `actor.<name>:<channel>` consumer span.\n * Used at every `eventBus.get(channel).subscribe(handler)` site inside\n * an actor (Stower, Gatherer, Matcher, Browser, Smelter), to attribute\n * each in-process subscriber's work to a span without scattering manual\n * `withSpan` calls across handler bodies.\n *\n * The span's parent is the active context at the time the handler\n * fires — which is the `bus.dispatch:<channel>` span on the gateway\n * (Subject.next runs synchronously inside the dispatch span), or the\n * `bus.emit:<channel>` span when an actor emits to itself.\n */\nexport async function withActorSpan<T>(\n actor: string,\n channel: string,\n fn: (span: Span) => Promise<T> | T,\n extraAttrs?: Attributes,\n): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(`actor.${actor}:${channel}`, fn, {\n kind: SpanKind.CONSUMER,\n attrs: {\n actor,\n 'bus.channel': channel,\n ...(extraAttrs ?? {}),\n },\n });\n } finally {\n recordHandlerDuration(actor, channel, performance.now() - start);\n }\n}\n\n// ── Log correlation ────────────────────────────────────────────────────\n\n/**\n * Read the active span's `trace_id` / `span_id` for log-line correlation.\n * Tier 3 of `.plans/OBSERVABILITY.md`. Each structured log line gets\n * tagged with these so a log query in CloudWatch / Loki / Datadog can\n * jump to the trace in Tempo / Jaeger / X-Ray.\n *\n * Returns `undefined` if no span is active, or if the active span's\n * context is invalid (uninitialized SDK, no-op tracer).\n */\nexport function getLogTraceContext(): { trace_id: string; span_id: string } | undefined {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return { trace_id: ctx.traceId, span_id: ctx.spanId };\n}\n\n// ── Metrics — Tier 3 ───────────────────────────────────────────────────\n\nconst METER_NAME = 'semiont';\n\nconst meter = () => metrics.getMeter(METER_NAME);\n\nlet _busEmitCounter: Counter | undefined;\nlet _replySuppressedCounter: Counter | undefined;\nlet _resumeGapCounter: Counter | undefined;\nlet _unanswerableCounter: Counter | undefined;\nlet _correlationRegistryGauge: ObservableGauge | undefined;\nlet _correlationRegistryProvider: (() => CorrelationRegistrySnapshot) | undefined;\nlet _handlerDurationHistogram: Histogram | undefined;\nlet _jobOutcomeCounter: Counter | undefined;\nlet _jobDurationHistogram: Histogram | undefined;\nlet _gatherDegradeCounter: Counter | undefined;\nlet _inferenceCallsCounter: Counter | undefined;\nlet _inferenceTokensCounter: Counter | undefined;\nlet _inferenceDurationHistogram: Histogram | undefined;\nlet _sseSubscribers: UpDownCounter | undefined;\nlet _jobQueueGauge: ObservableGauge | undefined;\nlet _jobQueueProvider: (() => Promise<JobQueueSnapshot> | JobQueueSnapshot) | undefined;\nlet _vectorIndexSizeGauge: ObservableGauge | undefined;\nlet _factPumpDepthGauge: ObservableGauge | undefined;\nlet _factPumpDepthProvider: (() => number) | undefined;\nlet _vectorIndexSizeProvider: (() => Promise<number> | number) | undefined;\n\n/** Snapshot of job-queue contents by status. Match `JobQueue.getStats()`. */\nexport interface JobQueueSnapshot {\n pending: number;\n running: number;\n complete: number;\n failed: number;\n cancelled: number;\n}\n\nfunction busEmitCounter(): Counter {\n if (!_busEmitCounter) {\n _busEmitCounter = meter().createCounter('semiont.bus.emit', {\n description: 'Bus emits by channel and scope',\n });\n }\n return _busEmitCounter;\n}\n\nfunction handlerDurationHistogram(): Histogram {\n if (!_handlerDurationHistogram) {\n _handlerDurationHistogram = meter().createHistogram('semiont.handler.duration', {\n description: 'In-process actor handler duration',\n unit: 'ms',\n });\n }\n return _handlerDurationHistogram;\n}\n\nfunction jobOutcomeCounter(): Counter {\n if (!_jobOutcomeCounter) {\n _jobOutcomeCounter = meter().createCounter('semiont.job.outcome', {\n description: 'Worker job completions by type and outcome',\n });\n }\n return _jobOutcomeCounter;\n}\n\nfunction jobDurationHistogram(): Histogram {\n if (!_jobDurationHistogram) {\n _jobDurationHistogram = meter().createHistogram('semiont.job.duration', {\n description: 'Worker job duration by type',\n unit: 'ms',\n });\n }\n return _jobDurationHistogram;\n}\n\nfunction inferenceCallsCounter(): Counter {\n if (!_inferenceCallsCounter) {\n _inferenceCallsCounter = meter().createCounter('semiont.inference.calls', {\n description: 'Inference API calls by provider, model, and outcome',\n });\n }\n return _inferenceCallsCounter;\n}\n\nfunction inferenceTokensCounter(): Counter {\n if (!_inferenceTokensCounter) {\n _inferenceTokensCounter = meter().createCounter('semiont.inference.tokens', {\n description: 'Inference token usage by provider, model, and direction',\n });\n }\n return _inferenceTokensCounter;\n}\n\nfunction inferenceDurationHistogram(): Histogram {\n if (!_inferenceDurationHistogram) {\n _inferenceDurationHistogram = meter().createHistogram('semiont.inference.duration', {\n description: 'Inference call duration by provider, model, and outcome',\n unit: 'ms',\n });\n }\n return _inferenceDurationHistogram;\n}\n\nfunction sseSubscribersCounter(): UpDownCounter {\n if (!_sseSubscribers) {\n _sseSubscribers = meter().createUpDownCounter('semiont.sse.subscribers', {\n description: 'Active SSE subscribers',\n });\n }\n return _sseSubscribers;\n}\n\nfunction replySuppressedCounter(): Counter {\n if (!_replySuppressedCounter) {\n _replySuppressedCounter = meter().createCounter('semiont.bus.reply.suppressed', {\n description: 'Correlated replies withheld from a non-owning subscriber',\n });\n }\n return _replySuppressedCounter;\n}\n\n/**\n * A correlated reply was withheld from a subscriber that does not own its\n * correlationId (CORRELATED-REPLY-ROUTING P5).\n *\n * Counts ONLY that case. A frame with no correlationId is a shape violation\n * (warned, not counted), and a cid nobody claimed is the structural in-process\n * case that fires constantly — counting either would drown the signal this\n * metric exists to show: the fan-out amplification the delivery filter removes.\n */\nexport function recordReplySuppressed(channel: string): void {\n replySuppressedCounter().add(1, { 'bus.channel': channel });\n}\n\nfunction resumeGapCounter(): Counter {\n if (!_resumeGapCounter) {\n _resumeGapCounter = meter().createCounter('semiont.bus.resume_gap', {\n description: 'SSE resumes that degraded to a gap because replay was unavailable',\n });\n }\n return _resumeGapCounter;\n}\n\n/**\n * An SSE resume could not be served and the client was told to fall back to\n * cache. This degradation is CORRECT by design and therefore silent — which is\n * exactly why it needs a number. A rising rate means clients are losing\n * history, and nothing else in the stack says so.\n */\nexport function recordResumeGap(reason: string): void {\n resumeGapCounter().add(1, { 'bus.resume_gap.reason': reason });\n}\n\nfunction unanswerableCounter(): Counter {\n if (!_unanswerableCounter) {\n _unanswerableCounter = meter().createCounter('semiont.bus.unanswerable', {\n description: 'Request emits that reached zero subscribers and were failed at the gateway',\n });\n }\n return _unanswerableCounter;\n}\n\n/**\n * A request-shaped emit reached no subscriber, so the gateway synthesized its\n * mapped failure (ARCHIVIST-STAYS-UP P3). By channel, this is the absence rate\n * of the service that answers it — the difference between \"it went down once\"\n * and \"it is flapping.\"\n */\nexport function recordUnanswerableRequest(channel: string): void {\n unanswerableCounter().add(1, { 'bus.channel': channel });\n}\n\n/** Claims held and reply payloads retained by a gateway's correlation registry. */\nexport interface CorrelationRegistrySnapshot {\n claims: number;\n retainedReplies: number;\n}\n\n/**\n * Register a callback returning the gateway's correlation-registry occupancy.\n *\n * COUNTS, not bytes: retention is count-budgeted today (byte-budgeting is a\n * known limit in CORRELATED-REPLY-ROUTING), so `retainedReplies` is a proxy for\n * heap, not a measure of it. It is still the closest observable to the question\n * two OOM investigations keep asking — a browse result can be 1-2 MB, and up to\n * REPLY_RETENTION_MAX of them are held at once.\n */\nexport function registerCorrelationRegistryProvider(\n provider: () => CorrelationRegistrySnapshot,\n): void {\n _correlationRegistryProvider = provider;\n if (!_correlationRegistryGauge) {\n _correlationRegistryGauge = meter().createObservableGauge('semiont.bus.correlation.size', {\n description: 'Correlation registry occupancy: live claims and retained reply payloads',\n });\n _correlationRegistryGauge.addCallback((observer) => {\n if (!_correlationRegistryProvider) return;\n const snap = _correlationRegistryProvider();\n observer.observe(snap.claims, { 'correlation.kind': 'claims' });\n observer.observe(snap.retainedReplies, { 'correlation.kind': 'retained_replies' });\n });\n }\n}\n\n/** Increment the bus-emit counter. Called at every transport `emit` site. */\nexport function recordBusEmit(channel: string, scope?: string): void {\n busEmitCounter().add(1, {\n 'bus.channel': channel,\n ...(scope ? { 'bus.scope': scope } : {}),\n });\n}\n\n/** Record an in-process actor handler's duration. */\nexport function recordHandlerDuration(actor: string, channel: string, durationMs: number): void {\n handlerDurationHistogram().record(durationMs, {\n actor,\n 'bus.channel': channel,\n });\n}\n\n/** Record a worker job's outcome and duration. */\nexport function recordJobOutcome(jobType: string, outcome: 'completed' | 'failed', durationMs: number): void {\n jobOutcomeCounter().add(1, { 'job.type': jobType, 'job.outcome': outcome });\n jobDurationHistogram().record(durationMs, { 'job.type': jobType, 'job.outcome': outcome });\n}\n\nlet _appendStageHistogram: Histogram | undefined;\nfunction appendStageHistogram(): Histogram {\n if (!_appendStageHistogram) {\n _appendStageHistogram = meter().createHistogram('semiont.record.append.duration', {\n description: 'Time spent in one stage of appending an event to the record, labeled by stage: persist (JSONL write + git), materialize (view rebuild), enrich, publish. The Archivist\\'s core write path.',\n unit: 'ms',\n });\n }\n return _appendStageHistogram;\n}\n\n/**\n * Record one stage of `EventStore.appendEvent` (ARCHIVIST-STAYS-UP P7).\n *\n * The append path is the one operation only the Archivist can perform, and it\n * was entirely dark: reads had `recordHandlerDuration` and the bus had its own\n * counters, while writes had nothing. Stage-labeled because the useful\n * question is never \"was the append slow\" but WHICH PART — and `materialize`\n * in particular does work proportional to a resource's annotation count, so it\n * degrades with history rather than with load.\n */\nexport function recordAppendStage(\n stage: 'persist' | 'materialize' | 'enrich' | 'publish',\n durationMs: number,\n): void {\n appendStageHistogram().record(durationMs, { 'record.stage': stage });\n}\n\nlet _gitCommandHistogram: Histogram | undefined;\nfunction gitCommandHistogram(): Histogram {\n if (!_gitCommandHistogram) {\n _gitCommandHistogram = meter().createHistogram('semiont.git.duration', {\n description: 'Wall time of a git subprocess. Async — this is latency, not event-loop blockage. Staging is deduped, so the `add` count is far below the number of appended events.',\n unit: 'ms',\n });\n }\n return _gitCommandHistogram;\n}\n\n/**\n * Record a git invocation. Read the `add` count against events appended: one\n * per event means deferred staging has stopped deduping.\n */\nexport function recordGitCommand(command: string, durationMs: number): void {\n gitCommandHistogram().record(durationMs, { 'git.command': command });\n}\n\nfunction gatherDegradeCounter(): Counter {\n if (!_gatherDegradeCounter) {\n _gatherDegradeCounter = meter().createCounter('semiont.gather.degraded', {\n description: 'Gathers that degraded because an eventually-consistent projection did not catch up within its read barrier (vectors: absent semanticContext; graph: projection-lag failure). Labeled by projection.',\n });\n }\n return _gatherDegradeCounter;\n}\n\n/**\n * Record a gather degraded by a projection read barrier: `'vectors'` — the\n * Smelter settle barrier timed out (semanticContext shipped absent);\n * `'graph'` — the Weaver applied barrier + poll floor exhausted (projection\n * lag surfaced as a distinct failure). Fleet-alertable counterpart of the\n * `[gather DEGRADED]` L4 breadcrumbs — a rising rate on either label means\n * that pipeline is not keeping up.\n */\nexport function recordGatherDegrade(projection: 'graph' | 'vectors'): void {\n gatherDegradeCounter().add(1, { projection });\n}\n\n/** Increment the SSE subscriber gauge — call on `/bus/subscribe` open. */\nexport function recordSubscriberConnect(): void {\n sseSubscribersCounter().add(1);\n}\n\n/** Decrement on disconnect. Pair with `recordSubscriberConnect`. */\nexport function recordSubscriberDisconnect(): void {\n sseSubscribersCounter().add(-1);\n}\n\n/**\n * Register a callback that returns the current job-queue snapshot.\n * Polled at the SDK's metric-collection interval. The single gauge\n * emits one observation per status (`pending`, `running`, …) tagged\n * with the `job.status` attribute. Idempotent — last registered\n * provider wins.\n */\nexport function registerJobQueueProvider(\n provider: () => Promise<JobQueueSnapshot> | JobQueueSnapshot,\n): void {\n _jobQueueProvider = provider;\n if (!_jobQueueGauge) {\n _jobQueueGauge = meter().createObservableGauge('semiont.job.queue.size', {\n description: 'Job queue size by status',\n });\n _jobQueueGauge.addCallback(async (observer) => {\n if (!_jobQueueProvider) return;\n const snap = await _jobQueueProvider();\n observer.observe(snap.pending, { 'job.status': 'pending' });\n observer.observe(snap.running, { 'job.status': 'running' });\n observer.observe(snap.complete, { 'job.status': 'complete' });\n observer.observe(snap.failed, { 'job.status': 'failed' });\n observer.observe(snap.cancelled, { 'job.status': 'cancelled' });\n });\n }\n}\n\n/**\n * Register a callback that returns the current vector-index size\n * (point count). Async to allow remote queries (Qdrant). Polled at\n * the metric-collection interval.\n */\n/**\n * Register the Archivist's fact-pump backlog — facts appended to the record\n * but not yet republished onto the bus.\n *\n * At rest this is zero. A value that climbs and does not come back means the\n * pump is outrunning its transport, which is the leading hypothesis for the\n * load-correlated heap growth in `bugs/absent-archivist-wedges-browse.md`\n * (ARCHIVIST-STAYS-UP P5). The backlog is deliberately unbounded today, so\n * this number is the only thing standing between \"the pump is behind\" and an\n * OOM whose cause is inferred from RSS after the fact.\n */\nexport function registerFactPumpDepthProvider(provider: () => number): void {\n _factPumpDepthProvider = provider;\n if (!_factPumpDepthGauge) {\n _factPumpDepthGauge = meter().createObservableGauge('semiont.archivist.fact_pump.depth', {\n description: 'Facts appended to the record but not yet published to the bus. Zero at rest; a rising floor means the pump is behind its transport.',\n });\n _factPumpDepthGauge.addCallback((observer) => {\n if (_factPumpDepthProvider) observer.observe(_factPumpDepthProvider());\n });\n }\n}\n\nlet _gitStagingFailureCounter: Counter | undefined;\n\n/**\n * A staging command that could not be run. Staging the index is a CONVENIENCE\n * — the event log is the system of record — so a failure here is degraded\n * service, never a reason to exit. But degraded must be VISIBLE: this counter\n * is what stops \"the index is quietly stale\" from being invisible.\n */\nexport function recordGitStagingFailure(reason: 'index-lock' | 'other'): void {\n if (!_gitStagingFailureCounter) {\n _gitStagingFailureCounter = meter().createCounter('semiont.git.staging.failures', {\n description: 'Staging commands abandoned after retries; the index may be stale',\n });\n }\n _gitStagingFailureCounter.add(1, { reason });\n}\n\n/**\n * Process lifetime telemetry (ARCHIVIST-GIT-STAGER-CRASH).\n *\n * A supervised process that dies and comes back is INVISIBLE in logs unless\n * someone greps for boot lines, and every request in flight when it died looks\n * to its caller like a hang. On 2026-09-08 that cost a multi-hour hunt through\n * search, qdrant, neo4j and the SSE transport for a crash loop that one metric\n * would have named immediately.\n *\n * `start_time` is the diagnostic, not uptime: a CHANGE in it is unambiguous\n * proof of a restart, and uptime is derivable from it.\n */\nconst PROCESS_START_TIME_SECONDS = Math.floor(Date.now() / 1000);\nlet _processStartTimeGauge: ObservableGauge | undefined;\nlet _restartCountGauge: ObservableGauge | undefined;\nlet _restartCountProvider: (() => Promise<number> | number) | undefined;\nlet _abnormalExitCounter: Counter | undefined;\n\n/** Register `semiont.process.start_time`. Called by `initObservability*`. */\nexport function registerProcessLifetimeMetrics(): void {\n if (_processStartTimeGauge) return;\n _processStartTimeGauge = meter().createObservableGauge('semiont.process.start_time', {\n description: 'Unix seconds at which this process started; a change means it restarted',\n unit: 's',\n });\n _processStartTimeGauge.addCallback((observer) => observer.observe(PROCESS_START_TIME_SECONDS));\n}\n\n/**\n * Supply a restart count. The Archivist's supervisor is POSIX shell and cannot\n * emit OTel, but it already keeps a durable event log on the state mount — so\n * the supervised child reads it and reports the count on the supervisor's\n * behalf.\n */\nexport function registerRestartCountProvider(\n provider: () => Promise<number> | number,\n): void {\n _restartCountProvider = provider;\n if (!_restartCountGauge) {\n _restartCountGauge = meter().createObservableGauge('semiont.process.restarts', {\n description: 'Times the supervisor has restarted this service',\n });\n _restartCountGauge.addCallback(async (observer) => {\n if (_restartCountProvider) observer.observe(await _restartCountProvider());\n });\n }\n}\n\n/**\n * Record that this process is dying abnormally, and mark the active span so a\n * trace shows a span that ENDED IN DEATH rather than one that simply never\n * ends. Never swallows: callers re-raise, so Node's own semantics are intact.\n */\nexport function recordAbnormalTermination(reason: string, detail?: string): void {\n if (!_abnormalExitCounter) {\n _abnormalExitCounter = meter().createCounter('semiont.process.abnormal_exit', {\n description: 'Process terminations that were not a clean shutdown',\n });\n }\n _abnormalExitCounter.add(1, { reason });\n const active = trace.getActiveSpan();\n if (active) {\n active.setStatus({ code: SpanStatusCode.ERROR, message: `${reason}: ${detail ?? ''}`.trim() });\n active.setAttribute('semiont.process.abnormal_exit', reason);\n active.end();\n }\n}\n\nexport function registerVectorIndexSizeProvider(\n provider: () => Promise<number> | number,\n): void {\n _vectorIndexSizeProvider = provider;\n if (!_vectorIndexSizeGauge) {\n _vectorIndexSizeGauge = meter().createObservableGauge('semiont.vector.index.size', {\n description: 'Vector store point count',\n });\n _vectorIndexSizeGauge.addCallback(async (observer) => {\n if (_vectorIndexSizeProvider) {\n const value = await _vectorIndexSizeProvider();\n observer.observe(value);\n }\n });\n }\n}\n\n/**\n * Record an inference call. Token counts are optional — providers that\n * don't expose them (or fail before generating) record only call count\n * and duration.\n */\nexport function recordInferenceUsage(opts: {\n provider: string;\n model: string;\n durationMs: number;\n outcome: 'success' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const baseAttrs = {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.outcome': opts.outcome,\n };\n inferenceCallsCounter().add(1, baseAttrs);\n inferenceDurationHistogram().record(opts.durationMs, baseAttrs);\n if (opts.inputTokens != null && opts.inputTokens > 0) {\n inferenceTokensCounter().add(opts.inputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'input',\n });\n }\n if (opts.outputTokens != null && opts.outputTokens > 0) {\n inferenceTokensCounter().add(opts.outputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'output',\n });\n }\n}\n\nlet _detectionCallCounter: Counter | undefined;\nlet _detectionDurationHistogram: Histogram | undefined;\nlet _detectionItemsHistogram: Histogram | undefined;\nlet _detectionTokensHistogram: Histogram | undefined;\n\nfunction detectionCallCounter(): Counter {\n if (!_detectionCallCounter) {\n _detectionCallCounter = meter().createCounter('semiont.detection.calls', {\n description: 'Detection model calls, labeled by motivation, outcome, subdivision depth and whether this was the floor re-roll.',\n });\n }\n return _detectionCallCounter;\n}\n\nfunction detectionDurationHistogram(): Histogram {\n if (!_detectionDurationHistogram) {\n _detectionDurationHistogram = meter().createHistogram('semiont.detection.call.duration', {\n description: 'Wall time of one detection model call, including the attempts that failed and were retried smaller.',\n unit: 'ms',\n });\n }\n return _detectionDurationHistogram;\n}\n\nfunction detectionItemsHistogram(): Histogram {\n if (!_detectionItemsHistogram) {\n _detectionItemsHistogram = meter().createHistogram('semiont.detection.call.items', {\n description: 'Annotations returned by one detection call. Against the input size on the same record, this is yield.',\n });\n }\n return _detectionItemsHistogram;\n}\n\nfunction detectionTokensHistogram(): Histogram {\n if (!_detectionTokensHistogram) {\n _detectionTokensHistogram = meter().createHistogram('semiont.detection.call.tokens', {\n description: \"Provider-reported tokens for one detection call, by direction. Deliberately separate from semiont.inference.tokens: that series is the authoritative total but carries no subdivision depth, and 'what does a depth-2 call cost' is the question every sizing decision asks.\",\n });\n }\n return _detectionTokensHistogram;\n}\n\n/**\n * Record one detection model call (DETECTION-QUALITY-THROUGHPUT P1).\n *\n * The adapters already record provider/model/duration/tokens for every\n * inference call. What they cannot know is the detection shape around it:\n * which motivation asked, how big the piece was, how many annotations came\n * back, how deep subdivision had descended, and whether this was the floor\n * re-roll. Those are the facts that distinguish a healthy call from a\n * expensive descent, and without them a slow detection run is one\n * undifferentiated number.\n *\n * FAILED attempts are recorded too, and that is the point: the calls paid for\n * and thrown away during a descent are exactly the cost later phases exist to\n * avoid, so a record only of successes would hide the thing being optimized.\n *\n * Tokens are the PROVIDER's counts, passed through — never estimated. Absent\n * means the provider did not report them.\n */\nexport function recordDetectionCall(opts: {\n label: string;\n pieceChars: number;\n durationMs: number;\n items: number;\n depth: number;\n reroll: boolean;\n outcome: 'success' | 'truncated' | 'timeout' | 'collapsed' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const attrs = {\n 'detection.label': opts.label,\n 'detection.outcome': opts.outcome,\n 'detection.depth': opts.depth,\n 'detection.reroll': opts.reroll,\n };\n detectionCallCounter().add(1, attrs);\n detectionDurationHistogram().record(opts.durationMs, attrs);\n detectionItemsHistogram().record(opts.items, attrs);\n if (opts.inputTokens !== undefined) {\n detectionTokensHistogram().record(opts.inputTokens, { ...attrs, 'detection.direction': 'input' });\n }\n if (opts.outputTokens !== undefined) {\n detectionTokensHistogram().record(opts.outputTokens, { ...attrs, 'detection.direction': 'output' });\n }\n}\n\nlet _anchorOutcomeCounter: Counter | undefined;\nfunction anchorOutcomeCounter(): Counter {\n if (!_anchorOutcomeCounter) {\n _anchorOutcomeCounter = meter().createCounter('semiont.detection.anchors', {\n description: 'Every annotation anchoring, labeled by the method that resolved it. EVERY outcome is counted, not just the risky ones, because a bare count of degraded anchors has no denominator — the rate is the precision signal.',\n });\n }\n return _anchorOutcomeCounter;\n}\n\n/**\n * Record how one annotation got anchored (DETECTION-QUALITY-THROUGHPUT P5).\n *\n * The selector-vs-source check is already a WRITE-TIME INVARIANT — both\n * `buildTextAnnotation` and `buildPdfAnnotation` throw on a selector that does\n * not match its source — so mechanical correctness is guaranteed rather than\n * sampled, and auditing it would measure a constant.\n *\n * What is genuinely uncertain is which anchoring METHOD got there. An `exact`\n * the model quoted verbatim and that appears once is certain; one resolved by\n * `first-of-many` (several occurrences, no usable context) or `fuzzy-match`\n * picked a plausible occurrence and may have picked wrong. Those were visible\n * only as log warnings — countable by a human reading worker output, which is\n * how 47 of them went unreviewed. As a rate they are the precision number that\n * sits beside the yield numbers.\n */\nexport function recordAnchorOutcome(label: string, method: string): void {\n anchorOutcomeCounter().add(1, { 'detection.label': label, 'anchor.method': method });\n}\n\n// ── Re-exports from @opentelemetry/api ─────────────────────────────────\n\nexport { SpanKind, SpanStatusCode, type Attributes, type Span } from '@opentelemetry/api';\n","/**\n * Process-level structured logger for Node entry points.\n *\n * Used by long-lived Node processes (gateway, workers, smelter) that\n * want JSON-structured stdout with active-span trace correlation. The\n * `trace_id` / `span_id` fields are populated from the current OTel\n * span context via `getLogTraceContext` — this is the same Tier 3\n * correlation that lets a grep through stdout line up with the trace\n * UI without manual stitching.\n *\n * Reads `LOG_LEVEL` (default `info`) and `LOG_FORMAT` (`json` default,\n * `simple` for human-friendly dev output).\n *\n * Co-located with `getLogTraceContext` deliberately: this is the\n * only reasonably-shaped consumer of that helper, and putting them in\n * the same package keeps the trace-id wiring in one place.\n */\n\nimport winston from 'winston';\nimport type { Logger } from '@semiont/core';\nimport { getLogTraceContext } from './index.js';\n\nconst traceContextFormat = winston.format((info) => {\n const trace = getLogTraceContext();\n if (trace) {\n info.trace_id = trace.trace_id;\n info.span_id = trace.span_id;\n }\n return info;\n})();\n\nexport function createProcessLogger(component: string): Logger {\n const level = process.env.LOG_LEVEL ?? 'info';\n const format = process.env.LOG_FORMAT === 'simple'\n ? winston.format.combine(\n winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),\n winston.format.errors({ stack: true }),\n traceContextFormat,\n winston.format.printf(({ level: lvl, message, timestamp, ...meta }) => {\n const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';\n return `${timestamp} [${lvl.toUpperCase()}] [${component}] ${message}${metaStr}`;\n }),\n )\n : winston.format.combine(\n winston.format.timestamp(),\n winston.format.errors({ stack: true }),\n traceContextFormat,\n winston.format.json(),\n );\n\n const logger = winston.createLogger({\n level,\n defaultMeta: { component },\n format,\n transports: [new winston.transports.Console()],\n });\n\n return logger;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/process-logger.ts"],"names":["trace"],"mappings":";;;;;AAqDA,wBAAA,CAAyB,MAAM;AAC7B,EAAA,MAAM,IAAA,GAAO,MAAM,aAAA,EAAc;AACjC,EAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,EAAA,MAAM,GAAA,GAAM,KAAK,WAAA,EAAY;AAC7B,EAAA,IAAI,CAAC,kBAAA,CAAmB,GAAG,CAAA,EAAG,OAAO,MAAA;AACrC,EAAA,OAAO,GAAA,CAAI,OAAA;AACb,CAAC,CAAA;AA+JM,SAAS,kBAAA,GAAwE;AACtF,EAAA,MAAM,IAAA,GAAO,MAAM,aAAA,EAAc;AACjC,EAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,EAAA,MAAM,GAAA,GAAM,KAAK,WAAA,EAAY;AAC7B,EAAA,IAAI,CAAC,kBAAA,CAAmB,GAAG,CAAA,EAAG,OAAO,MAAA;AACrC,EAAA,OAAO,EAAE,QAAA,EAAU,GAAA,CAAI,OAAA,EAAS,OAAA,EAAS,IAAI,MAAA,EAAO;AACtD;;;AC1MA,IAAM,kBAAA,GAAqB,OAAA,CAAQ,MAAA,CAAO,CAAC,IAAA,KAAS;AAClD,EAAA,MAAMA,SAAQ,kBAAA,EAAmB;AACjC,EAAA,IAAIA,MAAAA,EAAO;AACT,IAAA,IAAA,CAAK,WAAWA,MAAAA,CAAM,QAAA;AACtB,IAAA,IAAA,CAAK,UAAUA,MAAAA,CAAM,OAAA;AAAA,EACvB;AACA,EAAA,OAAO,IAAA;AACT,CAAC,CAAA,EAAE;AAEI,SAAS,oBAAoB,SAAA,EAA2B;AAC7D,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,SAAA,IAAa,MAAA;AACvC,EAAA,MAAM,SAAS,OAAA,CAAQ,GAAA,CAAI,UAAA,KAAe,QAAA,GACtC,QAAQ,MAAA,CAAO,OAAA;AAAA,IACb,QAAQ,MAAA,CAAO,SAAA,CAAU,EAAE,MAAA,EAAQ,uBAAuB,CAAA;AAAA,IAC1D,QAAQ,MAAA,CAAO,MAAA,CAAO,EAAE,KAAA,EAAO,MAAM,CAAA;AAAA,IACrC,kBAAA;AAAA,IACA,OAAA,CAAQ,MAAA,CAAO,MAAA,CAAO,CAAC,EAAE,KAAA,EAAO,GAAA,EAAK,OAAA,EAAS,SAAA,EAAW,GAAG,IAAA,EAAK,KAAM;AACrE,MAAA,MAAM,OAAA,GAAU,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,CAAE,MAAA,GAAS,CAAA,GAAI,CAAA,CAAA,EAAI,IAAA,CAAK,SAAA,CAAU,IAAI,CAAC,CAAA,CAAA,GAAK,EAAA;AAC5E,MAAA,OAAO,CAAA,EAAG,SAAS,CAAA,EAAA,EAAK,GAAA,CAAI,WAAA,EAAa,CAAA,GAAA,EAAM,SAAS,CAAA,EAAA,EAAK,OAAO,CAAA,EAAG,OAAO,CAAA,CAAA;AAAA,IAChF,CAAC;AAAA,GACH,GACA,QAAQ,MAAA,CAAO,OAAA;AAAA,IACb,OAAA,CAAQ,OAAO,SAAA,EAAU;AAAA,IACzB,QAAQ,MAAA,CAAO,MAAA,CAAO,EAAE,KAAA,EAAO,MAAM,CAAA;AAAA,IACrC,kBAAA;AAAA,IACA,OAAA,CAAQ,OAAO,IAAA;AAAK,GACtB;AAEJ,EAAA,MAAM,MAAA,GAAS,QAAQ,YAAA,CAAa;AAAA,IAClC,KAAA;AAAA,IACA,WAAA,EAAa,EAAE,SAAA,EAAU;AAAA,IACzB,MAAA;AAAA,IACA,YAAY,CAAC,IAAI,OAAA,CAAQ,UAAA,CAAW,SAAS;AAAA,GAC9C,CAAA;AAED,EAAA,OAAO,MAAA;AACT","file":"process-logger.js","sourcesContent":["/**\n * @semiont/observability — public API.\n *\n * Universal surface (works in Node + browser). For SDK *initialization*,\n * import from `@semiont/observability/node` or `/web` at the process entry\n * point. Everything else uses this module.\n *\n * Tier 2 of `.plans/OBSERVABILITY.md`. The public surface:\n *\n * - `withSpan(name, fn, options?)` — wrap an async block in a span;\n * `options` carries `kind` and `attrs`.\n * - `withActorSpan(actor, channel, fn, extraAttrs?)` — consumer-span\n * wrapper for bus-event handlers, with handler-duration recording.\n * - `injectTraceparent(payload)` / `extractTraceparent(payload)` — W3C\n * trace-context propagation across the SSE channel (the bus payload\n * gets a `_trace?: { traceparent }` sibling to `correlationId`).\n * - `withTraceparent(carrier, fn)` — run `fn` with the incoming\n * traceparent as the parent context.\n * - `getActiveTraceparent()` — read the active span's traceparent for\n * manual propagation (e.g. attaching to a fetch header or SSE field).\n * - `getLogTraceContext()` — active `trace_id` / `span_id` for log-line\n * correlation.\n * - Metric recorders (`recordBusEmit`, `recordHandlerDuration`,\n * `recordJobOutcome`, `recordSubscriberConnect` / `Disconnect`,\n * `recordInferenceUsage`) and gauge providers\n * (`registerJobQueueProvider`, `registerVectorIndexSizeProvider`).\n *\n * No-op when no exporter is configured: `@opentelemetry/api`'s default\n * tracer is a no-op, so `withSpan` is essentially free until\n * `initObservability*()` runs.\n */\n\nimport {\n context,\n isSpanContextValid,\n metrics,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n type Attributes,\n type Counter,\n type Histogram,\n type ObservableGauge,\n type Span,\n type UpDownCounter,\n} from '@opentelemetry/api';\nimport { setBusLogTraceIdProvider } from '@semiont/core';\n\n// Wire `busLog`'s trace-id provider once at module load. When an OTel\n// SDK is initialized (and a span is active when `busLog` fires), the\n// emitted line gets a `trace=<8hex>` suffix that correlates the\n// grep-timeline with the trace UI. No-op when no SDK is active.\nsetBusLogTraceIdProvider(() => {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return ctx.traceId;\n});\n\nconst TRACER_NAME = 'semiont';\n\nconst tracer = () => trace.getTracer(TRACER_NAME);\n\n// ── withSpan ───────────────────────────────────────────────────────────\n\n/**\n * Wrap an async block in a span. The span is started before `fn` runs and\n * ended after it resolves or rejects; exceptions are recorded and the span\n * status is set to ERROR. `kind` defaults to INTERNAL.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T> | T,\n options?: { kind?: SpanKind; attrs?: Attributes },\n): Promise<T> {\n const span = tracer().startSpan(name, {\n kind: options?.kind ?? SpanKind.INTERNAL,\n ...(options?.attrs ? { attributes: options.attrs } : {}),\n });\n try {\n return await context.with(trace.setSpan(context.active(), span), () => fn(span));\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: err instanceof Error ? err.message : String(err),\n });\n throw err;\n } finally {\n span.end();\n }\n}\n\n// ── Traceparent on bus payloads ────────────────────────────────────────\n\nconst TRACE_FIELD = '_trace';\n\n/**\n * Sibling of `correlationId` on bus payloads. Lives on the SSE event body\n * because SSE has no header trailer; the SDK strips it before delivering\n * the payload to subscribers. Additive — payloads without `_trace` parse\n * unchanged.\n */\nexport interface TraceCarrier {\n /** W3C `traceparent` header value (`00-<traceId>-<spanId>-<flags>`). */\n traceparent: string;\n /** W3C `tracestate` header value (vendor-specific extensions). */\n tracestate?: string;\n}\n\n/**\n * Read the active span's W3C traceparent (and tracestate). Returns\n * `undefined` if no span is active.\n */\nexport function getActiveTraceparent(): TraceCarrier | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n const traceparent = carrier['traceparent'];\n if (!traceparent) return undefined;\n return carrier['tracestate']\n ? { traceparent, tracestate: carrier['tracestate'] }\n : { traceparent };\n}\n\n/**\n * Attach the active span's trace-context to a payload object as\n * `_trace`. No-op when no span is active. Returns the same object\n * reference for chaining.\n */\nexport function injectTraceparent<T extends Record<string, unknown>>(payload: T): T {\n const carrier = getActiveTraceparent();\n if (carrier) {\n (payload as Record<string, unknown>)[TRACE_FIELD] = carrier;\n }\n return payload;\n}\n\n/**\n * Strip and return the `_trace` field from a payload. Mutates `payload`.\n * The field is internal plumbing and should not be visible to subscribers.\n */\nexport function extractTraceparent<T extends Record<string, unknown>>(\n payload: T,\n): TraceCarrier | undefined {\n const carrier = (payload as Record<string, unknown>)[TRACE_FIELD] as\n | TraceCarrier\n | undefined;\n if (carrier !== undefined) {\n delete (payload as Record<string, unknown>)[TRACE_FIELD];\n }\n if (!carrier || typeof carrier.traceparent !== 'string') return undefined;\n return carrier;\n}\n\n/**\n * Run `fn` with the given W3C traceparent set as the parent context.\n * Any spans started inside `fn` will be children of the incoming trace.\n * No-op if `carrier` is undefined.\n */\nexport function withTraceparent<T>(\n carrier: TraceCarrier | undefined,\n fn: () => T,\n): T {\n if (!carrier) return fn();\n const carrierObj: Record<string, string> = { traceparent: carrier.traceparent };\n if (carrier.tracestate) carrierObj['tracestate'] = carrier.tracestate;\n const ctx = propagation.extract(context.active(), carrierObj);\n return context.with(ctx, fn);\n}\n\n// ── Actor handler convenience ──────────────────────────────────────────\n\n/**\n * Wrap a bus-event handler in an `actor.<name>:<channel>` consumer span.\n * Used at every `eventBus.get(channel).subscribe(handler)` site inside\n * an actor (Stower, Gatherer, Matcher, Browser, Smelter), to attribute\n * each in-process subscriber's work to a span without scattering manual\n * `withSpan` calls across handler bodies.\n *\n * The span's parent is the active context at the time the handler\n * fires — which is the `bus.dispatch:<channel>` span on the gateway\n * (Subject.next runs synchronously inside the dispatch span), or the\n * `bus.emit:<channel>` span when an actor emits to itself.\n */\nexport async function withActorSpan<T>(\n actor: string,\n channel: string,\n fn: (span: Span) => Promise<T> | T,\n extraAttrs?: Attributes,\n): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(`actor.${actor}:${channel}`, fn, {\n kind: SpanKind.CONSUMER,\n attrs: {\n actor,\n 'bus.channel': channel,\n ...(extraAttrs ?? {}),\n },\n });\n } finally {\n recordHandlerDuration(actor, channel, performance.now() - start);\n }\n}\n\n// ── Log correlation ────────────────────────────────────────────────────\n\n/**\n * Read the active span's `trace_id` / `span_id` for log-line correlation.\n * Tier 3 of `.plans/OBSERVABILITY.md`. Each structured log line gets\n * tagged with these so a log query in CloudWatch / Loki / Datadog can\n * jump to the trace in Tempo / Jaeger / X-Ray.\n *\n * Returns `undefined` if no span is active, or if the active span's\n * context is invalid (uninitialized SDK, no-op tracer).\n */\nexport function getLogTraceContext(): { trace_id: string; span_id: string } | undefined {\n const span = trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n if (!isSpanContextValid(ctx)) return undefined;\n return { trace_id: ctx.traceId, span_id: ctx.spanId };\n}\n\n// ── Metrics — Tier 3 ───────────────────────────────────────────────────\n\nconst METER_NAME = 'semiont';\n\nconst meter = () => metrics.getMeter(METER_NAME);\n\nlet _busEmitCounter: Counter | undefined;\nlet _replySuppressedCounter: Counter | undefined;\nlet _resumeGapCounter: Counter | undefined;\nlet _unanswerableCounter: Counter | undefined;\nlet _correlationRegistryGauge: ObservableGauge | undefined;\nlet _correlationRegistryProvider: (() => CorrelationRegistrySnapshot) | undefined;\nlet _handlerDurationHistogram: Histogram | undefined;\nlet _jobOutcomeCounter: Counter | undefined;\nlet _jobDurationHistogram: Histogram | undefined;\nlet _gatherDegradeCounter: Counter | undefined;\nlet _inferenceCallsCounter: Counter | undefined;\nlet _inferenceTokensCounter: Counter | undefined;\nlet _inferenceDurationHistogram: Histogram | undefined;\nlet _sseSubscribers: UpDownCounter | undefined;\nlet _jobQueueGauge: ObservableGauge | undefined;\nlet _jobQueueProvider: (() => Promise<JobQueueSnapshot> | JobQueueSnapshot) | undefined;\nlet _vectorIndexSizeGauge: ObservableGauge | undefined;\nlet _factPumpDepthGauge: ObservableGauge | undefined;\nlet _factPumpDepthProvider: (() => number) | undefined;\nlet _vectorIndexSizeProvider: (() => Promise<number> | number) | undefined;\n\n/** Snapshot of job-queue contents by status. Match `JobQueue.getStats()`. */\nexport interface JobQueueSnapshot {\n pending: number;\n running: number;\n complete: number;\n failed: number;\n cancelled: number;\n}\n\nfunction busEmitCounter(): Counter {\n if (!_busEmitCounter) {\n _busEmitCounter = meter().createCounter('semiont.bus.emit', {\n description: 'Bus emits by channel and scope',\n });\n }\n return _busEmitCounter;\n}\n\nfunction handlerDurationHistogram(): Histogram {\n if (!_handlerDurationHistogram) {\n _handlerDurationHistogram = meter().createHistogram('semiont.handler.duration', {\n description: 'In-process actor handler duration',\n unit: 'ms',\n });\n }\n return _handlerDurationHistogram;\n}\n\nfunction jobOutcomeCounter(): Counter {\n if (!_jobOutcomeCounter) {\n _jobOutcomeCounter = meter().createCounter('semiont.job.outcome', {\n description: 'Worker job completions by type and outcome',\n });\n }\n return _jobOutcomeCounter;\n}\n\nfunction jobDurationHistogram(): Histogram {\n if (!_jobDurationHistogram) {\n _jobDurationHistogram = meter().createHistogram('semiont.job.duration', {\n description: 'Worker job duration by type',\n unit: 'ms',\n });\n }\n return _jobDurationHistogram;\n}\n\nfunction inferenceCallsCounter(): Counter {\n if (!_inferenceCallsCounter) {\n _inferenceCallsCounter = meter().createCounter('semiont.inference.calls', {\n description: 'Inference API calls by provider, model, and outcome',\n });\n }\n return _inferenceCallsCounter;\n}\n\nfunction inferenceTokensCounter(): Counter {\n if (!_inferenceTokensCounter) {\n _inferenceTokensCounter = meter().createCounter('semiont.inference.tokens', {\n description: 'Inference token usage by provider, model, and direction',\n });\n }\n return _inferenceTokensCounter;\n}\n\nfunction inferenceDurationHistogram(): Histogram {\n if (!_inferenceDurationHistogram) {\n _inferenceDurationHistogram = meter().createHistogram('semiont.inference.duration', {\n description: 'Inference call duration by provider, model, and outcome',\n unit: 'ms',\n });\n }\n return _inferenceDurationHistogram;\n}\n\nfunction sseSubscribersCounter(): UpDownCounter {\n if (!_sseSubscribers) {\n _sseSubscribers = meter().createUpDownCounter('semiont.sse.subscribers', {\n description: 'Active SSE subscribers',\n });\n }\n return _sseSubscribers;\n}\n\nfunction replySuppressedCounter(): Counter {\n if (!_replySuppressedCounter) {\n _replySuppressedCounter = meter().createCounter('semiont.bus.reply.suppressed', {\n description: 'Correlated replies withheld from a non-owning subscriber',\n });\n }\n return _replySuppressedCounter;\n}\n\n/**\n * A correlated reply was withheld from a subscriber that does not own its\n * correlationId (CORRELATED-REPLY-ROUTING P5).\n *\n * Counts ONLY that case. A frame with no correlationId is a shape violation\n * (warned, not counted), and a cid nobody claimed is the structural in-process\n * case that fires constantly — counting either would drown the signal this\n * metric exists to show: the fan-out amplification the delivery filter removes.\n */\nexport function recordReplySuppressed(channel: string): void {\n replySuppressedCounter().add(1, { 'bus.channel': channel });\n}\n\nfunction resumeGapCounter(): Counter {\n if (!_resumeGapCounter) {\n _resumeGapCounter = meter().createCounter('semiont.bus.resume_gap', {\n description: 'SSE resumes that degraded to a gap because replay was unavailable',\n });\n }\n return _resumeGapCounter;\n}\n\n/**\n * An SSE resume could not be served and the client was told to fall back to\n * cache. This degradation is CORRECT by design and therefore silent — which is\n * exactly why it needs a number. A rising rate means clients are losing\n * history, and nothing else in the stack says so.\n */\nexport function recordResumeGap(reason: string): void {\n resumeGapCounter().add(1, { 'bus.resume_gap.reason': reason });\n}\n\nfunction unanswerableCounter(): Counter {\n if (!_unanswerableCounter) {\n _unanswerableCounter = meter().createCounter('semiont.bus.unanswerable', {\n description: 'Request emits that reached zero subscribers and were failed at the gateway',\n });\n }\n return _unanswerableCounter;\n}\n\n/**\n * A request-shaped emit reached no subscriber, so the gateway synthesized its\n * mapped failure (ARCHIVIST-STAYS-UP P3). By channel, this is the absence rate\n * of the service that answers it — the difference between \"it went down once\"\n * and \"it is flapping.\"\n */\nexport function recordUnanswerableRequest(channel: string): void {\n unanswerableCounter().add(1, { 'bus.channel': channel });\n}\n\n/** Claims held and reply payloads retained by a gateway's correlation registry. */\nexport interface CorrelationRegistrySnapshot {\n claims: number;\n retainedReplies: number;\n}\n\n/**\n * Register a callback returning the gateway's correlation-registry occupancy.\n *\n * COUNTS, not bytes: retention is count-budgeted today (byte-budgeting is a\n * known limit in CORRELATED-REPLY-ROUTING), so `retainedReplies` is a proxy for\n * heap, not a measure of it. It is still the closest observable to the question\n * two OOM investigations keep asking — a browse result can be 1-2 MB, and up to\n * REPLY_RETENTION_MAX of them are held at once.\n */\nexport function registerCorrelationRegistryProvider(\n provider: () => CorrelationRegistrySnapshot,\n): void {\n _correlationRegistryProvider = provider;\n if (!_correlationRegistryGauge) {\n _correlationRegistryGauge = meter().createObservableGauge('semiont.bus.correlation.size', {\n description: 'Correlation registry occupancy: live claims and retained reply payloads',\n });\n _correlationRegistryGauge.addCallback((observer) => {\n if (!_correlationRegistryProvider) return;\n const snap = _correlationRegistryProvider();\n observer.observe(snap.claims, { 'correlation.kind': 'claims' });\n observer.observe(snap.retainedReplies, { 'correlation.kind': 'retained_replies' });\n });\n }\n}\n\n/** Increment the bus-emit counter. Called at every transport `emit` site. */\nexport function recordBusEmit(channel: string, scope?: string): void {\n busEmitCounter().add(1, {\n 'bus.channel': channel,\n ...(scope ? { 'bus.scope': scope } : {}),\n });\n}\n\n/** Record an in-process actor handler's duration. */\nexport function recordHandlerDuration(actor: string, channel: string, durationMs: number): void {\n handlerDurationHistogram().record(durationMs, {\n actor,\n 'bus.channel': channel,\n });\n}\n\n/** Record a worker job's outcome and duration. */\nexport function recordJobOutcome(jobType: string, outcome: 'completed' | 'failed', durationMs: number): void {\n jobOutcomeCounter().add(1, { 'job.type': jobType, 'job.outcome': outcome });\n jobDurationHistogram().record(durationMs, { 'job.type': jobType, 'job.outcome': outcome });\n}\n\nlet _appendStageHistogram: Histogram | undefined;\nfunction appendStageHistogram(): Histogram {\n if (!_appendStageHistogram) {\n _appendStageHistogram = meter().createHistogram('semiont.record.append.duration', {\n description: 'Time spent in one stage of appending an event to the record, labeled by stage: persist (JSONL write + git), materialize (view rebuild), enrich, publish. The Archivist\\'s core write path.',\n unit: 'ms',\n });\n }\n return _appendStageHistogram;\n}\n\n/**\n * Record one stage of `EventStore.appendEvent` (ARCHIVIST-STAYS-UP P7).\n *\n * The append path is the one operation only the Archivist can perform, and it\n * was entirely dark: reads had `recordHandlerDuration` and the bus had its own\n * counters, while writes had nothing. Stage-labeled because the useful\n * question is never \"was the append slow\" but WHICH PART — and `materialize`\n * in particular does work proportional to a resource's annotation count, so it\n * degrades with history rather than with load.\n */\nexport function recordAppendStage(\n stage: 'persist' | 'materialize' | 'enrich' | 'publish',\n durationMs: number,\n): void {\n appendStageHistogram().record(durationMs, { 'record.stage': stage });\n}\n\nlet _gitCommandHistogram: Histogram | undefined;\nfunction gitCommandHistogram(): Histogram {\n if (!_gitCommandHistogram) {\n _gitCommandHistogram = meter().createHistogram('semiont.git.duration', {\n description: 'Wall time of a git subprocess. Async — this is latency, not event-loop blockage. Staging is deduped, so the `add` count is far below the number of appended events.',\n unit: 'ms',\n });\n }\n return _gitCommandHistogram;\n}\n\n/**\n * Record a git invocation. Read the `add` count against events appended: one\n * per event means deferred staging has stopped deduping.\n */\nexport function recordGitCommand(command: string, durationMs: number): void {\n gitCommandHistogram().record(durationMs, { 'git.command': command });\n}\n\nfunction gatherDegradeCounter(): Counter {\n if (!_gatherDegradeCounter) {\n _gatherDegradeCounter = meter().createCounter('semiont.gather.degraded', {\n description: 'Gathers that degraded because an eventually-consistent projection did not catch up within its read barrier (vectors: absent semanticContext; graph: projection-lag failure). Labeled by projection.',\n });\n }\n return _gatherDegradeCounter;\n}\n\n/**\n * Record a gather degraded by a projection read barrier: `'vectors'` — the\n * Smelter settle barrier timed out (semanticContext shipped absent);\n * `'graph'` — the Weaver applied barrier + poll floor exhausted (projection\n * lag surfaced as a distinct failure). Fleet-alertable counterpart of the\n * `[gather DEGRADED]` L4 breadcrumbs — a rising rate on either label means\n * that pipeline is not keeping up.\n */\nexport function recordGatherDegrade(projection: 'graph' | 'vectors'): void {\n gatherDegradeCounter().add(1, { projection });\n}\n\n/** Increment the SSE subscriber gauge — call on `/bus/subscribe` open. */\nexport function recordSubscriberConnect(): void {\n sseSubscribersCounter().add(1);\n}\n\n/** Decrement on disconnect. Pair with `recordSubscriberConnect`. */\nexport function recordSubscriberDisconnect(): void {\n sseSubscribersCounter().add(-1);\n}\n\n/**\n * Register a callback that returns the current job-queue snapshot.\n * Polled at the SDK's metric-collection interval. The single gauge\n * emits one observation per status (`pending`, `running`, …) tagged\n * with the `job.status` attribute. Idempotent — last registered\n * provider wins.\n */\nexport function registerJobQueueProvider(\n provider: () => Promise<JobQueueSnapshot> | JobQueueSnapshot,\n): void {\n _jobQueueProvider = provider;\n if (!_jobQueueGauge) {\n _jobQueueGauge = meter().createObservableGauge('semiont.job.queue.size', {\n description: 'Job queue size by status',\n });\n _jobQueueGauge.addCallback(async (observer) => {\n if (!_jobQueueProvider) return;\n const snap = await _jobQueueProvider();\n observer.observe(snap.pending, { 'job.status': 'pending' });\n observer.observe(snap.running, { 'job.status': 'running' });\n observer.observe(snap.complete, { 'job.status': 'complete' });\n observer.observe(snap.failed, { 'job.status': 'failed' });\n observer.observe(snap.cancelled, { 'job.status': 'cancelled' });\n });\n }\n}\n\n/**\n * Register a callback that returns the current vector-index size\n * (point count). Async to allow remote queries (Qdrant). Polled at\n * the metric-collection interval.\n */\n/**\n * Register the Archivist's fact-pump backlog — facts appended to the record\n * but not yet republished onto the bus.\n *\n * At rest this is zero. A value that climbs and does not come back means the\n * pump is outrunning its transport, which is the leading hypothesis for the\n * load-correlated heap growth in `bugs/absent-archivist-wedges-browse.md`\n * (ARCHIVIST-STAYS-UP P5). The backlog is deliberately unbounded today, so\n * this number is the only thing standing between \"the pump is behind\" and an\n * OOM whose cause is inferred from RSS after the fact.\n */\nexport function registerFactPumpDepthProvider(provider: () => number): void {\n _factPumpDepthProvider = provider;\n if (!_factPumpDepthGauge) {\n _factPumpDepthGauge = meter().createObservableGauge('semiont.archivist.fact_pump.depth', {\n description: 'Facts appended to the record but not yet published to the bus. Zero at rest; a rising floor means the pump is behind its transport.',\n });\n _factPumpDepthGauge.addCallback((observer) => {\n if (_factPumpDepthProvider) observer.observe(_factPumpDepthProvider());\n });\n }\n}\n\nlet _gitStagingFailureCounter: Counter | undefined;\n\n/**\n * A staging command that could not be run. Staging the index is a CONVENIENCE\n * — the event log is the system of record — so a failure here is degraded\n * service, never a reason to exit. But degraded must be VISIBLE: this counter\n * is what stops \"the index is quietly stale\" from being invisible.\n */\nexport function recordGitStagingFailure(reason: 'index-lock' | 'other'): void {\n if (!_gitStagingFailureCounter) {\n _gitStagingFailureCounter = meter().createCounter('semiont.git.staging.failures', {\n description: 'Staging commands abandoned after retries; the index may be stale',\n });\n }\n _gitStagingFailureCounter.add(1, { reason });\n}\n\n/**\n * Process lifetime telemetry (ARCHIVIST-GIT-STAGER-CRASH).\n *\n * A supervised process that dies and comes back is INVISIBLE in logs unless\n * someone greps for boot lines, and every request in flight when it died looks\n * to its caller like a hang. On 2026-09-08 that cost a multi-hour hunt through\n * search, qdrant, neo4j and the SSE transport for a crash loop that one metric\n * would have named immediately.\n *\n * `start_time` is the diagnostic, not uptime: a CHANGE in it is unambiguous\n * proof of a restart, and uptime is derivable from it.\n */\nconst PROCESS_START_TIME_SECONDS = Math.floor(Date.now() / 1000);\nlet _processStartTimeGauge: ObservableGauge | undefined;\nlet _restartCountGauge: ObservableGauge | undefined;\nlet _restartCountProvider: (() => Promise<number | undefined> | number | undefined) | undefined;\nlet _abnormalExitCounter: Counter | undefined;\n\n/** Register `semiont.process.start_time`. Called by `initObservability*`. */\nexport function registerProcessLifetimeMetrics(): void {\n if (_processStartTimeGauge) return;\n _processStartTimeGauge = meter().createObservableGauge('semiont.process.start_time', {\n description: 'Unix seconds at which this process started; a change means it restarted',\n unit: 's',\n });\n _processStartTimeGauge.addCallback((observer) => observer.observe(PROCESS_START_TIME_SECONDS));\n}\n\n/**\n * Supply a restart count. A supervisor is POSIX shell and cannot emit OTel, but\n * it keeps a durable event log on the state mount — so the supervised child\n * reads it and reports the count on the supervisor's behalf.\n *\n * **Return `undefined` for \"cannot tell\", never `0`.** A process running\n * without a supervisor (dev, or a direct `node` invocation) has no restart\n * history to report, and 0 is not that — 0 is the value a HEALTHY supervised\n * process reports, so an unsupervised one publishing 0 is indistinguishable\n * from a supervised one that has never crashed. `undefined` skips the\n * observation entirely and the series simply has no point, which is what\n * \"unknown\" looks like in a time series.\n */\nexport function registerRestartCountProvider(\n provider: () => Promise<number | undefined> | number | undefined,\n): void {\n _restartCountProvider = provider;\n if (!_restartCountGauge) {\n _restartCountGauge = meter().createObservableGauge('semiont.process.restarts', {\n description: 'Times the supervisor has restarted this service',\n });\n _restartCountGauge.addCallback(async (observer) => {\n if (!_restartCountProvider) return;\n const count = await _restartCountProvider();\n // Not `if (count)` — 0 is a real, meaningful reading.\n if (count !== undefined) observer.observe(count);\n });\n }\n}\n\n/**\n * Record that this process is dying abnormally, and mark the active span so a\n * trace shows a span that ENDED IN DEATH rather than one that simply never\n * ends. Never swallows: callers re-raise, so Node's own semantics are intact.\n */\nexport function recordAbnormalTermination(reason: string, detail?: string): void {\n if (!_abnormalExitCounter) {\n _abnormalExitCounter = meter().createCounter('semiont.process.abnormal_exit', {\n description: 'Process terminations that were not a clean shutdown',\n });\n }\n _abnormalExitCounter.add(1, { reason });\n const active = trace.getActiveSpan();\n if (active) {\n active.setStatus({ code: SpanStatusCode.ERROR, message: `${reason}: ${detail ?? ''}`.trim() });\n active.setAttribute('semiont.process.abnormal_exit', reason);\n active.end();\n }\n}\n\nexport function registerVectorIndexSizeProvider(\n provider: () => Promise<number> | number,\n): void {\n _vectorIndexSizeProvider = provider;\n if (!_vectorIndexSizeGauge) {\n _vectorIndexSizeGauge = meter().createObservableGauge('semiont.vector.index.size', {\n description: 'Vector store point count',\n });\n _vectorIndexSizeGauge.addCallback(async (observer) => {\n if (_vectorIndexSizeProvider) {\n const value = await _vectorIndexSizeProvider();\n observer.observe(value);\n }\n });\n }\n}\n\n/**\n * Record an inference call. Token counts are optional — providers that\n * don't expose them (or fail before generating) record only call count\n * and duration.\n */\nexport function recordInferenceUsage(opts: {\n provider: string;\n model: string;\n durationMs: number;\n outcome: 'success' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const baseAttrs = {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.outcome': opts.outcome,\n };\n inferenceCallsCounter().add(1, baseAttrs);\n inferenceDurationHistogram().record(opts.durationMs, baseAttrs);\n if (opts.inputTokens != null && opts.inputTokens > 0) {\n inferenceTokensCounter().add(opts.inputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'input',\n });\n }\n if (opts.outputTokens != null && opts.outputTokens > 0) {\n inferenceTokensCounter().add(opts.outputTokens, {\n 'inference.provider': opts.provider,\n 'inference.model': opts.model,\n 'inference.direction': 'output',\n });\n }\n}\n\nlet _detectionCallCounter: Counter | undefined;\nlet _detectionDurationHistogram: Histogram | undefined;\nlet _detectionItemsHistogram: Histogram | undefined;\nlet _detectionTokensHistogram: Histogram | undefined;\n\nfunction detectionCallCounter(): Counter {\n if (!_detectionCallCounter) {\n _detectionCallCounter = meter().createCounter('semiont.detection.calls', {\n description: 'Detection model calls, labeled by motivation, outcome, subdivision depth and whether this was the floor re-roll.',\n });\n }\n return _detectionCallCounter;\n}\n\nfunction detectionDurationHistogram(): Histogram {\n if (!_detectionDurationHistogram) {\n _detectionDurationHistogram = meter().createHistogram('semiont.detection.call.duration', {\n description: 'Wall time of one detection model call, including the attempts that failed and were retried smaller.',\n unit: 'ms',\n });\n }\n return _detectionDurationHistogram;\n}\n\nfunction detectionItemsHistogram(): Histogram {\n if (!_detectionItemsHistogram) {\n _detectionItemsHistogram = meter().createHistogram('semiont.detection.call.items', {\n description: 'Annotations returned by one detection call. Against the input size on the same record, this is yield.',\n });\n }\n return _detectionItemsHistogram;\n}\n\nfunction detectionTokensHistogram(): Histogram {\n if (!_detectionTokensHistogram) {\n _detectionTokensHistogram = meter().createHistogram('semiont.detection.call.tokens', {\n description: \"Provider-reported tokens for one detection call, by direction. Deliberately separate from semiont.inference.tokens: that series is the authoritative total but carries no subdivision depth, and 'what does a depth-2 call cost' is the question every sizing decision asks.\",\n });\n }\n return _detectionTokensHistogram;\n}\n\n/**\n * Record one detection model call (DETECTION-QUALITY-THROUGHPUT P1).\n *\n * The adapters already record provider/model/duration/tokens for every\n * inference call. What they cannot know is the detection shape around it:\n * which motivation asked, how big the piece was, how many annotations came\n * back, how deep subdivision had descended, and whether this was the floor\n * re-roll. Those are the facts that distinguish a healthy call from a\n * expensive descent, and without them a slow detection run is one\n * undifferentiated number.\n *\n * FAILED attempts are recorded too, and that is the point: the calls paid for\n * and thrown away during a descent are exactly the cost later phases exist to\n * avoid, so a record only of successes would hide the thing being optimized.\n *\n * Tokens are the PROVIDER's counts, passed through — never estimated. Absent\n * means the provider did not report them.\n */\nexport function recordDetectionCall(opts: {\n label: string;\n pieceChars: number;\n durationMs: number;\n items: number;\n depth: number;\n reroll: boolean;\n outcome: 'success' | 'truncated' | 'timeout' | 'collapsed' | 'error';\n inputTokens?: number;\n outputTokens?: number;\n}): void {\n const attrs = {\n 'detection.label': opts.label,\n 'detection.outcome': opts.outcome,\n 'detection.depth': opts.depth,\n 'detection.reroll': opts.reroll,\n };\n detectionCallCounter().add(1, attrs);\n detectionDurationHistogram().record(opts.durationMs, attrs);\n detectionItemsHistogram().record(opts.items, attrs);\n if (opts.inputTokens !== undefined) {\n detectionTokensHistogram().record(opts.inputTokens, { ...attrs, 'detection.direction': 'input' });\n }\n if (opts.outputTokens !== undefined) {\n detectionTokensHistogram().record(opts.outputTokens, { ...attrs, 'detection.direction': 'output' });\n }\n}\n\nlet _anchorOutcomeCounter: Counter | undefined;\nfunction anchorOutcomeCounter(): Counter {\n if (!_anchorOutcomeCounter) {\n _anchorOutcomeCounter = meter().createCounter('semiont.detection.anchors', {\n description: 'Every annotation anchoring, labeled by the method that resolved it. EVERY outcome is counted, not just the risky ones, because a bare count of degraded anchors has no denominator — the rate is the precision signal.',\n });\n }\n return _anchorOutcomeCounter;\n}\n\n/**\n * Record how one annotation got anchored (DETECTION-QUALITY-THROUGHPUT P5).\n *\n * The selector-vs-source check is already a WRITE-TIME INVARIANT — both\n * `buildTextAnnotation` and `buildPdfAnnotation` throw on a selector that does\n * not match its source — so mechanical correctness is guaranteed rather than\n * sampled, and auditing it would measure a constant.\n *\n * What is genuinely uncertain is which anchoring METHOD got there. An `exact`\n * the model quoted verbatim and that appears once is certain; one resolved by\n * `first-of-many` (several occurrences, no usable context) or `fuzzy-match`\n * picked a plausible occurrence and may have picked wrong. Those were visible\n * only as log warnings — countable by a human reading worker output, which is\n * how 47 of them went unreviewed. As a rate they are the precision number that\n * sits beside the yield numbers.\n */\nexport function recordAnchorOutcome(label: string, method: string): void {\n anchorOutcomeCounter().add(1, { 'detection.label': label, 'anchor.method': method });\n}\n\n// ── Re-exports from @opentelemetry/api ─────────────────────────────────\n\nexport { SpanKind, SpanStatusCode, type Attributes, type Span } from '@opentelemetry/api';\n","/**\n * Process-level structured logger for Node entry points.\n *\n * Used by long-lived Node processes (gateway, workers, smelter) that\n * want JSON-structured stdout with active-span trace correlation. The\n * `trace_id` / `span_id` fields are populated from the current OTel\n * span context via `getLogTraceContext` — this is the same Tier 3\n * correlation that lets a grep through stdout line up with the trace\n * UI without manual stitching.\n *\n * Reads `LOG_LEVEL` (default `info`) and `LOG_FORMAT` (`json` default,\n * `simple` for human-friendly dev output).\n *\n * Co-located with `getLogTraceContext` deliberately: this is the\n * only reasonably-shaped consumer of that helper, and putting them in\n * the same package keeps the trace-id wiring in one place.\n */\n\nimport winston from 'winston';\nimport type { Logger } from '@semiont/core';\nimport { getLogTraceContext } from './index.js';\n\nconst traceContextFormat = winston.format((info) => {\n const trace = getLogTraceContext();\n if (trace) {\n info.trace_id = trace.trace_id;\n info.span_id = trace.span_id;\n }\n return info;\n})();\n\nexport function createProcessLogger(component: string): Logger {\n const level = process.env.LOG_LEVEL ?? 'info';\n const format = process.env.LOG_FORMAT === 'simple'\n ? winston.format.combine(\n winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),\n winston.format.errors({ stack: true }),\n traceContextFormat,\n winston.format.printf(({ level: lvl, message, timestamp, ...meta }) => {\n const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';\n return `${timestamp} [${lvl.toUpperCase()}] [${component}] ${message}${metaStr}`;\n }),\n )\n : winston.format.combine(\n winston.format.timestamp(),\n winston.format.errors({ stack: true }),\n traceContextFormat,\n winston.format.json(),\n );\n\n const logger = winston.createLogger({\n level,\n defaultMeta: { component },\n format,\n transports: [new winston.transports.Console()],\n });\n\n return logger;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semiont/observability",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.33",
|
|
4
4
|
"description": "OpenTelemetry-based tracing for Semiont — Tier 2 of OBSERVABILITY.md. Process-init helpers (Node + Web), withSpan helper, W3C traceparent inject/extract for bus payloads. No-op when no exporter is configured.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
73
73
|
"@opentelemetry/sdk-trace-web": "^2.11.0",
|
|
74
74
|
"@opentelemetry/semantic-conventions": "^1.43.0",
|
|
75
|
-
"@semiont/core": "0.5.
|
|
75
|
+
"@semiont/core": "0.5.33",
|
|
76
76
|
"winston": "^3.17.0"
|
|
77
77
|
}
|
|
78
78
|
}
|