forge-sql-orm 2.1.10 → 2.1.11

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.
Files changed (36) hide show
  1. package/README.md +202 -254
  2. package/dist/ForgeSQLORM.js +3238 -3227
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +3236 -3225
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLORM.d.ts +65 -11
  7. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLQueryBuilder.d.ts +66 -11
  9. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  10. package/dist/core/SystemTables.d.ts +82 -82
  11. package/dist/utils/cacheUtils.d.ts.map +1 -1
  12. package/dist/utils/forgeDriver.d.ts.map +1 -1
  13. package/dist/utils/forgeDriverProxy.d.ts +6 -2
  14. package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
  15. package/dist/utils/metadataContextUtils.d.ts +5 -2
  16. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  17. package/dist/utils/sqlUtils.d.ts +72 -1
  18. package/dist/utils/sqlUtils.d.ts.map +1 -1
  19. package/dist/webtriggers/index.d.ts +1 -1
  20. package/dist/webtriggers/index.d.ts.map +1 -1
  21. package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts +67 -0
  22. package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts.map +1 -0
  23. package/package.json +10 -10
  24. package/src/core/ForgeSQLORM.ts +164 -33
  25. package/src/core/ForgeSQLQueryBuilder.ts +65 -11
  26. package/src/core/SystemTables.ts +1 -1
  27. package/src/utils/cacheUtils.ts +3 -1
  28. package/src/utils/forgeDriver.ts +7 -34
  29. package/src/utils/forgeDriverProxy.ts +58 -6
  30. package/src/utils/metadataContextUtils.ts +21 -6
  31. package/src/utils/sqlUtils.ts +229 -2
  32. package/src/webtriggers/index.ts +1 -1
  33. package/src/webtriggers/slowQuerySchedulerTrigger.ts +82 -0
  34. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +0 -114
  35. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +0 -1
  36. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +0 -563
