@runtime-digital-twin/sdk 1.0.0
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/README.md +214 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/db-wrapper.d.ts +258 -0
- package/dist/db-wrapper.d.ts.map +1 -0
- package/dist/db-wrapper.js +636 -0
- package/dist/event-envelope.d.ts +35 -0
- package/dist/event-envelope.d.ts.map +1 -0
- package/dist/event-envelope.js +101 -0
- package/dist/fastify-plugin.d.ts +29 -0
- package/dist/fastify-plugin.d.ts.map +1 -0
- package/dist/fastify-plugin.js +243 -0
- package/dist/http-sentinels.d.ts +39 -0
- package/dist/http-sentinels.d.ts.map +1 -0
- package/dist/http-sentinels.js +169 -0
- package/dist/http-wrapper.d.ts +25 -0
- package/dist/http-wrapper.d.ts.map +1 -0
- package/dist/http-wrapper.js +477 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/invariants.d.ts +58 -0
- package/dist/invariants.d.ts.map +1 -0
- package/dist/invariants.js +192 -0
- package/dist/multi-service-edge-builder.d.ts +80 -0
- package/dist/multi-service-edge-builder.d.ts.map +1 -0
- package/dist/multi-service-edge-builder.js +107 -0
- package/dist/outbound-matcher.d.ts +192 -0
- package/dist/outbound-matcher.d.ts.map +1 -0
- package/dist/outbound-matcher.js +457 -0
- package/dist/peer-service-resolver.d.ts +22 -0
- package/dist/peer-service-resolver.d.ts.map +1 -0
- package/dist/peer-service-resolver.js +85 -0
- package/dist/redaction.d.ts +111 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +487 -0
- package/dist/replay-logger.d.ts +438 -0
- package/dist/replay-logger.d.ts.map +1 -0
- package/dist/replay-logger.js +434 -0
- package/dist/root-cause-analyzer.d.ts +45 -0
- package/dist/root-cause-analyzer.d.ts.map +1 -0
- package/dist/root-cause-analyzer.js +606 -0
- package/dist/shape-digest-utils.d.ts +45 -0
- package/dist/shape-digest-utils.d.ts.map +1 -0
- package/dist/shape-digest-utils.js +154 -0
- package/dist/trace-bundle-writer.d.ts +52 -0
- package/dist/trace-bundle-writer.d.ts.map +1 -0
- package/dist/trace-bundle-writer.js +267 -0
- package/dist/trace-loader.d.ts +69 -0
- package/dist/trace-loader.d.ts.map +1 -0
- package/dist/trace-loader.js +146 -0
- package/dist/trace-uploader.d.ts +25 -0
- package/dist/trace-uploader.d.ts.map +1 -0
- package/dist/trace-uploader.js +132 -0
- package/package.json +63 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DbQueryMatcher = void 0;
|
|
37
|
+
exports.normalizeSql = normalizeSql;
|
|
38
|
+
exports.computeParamsHash = computeParamsHash;
|
|
39
|
+
exports.extractOperation = extractOperation;
|
|
40
|
+
exports.compareDbQueries = compareDbQueries;
|
|
41
|
+
exports.parseRecordedDbQueries = parseRecordedDbQueries;
|
|
42
|
+
exports.wrapDbClient = wrapDbClient;
|
|
43
|
+
exports.formatDbQueryMismatch = formatDbQueryMismatch;
|
|
44
|
+
exports.formatUnexpectedDbQuery = formatUnexpectedDbQuery;
|
|
45
|
+
exports.formatMissingDbQuery = formatMissingDbQuery;
|
|
46
|
+
exports.formatBlockedDbAccess = formatBlockedDbAccess;
|
|
47
|
+
exports.formatDbQueryFailure = formatDbQueryFailure;
|
|
48
|
+
exports.createWrappedDbClient = createWrappedDbClient;
|
|
49
|
+
const crypto_1 = require("crypto");
|
|
50
|
+
const trace_bundle_writer_1 = require("./trace-bundle-writer");
|
|
51
|
+
const http_wrapper_1 = require("./http-wrapper");
|
|
52
|
+
/**
|
|
53
|
+
* Normalize SQL for matching: lowercase, trim whitespace, normalize spaces
|
|
54
|
+
*/
|
|
55
|
+
function normalizeSql(sql) {
|
|
56
|
+
return sql
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.trim()
|
|
59
|
+
.replace(/\s+/g, " ")
|
|
60
|
+
.replace(/\(\s+/g, "(")
|
|
61
|
+
.replace(/\s+\)/g, ")");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Compute hash of query parameters
|
|
65
|
+
*/
|
|
66
|
+
function computeParamsHash(params) {
|
|
67
|
+
const paramsStr = JSON.stringify(params);
|
|
68
|
+
const hash = (0, crypto_1.createHash)("sha256").update(paramsStr).digest("hex");
|
|
69
|
+
return `sha256:${hash}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract operation type from SQL
|
|
73
|
+
*/
|
|
74
|
+
function extractOperation(sql) {
|
|
75
|
+
const normalized = sql.trim().toUpperCase();
|
|
76
|
+
if (normalized.startsWith("SELECT"))
|
|
77
|
+
return "SELECT";
|
|
78
|
+
if (normalized.startsWith("INSERT"))
|
|
79
|
+
return "INSERT";
|
|
80
|
+
if (normalized.startsWith("UPDATE"))
|
|
81
|
+
return "UPDATE";
|
|
82
|
+
if (normalized.startsWith("DELETE"))
|
|
83
|
+
return "DELETE";
|
|
84
|
+
if (normalized.startsWith("CREATE"))
|
|
85
|
+
return "CREATE";
|
|
86
|
+
if (normalized.startsWith("DROP"))
|
|
87
|
+
return "DROP";
|
|
88
|
+
if (normalized.startsWith("ALTER"))
|
|
89
|
+
return "ALTER";
|
|
90
|
+
return "UNKNOWN";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Compare two queries and return differences
|
|
94
|
+
*/
|
|
95
|
+
function compareDbQueries(expected, actual) {
|
|
96
|
+
const diffs = [];
|
|
97
|
+
// SQL (normalized)
|
|
98
|
+
const expectedSql = expected.normalizedSql;
|
|
99
|
+
const actualSql = normalizeSql(actual.sql);
|
|
100
|
+
if (expectedSql !== actualSql) {
|
|
101
|
+
diffs.push({ field: "sql", expected: expectedSql, actual: actualSql });
|
|
102
|
+
}
|
|
103
|
+
// Params hash
|
|
104
|
+
const expectedParamsHash = expected.paramsHash;
|
|
105
|
+
const actualParamsHash = computeParamsHash(actual.params);
|
|
106
|
+
if (expectedParamsHash !== actualParamsHash) {
|
|
107
|
+
diffs.push({
|
|
108
|
+
field: "params",
|
|
109
|
+
expected: JSON.stringify(expected.params),
|
|
110
|
+
actual: JSON.stringify(actual.params),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { matches: diffs.length === 0, diffs };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Ordered DB query matcher for replay
|
|
117
|
+
*/
|
|
118
|
+
class DbQueryMatcher {
|
|
119
|
+
recordedQueries = [];
|
|
120
|
+
currentIndex = 0;
|
|
121
|
+
mode;
|
|
122
|
+
traceDir;
|
|
123
|
+
divergences = [];
|
|
124
|
+
logger = null;
|
|
125
|
+
constructor(recordedQueries, mode, traceDir, logger) {
|
|
126
|
+
this.recordedQueries = recordedQueries;
|
|
127
|
+
this.mode = mode;
|
|
128
|
+
this.traceDir = traceDir;
|
|
129
|
+
this.logger = logger || null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Set the logger for structured replay logging
|
|
133
|
+
*/
|
|
134
|
+
setLogger(logger) {
|
|
135
|
+
this.logger = logger;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get all recorded divergences
|
|
139
|
+
*/
|
|
140
|
+
getDivergences() {
|
|
141
|
+
return [...this.divergences];
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get current query index
|
|
145
|
+
*/
|
|
146
|
+
getCurrentIndex() {
|
|
147
|
+
return this.currentIndex;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get total recorded queries
|
|
151
|
+
*/
|
|
152
|
+
getTotalQueries() {
|
|
153
|
+
return this.recordedQueries.length;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if only SELECT queries are in the recorded set
|
|
157
|
+
*/
|
|
158
|
+
isReadOnly() {
|
|
159
|
+
return this.recordedQueries.every((q) => q.operation === "SELECT");
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Match the next DB query against the recorded sequence
|
|
163
|
+
*
|
|
164
|
+
* In strict mode: throws DbQueryMismatch on any mismatch
|
|
165
|
+
* In explain mode: emits divergence and returns best-effort match
|
|
166
|
+
*/
|
|
167
|
+
matchNext(actual) {
|
|
168
|
+
const queryIndex = this.currentIndex;
|
|
169
|
+
const operation = extractOperation(actual.sql);
|
|
170
|
+
const normalizedSql = normalizeSql(actual.sql);
|
|
171
|
+
const paramsHash = computeParamsHash(actual.params);
|
|
172
|
+
// Phase 1: Only support SELECT queries
|
|
173
|
+
if (operation !== "SELECT") {
|
|
174
|
+
const failure = {
|
|
175
|
+
type: "blocked_db_access",
|
|
176
|
+
location: `${this.traceDir}/events.jsonl:query[${queryIndex}]`,
|
|
177
|
+
expected: "SELECT query only (Phase 1)",
|
|
178
|
+
actual: `${operation} query`,
|
|
179
|
+
hint: "DB replay Phase 1 only supports SELECT queries. Write operations are blocked.",
|
|
180
|
+
query: actual,
|
|
181
|
+
};
|
|
182
|
+
// Log blocked query
|
|
183
|
+
this.logger?.dbBlocked({
|
|
184
|
+
operation,
|
|
185
|
+
sql: actual.sql.substring(0, 100),
|
|
186
|
+
reason: "Phase 1 only supports SELECT queries",
|
|
187
|
+
});
|
|
188
|
+
throw failure;
|
|
189
|
+
}
|
|
190
|
+
// Check if we've exceeded recorded queries
|
|
191
|
+
if (queryIndex >= this.recordedQueries.length) {
|
|
192
|
+
const failure = {
|
|
193
|
+
type: "unexpected_db_query",
|
|
194
|
+
location: `${this.traceDir}/events.jsonl:query[${queryIndex}]`,
|
|
195
|
+
expected: `<end of recorded queries after ${this.recordedQueries.length} query(s)>`,
|
|
196
|
+
actual: `${operation}: ${normalizedSql.substring(0, 50)}...`,
|
|
197
|
+
hint: "A DB query occurred during replay that was not recorded in the original trace.",
|
|
198
|
+
queryIndex,
|
|
199
|
+
service: this.traceDir,
|
|
200
|
+
query: actual,
|
|
201
|
+
};
|
|
202
|
+
// Log unexpected query
|
|
203
|
+
this.logger?.dbUnexpected({
|
|
204
|
+
queryIndex,
|
|
205
|
+
operation,
|
|
206
|
+
sql: normalizedSql,
|
|
207
|
+
paramsHash,
|
|
208
|
+
expectedQueryCount: this.recordedQueries.length,
|
|
209
|
+
});
|
|
210
|
+
if (this.mode === "strict") {
|
|
211
|
+
throw failure;
|
|
212
|
+
}
|
|
213
|
+
this.divergences.push({
|
|
214
|
+
type: "divergence",
|
|
215
|
+
severity: "error",
|
|
216
|
+
queryIndex,
|
|
217
|
+
message: `Unexpected DB query: ${actual.sql.substring(0, 50)}...`,
|
|
218
|
+
expected: null,
|
|
219
|
+
actual,
|
|
220
|
+
recoverable: false,
|
|
221
|
+
});
|
|
222
|
+
throw failure;
|
|
223
|
+
}
|
|
224
|
+
const expected = this.recordedQueries[queryIndex];
|
|
225
|
+
const { matches, diffs } = compareDbQueries(expected, actual);
|
|
226
|
+
if (matches) {
|
|
227
|
+
// Log successful match
|
|
228
|
+
this.logger?.dbMatch({
|
|
229
|
+
queryIndex,
|
|
230
|
+
operation,
|
|
231
|
+
sql: actual.sql,
|
|
232
|
+
normalizedSql,
|
|
233
|
+
paramsHash,
|
|
234
|
+
matchedSpanId: expected.spanId,
|
|
235
|
+
rowCount: expected.result.rowCount,
|
|
236
|
+
});
|
|
237
|
+
this.currentIndex++;
|
|
238
|
+
return expected;
|
|
239
|
+
}
|
|
240
|
+
// Explain mode: check if divergence is recoverable
|
|
241
|
+
// Only params difference is recoverable (same query structure)
|
|
242
|
+
const criticalDiffs = diffs.filter((d) => d.field === "sql");
|
|
243
|
+
const recoverable = criticalDiffs.length === 0;
|
|
244
|
+
// Log mismatch
|
|
245
|
+
this.logger?.dbMismatch({
|
|
246
|
+
queryIndex,
|
|
247
|
+
expected: {
|
|
248
|
+
operation: expected.operation,
|
|
249
|
+
sql: expected.normalizedSql,
|
|
250
|
+
paramsHash: expected.paramsHash,
|
|
251
|
+
},
|
|
252
|
+
actual: {
|
|
253
|
+
operation,
|
|
254
|
+
sql: normalizedSql,
|
|
255
|
+
paramsHash,
|
|
256
|
+
},
|
|
257
|
+
differences: diffs,
|
|
258
|
+
recoverable,
|
|
259
|
+
});
|
|
260
|
+
// Mismatch detected
|
|
261
|
+
const failure = {
|
|
262
|
+
type: "db_query_mismatch",
|
|
263
|
+
location: `${this.traceDir}/events.jsonl:query[${queryIndex}]`,
|
|
264
|
+
expected: `${expected.operation}: ${expected.normalizedSql.substring(0, 50)}...`,
|
|
265
|
+
actual: `${operation}: ${normalizedSql.substring(0, 50)}...`,
|
|
266
|
+
hint: "DB query does not match recorded trace. Check for non-determinism or code changes.",
|
|
267
|
+
queryIndex,
|
|
268
|
+
service: this.traceDir,
|
|
269
|
+
diff: diffs,
|
|
270
|
+
};
|
|
271
|
+
if (this.mode === "strict") {
|
|
272
|
+
throw failure;
|
|
273
|
+
}
|
|
274
|
+
this.divergences.push({
|
|
275
|
+
type: "divergence",
|
|
276
|
+
severity: recoverable ? "warning" : "error",
|
|
277
|
+
queryIndex,
|
|
278
|
+
message: `DB query mismatch: ${diffs
|
|
279
|
+
.map((d) => `${d.field}`)
|
|
280
|
+
.join(", ")}`,
|
|
281
|
+
expected,
|
|
282
|
+
actual,
|
|
283
|
+
recoverable,
|
|
284
|
+
});
|
|
285
|
+
console.warn(`[Replay] DB query divergence at query ${queryIndex}: ${failure.expected} vs ${failure.actual}`);
|
|
286
|
+
for (const diff of diffs) {
|
|
287
|
+
console.warn(` ${diff.field}: expected="${diff.expected?.substring(0, 50)}" actual="${diff.actual?.substring(0, 50)}"`);
|
|
288
|
+
}
|
|
289
|
+
if (!recoverable) {
|
|
290
|
+
throw failure;
|
|
291
|
+
}
|
|
292
|
+
// Continue with the recorded response despite param differences
|
|
293
|
+
this.currentIndex++;
|
|
294
|
+
return expected;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if all recorded queries have been matched
|
|
298
|
+
*/
|
|
299
|
+
isComplete() {
|
|
300
|
+
return this.currentIndex >= this.recordedQueries.length;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Get summary of unmatched queries
|
|
304
|
+
*/
|
|
305
|
+
getUnmatchedQueries() {
|
|
306
|
+
return this.recordedQueries.slice(this.currentIndex);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Finalize replay and check for missing DB queries
|
|
310
|
+
*/
|
|
311
|
+
finalize() {
|
|
312
|
+
const missingQueries = [];
|
|
313
|
+
const unmatchedQueries = this.getUnmatchedQueries();
|
|
314
|
+
for (let i = 0; i < unmatchedQueries.length; i++) {
|
|
315
|
+
const recordedQuery = unmatchedQueries[i];
|
|
316
|
+
const queryIndex = this.currentIndex + i;
|
|
317
|
+
const failure = {
|
|
318
|
+
type: "missing_db_query",
|
|
319
|
+
location: `${this.traceDir}/events.jsonl:query[${queryIndex}]`,
|
|
320
|
+
expected: `${recordedQuery.operation}: ${recordedQuery.normalizedSql.substring(0, 50)}...`,
|
|
321
|
+
actual: "<query never executed>",
|
|
322
|
+
hint: "A recorded DB query was never executed during replay.",
|
|
323
|
+
queryIndex,
|
|
324
|
+
service: this.traceDir,
|
|
325
|
+
spanId: recordedQuery.spanId,
|
|
326
|
+
recordedQuery,
|
|
327
|
+
};
|
|
328
|
+
missingQueries.push(failure);
|
|
329
|
+
// Log missing query
|
|
330
|
+
this.logger?.dbMissing({
|
|
331
|
+
queryIndex,
|
|
332
|
+
operation: recordedQuery.operation,
|
|
333
|
+
sql: recordedQuery.normalizedSql,
|
|
334
|
+
spanId: recordedQuery.spanId,
|
|
335
|
+
});
|
|
336
|
+
this.divergences.push({
|
|
337
|
+
type: "divergence",
|
|
338
|
+
severity: "error",
|
|
339
|
+
queryIndex,
|
|
340
|
+
message: `Missing DB query: ${recordedQuery.operation} ${recordedQuery.normalizedSql.substring(0, 50)}...`,
|
|
341
|
+
expected: recordedQuery,
|
|
342
|
+
actual: { sql: "", params: [] },
|
|
343
|
+
recoverable: false,
|
|
344
|
+
});
|
|
345
|
+
console.warn(`[Replay] Missing query ${queryIndex}: expected ${recordedQuery.operation} ${recordedQuery.normalizedSql.substring(0, 50)}...`);
|
|
346
|
+
}
|
|
347
|
+
if (missingQueries.length > 0 && this.mode === "strict") {
|
|
348
|
+
throw missingQueries[0];
|
|
349
|
+
}
|
|
350
|
+
return missingQueries;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
exports.DbQueryMatcher = DbQueryMatcher;
|
|
354
|
+
/**
|
|
355
|
+
* Parse recorded DB queries from trace events
|
|
356
|
+
*/
|
|
357
|
+
function parseRecordedDbQueries(events) {
|
|
358
|
+
const queries = [];
|
|
359
|
+
for (const event of events) {
|
|
360
|
+
if (event.type === "db.query") {
|
|
361
|
+
queries.push({
|
|
362
|
+
spanId: event.spanId,
|
|
363
|
+
operation: event.operation,
|
|
364
|
+
sql: event.sql,
|
|
365
|
+
normalizedSql: normalizeSql(event.sql),
|
|
366
|
+
params: event.params || [],
|
|
367
|
+
paramsHash: computeParamsHash(event.params || []),
|
|
368
|
+
result: {
|
|
369
|
+
rows: event.rows || [],
|
|
370
|
+
rowCount: event.rowCount || 0,
|
|
371
|
+
resultHash: event.resultHash || null,
|
|
372
|
+
resultBlob: event.resultBlob || null,
|
|
373
|
+
},
|
|
374
|
+
durationMs: event.durationMs || 0,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return queries;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Wrap a DB client for recording or replay
|
|
382
|
+
*
|
|
383
|
+
* During recording: captures queries and results
|
|
384
|
+
* During replay: returns recorded results, blocks live DB access
|
|
385
|
+
*/
|
|
386
|
+
function wrapDbClient(client, options) {
|
|
387
|
+
const { mode, matcher, readBlob } = options;
|
|
388
|
+
let queryCount = 0;
|
|
389
|
+
if (mode === "replay") {
|
|
390
|
+
if (!matcher) {
|
|
391
|
+
throw new Error("DbQueryMatcher is required for replay mode");
|
|
392
|
+
}
|
|
393
|
+
// Replay mode: return recorded results, never touch live DB
|
|
394
|
+
return {
|
|
395
|
+
async query(sql, params = []) {
|
|
396
|
+
queryCount++;
|
|
397
|
+
// Match against recorded queries
|
|
398
|
+
const recorded = matcher.matchNext({ sql, params });
|
|
399
|
+
// Return recorded result
|
|
400
|
+
let rows = recorded.result.rows;
|
|
401
|
+
// If rows are stored in blob, read from blob
|
|
402
|
+
if (recorded.result.resultBlob && readBlob && rows.length === 0) {
|
|
403
|
+
const blobRows = await readBlob(recorded.result.resultBlob);
|
|
404
|
+
if (blobRows) {
|
|
405
|
+
rows = blobRows;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
rows,
|
|
410
|
+
rowCount: recorded.result.rowCount,
|
|
411
|
+
};
|
|
412
|
+
},
|
|
413
|
+
getQueryCount() {
|
|
414
|
+
return queryCount;
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// Record mode: capture queries and forward to real client
|
|
419
|
+
if (!client) {
|
|
420
|
+
throw new Error("DbClient is required for record mode");
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
async query(sql, params = []) {
|
|
424
|
+
queryCount++;
|
|
425
|
+
const { bundle, spanId } = (0, http_wrapper_1.getTraceContext)();
|
|
426
|
+
const operation = extractOperation(sql);
|
|
427
|
+
const startTime = Date.now();
|
|
428
|
+
const querySpanId = (0, trace_bundle_writer_1.generateSpanId)();
|
|
429
|
+
try {
|
|
430
|
+
// Execute the real query
|
|
431
|
+
const result = await client.query(sql, params);
|
|
432
|
+
const endTime = Date.now();
|
|
433
|
+
const durationMs = endTime - startTime;
|
|
434
|
+
// Record the query if we have a trace context
|
|
435
|
+
if (bundle) {
|
|
436
|
+
// Store result rows (potentially as blob if large)
|
|
437
|
+
const { bodyHash: resultHash, bodyBlob: resultBlob } = await (0, trace_bundle_writer_1.processBody)(result.rows, bundle.writeBlob);
|
|
438
|
+
await bundle.writeEvent({
|
|
439
|
+
type: "db.query",
|
|
440
|
+
timestamp: startTime,
|
|
441
|
+
spanId: querySpanId,
|
|
442
|
+
parentSpanId: spanId,
|
|
443
|
+
operation,
|
|
444
|
+
sql,
|
|
445
|
+
params,
|
|
446
|
+
rows: resultBlob ? [] : result.rows, // Inline if small, empty if in blob
|
|
447
|
+
resultHash,
|
|
448
|
+
resultBlob,
|
|
449
|
+
rowCount: result.rowCount,
|
|
450
|
+
durationMs,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
const endTime = Date.now();
|
|
457
|
+
// Record error if we have a trace context
|
|
458
|
+
if (bundle) {
|
|
459
|
+
const err = error;
|
|
460
|
+
await bundle.writeEvent({
|
|
461
|
+
type: "error",
|
|
462
|
+
timestamp: endTime,
|
|
463
|
+
spanId: querySpanId,
|
|
464
|
+
parentSpanId: spanId,
|
|
465
|
+
error: {
|
|
466
|
+
name: err.name || "Error",
|
|
467
|
+
message: err.message || String(error),
|
|
468
|
+
stack: err.stack || undefined,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
getQueryCount() {
|
|
476
|
+
return queryCount;
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Format DbQueryMismatch for CLI output
|
|
482
|
+
*/
|
|
483
|
+
function formatDbQueryMismatch(failure) {
|
|
484
|
+
const lines = [
|
|
485
|
+
`[Replay Error] ${failure.type}`,
|
|
486
|
+
` Location: ${failure.location}`,
|
|
487
|
+
` Service: ${failure.service}`,
|
|
488
|
+
` Query #${failure.queryIndex}`,
|
|
489
|
+
` Expected: ${failure.expected}`,
|
|
490
|
+
` Actual: ${failure.actual}`,
|
|
491
|
+
` Differences:`,
|
|
492
|
+
];
|
|
493
|
+
for (const diff of failure.diff) {
|
|
494
|
+
lines.push(` ${diff.field}: "${diff.expected?.substring(0, 50)}" → "${diff.actual?.substring(0, 50)}"`);
|
|
495
|
+
}
|
|
496
|
+
lines.push(` Hint: ${failure.hint}`);
|
|
497
|
+
return lines.join("\n");
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Format UnexpectedDbQuery for CLI output
|
|
501
|
+
*/
|
|
502
|
+
function formatUnexpectedDbQuery(failure) {
|
|
503
|
+
const lines = [
|
|
504
|
+
`[Replay Error] ${failure.type}`,
|
|
505
|
+
` Location: ${failure.location}`,
|
|
506
|
+
` Service: ${failure.service}`,
|
|
507
|
+
` Query #${failure.queryIndex}`,
|
|
508
|
+
` Expected: ${failure.expected}`,
|
|
509
|
+
` Actual: ${failure.actual}`,
|
|
510
|
+
` Query: ${failure.query.sql.substring(0, 100)}...`,
|
|
511
|
+
` Hint: ${failure.hint}`,
|
|
512
|
+
];
|
|
513
|
+
return lines.join("\n");
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Format MissingDbQuery for CLI output
|
|
517
|
+
*/
|
|
518
|
+
function formatMissingDbQuery(failure) {
|
|
519
|
+
const lines = [
|
|
520
|
+
`[Replay Error] ${failure.type}`,
|
|
521
|
+
` Location: ${failure.location}`,
|
|
522
|
+
` Service: ${failure.service}`,
|
|
523
|
+
` Span ID: ${failure.spanId}`,
|
|
524
|
+
` Query #${failure.queryIndex}`,
|
|
525
|
+
` Expected: ${failure.expected}`,
|
|
526
|
+
` Actual: ${failure.actual}`,
|
|
527
|
+
` Hint: ${failure.hint}`,
|
|
528
|
+
];
|
|
529
|
+
return lines.join("\n");
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Format BlockedDbAccess for CLI output
|
|
533
|
+
*/
|
|
534
|
+
function formatBlockedDbAccess(failure) {
|
|
535
|
+
const lines = [
|
|
536
|
+
`[Replay Error] ${failure.type}`,
|
|
537
|
+
` Location: ${failure.location}`,
|
|
538
|
+
` Expected: ${failure.expected}`,
|
|
539
|
+
` Actual: ${failure.actual}`,
|
|
540
|
+
` Query: ${failure.query.sql.substring(0, 100)}...`,
|
|
541
|
+
` Hint: ${failure.hint}`,
|
|
542
|
+
];
|
|
543
|
+
return lines.join("\n");
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Format any DB query failure for CLI output
|
|
547
|
+
*/
|
|
548
|
+
function formatDbQueryFailure(failure) {
|
|
549
|
+
switch (failure.type) {
|
|
550
|
+
case "db_query_mismatch":
|
|
551
|
+
return formatDbQueryMismatch(failure);
|
|
552
|
+
case "unexpected_db_query":
|
|
553
|
+
return formatUnexpectedDbQuery(failure);
|
|
554
|
+
case "missing_db_query":
|
|
555
|
+
return formatMissingDbQuery(failure);
|
|
556
|
+
case "blocked_db_access":
|
|
557
|
+
return formatBlockedDbAccess(failure);
|
|
558
|
+
default:
|
|
559
|
+
return `[Replay Error] Unknown DB failure type`;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async function createWrappedDbClient(options = {}) {
|
|
563
|
+
const { realClient, traceDir, replayMode, readBlob } = options;
|
|
564
|
+
// Detect replay mode
|
|
565
|
+
const mode = process.env.RDT_MODE || process.env.RDT_REPLAY === "1" || process.env.RDT_REPLAY === "true"
|
|
566
|
+
? "replay"
|
|
567
|
+
: "record";
|
|
568
|
+
if (mode === "replay") {
|
|
569
|
+
// Replay mode: create matcher from trace
|
|
570
|
+
const actualTraceDir = traceDir || process.env.RDT_TRACE_DIR || process.env.TRACE_DIR;
|
|
571
|
+
if (!actualTraceDir) {
|
|
572
|
+
console.warn("[DB Wrapper] Replay mode detected but RDT_TRACE_DIR not set. DB queries will be disabled.");
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
// Dynamically import fs/path modules to avoid issues in browser environments
|
|
577
|
+
const { readFile } = await Promise.resolve().then(() => __importStar(require("fs/promises")));
|
|
578
|
+
const { createReadStream } = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
579
|
+
const { createInterface } = await Promise.resolve().then(() => __importStar(require("readline")));
|
|
580
|
+
const { join } = await Promise.resolve().then(() => __importStar(require("path")));
|
|
581
|
+
// Load events and create matcher
|
|
582
|
+
const eventsPath = join(actualTraceDir, "events.jsonl");
|
|
583
|
+
const events = [];
|
|
584
|
+
try {
|
|
585
|
+
const fileStream = createReadStream(eventsPath);
|
|
586
|
+
const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
587
|
+
for await (const line of rl) {
|
|
588
|
+
const trimmed = line.trim();
|
|
589
|
+
if (trimmed) {
|
|
590
|
+
try {
|
|
591
|
+
events.push(JSON.parse(trimmed));
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// Skip malformed lines
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
console.warn(`[DB Wrapper] Failed to load trace events from ${eventsPath}: ${error}`);
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const recordedQueries = parseRecordedDbQueries(events);
|
|
604
|
+
if (recordedQueries.length === 0) {
|
|
605
|
+
// No DB queries in trace - return null (service can handle this)
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
const actualReplayMode = (replayMode || process.env.RDT_REPLAY_MODE || "strict");
|
|
609
|
+
const matcher = new DbQueryMatcher(recordedQueries, actualReplayMode, actualTraceDir);
|
|
610
|
+
// Default blob reader if not provided
|
|
611
|
+
const actualReadBlob = readBlob || (async (blobRef) => {
|
|
612
|
+
const hash = blobRef.startsWith("sha256:") ? blobRef.substring(7) : blobRef;
|
|
613
|
+
const blobPath = join(actualTraceDir, "blobs", `${hash}.json`);
|
|
614
|
+
try {
|
|
615
|
+
return JSON.parse(await readFile(blobPath, "utf8"));
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
return wrapDbClient(null, { mode: "replay", matcher, readBlob: actualReadBlob });
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
console.warn(`[DB Wrapper] Failed to create replay DB client: ${error}`);
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
// Record mode: wrap real client
|
|
630
|
+
if (!realClient) {
|
|
631
|
+
console.warn("[DB Wrapper] Record mode but no realClient provided. DB queries will be disabled.");
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
return wrapDbClient(realClient, { mode: "record" });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base event envelope - all trace events must include these fields
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Base event envelope interface
|
|
6
|
+
*/
|
|
7
|
+
export interface BaseEventEnvelope {
|
|
8
|
+
type: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
traceId: string;
|
|
11
|
+
spanId: string;
|
|
12
|
+
parentSpanId: string | null;
|
|
13
|
+
serviceName: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a base event envelope with required fields
|
|
17
|
+
*
|
|
18
|
+
* @param type - Event type (e.g., 'http.request.inbound')
|
|
19
|
+
* @param timestamp - Event timestamp (milliseconds since epoch)
|
|
20
|
+
* @param traceId - Trace ID
|
|
21
|
+
* @param spanId - Span ID
|
|
22
|
+
* @param parentSpanId - Parent span ID (null for root spans)
|
|
23
|
+
* @param serviceName - Optional service name (will be resolved if not provided)
|
|
24
|
+
* @param additionalFields - Additional event-specific fields
|
|
25
|
+
* @returns Complete event object with base envelope + additional fields
|
|
26
|
+
*/
|
|
27
|
+
export declare function createBaseEvent(type: string, timestamp: number, traceId: string, spanId: string, parentSpanId: string | null, serviceName?: string, additionalFields?: Record<string, any>): BaseEventEnvelope & Record<string, any>;
|
|
28
|
+
/**
|
|
29
|
+
* Validate that an event has all required base envelope fields
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateBaseEnvelope(event: any): {
|
|
32
|
+
valid: boolean;
|
|
33
|
+
errors: string[];
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=event-envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-envelope.d.ts","sourceRoot":"","sources":["../src/event-envelope.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB;AAwCD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,CAAC,EAAE,MAAM,EACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACrC,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAiBzC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,GAAG,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA0BrF"}
|