@opengeni/observability 0.8.29 → 0.8.30
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/export-queue.d.ts +13 -0
- package/dist/failure-diagnostic.d.ts +50 -0
- package/dist/index.d.ts +47 -24
- package/dist/index.js +575 -47
- package/dist/index.js.map +1 -1
- package/dist/trace-context.d.ts +19 -0
- package/package.json +2 -2
- package/src/export-queue.ts +74 -0
- package/src/failure-diagnostic.ts +203 -0
- package/src/index.ts +404 -46
- package/src/trace-context.ts +78 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,280 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createHash } from "crypto";
|
|
2
|
+
import { createHash as createHash3 } from "crypto";
|
|
3
3
|
import { collectDefaultMetrics, Counter, Gauge, Histogram, Registry } from "prom-client";
|
|
4
4
|
import { SandboxBackend } from "@opengeni/contracts";
|
|
5
|
+
|
|
6
|
+
// src/trace-context.ts
|
|
7
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
var storage = new AsyncLocalStorage();
|
|
10
|
+
function ownData(value, key) {
|
|
11
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
12
|
+
try {
|
|
13
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
14
|
+
return descriptor && "value" in descriptor ? descriptor.value : void 0;
|
|
15
|
+
} catch {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function validTraceContext(value) {
|
|
20
|
+
const traceId = ownData(value, "traceId");
|
|
21
|
+
const spanId = ownData(value, "spanId");
|
|
22
|
+
if (typeof traceId !== "string" || typeof spanId !== "string") return void 0;
|
|
23
|
+
if (!/^[0-9a-f]{32}$/.test(traceId) || /^0+$/.test(traceId)) return void 0;
|
|
24
|
+
if (!/^[0-9a-f]{16}$/.test(spanId) || /^0+$/.test(spanId)) return void 0;
|
|
25
|
+
return { traceId, spanId };
|
|
26
|
+
}
|
|
27
|
+
function currentTraceContext() {
|
|
28
|
+
return validTraceContext(storage.getStore());
|
|
29
|
+
}
|
|
30
|
+
function withTraceContext(context, run) {
|
|
31
|
+
const valid = validTraceContext(context);
|
|
32
|
+
const addLink = ownData(context, "addLink");
|
|
33
|
+
return storage.run(
|
|
34
|
+
valid ? {
|
|
35
|
+
...valid,
|
|
36
|
+
addLink: typeof addLink === "function" ? addLink : void 0
|
|
37
|
+
} : void 0,
|
|
38
|
+
run
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function admissionTraceContext(eventId) {
|
|
42
|
+
if (typeof eventId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(eventId))
|
|
43
|
+
return void 0;
|
|
44
|
+
const digest = createHash("sha256").update("opengeni:accepted-event-trace:v1\0").update(eventId.toLowerCase()).digest("hex");
|
|
45
|
+
return validTraceContext({ traceId: digest.slice(0, 32), spanId: digest.slice(32, 48) });
|
|
46
|
+
}
|
|
47
|
+
function linkCurrentSpanToAdmission(eventId) {
|
|
48
|
+
const link = admissionTraceContext(eventId);
|
|
49
|
+
if (link) storage.getStore()?.addLink?.(link);
|
|
50
|
+
}
|
|
51
|
+
function parseTraceparent(value) {
|
|
52
|
+
if (typeof value !== "string") return void 0;
|
|
53
|
+
const match = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/.exec(value ?? "");
|
|
54
|
+
return match ? validTraceContext({ traceId: match[1], spanId: match[2] }) : void 0;
|
|
55
|
+
}
|
|
56
|
+
function traceparent(context) {
|
|
57
|
+
const valid = validTraceContext(context);
|
|
58
|
+
return valid ? `00-${valid.traceId}-${valid.spanId}-01` : void 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/export-queue.ts
|
|
62
|
+
var ExportQueue = class {
|
|
63
|
+
constructor(observer, capacity = 256) {
|
|
64
|
+
this.observer = observer;
|
|
65
|
+
this.capacity = capacity;
|
|
66
|
+
}
|
|
67
|
+
queue = [];
|
|
68
|
+
running;
|
|
69
|
+
observe(outcome) {
|
|
70
|
+
try {
|
|
71
|
+
this.observer(outcome);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
enqueue(send) {
|
|
76
|
+
if (this.queue.length >= this.capacity) {
|
|
77
|
+
this.observe("dropped");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.queue.push(send);
|
|
81
|
+
this.start();
|
|
82
|
+
}
|
|
83
|
+
start() {
|
|
84
|
+
if (!this.running) {
|
|
85
|
+
this.running = Promise.resolve().then(() => this.drain()).finally(() => {
|
|
86
|
+
this.running = void 0;
|
|
87
|
+
if (this.queue.length) this.start();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async drain() {
|
|
92
|
+
while (this.queue.length) {
|
|
93
|
+
const send = this.queue.shift();
|
|
94
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
95
|
+
try {
|
|
96
|
+
await send();
|
|
97
|
+
this.observe("exported");
|
|
98
|
+
break;
|
|
99
|
+
} catch {
|
|
100
|
+
if (attempt === 2) this.observe("failed");
|
|
101
|
+
else {
|
|
102
|
+
this.observe("retried");
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** attempt));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async flush(timeoutMs = 1e3) {
|
|
110
|
+
if (!this.running) return;
|
|
111
|
+
let timer;
|
|
112
|
+
try {
|
|
113
|
+
await Promise.race([
|
|
114
|
+
this.running,
|
|
115
|
+
new Promise((resolve) => {
|
|
116
|
+
timer = setTimeout(
|
|
117
|
+
resolve,
|
|
118
|
+
Number.isFinite(timeoutMs) ? Math.max(1, Math.min(timeoutMs, 5e3)) : 1e3
|
|
119
|
+
);
|
|
120
|
+
})
|
|
121
|
+
]);
|
|
122
|
+
} finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/failure-diagnostic.ts
|
|
129
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
130
|
+
import { SessionEventType } from "@opengeni/contracts";
|
|
131
|
+
var CODES = [
|
|
132
|
+
"api_startup_failed",
|
|
133
|
+
"api_unhandled_rejection",
|
|
134
|
+
"api_uncaught_exception",
|
|
135
|
+
"db_deadlock",
|
|
136
|
+
"db_serialization_failure",
|
|
137
|
+
"db_failure",
|
|
138
|
+
"retained_process_fenced",
|
|
139
|
+
"retained_process_proof_failed"
|
|
140
|
+
];
|
|
141
|
+
var STAGES = [
|
|
142
|
+
"startup",
|
|
143
|
+
"running",
|
|
144
|
+
"session_events.append_generic",
|
|
145
|
+
"session_events.append_for_turn_attempt",
|
|
146
|
+
"preclaim",
|
|
147
|
+
"session_attempts.claim",
|
|
148
|
+
"failure_settlement",
|
|
149
|
+
"sandbox_retained_processes.proof"
|
|
150
|
+
];
|
|
151
|
+
var RETRIES = ["not_retryable", "exhausted", "unknown"];
|
|
152
|
+
var CONSTRAINTS = /* @__PURE__ */ new Set([
|
|
153
|
+
"session_events_workspace_account_fk",
|
|
154
|
+
"session_events_turn_association_check",
|
|
155
|
+
"session_events_payload_bytes_check",
|
|
156
|
+
"session_events_type_bytes_check",
|
|
157
|
+
"session_events_duplicate_classification_check",
|
|
158
|
+
"sandbox_retained_processes_reconcile_proof_check",
|
|
159
|
+
"sandbox_retained_processes_reconcile_claim_check",
|
|
160
|
+
"sandbox_retained_processes_settlement_check"
|
|
161
|
+
]);
|
|
162
|
+
var ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
163
|
+
"Error",
|
|
164
|
+
"TypeError",
|
|
165
|
+
"RangeError",
|
|
166
|
+
"AggregateError",
|
|
167
|
+
"PostgresError",
|
|
168
|
+
"DrizzleQueryError",
|
|
169
|
+
"SessionEventPersistenceError",
|
|
170
|
+
"SandboxWorkspaceMutationFencedError"
|
|
171
|
+
]);
|
|
172
|
+
var DIAGNOSTIC_SOURCE_FILES = [
|
|
173
|
+
"apps/api/src/index.ts",
|
|
174
|
+
"apps/api/src/app.ts",
|
|
175
|
+
"apps/api/src/fatal-process-boundary.ts",
|
|
176
|
+
"apps/api/dist/process/index.js",
|
|
177
|
+
"apps/worker/src/activities/agent-turn/run.ts",
|
|
178
|
+
"apps/worker/src/activities/agent-turn/claim.ts",
|
|
179
|
+
"apps/worker/src/activities/agent-turn/failure-settlement.ts",
|
|
180
|
+
"apps/worker/src/activities/sandbox-lease.ts",
|
|
181
|
+
"apps/worker/dist/process/index.js",
|
|
182
|
+
"packages/db/src/index.ts",
|
|
183
|
+
"packages/db/src/persistence-errors.ts",
|
|
184
|
+
"packages/storage/src/index.ts",
|
|
185
|
+
"packages/observability/src/index.ts"
|
|
186
|
+
];
|
|
187
|
+
var DIAGNOSTIC_POSTGRES_FUNCTIONS = ["admit_session_attempt_personal_resources"];
|
|
188
|
+
function reviewedSource(location) {
|
|
189
|
+
return DIAGNOSTIC_SOURCE_FILES.find((file) => location === file || location.endsWith(`/${file}`));
|
|
190
|
+
}
|
|
191
|
+
function postgresContext(error) {
|
|
192
|
+
if (errorKind(error) !== "PostgresError") return [];
|
|
193
|
+
const where = own(error, "where");
|
|
194
|
+
if (typeof where !== "string") return [];
|
|
195
|
+
return where.slice(0, 8192).split("\n").slice(0, 16).flatMap((value) => {
|
|
196
|
+
const match = /^PL\/pgSQL function (?:public\.)?([a-z_]+)\([^\n]*\) line (\d{1,7}) at /.exec(
|
|
197
|
+
value
|
|
198
|
+
);
|
|
199
|
+
if (!match || !DIAGNOSTIC_POSTGRES_FUNCTIONS.some((name) => name === match[1])) return [];
|
|
200
|
+
return [{ functionName: match[1], line: Number(match[2]) }];
|
|
201
|
+
}).slice(0, 8);
|
|
202
|
+
}
|
|
203
|
+
function own(value, key) {
|
|
204
|
+
try {
|
|
205
|
+
if (!value || typeof value !== "object") return void 0;
|
|
206
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
207
|
+
return descriptor && "value" in descriptor ? descriptor.value : void 0;
|
|
208
|
+
} catch {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function errorKind(error) {
|
|
213
|
+
const name = own(error, "name");
|
|
214
|
+
if (typeof name === "string" && ERROR_NAMES.has(name)) return name;
|
|
215
|
+
try {
|
|
216
|
+
const prototype = Object.getPrototypeOf(error);
|
|
217
|
+
if (prototype === TypeError.prototype) return "TypeError";
|
|
218
|
+
if (prototype === RangeError.prototype) return "RangeError";
|
|
219
|
+
if (prototype === AggregateError.prototype) return "AggregateError";
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
return "Error";
|
|
223
|
+
}
|
|
224
|
+
function failureDiagnostic(input, revision) {
|
|
225
|
+
const causes = [];
|
|
226
|
+
const pgContext = [];
|
|
227
|
+
const seen = /* @__PURE__ */ new Set();
|
|
228
|
+
let error = input.error;
|
|
229
|
+
while (error && typeof error === "object" && !seen.has(error) && causes.length < 4) {
|
|
230
|
+
seen.add(error);
|
|
231
|
+
pgContext.push(...postgresContext(error).slice(0, 8 - pgContext.length));
|
|
232
|
+
const stack = own(error, "stack");
|
|
233
|
+
const frames = typeof stack === "string" ? stack.slice(0, 16384).split("\n").slice(1, 33).flatMap((frame) => {
|
|
234
|
+
const match = /(?:\(|\s)([^\s()]+):(\d{1,7}):(\d{1,7})\)?$/.exec(frame);
|
|
235
|
+
if (!match) return [];
|
|
236
|
+
return [
|
|
237
|
+
{
|
|
238
|
+
locationHash: createHash2("sha256").update(match[1]).digest("hex"),
|
|
239
|
+
...reviewedSource(match[1]) ? { source: reviewedSource(match[1]) } : {},
|
|
240
|
+
line: Number(match[2]),
|
|
241
|
+
column: Number(match[3])
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
}) : [];
|
|
245
|
+
causes.push({
|
|
246
|
+
kind: errorKind(error),
|
|
247
|
+
frames
|
|
248
|
+
});
|
|
249
|
+
error = own(error, "cause");
|
|
250
|
+
}
|
|
251
|
+
const uuid = (value) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? value : void 0;
|
|
252
|
+
return {
|
|
253
|
+
schema: "opengeni.failure-diagnostic.v1",
|
|
254
|
+
diagnosticId: randomUUID(),
|
|
255
|
+
code: CODES.includes(input.code) ? input.code : "db_failure",
|
|
256
|
+
stage: STAGES.includes(input.stage) ? input.stage : "failure_settlement",
|
|
257
|
+
retryDecision: input.retryDecision && RETRIES.includes(input.retryDecision) ? input.retryDecision : "unknown",
|
|
258
|
+
attemptId: uuid(input.attemptId),
|
|
259
|
+
sessionId: uuid(input.sessionId),
|
|
260
|
+
processId: uuid(input.processId),
|
|
261
|
+
turnId: uuid(input.turnId),
|
|
262
|
+
attempts: Number.isSafeInteger(input.attempts) && input.attempts >= 0 && input.attempts <= 1e6 ? input.attempts : void 0,
|
|
263
|
+
sqlState: typeof input.sqlState === "string" && /^[0-9A-Z]{5}$/.test(input.sqlState) ? input.sqlState : void 0,
|
|
264
|
+
constraint: input.constraint && CONSTRAINTS.has(input.constraint) ? input.constraint : void 0,
|
|
265
|
+
deploymentRevision: revision && /^[0-9a-f]{40}$/.test(revision) ? revision : void 0,
|
|
266
|
+
causes,
|
|
267
|
+
postgresContext: pgContext,
|
|
268
|
+
eventTypes: (input.eventTypes ?? []).slice(0, 32).filter((type) => SessionEventType.safeParse(type).success)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/index.ts
|
|
5
273
|
function sandboxLeaseTelemetryKey(workspaceId, sandboxGroupId) {
|
|
6
|
-
return `slk_${
|
|
274
|
+
return `slk_${createHash3("sha256").update("opengeni:sandbox-lease-telemetry:v1\0").update(workspaceId).update("\0").update(sandboxGroupId).digest("hex").slice(0, 32)}`;
|
|
275
|
+
}
|
|
276
|
+
function turnExecutionTelemetryKey(workspaceId, sessionId, attemptId) {
|
|
277
|
+
return `turn_${createHash3("sha256").update("opengeni:turn-execution-telemetry:v1\0").update(workspaceId).update("\0").update(sessionId).update("\0").update(attemptId).digest("hex").slice(0, 32)}`;
|
|
7
278
|
}
|
|
8
279
|
var OPENGENI_OBSERVABILITY_DISTRIBUTION = {
|
|
9
280
|
monitoringNamespaceLabel: "opengeni.ai/monitoring",
|
|
@@ -51,26 +322,6 @@ var SANDBOX_OPERATION_NAMES = /* @__PURE__ */ new Set([
|
|
|
51
322
|
"serializeSessionState"
|
|
52
323
|
]);
|
|
53
324
|
var TENANCY_COMPATIBILITY_LANES = [
|
|
54
|
-
/**
|
|
55
|
-
* A provider connection carrying `authority_scope = 'legacy_user'` was
|
|
56
|
-
* resolved for accepted use: a personal row with no common authority or
|
|
57
|
-
* grant, admitted through `legacy_user_compatibility` provenance instead of
|
|
58
|
-
* an explicit delegation. Migration 0256 backfilled the existing rows, but
|
|
59
|
-
* its `bind_connection_authority` trigger still classifies a NEW personal
|
|
60
|
-
* connection this way whenever the inserting subject has no active
|
|
61
|
-
* organization membership, so this counter is expected to stay above zero
|
|
62
|
-
* until that classification changes. That is the finding, not a defect.
|
|
63
|
-
*/
|
|
64
|
-
"connection_legacy_user",
|
|
65
|
-
/**
|
|
66
|
-
* A workspace-scope connection ref carried no connection id, so the
|
|
67
|
-
* accepted-use authority (migration 0279) could not identify the exact row
|
|
68
|
-
* and the request fell back to the unprivileged pre-snapshot resolution.
|
|
69
|
-
* This lane writes no `connection_use_audit_facts` row and leaves no durable
|
|
70
|
-
* trace of any kind, so a use counter is the only possible evidence it was
|
|
71
|
-
* taken at all.
|
|
72
|
-
*/
|
|
73
|
-
"connection_pre_snapshot_ref",
|
|
74
325
|
/**
|
|
75
326
|
* A NEW persistable `/workspace` mutation was refused because the writer
|
|
76
327
|
* behind it has no recorded authority (`authority_unattributed`): either a
|
|
@@ -376,6 +627,17 @@ var PUBLIC_TELEMETRY_ERROR_ORIGINS = /* @__PURE__ */ new Set([
|
|
|
376
627
|
"worker",
|
|
377
628
|
"worker-lifecycle"
|
|
378
629
|
]);
|
|
630
|
+
var MATERIALIZATION_FAILURE_REASONS = /* @__PURE__ */ new Set([
|
|
631
|
+
"path_not_visible",
|
|
632
|
+
"command_failed",
|
|
633
|
+
"command_pending",
|
|
634
|
+
"invalid_response",
|
|
635
|
+
"command_error"
|
|
636
|
+
]);
|
|
637
|
+
var MATERIALIZATION_FAILURE_METRIC = {
|
|
638
|
+
name: "opengeni_sandbox_materialization_verification_failures_total",
|
|
639
|
+
help: "Sandbox destination visibility-check failures by bounded reason; exact command evidence is retained in the session failure event."
|
|
640
|
+
};
|
|
379
641
|
function createObservability(settings, options) {
|
|
380
642
|
return new Observability(settings, options);
|
|
381
643
|
}
|
|
@@ -409,14 +671,54 @@ var Observability = class {
|
|
|
409
671
|
});
|
|
410
672
|
this.registerSandboxRolloutConfig();
|
|
411
673
|
this.registerTenancyCompatibilityLanes();
|
|
674
|
+
if (["worker", "worker-control", "worker-turn", "api"].includes(options.component)) {
|
|
675
|
+
for (const backend of [...SANDBOX_OPERATION_BACKENDS, "unknown"]) {
|
|
676
|
+
for (const reason of [...MATERIALIZATION_FAILURE_REASONS, "unknown"]) {
|
|
677
|
+
this.incrementCounter({
|
|
678
|
+
...MATERIALIZATION_FAILURE_METRIC,
|
|
679
|
+
labels: { backend, reason },
|
|
680
|
+
amount: 0
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (options.component === "worker" || options.component === "worker-control") {
|
|
686
|
+
for (const outcome of WORKFLOW_WAKE_OUTCOMES) {
|
|
687
|
+
this.incrementCounter({ ...WORKFLOW_WAKE_METRIC, labels: { outcome }, amount: 0 });
|
|
688
|
+
}
|
|
689
|
+
for (const reason of [...WORKFLOW_WAKE_ADMISSION_BLOCKERS, "unknown"]) {
|
|
690
|
+
this.incrementCounter({ ...WORKFLOW_WAKE_BLOCKER_METRIC, labels: { reason }, amount: 0 });
|
|
691
|
+
}
|
|
692
|
+
}
|
|
412
693
|
}
|
|
413
694
|
}
|
|
695
|
+
spanBatch = [];
|
|
696
|
+
spanBatchScheduled = false;
|
|
697
|
+
diagnosticQueue = new ExportQueue((outcome) => {
|
|
698
|
+
this.incrementCounter({
|
|
699
|
+
name: "opengeni_diagnostic_exports_total",
|
|
700
|
+
help: "Protected diagnostic export outcomes.",
|
|
701
|
+
labels: { outcome }
|
|
702
|
+
});
|
|
703
|
+
});
|
|
414
704
|
registry = new Registry();
|
|
415
705
|
counters = /* @__PURE__ */ new Map();
|
|
416
706
|
gauges = /* @__PURE__ */ new Map();
|
|
417
707
|
histograms = /* @__PURE__ */ new Map();
|
|
418
708
|
registrations = /* @__PURE__ */ new Map();
|
|
419
|
-
|
|
709
|
+
exportQueue = new ExportQueue((outcome) => {
|
|
710
|
+
this.incrementCounter({
|
|
711
|
+
name: "opengeni_telemetry_exports_total",
|
|
712
|
+
help: "Bounded telemetry export outcomes.",
|
|
713
|
+
labels: { outcome }
|
|
714
|
+
});
|
|
715
|
+
if (outcome === "failed")
|
|
716
|
+
this.warn("OTLP span export failed", {
|
|
717
|
+
errorClass: "TelemetryExportError",
|
|
718
|
+
errorCode: "otlp_export_failed",
|
|
719
|
+
origin: "observability"
|
|
720
|
+
});
|
|
721
|
+
}, 8);
|
|
420
722
|
now;
|
|
421
723
|
exporter;
|
|
422
724
|
resourceAttributes;
|
|
@@ -479,6 +781,7 @@ var Observability = class {
|
|
|
479
781
|
}
|
|
480
782
|
return;
|
|
481
783
|
}
|
|
784
|
+
const context = currentTraceContext();
|
|
482
785
|
const record = {
|
|
483
786
|
timestamp: new Date(this.now()).toISOString(),
|
|
484
787
|
level,
|
|
@@ -486,7 +789,8 @@ var Observability = class {
|
|
|
486
789
|
service: this.settings.serviceName,
|
|
487
790
|
environment: this.settings.environment,
|
|
488
791
|
component: this.options.component,
|
|
489
|
-
...cleanAttributes(publicAttributes)
|
|
792
|
+
...cleanAttributes(publicAttributes),
|
|
793
|
+
...context ? { traceId: context.traceId, spanId: context.spanId } : {}
|
|
490
794
|
};
|
|
491
795
|
const serialized = JSON.stringify(record);
|
|
492
796
|
if (level === "warn") {
|
|
@@ -497,14 +801,25 @@ var Observability = class {
|
|
|
497
801
|
console.log(serialized);
|
|
498
802
|
}
|
|
499
803
|
}
|
|
500
|
-
startSpan(name, attributes = {}) {
|
|
501
|
-
const
|
|
804
|
+
startSpan(name, attributes = {}, options = {}) {
|
|
805
|
+
const parent = options.parent === null ? void 0 : validTraceContext(options.parent ?? currentTraceContext());
|
|
806
|
+
const links = (options.links ?? []).slice(0, 8).flatMap((link) => {
|
|
807
|
+
const valid = validTraceContext(link);
|
|
808
|
+
return valid ? [valid] : [];
|
|
809
|
+
});
|
|
810
|
+
const traceId = parent?.traceId ?? randomHex(16);
|
|
502
811
|
const spanId = randomHex(8);
|
|
503
|
-
const
|
|
812
|
+
const now = this.now();
|
|
813
|
+
const startMs = Number.isFinite(options.startTimeMs) ? Math.min(now, Math.max(0, options.startTimeMs)) : now;
|
|
504
814
|
let ended = false;
|
|
505
815
|
return {
|
|
506
816
|
traceId,
|
|
507
817
|
spanId,
|
|
818
|
+
addLink: (context) => {
|
|
819
|
+
const valid = validTraceContext(context);
|
|
820
|
+
if (!ended && valid && links.length < 8 && !links.some((link) => link.traceId === valid.traceId && link.spanId === valid.spanId))
|
|
821
|
+
links.push(valid);
|
|
822
|
+
},
|
|
508
823
|
end: (input = {}) => {
|
|
509
824
|
if (ended) {
|
|
510
825
|
return;
|
|
@@ -515,7 +830,9 @@ var Observability = class {
|
|
|
515
830
|
this.exportSpan({
|
|
516
831
|
traceId,
|
|
517
832
|
spanId,
|
|
518
|
-
|
|
833
|
+
...parent ? { parentSpanId: parent.spanId } : {},
|
|
834
|
+
links,
|
|
835
|
+
name: boundedOtlpString(name),
|
|
519
836
|
startMs,
|
|
520
837
|
endMs: this.now(),
|
|
521
838
|
attributes: {
|
|
@@ -621,11 +938,68 @@ var Observability = class {
|
|
|
621
938
|
}
|
|
622
939
|
return await this.registry.metrics();
|
|
623
940
|
}
|
|
624
|
-
/**
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
941
|
+
/** Emit a content-free anchor after successful, non-replayed admission. */
|
|
942
|
+
recordAdmissionTrace(eventId) {
|
|
943
|
+
const identity = admissionTraceContext(eventId);
|
|
944
|
+
if (!identity) return;
|
|
945
|
+
const current = currentTraceContext();
|
|
946
|
+
const now = this.now();
|
|
947
|
+
this.exportSpan({
|
|
948
|
+
...identity,
|
|
949
|
+
name: "api.turn.admitted",
|
|
950
|
+
startMs: now,
|
|
951
|
+
endMs: now,
|
|
952
|
+
attributes: {},
|
|
953
|
+
links: current ? [current] : []
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/** Emit a closed protected record independently of application persistence. */
|
|
957
|
+
recordFailureDiagnostic(input) {
|
|
958
|
+
const record = failureDiagnostic(input, this.settings.deploymentRevision);
|
|
959
|
+
const endpoint = this.settings.observabilityDiagnosticsEndpoint;
|
|
960
|
+
if (!endpoint) {
|
|
961
|
+
this.incrementCounter({
|
|
962
|
+
name: "opengeni_diagnostic_exports_total",
|
|
963
|
+
help: "Protected diagnostic export outcomes.",
|
|
964
|
+
labels: { outcome: "disabled" }
|
|
965
|
+
});
|
|
966
|
+
return record.diagnosticId;
|
|
628
967
|
}
|
|
968
|
+
const context = currentTraceContext();
|
|
969
|
+
const body = {
|
|
970
|
+
resourceLogs: [
|
|
971
|
+
{
|
|
972
|
+
resource: { attributes: otlpAttributes(this.resourceAttributes) },
|
|
973
|
+
scopeLogs: [
|
|
974
|
+
{
|
|
975
|
+
scope: { name: "@opengeni/observability/diagnostics", version: "1" },
|
|
976
|
+
logRecords: [
|
|
977
|
+
{
|
|
978
|
+
timeUnixNano: millisToNanos(this.now()),
|
|
979
|
+
severityNumber: 17,
|
|
980
|
+
severityText: "ERROR",
|
|
981
|
+
...context ? { traceId: context.traceId, spanId: context.spanId } : {},
|
|
982
|
+
body: { stringValue: JSON.stringify(record) }
|
|
983
|
+
}
|
|
984
|
+
]
|
|
985
|
+
}
|
|
986
|
+
]
|
|
987
|
+
}
|
|
988
|
+
]
|
|
989
|
+
};
|
|
990
|
+
this.diagnosticQueue.enqueue(
|
|
991
|
+
() => this.exporter(
|
|
992
|
+
`${endpoint.replace(/\/$/, "")}/v1/logs`,
|
|
993
|
+
body,
|
|
994
|
+
parseHeaders(this.settings.observabilityDiagnosticsHeaders ?? "")
|
|
995
|
+
)
|
|
996
|
+
);
|
|
997
|
+
return record.diagnosticId;
|
|
998
|
+
}
|
|
999
|
+
/** Best-effort bounded drain, including when a custom transport never settles. */
|
|
1000
|
+
async flush(timeoutMs = 1e3) {
|
|
1001
|
+
this.submitSpanBatch();
|
|
1002
|
+
await Promise.all([this.exportQueue.flush(timeoutMs), this.diagnosticQueue.flush(timeoutMs)]);
|
|
629
1003
|
}
|
|
630
1004
|
counter(name, help, labelNames) {
|
|
631
1005
|
const existing = this.counters.get(name);
|
|
@@ -677,7 +1051,6 @@ var Observability = class {
|
|
|
677
1051
|
if (!this.settings.observabilityOtlpEndpoint) {
|
|
678
1052
|
return;
|
|
679
1053
|
}
|
|
680
|
-
const endpoint = `${this.settings.observabilityOtlpEndpoint.replace(/\/$/, "")}/v1/traces`;
|
|
681
1054
|
const body = {
|
|
682
1055
|
resourceSpans: [
|
|
683
1056
|
{
|
|
@@ -694,6 +1067,8 @@ var Observability = class {
|
|
|
694
1067
|
{
|
|
695
1068
|
traceId: span.traceId,
|
|
696
1069
|
spanId: span.spanId,
|
|
1070
|
+
...span.parentSpanId ? { parentSpanId: span.parentSpanId } : {},
|
|
1071
|
+
links: span.links,
|
|
697
1072
|
name: span.name,
|
|
698
1073
|
kind: 1,
|
|
699
1074
|
startTimeUnixNano: millisToNanos(span.startMs),
|
|
@@ -707,19 +1082,28 @@ var Observability = class {
|
|
|
707
1082
|
}
|
|
708
1083
|
]
|
|
709
1084
|
};
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
errorCode: "otlp_export_failed",
|
|
717
|
-
origin: "observability"
|
|
1085
|
+
this.spanBatch.push(body.resourceSpans[0]);
|
|
1086
|
+
if (!this.spanBatchScheduled) {
|
|
1087
|
+
this.spanBatchScheduled = true;
|
|
1088
|
+
queueMicrotask(() => {
|
|
1089
|
+
this.spanBatchScheduled = false;
|
|
1090
|
+
this.submitSpanBatch();
|
|
718
1091
|
});
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1092
|
+
}
|
|
1093
|
+
if (this.spanBatch.length >= 32) this.submitSpanBatch();
|
|
1094
|
+
}
|
|
1095
|
+
submitSpanBatch() {
|
|
1096
|
+
if (!this.spanBatch.length || !this.settings.observabilityOtlpEndpoint) return;
|
|
1097
|
+
const resourceSpans = this.spanBatch;
|
|
1098
|
+
this.spanBatch = [];
|
|
1099
|
+
const endpoint = `${this.settings.observabilityOtlpEndpoint.replace(/\/$/, "")}/v1/traces`;
|
|
1100
|
+
this.exportQueue.enqueue(
|
|
1101
|
+
() => this.exporter(
|
|
1102
|
+
endpoint,
|
|
1103
|
+
{ resourceSpans },
|
|
1104
|
+
parseHeaders(this.settings.observabilityOtlpHeaders)
|
|
1105
|
+
)
|
|
1106
|
+
);
|
|
723
1107
|
}
|
|
724
1108
|
};
|
|
725
1109
|
function sandboxOperationMetricObserver(observability) {
|
|
@@ -738,6 +1122,13 @@ function sandboxOperationMetricObserver(observability) {
|
|
|
738
1122
|
labels: { backend, op },
|
|
739
1123
|
value: Math.max(0, observation.durationMs) / 1e3
|
|
740
1124
|
});
|
|
1125
|
+
if (op === "materializeEntry" && observation.outcome === "failed" && observation.materializationFailureReason) {
|
|
1126
|
+
const reason = MATERIALIZATION_FAILURE_REASONS.has(observation.materializationFailureReason) ? observation.materializationFailureReason : "unknown";
|
|
1127
|
+
observability.incrementCounter({
|
|
1128
|
+
...MATERIALIZATION_FAILURE_METRIC,
|
|
1129
|
+
labels: { backend, reason }
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
741
1132
|
} catch {
|
|
742
1133
|
try {
|
|
743
1134
|
observability.incrementCounter({
|
|
@@ -750,6 +1141,32 @@ function sandboxOperationMetricObserver(observability) {
|
|
|
750
1141
|
}
|
|
751
1142
|
};
|
|
752
1143
|
}
|
|
1144
|
+
function sandboxCaptureWaitMetricObserver(observability) {
|
|
1145
|
+
return (observation) => {
|
|
1146
|
+
const stage = observation.captureWaitStage;
|
|
1147
|
+
if (stage !== "admission" && stage !== "provider") return;
|
|
1148
|
+
if (!Number.isFinite(observation.durationMs) || observation.durationMs < 0) return;
|
|
1149
|
+
const backend = SANDBOX_OPERATION_BACKENDS.has(observation.backend) ? observation.backend : "unknown";
|
|
1150
|
+
const op = SANDBOX_OPERATION_NAMES.has(observation.op) ? observation.op : "unknown";
|
|
1151
|
+
const outcome = observation.outcome === "ok" ? "completed" : "failed";
|
|
1152
|
+
try {
|
|
1153
|
+
observability.observeHistogram({
|
|
1154
|
+
name: "opengeni_sandbox_capture_wait_duration_seconds",
|
|
1155
|
+
help: "Workspace capture gate wait duration across every routed operation, not only startup.",
|
|
1156
|
+
labels: { backend, op, stage, outcome },
|
|
1157
|
+
value: observation.durationMs / 1e3
|
|
1158
|
+
});
|
|
1159
|
+
observability.startSpan(
|
|
1160
|
+
`sandbox.capture_wait.${stage}`,
|
|
1161
|
+
{ backend, outcome },
|
|
1162
|
+
{
|
|
1163
|
+
startTimeMs: Date.now() - observation.durationMs
|
|
1164
|
+
}
|
|
1165
|
+
).end({ ...outcome === "failed" ? { error: true } : {} });
|
|
1166
|
+
} catch {
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
753
1170
|
function interactionOperationMetricObserver(observability) {
|
|
754
1171
|
if (!observability) return () => void 0;
|
|
755
1172
|
return (observation) => {
|
|
@@ -858,6 +1275,70 @@ function workspaceInsightsMetricObserver(observability) {
|
|
|
858
1275
|
function boundedMetricEnum(allowed, value) {
|
|
859
1276
|
return allowed.has(value) ? value : "unknown";
|
|
860
1277
|
}
|
|
1278
|
+
var WORKFLOW_WAKE_ADMISSION_BLOCKERS = /* @__PURE__ */ new Set([
|
|
1279
|
+
"pending_agent_steer",
|
|
1280
|
+
"pending_prompt_turn",
|
|
1281
|
+
"pending_quiescence",
|
|
1282
|
+
"pending_machine_input",
|
|
1283
|
+
"pending_input_wait"
|
|
1284
|
+
]);
|
|
1285
|
+
var WORKFLOW_WAKE_OUTCOMES = [
|
|
1286
|
+
"signal_accepted",
|
|
1287
|
+
"acknowledged",
|
|
1288
|
+
"pending_admission",
|
|
1289
|
+
"unconfirmed",
|
|
1290
|
+
"failed"
|
|
1291
|
+
];
|
|
1292
|
+
var WORKFLOW_WAKE_METRIC = {
|
|
1293
|
+
name: "opengeni_session_workflow_wake_observations_total",
|
|
1294
|
+
help: "Workflow wake dispatch observations; signal acceptance overlaps with admission outcomes, and retries are counted again."
|
|
1295
|
+
};
|
|
1296
|
+
var WORKFLOW_WAKE_BLOCKER_METRIC = {
|
|
1297
|
+
name: "opengeni_session_workflow_wake_admission_blockers_total",
|
|
1298
|
+
help: "Workflow wake dispatch attempts awaiting durable admission, by bounded blocker; repeated revisions count again."
|
|
1299
|
+
};
|
|
1300
|
+
function recordWorkflowWakeReconciliation(observability, result) {
|
|
1301
|
+
const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
1302
|
+
try {
|
|
1303
|
+
for (const [outcome, amount] of [
|
|
1304
|
+
["signal_accepted", result.signaled],
|
|
1305
|
+
["acknowledged", result.delivered],
|
|
1306
|
+
["pending_admission", result.pendingAdmission],
|
|
1307
|
+
["unconfirmed", result.unconfirmed],
|
|
1308
|
+
["failed", result.failed]
|
|
1309
|
+
]) {
|
|
1310
|
+
observability.incrementCounter({
|
|
1311
|
+
...WORKFLOW_WAKE_METRIC,
|
|
1312
|
+
labels: { outcome },
|
|
1313
|
+
amount: count(amount)
|
|
1314
|
+
});
|
|
1315
|
+
if (count(amount) > 0) {
|
|
1316
|
+
observability.info("Session workflow wake dispatch observed", {
|
|
1317
|
+
surface: "session_workflow_wake",
|
|
1318
|
+
outcome,
|
|
1319
|
+
attempts: count(amount)
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
for (const [blocker, amount] of Object.entries(result.pendingAdmissionBlockers)) {
|
|
1324
|
+
if (count(amount) === 0) continue;
|
|
1325
|
+
const reason = boundedMetricEnum(WORKFLOW_WAKE_ADMISSION_BLOCKERS, blocker);
|
|
1326
|
+
observability.incrementCounter({
|
|
1327
|
+
...WORKFLOW_WAKE_BLOCKER_METRIC,
|
|
1328
|
+
labels: { reason },
|
|
1329
|
+
amount: count(amount)
|
|
1330
|
+
});
|
|
1331
|
+
observability.info("Session workflow wake admission pending", {
|
|
1332
|
+
surface: "session_workflow_wake",
|
|
1333
|
+
outcome: "pending_admission",
|
|
1334
|
+
reason,
|
|
1335
|
+
attempts: count(amount)
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
} catch {
|
|
1339
|
+
recordObserverFailure(observability, "session_workflow_wake");
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
861
1342
|
function boundedMetricDuration(durationMs) {
|
|
862
1343
|
return Number.isFinite(durationMs) ? Math.max(0, durationMs) / 1e3 : 0;
|
|
863
1344
|
}
|
|
@@ -874,7 +1355,7 @@ function recordObserverFailure(observability, observer) {
|
|
|
874
1355
|
function logStartupDependencyRetry(observability, event) {
|
|
875
1356
|
const dependency = PUBLIC_STARTUP_DEPENDENCIES.has(event.label) ? event.label : "Dependency";
|
|
876
1357
|
observability.warn(
|
|
877
|
-
`Startup dependency ${dependency}
|
|
1358
|
+
`Startup dependency ${dependency} check failed; retrying (${event.attempt}/${event.attempts} in ${event.delayMs}ms)`,
|
|
878
1359
|
{
|
|
879
1360
|
dependency,
|
|
880
1361
|
attempt: event.attempt,
|
|
@@ -905,6 +1386,7 @@ function projectPublicTelemetryAttributes(attributes) {
|
|
|
905
1386
|
...projectStartupDependencyAttributes(attributes),
|
|
906
1387
|
...projectPublicChannelADiagnosticAttributes(attributes),
|
|
907
1388
|
...projectApiFatalDiagnosticAttributes(attributes),
|
|
1389
|
+
...projectSnapshotDiagnosticAttributes(attributes),
|
|
908
1390
|
...projectPublicDiagnosticAttributes(attributes)
|
|
909
1391
|
};
|
|
910
1392
|
}
|
|
@@ -958,6 +1440,42 @@ function projectPublicChannelADiagnosticAttributes(attributes) {
|
|
|
958
1440
|
...typeof sandboxLeaseKey === "string" && PUBLIC_TELEMETRY_OPAQUE_ATTRIBUTE_PATTERNS.get("sandboxLeaseKey")?.test(sandboxLeaseKey) ? { sandboxLeaseKey } : {}
|
|
959
1441
|
};
|
|
960
1442
|
}
|
|
1443
|
+
function projectSnapshotDiagnosticAttributes(attributes) {
|
|
1444
|
+
if (attributes.errorClass !== "SnapshotOperationError") return {};
|
|
1445
|
+
const projected = {};
|
|
1446
|
+
const causeName = attributes.causeName;
|
|
1447
|
+
if (typeof causeName === "string" && ["SnapshotTimeoutError", "SandboxProviderCaptureTimeoutError", "SandboxProviderError"].includes(
|
|
1448
|
+
causeName
|
|
1449
|
+
))
|
|
1450
|
+
projected.causeName = causeName;
|
|
1451
|
+
const name = attributes.providerErrorName;
|
|
1452
|
+
if (typeof name === "string" && [
|
|
1453
|
+
"ClientError",
|
|
1454
|
+
"TimeoutError",
|
|
1455
|
+
"ConnectionError",
|
|
1456
|
+
"AuthError",
|
|
1457
|
+
"NotFoundError",
|
|
1458
|
+
"InvalidError",
|
|
1459
|
+
"RemoteError",
|
|
1460
|
+
"AbortError"
|
|
1461
|
+
].includes(name))
|
|
1462
|
+
projected.providerErrorName = name;
|
|
1463
|
+
const grpc = attributes.providerGrpcCode;
|
|
1464
|
+
if (name === "ClientError" && typeof grpc === "number" && Number.isInteger(grpc) && grpc >= 0 && grpc <= 16)
|
|
1465
|
+
projected.providerGrpcCode = grpc;
|
|
1466
|
+
const http = attributes.providerHttpStatus;
|
|
1467
|
+
if (typeof http === "number" && Number.isInteger(http) && http >= 100 && http <= 599)
|
|
1468
|
+
projected.providerHttpStatus = http;
|
|
1469
|
+
if (typeof attributes.providerRetryable === "boolean")
|
|
1470
|
+
projected.providerRetryable = attributes.providerRetryable;
|
|
1471
|
+
const leaseKey = attributes.sandboxLeaseKey;
|
|
1472
|
+
if (typeof leaseKey === "string" && /^slk_[0-9a-f]{32}$/.test(leaseKey))
|
|
1473
|
+
projected.sandboxLeaseKey = leaseKey;
|
|
1474
|
+
const epoch = attributes.leaseEpoch;
|
|
1475
|
+
if (typeof epoch === "number" && Number.isSafeInteger(epoch) && epoch >= 0)
|
|
1476
|
+
projected.leaseEpoch = epoch;
|
|
1477
|
+
return projected;
|
|
1478
|
+
}
|
|
961
1479
|
function projectPublicDiagnosticAttributes(attributes) {
|
|
962
1480
|
const errorClass = attributes.errorClass;
|
|
963
1481
|
const errorCode = attributes.errorCode;
|
|
@@ -1043,7 +1561,8 @@ async function defaultExporter(url, body, headers) {
|
|
|
1043
1561
|
"content-type": "application/json",
|
|
1044
1562
|
...headers
|
|
1045
1563
|
},
|
|
1046
|
-
body: JSON.stringify(body)
|
|
1564
|
+
body: JSON.stringify(body),
|
|
1565
|
+
signal: AbortSignal.timeout(1e3)
|
|
1047
1566
|
});
|
|
1048
1567
|
if (!response.ok) {
|
|
1049
1568
|
throw new Error(`OTLP endpoint returned HTTP ${response.status}`);
|
|
@@ -1053,15 +1572,24 @@ export {
|
|
|
1053
1572
|
OPENGENI_OBSERVABILITY_DISTRIBUTION,
|
|
1054
1573
|
Observability,
|
|
1055
1574
|
TENANCY_COMPATIBILITY_LANES,
|
|
1575
|
+
admissionTraceContext,
|
|
1056
1576
|
createObservability,
|
|
1577
|
+
currentTraceContext,
|
|
1057
1578
|
interactionAuthMetricObserver,
|
|
1058
1579
|
interactionInterventionMetricObserver,
|
|
1059
1580
|
interactionOperationMetricObserver,
|
|
1581
|
+
linkCurrentSpanToAdmission,
|
|
1060
1582
|
logStartupDependencyRetry,
|
|
1061
1583
|
parseHeaders,
|
|
1584
|
+
parseTraceparent,
|
|
1062
1585
|
recordTenancyCompatibilityLaneUse,
|
|
1586
|
+
recordWorkflowWakeReconciliation,
|
|
1587
|
+
sandboxCaptureWaitMetricObserver,
|
|
1063
1588
|
sandboxLeaseTelemetryKey,
|
|
1064
1589
|
sandboxOperationMetricObserver,
|
|
1590
|
+
traceparent,
|
|
1591
|
+
turnExecutionTelemetryKey,
|
|
1592
|
+
withTraceContext,
|
|
1065
1593
|
workspaceInsightsMetricObserver
|
|
1066
1594
|
};
|
|
1067
1595
|
//# sourceMappingURL=index.js.map
|