@@ -1,114 +0,0 @@
1
- import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
2
- import { OperationType } from "../utils/requestTypeContextUtils";
3
- interface TriggerOptions {
4
- warnThresholdMs?: number;
5
- memoryThresholdBytes?: number;
6
- showPlan?: boolean;
7
- operationType?: OperationType;
8
- topN?: number;
9
- hours?: number;
10
- tables?: "SUMMARY_AND_HISTORY" | "CLUSTER_SUMMARY_AND_HISTORY";
11
- }
12
- interface TriggerResponse {
13
- headers: {
14
- "Content-Type": string[];
15
- };
16
- statusCode: number;
17
- statusText?: string;
18
- body: string;
19
- }
20
- /**
21
- * Performance monitoring scheduler trigger for Atlassian Forge SQL.
22
- *
23
- * This trigger analyzes query performance from the last hour and identifies slow or memory-intensive queries
24
- * that exceed configurable thresholds. It's designed specifically for Atlassian Forge's 16 MiB memory limit
25
- * and provides detailed insights for query optimization.
26
- *
27
- * ## Key Features
28
- * - **Memory-focused monitoring**: Primary focus on memory usage with configurable thresholds
29
- * - **Atlassian 16 MiB limit awareness**: Designed specifically for Forge SQL's memory constraints
30
- * - **Execution plan analysis**: Shows detailed query plans to help optimize memory consumption
31
- * - **Configurable thresholds**: Set custom memory usage and latency thresholds
32
- * - **Automatic filtering**: Excludes system queries (`Use`, `Set`, `Show`) and empty queries
33
- * - **Scheduled monitoring**: Run automatically on configurable intervals
34
- *
35
- * ## OR Logic Thresholds
36
- * Statements are included if they exceed **EITHER** threshold:
37
- * - `avgLatencyMs > warnThresholdMs` **OR**
38
- * - `avgMemBytes > memoryThresholdBytes`
39
- *
40
- * ## Configuration Tips
41
- * - **Memory-only monitoring**: Set `warnThresholdMs` to 10000ms (effectively disabled)
42
- * - **Latency-only monitoring**: Set `memoryThresholdBytes` to 16MB (16 * 1024 * 1024) (effectively disabled)
43
- * - **Combined monitoring**: Use both thresholds for comprehensive monitoring
44
- * - **Conservative monitoring**: 4MB warning (25% of 16MB limit)
45
- * - **Default monitoring**: 8MB warning (50% of 16MB limit)
46
- * - **Aggressive monitoring**: 12MB warning (75% of 16MB limit)
47
- *
48
- * ## Exclusions
49
- * - Statements with empty `digestText` or `digest`
50
- * - Service statements (`Use`, `Set`, `Show`)
51
- * - Queries that don't exceed either threshold
52
- *
53
- * @param orm - ForgeSQL ORM instance (required)
54
- * @param options - Configuration options
55
- * @param options.warnThresholdMs - Milliseconds threshold for latency monitoring (default: 300ms)
56
- * @param options.memoryThresholdBytes - Bytes threshold for memory usage monitoring (default: 8MB)
57
- * @param options.showPlan - Whether to include execution plan in logs (default: false)
58
- * @param options.operationType - Operation type context for query execution (default: "DML")
59
- * @param options.topN - Number of top slow queries to return (default: 1)
60
- * @param options.hours - Number of hours to look back (default: 1)
61
- * @param options.tables - Table configuration to use (default: "CLUSTER_SUMMARY_AND_HISTORY")
62
- * @returns Promise<TriggerResponse> - HTTP response with query results or error
63
- *
64
- * @example
65
- * ```typescript
66
- * import ForgeSQL, { topSlowestStatementLastHourTrigger } from "forge-sql-orm";
67
- *
68
- * const forgeSQL = new ForgeSQL();
69
- *
70
- * // Default thresholds: 300ms latency OR 8MB memory
71
- * export const performanceTrigger = () =>
72
- * topSlowestStatementLastHourTrigger(forgeSQL);
73
- *
74
- * // Conservative memory monitoring: 4MB threshold
75
- * export const conservativeTrigger = () =>
76
- * topSlowestStatementLastHourTrigger(forgeSQL, {
77
- * memoryThresholdBytes: 4 * 1024 * 1024
78
- * });
79
- *
80
- * // Memory-only monitoring: 4MB threshold (latency effectively disabled)
81
- * export const memoryOnlyTrigger = () =>
82
- * topSlowestStatementLastHourTrigger(forgeSQL, {
83
- * warnThresholdMs: 10000,
84
- * memoryThresholdBytes: 4 * 1024 * 1024
85
- * });
86
- *
87
- * // With execution plan in logs
88
- * export const withPlanTrigger = () =>
89
- * topSlowestStatementLastHourTrigger(forgeSQL, { showPlan: true });
90
- * ```
91
- *
92
- * @example
93
- * ```yaml
94
- * # manifest.yml configuration
95
- * scheduledTrigger:
96
- * - key: performance-trigger
97
- * function: performanceTrigger
98
- * interval: hour
99
- *
100
- * function:
101
- * - key: performanceTrigger
102
- * handler: index.performanceTrigger
103
- * ```
104
- */
105
- /**
106
- * Main scheduler trigger function to log the single slowest SQL statement from the last hour.
107
- *
108
- * @param orm - ForgeSQL ORM instance (required)
109
- * @param options - Configuration options
110
- * @returns Promise<TriggerResponse> - HTTP response with query results or error
111
- */
112
- export declare const topSlowestStatementLastHourTrigger: (orm: ForgeSqlOperation, options?: TriggerOptions) => Promise<TriggerResponse>;
113
- export {};
114
- //# sourceMappingURL=topSlowestStatementLastHourTrigger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"topSlowestStatementLastHourTrigger.d.ts","sourceRoot":"","sources":["../../src/webtriggers/topSlowestStatementLastHourTrigger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AASjE,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAcjE,UAAU,cAAc;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,qBAAqB,GAAG,6BAA6B,CAAC;CAChE;AA2BD,UAAU,eAAe;IACvB,OAAO,EAAE;QAAE,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAgWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;AACH;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,GAC7C,KAAK,iBAAiB,EACtB,UAAU,cAAc,KACvB,OAAO,CAAC,eAAe,CAoDzB,CAAC"}
@@ -1,563 +0,0 @@
1
- import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
2
- import {
3
- clusterStatementsSummary,
4
- clusterStatementsSummaryHistory,
5
- statementsSummary,
6
- } from "../core/SystemTables";
7
- import { desc, gte, sql } from "drizzle-orm";
8
- import { unionAll } from "drizzle-orm/mysql-core";
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
- });
227
-
228
- /**
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.
418
- *
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.
422
- *
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
430
- *
431
- * ## OR Logic Thresholds
432
- * Statements are included if they exceed **EITHER** threshold:
433
- * - `avgLatencyMs > warnThresholdMs` **OR**
434
- * - `avgMemBytes > memoryThresholdBytes`
435
- *
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)
443
- *
444
- * ## Exclusions
445
- * - Statements with empty `digestText` or `digest`
446
- * - Service statements (`Use`, `Set`, `Show`)
447
- * - Queries that don't exceed either threshold
448
- *
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
459
- *
460
- * @example
461
- * ```typescript
462
- * import ForgeSQL, { topSlowestStatementLastHourTrigger } from "forge-sql-orm";
463
- *
464
- * const forgeSQL = new ForgeSQL();
465
- *
466
- * // Default thresholds: 300ms latency OR 8MB memory
467
- * export const performanceTrigger = () =>
468
- * topSlowestStatementLastHourTrigger(forgeSQL);
469
- *
470
- * // Conservative memory monitoring: 4MB threshold
471
- * export const conservativeTrigger = () =>
472
- * topSlowestStatementLastHourTrigger(forgeSQL, {
473
- * memoryThresholdBytes: 4 * 1024 * 1024
474
- * });
475
- *
476
- * // Memory-only monitoring: 4MB threshold (latency effectively disabled)
477
- * export const memoryOnlyTrigger = () =>
478
- * topSlowestStatementLastHourTrigger(forgeSQL, {
479
- * warnThresholdMs: 10000,
480
- * memoryThresholdBytes: 4 * 1024 * 1024
481
- * });
482
- *
483
- * // With execution plan in logs
484
- * export const withPlanTrigger = () =>
485
- * topSlowestStatementLastHourTrigger(forgeSQL, { showPlan: true });
486
- * ```
487
- *
488
- * @example
489
- * ```yaml
490
- * # manifest.yml configuration
491
- * scheduledTrigger:
492
- * - key: performance-trigger
493
- * function: performanceTrigger
494
- * interval: hour
495
- *
496
- * function:
497
- * - key: performanceTrigger
498
- * handler: index.performanceTrigger
499
- * ```
500
- */
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
- */
508
- export const topSlowestStatementLastHourTrigger = async (
509
- orm: ForgeSqlOperation,
510
- options?: TriggerOptions,
511
- ): Promise<TriggerResponse> => {
512
- // Validate required parameters
513
- if (!orm) {
514
- return createErrorResponse("ORM instance is required");
515
- }
516
-
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,
526
- };
527
-
528
- try {
529
- // Build the combined query from multiple tables
530
- const combined = buildCombinedQuery(orm, mergedOptions);
531
-
532
- // Build the grouped query with filtering and aggregation
533
- const grouped = buildGroupedQuery(orm, combined);
534
-
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",
542
- );
543
-
544
- // Format the results for output
545
- const formatted = formatQueryResults(rows, mergedOptions);
546
-
547
- // Log the results to console
548
- logQueryResults(formatted, mergedOptions);
549
-
550
- // Return success response
551
- return createSuccessResponse(formatted, mergedOptions);
552
- } catch (error: any) {
553
- // Log error details for debugging
554
- // eslint-disable-next-line no-console
555
- console.warn(
556
- "Error in topSlowestStatementLastHourTrigger (one-off errors can be ignored; if it recurs, investigate):",
557
- error?.cause?.context?.debug?.sqlMessage ?? error?.cause ?? error,
558
- );
559
-
560
- // Return error response
561
- return createErrorResponse("Failed to fetch or log slow queries", error);
562
- }
563
- };