forge-sql-orm 2.1.4 → 2.1.6
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 +195 -27
- package/dist/ForgeSQLORM.js +632 -192
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +632 -192
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +114 -3
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +125 -7
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/core/SystemTables.d.ts +3654 -0
- package/dist/core/SystemTables.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts +2 -2
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/utils/cacheContextUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/forgeDriver.d.ts +71 -3
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts +11 -0
- package/dist/utils/metadataContextUtils.d.ts.map +1 -0
- package/dist/utils/requestTypeContextUtils.d.ts +8 -0
- package/dist/utils/requestTypeContextUtils.d.ts.map +1 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropTablesMigrationWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/fetchSchemaWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +85 -43
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/core/ForgeSQLCrudOperations.ts +3 -0
- package/src/core/ForgeSQLORM.ts +287 -9
- package/src/core/ForgeSQLQueryBuilder.ts +138 -8
- package/src/core/ForgeSQLSelectOperations.ts +2 -0
- package/src/core/SystemTables.ts +16 -0
- package/src/lib/drizzle/extensions/additionalActions.ts +10 -12
- package/src/utils/cacheContextUtils.ts +4 -2
- package/src/utils/cacheUtils.ts +20 -8
- package/src/utils/forgeDriver.ts +223 -23
- package/src/utils/forgeDriverProxy.ts +2 -0
- package/src/utils/metadataContextUtils.ts +22 -0
- package/src/utils/requestTypeContextUtils.ts +11 -0
- package/src/utils/sqlUtils.ts +1 -0
- package/src/webtriggers/applyMigrationsWebTrigger.ts +9 -6
- package/src/webtriggers/clearCacheSchedulerTrigger.ts +1 -0
- package/src/webtriggers/dropMigrationWebTrigger.ts +2 -0
- package/src/webtriggers/dropTablesMigrationWebTrigger.ts +2 -0
- package/src/webtriggers/fetchSchemaWebTrigger.ts +1 -0
- package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +515 -257
|
@@ -1,305 +1,563 @@
|
|
|
1
1
|
import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
clusterStatementsSummary,
|
|
4
|
+
clusterStatementsSummaryHistory,
|
|
5
|
+
statementsSummary,
|
|
6
|
+
} from "../core/SystemTables";
|
|
3
7
|
import { desc, gte, sql } from "drizzle-orm";
|
|
4
8
|
import { unionAll } from "drizzle-orm/mysql-core";
|
|
5
9
|
import { formatLimitOffset } from "../utils/sqlUtils";
|
|
10
|
+
import { OperationType } from "../utils/requestTypeContextUtils";
|
|
11
|
+
|
|
12
|
+
// Constants
|
|
13
|
+
const DEFAULT_MEMORY_THRESHOLD = 8 * 1024 * 1024; // 8MB
|
|
14
|
+
const DEFAULT_TIMEOUT = 300; // 300ms
|
|
15
|
+
const DEFAULT_TOP_N = 1;
|
|
16
|
+
const DEFAULT_HOURS = 1;
|
|
17
|
+
const DEFAULT_TABLES = "CLUSTER_SUMMARY_AND_HISTORY" as const;
|
|
18
|
+
const MAX_QUERY_TIMEOUT_MS = 3_000;
|
|
19
|
+
const MAX_SQL_LENGTH = 1000;
|
|
20
|
+
const RETRY_ATTEMPTS = 2;
|
|
21
|
+
const RETRY_BASE_DELAY_MS = 1_000;
|
|
22
|
+
|
|
23
|
+
// Types
|
|
24
|
+
interface TriggerOptions {
|
|
25
|
+
warnThresholdMs?: number;
|
|
26
|
+
memoryThresholdBytes?: number;
|
|
27
|
+
showPlan?: boolean;
|
|
28
|
+
operationType?: OperationType;
|
|
29
|
+
topN?: number;
|
|
30
|
+
hours?: number;
|
|
31
|
+
tables?: "SUMMARY_AND_HISTORY" | "CLUSTER_SUMMARY_AND_HISTORY";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface FormattedQueryResult {
|
|
35
|
+
rank: number;
|
|
36
|
+
digest: string;
|
|
37
|
+
stmtType: string;
|
|
38
|
+
schemaName: string;
|
|
39
|
+
execCount: number;
|
|
40
|
+
avgLatencyMs: number;
|
|
41
|
+
maxLatencyMs: number;
|
|
42
|
+
minLatencyMs: number;
|
|
43
|
+
avgProcessTimeMs: number;
|
|
44
|
+
avgWaitTimeMs: number;
|
|
45
|
+
avgBackoffTimeMs: number;
|
|
46
|
+
avgMemMB: number;
|
|
47
|
+
maxMemMB: number;
|
|
48
|
+
avgMemBytes: number;
|
|
49
|
+
maxMemBytes: number;
|
|
50
|
+
avgTotalKeys: number;
|
|
51
|
+
firstSeen: string;
|
|
52
|
+
lastSeen: string;
|
|
53
|
+
planInCache: boolean;
|
|
54
|
+
planCacheHits: number;
|
|
55
|
+
digestText: string;
|
|
56
|
+
plan?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface TriggerResponse {
|
|
60
|
+
headers: { "Content-Type": string[] };
|
|
61
|
+
statusCode: number;
|
|
62
|
+
statusText?: string;
|
|
63
|
+
body: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Utility Functions
|
|
67
|
+
/**
|
|
68
|
+
* Converts nanoseconds to milliseconds for better readability
|
|
69
|
+
*/
|
|
70
|
+
const nsToMs = (value: unknown): number => {
|
|
71
|
+
const n = Number(value);
|
|
72
|
+
return Number.isFinite(n) ? n / 1e6 : NaN;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Converts bytes to megabytes for better readability
|
|
77
|
+
*/
|
|
78
|
+
const bytesToMB = (value: unknown): number => {
|
|
79
|
+
const n = Number(value);
|
|
80
|
+
return Number.isFinite(n) ? n / (1024 * 1024) : NaN;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* JSON stringify replacer to handle BigInt values safely
|
|
85
|
+
*/
|
|
86
|
+
const jsonSafeStringify = (value: unknown): string =>
|
|
87
|
+
JSON.stringify(value, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sanitizes SQL for safe logging by removing comments, replacing literals, and truncating
|
|
91
|
+
*/
|
|
92
|
+
const sanitizeSQL = (sql: string, maxLen = MAX_SQL_LENGTH): string => {
|
|
93
|
+
let s = sql;
|
|
94
|
+
|
|
95
|
+
// Remove comments (-- ... and /* ... */)
|
|
96
|
+
s = s.replace(/--[^\n\r]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
97
|
+
|
|
98
|
+
// Replace string literals with '?'
|
|
99
|
+
s = s.replace(/'(?:\\'|[^'])*'/g, "?");
|
|
100
|
+
|
|
101
|
+
// Replace numbers with '?'
|
|
102
|
+
s = s.replace(/\b-?\d+(?:\.\d+)?\b/g, "?");
|
|
103
|
+
|
|
104
|
+
// Normalize whitespace
|
|
105
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
106
|
+
|
|
107
|
+
// Truncate long queries
|
|
108
|
+
if (s.length > maxLen) {
|
|
109
|
+
s = s.slice(0, maxLen) + " …[truncated]";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return s;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Promise timeout helper that rejects if the promise doesn't settle within the specified time
|
|
117
|
+
*/
|
|
118
|
+
const withTimeout = async <T>(promise: Promise<T>, ms: number): Promise<T> => {
|
|
119
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
120
|
+
try {
|
|
121
|
+
return await Promise.race<T>([
|
|
122
|
+
promise,
|
|
123
|
+
new Promise<T>((_resolve, reject) => {
|
|
124
|
+
timer = setTimeout(() => reject(new Error(`TIMEOUT:${ms}`)), ms);
|
|
125
|
+
}),
|
|
126
|
+
]);
|
|
127
|
+
} finally {
|
|
128
|
+
if (timer) clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Sleep utility function
|
|
134
|
+
*/
|
|
135
|
+
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Executes a task with retries and exponential backoff
|
|
139
|
+
*/
|
|
140
|
+
const executeWithRetries = async <T>(task: () => Promise<T>, label: string): Promise<T> => {
|
|
141
|
+
let attempt = 0;
|
|
142
|
+
let delay = RETRY_BASE_DELAY_MS;
|
|
143
|
+
|
|
144
|
+
while (true) {
|
|
145
|
+
try {
|
|
146
|
+
attempt++;
|
|
147
|
+
return await task();
|
|
148
|
+
} catch (error: any) {
|
|
149
|
+
const msg = String(error?.message ?? error);
|
|
150
|
+
const isTimeout = msg.startsWith("TIMEOUT:");
|
|
151
|
+
|
|
152
|
+
if (attempt > RETRY_ATTEMPTS) throw error;
|
|
153
|
+
// eslint-disable-next-line no-console
|
|
154
|
+
console.warn(
|
|
155
|
+
`${label}: attempt ${attempt} failed${isTimeout ? " (timeout)" : ""}; retrying in ${delay}ms...`,
|
|
156
|
+
error,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await sleep(delay);
|
|
160
|
+
delay *= 2; // Exponential backoff
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Creates error response for failed operations
|
|
167
|
+
*/
|
|
168
|
+
const createErrorResponse = (message: string, error?: any): TriggerResponse => ({
|
|
169
|
+
headers: { "Content-Type": ["application/json"] },
|
|
170
|
+
statusCode: 500,
|
|
171
|
+
statusText: "Internal Server Error",
|
|
172
|
+
body: jsonSafeStringify({
|
|
173
|
+
success: false,
|
|
174
|
+
message,
|
|
175
|
+
error: error?.cause?.context?.debug?.sqlMessage ?? error?.cause?.message ?? error?.message,
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates success response with query results
|
|
182
|
+
*/
|
|
183
|
+
const createSuccessResponse = (
|
|
184
|
+
formatted: FormattedQueryResult[],
|
|
185
|
+
options: Required<TriggerOptions>,
|
|
186
|
+
): TriggerResponse => ({
|
|
187
|
+
headers: { "Content-Type": ["application/json"] },
|
|
188
|
+
statusCode: 200,
|
|
189
|
+
statusText: "OK",
|
|
190
|
+
body: jsonSafeStringify({
|
|
191
|
+
success: true,
|
|
192
|
+
window: `last_${options.hours}h`,
|
|
193
|
+
top: options.topN,
|
|
194
|
+
warnThresholdMs: options.warnThresholdMs,
|
|
195
|
+
memoryThresholdBytes: options.memoryThresholdBytes,
|
|
196
|
+
showPlan: options.showPlan,
|
|
197
|
+
rows: formatted,
|
|
198
|
+
generatedAt: new Date().toISOString(),
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Query Building Functions
|
|
203
|
+
/**
|
|
204
|
+
* Creates the selection shape for query results
|
|
205
|
+
*/
|
|
206
|
+
const createSelectShape = (table: any) => ({
|
|
207
|
+
digest: table.digest,
|
|
208
|
+
stmtType: table.stmtType,
|
|
209
|
+
schemaName: table.schemaName,
|
|
210
|
+
execCount: table.execCount,
|
|
211
|
+
avgLatencyNs: table.avgLatency,
|
|
212
|
+
maxLatencyNs: table.maxLatency,
|
|
213
|
+
minLatencyNs: table.minLatency,
|
|
214
|
+
avgProcessTimeNs: table.avgProcessTime,
|
|
215
|
+
avgWaitTimeNs: table.avgWaitTime,
|
|
216
|
+
avgBackoffTimeNs: table.avgBackoffTime,
|
|
217
|
+
avgTotalKeys: table.avgTotalKeys,
|
|
218
|
+
firstSeen: table.firstSeen,
|
|
219
|
+
lastSeen: table.lastSeen,
|
|
220
|
+
planInCache: table.planInCache,
|
|
221
|
+
planCacheHits: table.planCacheHits,
|
|
222
|
+
digestText: table.digestText,
|
|
223
|
+
plan: table.plan,
|
|
224
|
+
avgMemBytes: (table as any).avgMem,
|
|
225
|
+
maxMemBytes: (table as any).maxMem,
|
|
226
|
+
});
|
|
6
227
|
|
|
7
228
|
/**
|
|
8
|
-
*
|
|
229
|
+
* Builds the combined query from multiple tables
|
|
230
|
+
*/
|
|
231
|
+
const buildCombinedQuery = (orm: ForgeSqlOperation, options: Required<TriggerOptions>) => {
|
|
232
|
+
const summaryHistory = statementsSummary;
|
|
233
|
+
const summary = statementsSummary;
|
|
234
|
+
const summaryHistoryCluster = clusterStatementsSummaryHistory;
|
|
235
|
+
const summaryCluster = clusterStatementsSummary;
|
|
236
|
+
|
|
237
|
+
// Time filters for last N hours
|
|
238
|
+
const lastHoursFilter = (table: any) =>
|
|
239
|
+
gte(table.summaryEndTime, sql`DATE_SUB(NOW(), INTERVAL ${options.hours} HOUR)`);
|
|
240
|
+
|
|
241
|
+
// Build queries for each table
|
|
242
|
+
const qHistory = orm
|
|
243
|
+
.getDrizzleQueryBuilder()
|
|
244
|
+
.select(createSelectShape(summaryHistory))
|
|
245
|
+
.from(summaryHistory)
|
|
246
|
+
.where(lastHoursFilter(summaryHistory));
|
|
247
|
+
|
|
248
|
+
const qSummary = orm
|
|
249
|
+
.getDrizzleQueryBuilder()
|
|
250
|
+
.select(createSelectShape(summary))
|
|
251
|
+
.from(summary)
|
|
252
|
+
.where(lastHoursFilter(summary));
|
|
253
|
+
|
|
254
|
+
const qHistoryCluster = orm
|
|
255
|
+
.getDrizzleQueryBuilder()
|
|
256
|
+
.select(createSelectShape(summaryHistoryCluster))
|
|
257
|
+
.from(summaryHistoryCluster)
|
|
258
|
+
.where(lastHoursFilter(summaryHistoryCluster));
|
|
259
|
+
|
|
260
|
+
const qSummaryCluster = orm
|
|
261
|
+
.getDrizzleQueryBuilder()
|
|
262
|
+
.select(createSelectShape(summaryCluster))
|
|
263
|
+
.from(summaryCluster)
|
|
264
|
+
.where(lastHoursFilter(summaryCluster));
|
|
265
|
+
|
|
266
|
+
// Combine tables based on configuration
|
|
267
|
+
switch (options.tables) {
|
|
268
|
+
case "SUMMARY_AND_HISTORY":
|
|
269
|
+
return unionAll(qHistory, qSummary).as("combined");
|
|
270
|
+
case "CLUSTER_SUMMARY_AND_HISTORY":
|
|
271
|
+
return unionAll(qHistoryCluster, qSummaryCluster).as("combined");
|
|
272
|
+
default:
|
|
273
|
+
throw new Error(`Unsupported table configuration: ${options.tables}`);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Builds the final grouped query with filtering and sorting
|
|
279
|
+
*/
|
|
280
|
+
const buildGroupedQuery = (orm: ForgeSqlOperation, combined: any) => {
|
|
281
|
+
return orm
|
|
282
|
+
.getDrizzleQueryBuilder()
|
|
283
|
+
.select({
|
|
284
|
+
digest: combined.digest,
|
|
285
|
+
stmtType: combined.stmtType,
|
|
286
|
+
schemaName: combined.schemaName,
|
|
287
|
+
execCount: sql<number>`SUM(${combined.execCount})`.as("execCount"),
|
|
288
|
+
avgLatencyNs: sql<number>`MAX(${combined.avgLatencyNs})`.as("avgLatencyNs"),
|
|
289
|
+
maxLatencyNs: sql<number>`MAX(${combined.maxLatencyNs})`.as("maxLatencyNs"),
|
|
290
|
+
minLatencyNs: sql<number>`MIN(${combined.minLatencyNs})`.as("minLatencyNs"),
|
|
291
|
+
avgProcessTimeNs: sql<number>`MAX(${combined.avgProcessTimeNs})`.as("avgProcessTimeNs"),
|
|
292
|
+
avgWaitTimeNs: sql<number>`MAX(${combined.avgWaitTimeNs})`.as("avgWaitTimeNs"),
|
|
293
|
+
avgBackoffTimeNs: sql<number>`MAX(${combined.avgBackoffTimeNs})`.as("avgBackoffTimeNs"),
|
|
294
|
+
avgMemBytes: sql<number>`MAX(${combined.avgMemBytes})`.as("avgMemBytes"),
|
|
295
|
+
maxMemBytes: sql<number>`MAX(${combined.maxMemBytes})`.as("maxMemBytes"),
|
|
296
|
+
avgTotalKeys: sql<number>`MAX(${combined.avgTotalKeys})`.as("avgTotalKeys"),
|
|
297
|
+
firstSeen: sql<string>`MIN(${combined.firstSeen})`.as("firstSeen"),
|
|
298
|
+
lastSeen: sql<string>`MAX(${combined.lastSeen})`.as("lastSeen"),
|
|
299
|
+
planInCache: sql<boolean>`MAX(${combined.planInCache})`.as("planInCache"),
|
|
300
|
+
planCacheHits: sql<number>`SUM(${combined.planCacheHits})`.as("planCacheHits"),
|
|
301
|
+
digestText: sql<string>`MAX(${combined.digestText})`.as("digestText"),
|
|
302
|
+
plan: sql<string>`MAX(${combined.plan})`.as("plan"),
|
|
303
|
+
})
|
|
304
|
+
.from(combined)
|
|
305
|
+
.where(
|
|
306
|
+
sql`COALESCE(${combined.digest}, '') <> '' AND COALESCE(${combined.digestText}, '') <> '' AND COALESCE(${combined.stmtType}, '') NOT IN ('Use','Set','Show')`,
|
|
307
|
+
)
|
|
308
|
+
.groupBy(combined.digest, combined.stmtType, combined.schemaName)
|
|
309
|
+
.as("grouped");
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Builds the final query with filtering, sorting, and limiting
|
|
314
|
+
*/
|
|
315
|
+
const buildFinalQuery = (
|
|
316
|
+
orm: ForgeSqlOperation,
|
|
317
|
+
grouped: any,
|
|
318
|
+
options: Required<TriggerOptions>,
|
|
319
|
+
) => {
|
|
320
|
+
const thresholdNs = Math.floor(options.warnThresholdMs * 1e6);
|
|
321
|
+
const memoryThresholdBytes = options.memoryThresholdBytes;
|
|
322
|
+
|
|
323
|
+
const query = orm
|
|
324
|
+
.getDrizzleQueryBuilder()
|
|
325
|
+
.select({
|
|
326
|
+
digest: grouped.digest,
|
|
327
|
+
stmtType: grouped.stmtType,
|
|
328
|
+
schemaName: grouped.schemaName,
|
|
329
|
+
execCount: grouped.execCount,
|
|
330
|
+
avgLatencyNs: grouped.avgLatencyNs,
|
|
331
|
+
maxLatencyNs: grouped.maxLatencyNs,
|
|
332
|
+
minLatencyNs: grouped.minLatencyNs,
|
|
333
|
+
avgProcessTimeNs: grouped.avgProcessTimeNs,
|
|
334
|
+
avgWaitTimeNs: grouped.avgWaitTimeNs,
|
|
335
|
+
avgBackoffTimeNs: grouped.avgBackoffTimeNs,
|
|
336
|
+
avgMemBytes: grouped.avgMemBytes,
|
|
337
|
+
maxMemBytes: grouped.maxMemBytes,
|
|
338
|
+
avgTotalKeys: grouped.avgTotalKeys,
|
|
339
|
+
firstSeen: grouped.firstSeen,
|
|
340
|
+
lastSeen: grouped.lastSeen,
|
|
341
|
+
planInCache: grouped.planInCache,
|
|
342
|
+
planCacheHits: grouped.planCacheHits,
|
|
343
|
+
digestText: grouped.digestText,
|
|
344
|
+
plan: grouped.plan,
|
|
345
|
+
})
|
|
346
|
+
.from(grouped)
|
|
347
|
+
.where(
|
|
348
|
+
sql`${grouped.avgLatencyNs} > ${thresholdNs} OR ${grouped.avgMemBytes} > ${memoryThresholdBytes}`,
|
|
349
|
+
)
|
|
350
|
+
.orderBy(desc(grouped.avgLatencyNs))
|
|
351
|
+
.limit(formatLimitOffset(options.topN));
|
|
352
|
+
|
|
353
|
+
// Execute with DDL context if specified
|
|
354
|
+
if (options.operationType === "DDL") {
|
|
355
|
+
return orm.executeDDLActions(async () => await query);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return query;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Formats query results for output
|
|
363
|
+
*/
|
|
364
|
+
const formatQueryResults = (
|
|
365
|
+
rows: any[],
|
|
366
|
+
options: Required<TriggerOptions>,
|
|
367
|
+
): FormattedQueryResult[] => {
|
|
368
|
+
return rows.map((row, index) => ({
|
|
369
|
+
rank: index + 1,
|
|
370
|
+
digest: row.digest,
|
|
371
|
+
stmtType: row.stmtType,
|
|
372
|
+
schemaName: row.schemaName,
|
|
373
|
+
execCount: row.execCount,
|
|
374
|
+
avgLatencyMs: nsToMs(row.avgLatencyNs),
|
|
375
|
+
maxLatencyMs: nsToMs(row.maxLatencyNs),
|
|
376
|
+
minLatencyMs: nsToMs(row.minLatencyNs),
|
|
377
|
+
avgProcessTimeMs: nsToMs(row.avgProcessTimeNs),
|
|
378
|
+
avgWaitTimeMs: nsToMs(row.avgWaitTimeNs),
|
|
379
|
+
avgBackoffTimeMs: nsToMs(row.avgBackoffTimeNs),
|
|
380
|
+
avgMemMB: bytesToMB(row.avgMemBytes),
|
|
381
|
+
maxMemMB: bytesToMB(row.maxMemBytes),
|
|
382
|
+
avgMemBytes: row.avgMemBytes,
|
|
383
|
+
maxMemBytes: row.maxMemBytes,
|
|
384
|
+
avgTotalKeys: row.avgTotalKeys,
|
|
385
|
+
firstSeen: row.firstSeen,
|
|
386
|
+
lastSeen: row.lastSeen,
|
|
387
|
+
planInCache: row.planInCache,
|
|
388
|
+
planCacheHits: row.planCacheHits,
|
|
389
|
+
digestText: options.operationType === "DDL" ? row.digestText : sanitizeSQL(row.digestText),
|
|
390
|
+
plan: options.showPlan ? row.plan : undefined,
|
|
391
|
+
}));
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Logs formatted query results to console
|
|
396
|
+
*/
|
|
397
|
+
const logQueryResults = (
|
|
398
|
+
formatted: FormattedQueryResult[],
|
|
399
|
+
options: Required<TriggerOptions>,
|
|
400
|
+
): void => {
|
|
401
|
+
for (const result of formatted) {
|
|
402
|
+
// eslint-disable-next-line no-console
|
|
403
|
+
console.warn(
|
|
404
|
+
`${result.rank}. ${result.stmtType} avg=${result.avgLatencyMs?.toFixed?.(2)}ms max=${result.maxLatencyMs?.toFixed?.(2)}ms mem≈${result.avgMemMB?.toFixed?.(2)}MB(max ${result.maxMemMB?.toFixed?.(2)}MB) exec=${result.execCount} \n` +
|
|
405
|
+
` digest=${result.digest}\n` +
|
|
406
|
+
` sql=${(result.digestText || "").slice(0, 300)}${result.digestText && result.digestText.length > 300 ? "…" : ""}`,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
if (options.showPlan && result.plan) {
|
|
410
|
+
// eslint-disable-next-line no-console
|
|
411
|
+
console.warn(` full plan:\n${result.plan}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Performance monitoring scheduler trigger for Atlassian Forge SQL.
|
|
9
418
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* and
|
|
419
|
+
* This trigger analyzes query performance from the last hour and identifies slow or memory-intensive queries
|
|
420
|
+
* that exceed configurable thresholds. It's designed specifically for Atlassian Forge's 16 MiB memory limit
|
|
421
|
+
* and provides detailed insights for query optimization.
|
|
13
422
|
*
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
423
|
+
* ## Key Features
|
|
424
|
+
* - **Memory-focused monitoring**: Primary focus on memory usage with configurable thresholds
|
|
425
|
+
* - **Atlassian 16 MiB limit awareness**: Designed specifically for Forge SQL's memory constraints
|
|
426
|
+
* - **Execution plan analysis**: Shows detailed query plans to help optimize memory consumption
|
|
427
|
+
* - **Configurable thresholds**: Set custom memory usage and latency thresholds
|
|
428
|
+
* - **Automatic filtering**: Excludes system queries (`Use`, `Set`, `Show`) and empty queries
|
|
429
|
+
* - **Scheduled monitoring**: Run automatically on configurable intervals
|
|
17
430
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
431
|
+
* ## OR Logic Thresholds
|
|
432
|
+
* Statements are included if they exceed **EITHER** threshold:
|
|
433
|
+
* - `avgLatencyMs > warnThresholdMs` **OR**
|
|
434
|
+
* - `avgMemBytes > memoryThresholdBytes`
|
|
22
435
|
*
|
|
23
|
-
*
|
|
436
|
+
* ## Configuration Tips
|
|
437
|
+
* - **Memory-only monitoring**: Set `warnThresholdMs` to 10000ms (effectively disabled)
|
|
438
|
+
* - **Latency-only monitoring**: Set `memoryThresholdBytes` to 16MB (16 * 1024 * 1024) (effectively disabled)
|
|
439
|
+
* - **Combined monitoring**: Use both thresholds for comprehensive monitoring
|
|
440
|
+
* - **Conservative monitoring**: 4MB warning (25% of 16MB limit)
|
|
441
|
+
* - **Default monitoring**: 8MB warning (50% of 16MB limit)
|
|
442
|
+
* - **Aggressive monitoring**: 12MB warning (75% of 16MB limit)
|
|
24
443
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
444
|
+
* ## Exclusions
|
|
445
|
+
* - Statements with empty `digestText` or `digest`
|
|
446
|
+
* - Service statements (`Use`, `Set`, `Show`)
|
|
447
|
+
* - Queries that don't exceed either threshold
|
|
28
448
|
*
|
|
29
|
-
* @param orm
|
|
30
|
-
* @param
|
|
31
|
-
* @param
|
|
32
|
-
* @
|
|
449
|
+
* @param orm - ForgeSQL ORM instance (required)
|
|
450
|
+
* @param options - Configuration options
|
|
451
|
+
* @param options.warnThresholdMs - Milliseconds threshold for latency monitoring (default: 300ms)
|
|
452
|
+
* @param options.memoryThresholdBytes - Bytes threshold for memory usage monitoring (default: 8MB)
|
|
453
|
+
* @param options.showPlan - Whether to include execution plan in logs (default: false)
|
|
454
|
+
* @param options.operationType - Operation type context for query execution (default: "DML")
|
|
455
|
+
* @param options.topN - Number of top slow queries to return (default: 1)
|
|
456
|
+
* @param options.hours - Number of hours to look back (default: 1)
|
|
457
|
+
* @param options.tables - Table configuration to use (default: "CLUSTER_SUMMARY_AND_HISTORY")
|
|
458
|
+
* @returns Promise<TriggerResponse> - HTTP response with query results or error
|
|
33
459
|
*
|
|
34
460
|
* @example
|
|
35
|
-
* ```
|
|
461
|
+
* ```typescript
|
|
36
462
|
* import ForgeSQL, { topSlowestStatementLastHourTrigger } from "forge-sql-orm";
|
|
37
463
|
*
|
|
38
|
-
* const
|
|
464
|
+
* const forgeSQL = new ForgeSQL();
|
|
39
465
|
*
|
|
40
466
|
* // Default thresholds: 300ms latency OR 8MB memory
|
|
41
|
-
* export const
|
|
42
|
-
* topSlowestStatementLastHourTrigger(
|
|
467
|
+
* export const performanceTrigger = () =>
|
|
468
|
+
* topSlowestStatementLastHourTrigger(forgeSQL);
|
|
43
469
|
*
|
|
44
|
-
* //
|
|
45
|
-
* export const
|
|
46
|
-
* topSlowestStatementLastHourTrigger(
|
|
470
|
+
* // Conservative memory monitoring: 4MB threshold
|
|
471
|
+
* export const conservativeTrigger = () =>
|
|
472
|
+
* topSlowestStatementLastHourTrigger(forgeSQL, {
|
|
473
|
+
* memoryThresholdBytes: 4 * 1024 * 1024
|
|
474
|
+
* });
|
|
47
475
|
*
|
|
48
|
-
* //
|
|
476
|
+
* // Memory-only monitoring: 4MB threshold (latency effectively disabled)
|
|
49
477
|
* export const memoryOnlyTrigger = () =>
|
|
50
|
-
* topSlowestStatementLastHourTrigger(
|
|
478
|
+
* topSlowestStatementLastHourTrigger(forgeSQL, {
|
|
479
|
+
* warnThresholdMs: 10000,
|
|
480
|
+
* memoryThresholdBytes: 4 * 1024 * 1024
|
|
481
|
+
* });
|
|
51
482
|
*
|
|
52
|
-
* //
|
|
53
|
-
* export const
|
|
54
|
-
* topSlowestStatementLastHourTrigger(
|
|
483
|
+
* // With execution plan in logs
|
|
484
|
+
* export const withPlanTrigger = () =>
|
|
485
|
+
* topSlowestStatementLastHourTrigger(forgeSQL, { showPlan: true });
|
|
55
486
|
* ```
|
|
56
487
|
*
|
|
57
488
|
* @example
|
|
58
489
|
* ```yaml
|
|
490
|
+
* # manifest.yml configuration
|
|
59
491
|
* scheduledTrigger:
|
|
60
|
-
* - key:
|
|
61
|
-
* function:
|
|
492
|
+
* - key: performance-trigger
|
|
493
|
+
* function: performanceTrigger
|
|
62
494
|
* interval: hour
|
|
63
495
|
*
|
|
64
496
|
* function:
|
|
65
|
-
* - key:
|
|
66
|
-
* handler: index.
|
|
497
|
+
* - key: performanceTrigger
|
|
498
|
+
* handler: index.performanceTrigger
|
|
67
499
|
* ```
|
|
68
500
|
*/
|
|
69
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Main scheduler trigger function to log the single slowest SQL statement from the last hour.
|
|
503
|
+
*
|
|
504
|
+
* @param orm - ForgeSQL ORM instance (required)
|
|
505
|
+
* @param options - Configuration options
|
|
506
|
+
* @returns Promise<TriggerResponse> - HTTP response with query results or error
|
|
507
|
+
*/
|
|
70
508
|
export const topSlowestStatementLastHourTrigger = async (
|
|
71
509
|
orm: ForgeSqlOperation,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const nsToMs = (v: unknown) => {
|
|
79
|
-
const n = Number(v);
|
|
80
|
-
return Number.isFinite(n) ? n / 1e6 : NaN;
|
|
81
|
-
};
|
|
510
|
+
options?: TriggerOptions,
|
|
511
|
+
): Promise<TriggerResponse> => {
|
|
512
|
+
// Validate required parameters
|
|
513
|
+
if (!orm) {
|
|
514
|
+
return createErrorResponse("ORM instance is required");
|
|
515
|
+
}
|
|
82
516
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
517
|
+
// Merge options with defaults
|
|
518
|
+
const mergedOptions: Required<TriggerOptions> = {
|
|
519
|
+
warnThresholdMs: options?.warnThresholdMs ?? DEFAULT_TIMEOUT,
|
|
520
|
+
memoryThresholdBytes: options?.memoryThresholdBytes ?? DEFAULT_MEMORY_THRESHOLD,
|
|
521
|
+
showPlan: options?.showPlan ?? false,
|
|
522
|
+
operationType: options?.operationType ?? "DML",
|
|
523
|
+
topN: options?.topN ?? DEFAULT_TOP_N,
|
|
524
|
+
hours: options?.hours ?? DEFAULT_HOURS,
|
|
525
|
+
tables: options?.tables ?? DEFAULT_TABLES,
|
|
87
526
|
};
|
|
88
527
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
528
|
+
try {
|
|
529
|
+
// Build the combined query from multiple tables
|
|
530
|
+
const combined = buildCombinedQuery(orm, mergedOptions);
|
|
92
531
|
|
|
93
|
-
|
|
94
|
-
|
|
532
|
+
// Build the grouped query with filtering and aggregation
|
|
533
|
+
const grouped = buildGroupedQuery(orm, combined);
|
|
95
534
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
stmtType: t.stmtType,
|
|
104
|
-
schemaName: t.schemaName,
|
|
105
|
-
execCount: t.execCount,
|
|
106
|
-
|
|
107
|
-
avgLatencyNs: t.avgLatency,
|
|
108
|
-
maxLatencyNs: t.maxLatency,
|
|
109
|
-
minLatencyNs: t.minLatency,
|
|
110
|
-
|
|
111
|
-
avgProcessTimeNs: t.avgProcessTime,
|
|
112
|
-
avgWaitTimeNs: t.avgWaitTime,
|
|
113
|
-
avgBackoffTimeNs: t.avgBackoffTime,
|
|
114
|
-
|
|
115
|
-
avgTotalKeys: t.avgTotalKeys,
|
|
116
|
-
firstSeen: t.firstSeen,
|
|
117
|
-
lastSeen: t.lastSeen,
|
|
118
|
-
planInCache: t.planInCache,
|
|
119
|
-
planCacheHits: t.planCacheHits,
|
|
120
|
-
digestText: t.digestText,
|
|
121
|
-
plan: t.plan,
|
|
122
|
-
avgMemBytes: (t as any).avgMem,
|
|
123
|
-
maxMemBytes: (t as any).maxMem,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Filters: Only include rows from the last hour for each table
|
|
127
|
-
const lastHourFilterHistory = gte(
|
|
128
|
-
summaryHistory.summaryEndTime,
|
|
129
|
-
sql`DATE_SUB(NOW(), INTERVAL 1 HOUR)`,
|
|
130
|
-
);
|
|
131
|
-
const lastHourFilterSummary = gte(
|
|
132
|
-
summary.summaryEndTime,
|
|
133
|
-
sql`DATE_SUB(NOW(), INTERVAL 1 HOUR)`,
|
|
535
|
+
// Build the final query with filtering, sorting, and limiting
|
|
536
|
+
const finalQuery = buildFinalQuery(orm, grouped, mergedOptions);
|
|
537
|
+
|
|
538
|
+
// Execute the query with retries and timeout
|
|
539
|
+
const rows = await executeWithRetries(
|
|
540
|
+
() => withTimeout(finalQuery, MAX_QUERY_TIMEOUT_MS),
|
|
541
|
+
"topSlowestStatementLastHourTrigger",
|
|
134
542
|
);
|
|
135
543
|
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
.getDrizzleQueryBuilder()
|
|
139
|
-
.select(selectShape(summaryHistory))
|
|
140
|
-
.from(summaryHistory)
|
|
141
|
-
.where(lastHourFilterHistory);
|
|
142
|
-
|
|
143
|
-
// Query for summary table (last hour)
|
|
144
|
-
const qSummary = orm
|
|
145
|
-
.getDrizzleQueryBuilder()
|
|
146
|
-
.select(selectShape(summary))
|
|
147
|
-
.from(summary)
|
|
148
|
-
.where(lastHourFilterSummary);
|
|
149
|
-
|
|
150
|
-
// Use UNION ALL to combine results from both tables (avoids duplicates, keeps all rows)
|
|
151
|
-
// This is necessary because some statements may only be present in one of the tables.
|
|
152
|
-
const combined = unionAll(qHistory, qSummary).as("combined");
|
|
153
|
-
|
|
154
|
-
// Threshold in nanoseconds (warnThresholdMs → ns)
|
|
155
|
-
const thresholdNs = Math.floor(warnThresholdMs * 1e6);
|
|
156
|
-
// memoryThresholdBytes is already provided in bytes (default 8MB)
|
|
157
|
-
|
|
158
|
-
// Group duplicates by digest+stmtType+schemaName and aggregate metrics
|
|
159
|
-
const grouped = orm
|
|
160
|
-
.getDrizzleQueryBuilder()
|
|
161
|
-
.select({
|
|
162
|
-
digest: combined.digest,
|
|
163
|
-
stmtType: combined.stmtType,
|
|
164
|
-
schemaName: combined.schemaName,
|
|
165
|
-
execCount: sql<number>`SUM(${combined.execCount})`.as("execCount"),
|
|
166
|
-
|
|
167
|
-
avgLatencyNs: sql<number>`MAX(${combined.avgLatencyNs})`.as("avgLatencyNs"),
|
|
168
|
-
maxLatencyNs: sql<number>`MAX(${combined.maxLatencyNs})`.as("maxLatencyNs"),
|
|
169
|
-
minLatencyNs: sql<number>`MIN(${combined.minLatencyNs})`.as("minLatencyNs"),
|
|
170
|
-
|
|
171
|
-
avgProcessTimeNs: sql<number>`MAX(${combined.avgProcessTimeNs})`.as("avgProcessTimeNs"),
|
|
172
|
-
avgWaitTimeNs: sql<number>`MAX(${combined.avgWaitTimeNs})`.as("avgWaitTimeNs"),
|
|
173
|
-
avgBackoffTimeNs: sql<number>`MAX(${combined.avgBackoffTimeNs})`.as("avgBackoffTimeNs"),
|
|
174
|
-
|
|
175
|
-
avgMemBytes: sql<number>`MAX(${combined.avgMemBytes})`.as("avgMemBytes"),
|
|
176
|
-
maxMemBytes: sql<number>`MAX(${combined.maxMemBytes})`.as("maxMemBytes"),
|
|
177
|
-
|
|
178
|
-
avgTotalKeys: sql<number>`MAX(${combined.avgTotalKeys})`.as("avgTotalKeys"),
|
|
179
|
-
firstSeen: sql<string>`MIN(${combined.firstSeen})`.as("firstSeen"),
|
|
180
|
-
lastSeen: sql<string>`MAX(${combined.lastSeen})`.as("lastSeen"),
|
|
181
|
-
planInCache: sql<boolean>`MAX(${combined.planInCache})`.as("planInCache"),
|
|
182
|
-
planCacheHits: sql<number>`SUM(${combined.planCacheHits})`.as("planCacheHits"),
|
|
183
|
-
// Prefer a non-empty sample text/plan via MAX; acceptable for de-dup
|
|
184
|
-
digestText: sql<string>`MAX(${combined.digestText})`.as("digestText"),
|
|
185
|
-
plan: sql<string>`MAX(${combined.plan})`.as("plan"),
|
|
186
|
-
})
|
|
187
|
-
.from(combined)
|
|
188
|
-
.where(
|
|
189
|
-
sql`COALESCE(${combined.digest}, '') <> '' AND COALESCE(${combined.digestText}, '') <> '' AND COALESCE(${combined.stmtType}, '') NOT IN ('Use','Set','Show')`,
|
|
190
|
-
)
|
|
191
|
-
.groupBy(combined.digest, combined.stmtType, combined.schemaName)
|
|
192
|
-
.as("grouped");
|
|
193
|
-
|
|
194
|
-
// Final selection: filter by threshold, sort by avg latency desc, limit TOP_N
|
|
195
|
-
const rows = await orm
|
|
196
|
-
.getDrizzleQueryBuilder()
|
|
197
|
-
.select({
|
|
198
|
-
digest: grouped.digest,
|
|
199
|
-
stmtType: grouped.stmtType,
|
|
200
|
-
schemaName: grouped.schemaName,
|
|
201
|
-
execCount: grouped.execCount,
|
|
202
|
-
|
|
203
|
-
avgLatencyNs: grouped.avgLatencyNs,
|
|
204
|
-
maxLatencyNs: grouped.maxLatencyNs,
|
|
205
|
-
minLatencyNs: grouped.minLatencyNs,
|
|
206
|
-
|
|
207
|
-
avgProcessTimeNs: grouped.avgProcessTimeNs,
|
|
208
|
-
avgWaitTimeNs: grouped.avgWaitTimeNs,
|
|
209
|
-
avgBackoffTimeNs: grouped.avgBackoffTimeNs,
|
|
210
|
-
|
|
211
|
-
avgMemBytes: grouped.avgMemBytes,
|
|
212
|
-
maxMemBytes: grouped.maxMemBytes,
|
|
213
|
-
|
|
214
|
-
avgTotalKeys: grouped.avgTotalKeys,
|
|
215
|
-
firstSeen: grouped.firstSeen,
|
|
216
|
-
lastSeen: grouped.lastSeen,
|
|
217
|
-
planInCache: grouped.planInCache,
|
|
218
|
-
planCacheHits: grouped.planCacheHits,
|
|
219
|
-
digestText: grouped.digestText,
|
|
220
|
-
plan: grouped.plan,
|
|
221
|
-
})
|
|
222
|
-
.from(grouped)
|
|
223
|
-
.where(
|
|
224
|
-
sql`${grouped.avgLatencyNs} > ${thresholdNs} OR ${grouped.avgMemBytes} > ${memoryThresholdBytes}`,
|
|
225
|
-
)
|
|
226
|
-
.orderBy(desc(grouped.avgLatencyNs))
|
|
227
|
-
.limit(formatLimitOffset(TOP_N));
|
|
228
|
-
|
|
229
|
-
// Map each row into a formatted object with ms and rank, for easier consumption/logging
|
|
230
|
-
const formatted = rows.map((r, i) => ({
|
|
231
|
-
rank: i + 1, // 1-based rank in the top N
|
|
232
|
-
digest: r.digest,
|
|
233
|
-
stmtType: r.stmtType,
|
|
234
|
-
schemaName: r.schemaName,
|
|
235
|
-
execCount: r.execCount,
|
|
236
|
-
avgLatencyMs: nsToMs(r.avgLatencyNs), // Convert ns to ms for readability
|
|
237
|
-
maxLatencyMs: nsToMs(r.maxLatencyNs),
|
|
238
|
-
minLatencyMs: nsToMs(r.minLatencyNs),
|
|
239
|
-
avgProcessTimeMs: nsToMs(r.avgProcessTimeNs),
|
|
240
|
-
avgWaitTimeMs: nsToMs(r.avgWaitTimeNs),
|
|
241
|
-
avgBackoffTimeMs: nsToMs(r.avgBackoffTimeNs),
|
|
242
|
-
avgMemMB: bytesToMB(r.avgMemBytes),
|
|
243
|
-
maxMemMB: bytesToMB(r.maxMemBytes),
|
|
244
|
-
avgMemBytes: r.avgMemBytes,
|
|
245
|
-
maxMemBytes: r.maxMemBytes,
|
|
246
|
-
avgTotalKeys: r.avgTotalKeys,
|
|
247
|
-
firstSeen: r.firstSeen,
|
|
248
|
-
lastSeen: r.lastSeen,
|
|
249
|
-
planInCache: r.planInCache,
|
|
250
|
-
planCacheHits: r.planCacheHits,
|
|
251
|
-
digestText: r.digestText,
|
|
252
|
-
plan: r.plan,
|
|
253
|
-
}));
|
|
254
|
-
|
|
255
|
-
// Log each entry (SQL already filtered by threshold)
|
|
256
|
-
for (const f of formatted) {
|
|
257
|
-
// eslint-disable-next-line no-console
|
|
258
|
-
console.warn(
|
|
259
|
-
`${f.rank}. ${f.stmtType} avg=${f.avgLatencyMs?.toFixed?.(2)}ms max=${f.maxLatencyMs?.toFixed?.(2)}ms mem≈${f.avgMemMB?.toFixed?.(2)}MB(max ${f.maxMemMB?.toFixed?.(2)}MB) exec=${f.execCount} \n` +
|
|
260
|
-
` digest=${f.digest}\n` +
|
|
261
|
-
` sql=${(f.digestText || "").slice(0, 300)}${f.digestText && f.digestText.length > 300 ? "…" : ""}`,
|
|
262
|
-
);
|
|
263
|
-
if (f.plan) {
|
|
264
|
-
// print full plan separately (not truncated)
|
|
265
|
-
// eslint-disable-next-line no-console
|
|
266
|
-
console.warn(` full plan:\n${f.plan}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
544
|
+
// Format the results for output
|
|
545
|
+
const formatted = formatQueryResults(rows, mergedOptions);
|
|
269
546
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
body: jsonSafeStringify({
|
|
276
|
-
success: true,
|
|
277
|
-
window: "last_1h",
|
|
278
|
-
top: TOP_N,
|
|
279
|
-
warnThresholdMs,
|
|
280
|
-
memoryThresholdBytes,
|
|
281
|
-
rows: formatted,
|
|
282
|
-
generatedAt: new Date().toISOString(),
|
|
283
|
-
}),
|
|
284
|
-
};
|
|
547
|
+
// Log the results to console
|
|
548
|
+
logQueryResults(formatted, mergedOptions);
|
|
549
|
+
|
|
550
|
+
// Return success response
|
|
551
|
+
return createSuccessResponse(formatted, mergedOptions);
|
|
285
552
|
} catch (error: any) {
|
|
286
|
-
//
|
|
287
|
-
// This ensures the scheduler never crashes and always returns a response.
|
|
553
|
+
// Log error details for debugging
|
|
288
554
|
// eslint-disable-next-line no-console
|
|
289
|
-
console.
|
|
290
|
-
"Error in topSlowestStatementLastHourTrigger:",
|
|
555
|
+
console.warn(
|
|
556
|
+
"Error in topSlowestStatementLastHourTrigger (one-off errors can be ignored; if it recurs, investigate):",
|
|
291
557
|
error?.cause?.context?.debug?.sqlMessage ?? error?.cause ?? error,
|
|
292
558
|
);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
statusText: "Internal Server Error",
|
|
297
|
-
body: jsonSafeStringify({
|
|
298
|
-
success: false,
|
|
299
|
-
message: "Failed to fetch or log slow queries",
|
|
300
|
-
error: error?.cause?.context?.debug?.sqlMessage ?? error?.cause?.message,
|
|
301
|
-
timestamp: new Date().toISOString(),
|
|
302
|
-
}),
|
|
303
|
-
};
|
|
559
|
+
|
|
560
|
+
// Return error response
|
|
561
|
+
return createErrorResponse("Failed to fetch or log slow queries", error);
|
|
304
562
|
}
|
|
305
563
|
};
|