@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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { SessionEventType } from "@opengeni/contracts";
|
|
3
|
+
|
|
4
|
+
const CODES = [
|
|
5
|
+
"api_startup_failed",
|
|
6
|
+
"api_unhandled_rejection",
|
|
7
|
+
"api_uncaught_exception",
|
|
8
|
+
"db_deadlock",
|
|
9
|
+
"db_serialization_failure",
|
|
10
|
+
"db_failure",
|
|
11
|
+
"retained_process_fenced",
|
|
12
|
+
"retained_process_proof_failed",
|
|
13
|
+
] as const;
|
|
14
|
+
const STAGES = [
|
|
15
|
+
"startup",
|
|
16
|
+
"running",
|
|
17
|
+
"session_events.append_generic",
|
|
18
|
+
"session_events.append_for_turn_attempt",
|
|
19
|
+
"preclaim",
|
|
20
|
+
"session_attempts.claim",
|
|
21
|
+
"failure_settlement",
|
|
22
|
+
"sandbox_retained_processes.proof",
|
|
23
|
+
] as const;
|
|
24
|
+
const RETRIES = ["not_retryable", "exhausted", "unknown"] as const;
|
|
25
|
+
// Reviewed schema names, never a syntactic allowlist for arbitrary driver strings.
|
|
26
|
+
const CONSTRAINTS = new Set([
|
|
27
|
+
"session_events_workspace_account_fk",
|
|
28
|
+
"session_events_turn_association_check",
|
|
29
|
+
"session_events_payload_bytes_check",
|
|
30
|
+
"session_events_type_bytes_check",
|
|
31
|
+
"session_events_duplicate_classification_check",
|
|
32
|
+
"sandbox_retained_processes_reconcile_proof_check",
|
|
33
|
+
"sandbox_retained_processes_reconcile_claim_check",
|
|
34
|
+
"sandbox_retained_processes_settlement_check",
|
|
35
|
+
]);
|
|
36
|
+
const ERROR_NAMES = new Set([
|
|
37
|
+
"Error",
|
|
38
|
+
"TypeError",
|
|
39
|
+
"RangeError",
|
|
40
|
+
"AggregateError",
|
|
41
|
+
"PostgresError",
|
|
42
|
+
"DrizzleQueryError",
|
|
43
|
+
"SessionEventPersistenceError",
|
|
44
|
+
"SandboxWorkspaceMutationFencedError",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// Human-reviewed code locations, not a regex permission to publish any path.
|
|
48
|
+
export const DIAGNOSTIC_SOURCE_FILES = [
|
|
49
|
+
"apps/api/src/index.ts",
|
|
50
|
+
"apps/api/src/app.ts",
|
|
51
|
+
"apps/api/src/fatal-process-boundary.ts",
|
|
52
|
+
"apps/api/dist/process/index.js",
|
|
53
|
+
"apps/worker/src/activities/agent-turn/run.ts",
|
|
54
|
+
"apps/worker/src/activities/agent-turn/claim.ts",
|
|
55
|
+
"apps/worker/src/activities/agent-turn/failure-settlement.ts",
|
|
56
|
+
"apps/worker/src/activities/sandbox-lease.ts",
|
|
57
|
+
"apps/worker/dist/process/index.js",
|
|
58
|
+
"packages/db/src/index.ts",
|
|
59
|
+
"packages/db/src/persistence-errors.ts",
|
|
60
|
+
"packages/storage/src/index.ts",
|
|
61
|
+
"packages/observability/src/index.ts",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
export const DIAGNOSTIC_POSTGRES_FUNCTIONS = ["admit_session_attempt_personal_resources"] as const;
|
|
65
|
+
|
|
66
|
+
function reviewedSource(location: string): string | undefined {
|
|
67
|
+
// Match an exact reviewed suffix; discard the host/deployment path entirely.
|
|
68
|
+
return DIAGNOSTIC_SOURCE_FILES.find((file) => location === file || location.endsWith(`/${file}`));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function postgresContext(error: unknown): Array<{ functionName: string; line: number }> {
|
|
72
|
+
if (errorKind(error) !== "PostgresError") return [];
|
|
73
|
+
const where = own(error, "where");
|
|
74
|
+
if (typeof where !== "string") return [];
|
|
75
|
+
return where
|
|
76
|
+
.slice(0, 8_192)
|
|
77
|
+
.split("\n")
|
|
78
|
+
.slice(0, 16)
|
|
79
|
+
.flatMap((value) => {
|
|
80
|
+
const match = /^PL\/pgSQL function (?:public\.)?([a-z_]+)\([^\n]*\) line (\d{1,7}) at /.exec(
|
|
81
|
+
value,
|
|
82
|
+
);
|
|
83
|
+
if (!match || !DIAGNOSTIC_POSTGRES_FUNCTIONS.some((name) => name === match[1])) return [];
|
|
84
|
+
return [{ functionName: match[1]!, line: Number(match[2]) }];
|
|
85
|
+
})
|
|
86
|
+
.slice(0, 8);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type FailureDiagnosticInput = {
|
|
90
|
+
code: (typeof CODES)[number];
|
|
91
|
+
stage: (typeof STAGES)[number];
|
|
92
|
+
retryDecision?: (typeof RETRIES)[number];
|
|
93
|
+
error: unknown;
|
|
94
|
+
attemptId?: string;
|
|
95
|
+
sessionId?: string;
|
|
96
|
+
processId?: string;
|
|
97
|
+
turnId?: string;
|
|
98
|
+
attempts?: number;
|
|
99
|
+
sqlState?: string | null;
|
|
100
|
+
constraint?: string;
|
|
101
|
+
eventTypes?: readonly string[];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function own(value: unknown, key: string): unknown {
|
|
105
|
+
try {
|
|
106
|
+
if (!value || typeof value !== "object") return undefined;
|
|
107
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
108
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function errorKind(error: unknown): string {
|
|
115
|
+
const name = own(error, "name");
|
|
116
|
+
if (typeof name === "string" && ERROR_NAMES.has(name)) return name;
|
|
117
|
+
try {
|
|
118
|
+
const prototype = Object.getPrototypeOf(error);
|
|
119
|
+
if (prototype === TypeError.prototype) return "TypeError";
|
|
120
|
+
if (prototype === RangeError.prototype) return "RangeError";
|
|
121
|
+
if (prototype === AggregateError.prototype) return "AggregateError";
|
|
122
|
+
} catch {
|
|
123
|
+
/* Untrusted proxies do not contribute type information. */
|
|
124
|
+
}
|
|
125
|
+
return "Error";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** No arbitrary property reads, Error.toJSON, messages, SQL, parameters or function names. */
|
|
129
|
+
export function failureDiagnostic(input: FailureDiagnosticInput, revision?: string) {
|
|
130
|
+
const causes: Array<{
|
|
131
|
+
kind: string;
|
|
132
|
+
frames: Array<{ locationHash: string; source?: string; line: number; column: number }>;
|
|
133
|
+
}> = [];
|
|
134
|
+
const pgContext: Array<{ functionName: string; line: number }> = [];
|
|
135
|
+
const seen = new Set<unknown>();
|
|
136
|
+
let error = input.error;
|
|
137
|
+
while (error && typeof error === "object" && !seen.has(error) && causes.length < 4) {
|
|
138
|
+
seen.add(error);
|
|
139
|
+
pgContext.push(...postgresContext(error).slice(0, 8 - pgContext.length));
|
|
140
|
+
const stack = own(error, "stack");
|
|
141
|
+
const frames =
|
|
142
|
+
typeof stack === "string"
|
|
143
|
+
? stack
|
|
144
|
+
.slice(0, 16_384)
|
|
145
|
+
.split("\n")
|
|
146
|
+
.slice(1, 33)
|
|
147
|
+
.flatMap((frame) => {
|
|
148
|
+
const match = /(?:\(|\s)([^\s()]+):(\d{1,7}):(\d{1,7})\)?$/.exec(frame);
|
|
149
|
+
if (!match) return [];
|
|
150
|
+
// Even a path/function can contain a credential. Keep source-location
|
|
151
|
+
// fingerprints plus line/column, not untrusted textual stack bytes.
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
locationHash: createHash("sha256").update(match[1]!).digest("hex"),
|
|
155
|
+
...(reviewedSource(match[1]!) ? { source: reviewedSource(match[1]!)! } : {}),
|
|
156
|
+
line: Number(match[2]),
|
|
157
|
+
column: Number(match[3]),
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
})
|
|
161
|
+
: [];
|
|
162
|
+
causes.push({
|
|
163
|
+
kind: errorKind(error),
|
|
164
|
+
frames,
|
|
165
|
+
});
|
|
166
|
+
error = own(error, "cause");
|
|
167
|
+
}
|
|
168
|
+
const uuid = (value: unknown) =>
|
|
169
|
+
typeof value === "string" &&
|
|
170
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
|
|
171
|
+
? value
|
|
172
|
+
: undefined;
|
|
173
|
+
return {
|
|
174
|
+
schema: "opengeni.failure-diagnostic.v1",
|
|
175
|
+
diagnosticId: randomUUID(),
|
|
176
|
+
code: CODES.includes(input.code) ? input.code : "db_failure",
|
|
177
|
+
stage: STAGES.includes(input.stage) ? input.stage : "failure_settlement",
|
|
178
|
+
retryDecision:
|
|
179
|
+
input.retryDecision && RETRIES.includes(input.retryDecision)
|
|
180
|
+
? input.retryDecision
|
|
181
|
+
: "unknown",
|
|
182
|
+
attemptId: uuid(input.attemptId),
|
|
183
|
+
sessionId: uuid(input.sessionId),
|
|
184
|
+
processId: uuid(input.processId),
|
|
185
|
+
turnId: uuid(input.turnId),
|
|
186
|
+
attempts:
|
|
187
|
+
Number.isSafeInteger(input.attempts) && input.attempts! >= 0 && input.attempts! <= 1_000_000
|
|
188
|
+
? input.attempts
|
|
189
|
+
: undefined,
|
|
190
|
+
sqlState:
|
|
191
|
+
typeof input.sqlState === "string" && /^[0-9A-Z]{5}$/.test(input.sqlState)
|
|
192
|
+
? input.sqlState
|
|
193
|
+
: undefined,
|
|
194
|
+
constraint:
|
|
195
|
+
input.constraint && CONSTRAINTS.has(input.constraint) ? input.constraint : undefined,
|
|
196
|
+
deploymentRevision: revision && /^[0-9a-f]{40}$/.test(revision) ? revision : undefined,
|
|
197
|
+
causes,
|
|
198
|
+
postgresContext: pgContext,
|
|
199
|
+
eventTypes: (input.eventTypes ?? [])
|
|
200
|
+
.slice(0, 32)
|
|
201
|
+
.filter((type) => SessionEventType.safeParse(type).success),
|
|
202
|
+
};
|
|
203
|
+
}
|