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.
- package/README.md +202 -254
- package/dist/ForgeSQLORM.js +3238 -3227
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +3236 -3225
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +65 -11
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +66 -11
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/SystemTables.d.ts +82 -82
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts +6 -2
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts +5 -2
- package/dist/utils/metadataContextUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.d.ts +72 -1
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/index.d.ts +1 -1
- package/dist/webtriggers/index.d.ts.map +1 -1
- package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts +67 -0
- package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts.map +1 -0
- package/package.json +10 -10
- package/src/core/ForgeSQLORM.ts +164 -33
- package/src/core/ForgeSQLQueryBuilder.ts +65 -11
- package/src/core/SystemTables.ts +1 -1
- package/src/utils/cacheUtils.ts +3 -1
- package/src/utils/forgeDriver.ts +7 -34
- package/src/utils/forgeDriverProxy.ts +58 -6
- package/src/utils/metadataContextUtils.ts +21 -6
- package/src/utils/sqlUtils.ts +229 -2
- package/src/webtriggers/index.ts +1 -1
- package/src/webtriggers/slowQuerySchedulerTrigger.ts +82 -0
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +0 -114
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +0 -1
- 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
|
-
};
|