@berthojoris/mcp-mysql-server 1.9.3 → 1.10.1
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/CHANGELOG.md +47 -0
- package/DOCUMENTATIONS.md +3847 -3145
- package/README.md +151 -13
- package/bin/mcp-mysql.js +110 -53
- package/dist/config/featureConfig.d.ts +72 -9
- package/dist/config/featureConfig.js +415 -87
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.js +7 -1
- package/dist/index.d.ts +83 -1
- package/dist/index.js +75 -2
- package/dist/mcp-server.js +179 -7
- package/dist/security/securityLayer.d.ts +5 -1
- package/dist/security/securityLayer.js +18 -1
- package/dist/tools/performanceTools.d.ts +111 -0
- package/dist/tools/performanceTools.js +623 -0
- package/dist/tools/utilityTools.js +115 -24
- package/package.json +1 -1
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PerformanceTools = void 0;
|
|
7
|
+
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
|
+
class PerformanceTools {
|
|
9
|
+
constructor(security) {
|
|
10
|
+
this.db = connection_1.default.getInstance();
|
|
11
|
+
this.security = security;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get comprehensive performance metrics
|
|
15
|
+
*/
|
|
16
|
+
async getPerformanceMetrics() {
|
|
17
|
+
try {
|
|
18
|
+
const metrics = {};
|
|
19
|
+
// Get query performance metrics
|
|
20
|
+
const perfQuery = `
|
|
21
|
+
SELECT
|
|
22
|
+
SUM(TIMER_WAIT) / 1000000000000 as total_execution_time_sec,
|
|
23
|
+
SUM(LOCK_TIME) / 1000000000000 as total_lock_time_sec,
|
|
24
|
+
SUM(ROWS_EXAMINED) as total_rows_examined,
|
|
25
|
+
SUM(ROWS_SENT) as total_rows_sent,
|
|
26
|
+
SUM(ROWS_AFFECTED) as total_rows_affected,
|
|
27
|
+
SUM(SELECT_FULL_JOIN) as full_table_scans,
|
|
28
|
+
SUM(NO_INDEX_USED) as queries_without_indexes,
|
|
29
|
+
SUM(NO_GOOD_INDEX_USED) as queries_with_bad_indexes,
|
|
30
|
+
COUNT(*) as total_queries
|
|
31
|
+
FROM performance_schema.events_statements_summary_global_by_event_name
|
|
32
|
+
WHERE EVENT_NAME LIKE 'statement/sql/%'
|
|
33
|
+
`;
|
|
34
|
+
const perfResult = await this.db.query(perfQuery);
|
|
35
|
+
metrics.query_performance = perfResult[0] || {};
|
|
36
|
+
// Get connection metrics
|
|
37
|
+
const connQuery = `
|
|
38
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
|
39
|
+
'Threads_connected', 'Threads_running', 'Threads_created',
|
|
40
|
+
'Connections', 'Max_used_connections', 'Aborted_connects', 'Aborted_clients'
|
|
41
|
+
)
|
|
42
|
+
`;
|
|
43
|
+
const connResult = await this.db.query(connQuery);
|
|
44
|
+
metrics.connections = {};
|
|
45
|
+
for (const row of connResult) {
|
|
46
|
+
metrics.connections[row.Variable_name.toLowerCase()] = row.Value;
|
|
47
|
+
}
|
|
48
|
+
// Get table cache metrics
|
|
49
|
+
const cacheQuery = `
|
|
50
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
|
51
|
+
'Open_tables', 'Opened_tables', 'Table_open_cache_hits',
|
|
52
|
+
'Table_open_cache_misses', 'Table_open_cache_overflows'
|
|
53
|
+
)
|
|
54
|
+
`;
|
|
55
|
+
const cacheResult = await this.db.query(cacheQuery);
|
|
56
|
+
metrics.table_cache = {};
|
|
57
|
+
for (const row of cacheResult) {
|
|
58
|
+
metrics.table_cache[row.Variable_name.toLowerCase()] = row.Value;
|
|
59
|
+
}
|
|
60
|
+
// Get InnoDB metrics
|
|
61
|
+
const innodbQuery = `
|
|
62
|
+
SHOW GLOBAL STATUS WHERE Variable_name LIKE 'Innodb_%'
|
|
63
|
+
AND Variable_name IN (
|
|
64
|
+
'Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests',
|
|
65
|
+
'Innodb_buffer_pool_pages_total', 'Innodb_buffer_pool_pages_free',
|
|
66
|
+
'Innodb_buffer_pool_pages_data', 'Innodb_buffer_pool_pages_dirty',
|
|
67
|
+
'Innodb_row_lock_waits', 'Innodb_row_lock_time', 'Innodb_rows_read',
|
|
68
|
+
'Innodb_rows_inserted', 'Innodb_rows_updated', 'Innodb_rows_deleted'
|
|
69
|
+
)
|
|
70
|
+
`;
|
|
71
|
+
const innodbResult = await this.db.query(innodbQuery);
|
|
72
|
+
metrics.innodb = {};
|
|
73
|
+
for (const row of innodbResult) {
|
|
74
|
+
metrics.innodb[row.Variable_name.toLowerCase()] = row.Value;
|
|
75
|
+
}
|
|
76
|
+
// Calculate buffer pool hit ratio
|
|
77
|
+
if (metrics.innodb.innodb_buffer_pool_read_requests && metrics.innodb.innodb_buffer_pool_reads) {
|
|
78
|
+
const requests = parseFloat(metrics.innodb.innodb_buffer_pool_read_requests);
|
|
79
|
+
const reads = parseFloat(metrics.innodb.innodb_buffer_pool_reads);
|
|
80
|
+
metrics.innodb.buffer_pool_hit_ratio = ((requests - reads) / requests * 100).toFixed(2) + '%';
|
|
81
|
+
}
|
|
82
|
+
// Get slow query metrics
|
|
83
|
+
const slowQuery = `
|
|
84
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN ('Slow_queries', 'Questions')
|
|
85
|
+
`;
|
|
86
|
+
const slowResult = await this.db.query(slowQuery);
|
|
87
|
+
metrics.slow_queries = {};
|
|
88
|
+
for (const row of slowResult) {
|
|
89
|
+
metrics.slow_queries[row.Variable_name.toLowerCase()] = row.Value;
|
|
90
|
+
}
|
|
91
|
+
// Calculate slow query percentage
|
|
92
|
+
if (metrics.slow_queries.questions && metrics.slow_queries.slow_queries) {
|
|
93
|
+
const total = parseFloat(metrics.slow_queries.questions);
|
|
94
|
+
const slow = parseFloat(metrics.slow_queries.slow_queries);
|
|
95
|
+
metrics.slow_queries.slow_query_percentage = ((slow / total) * 100).toFixed(4) + '%';
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
status: 'success',
|
|
99
|
+
data: metrics,
|
|
100
|
+
queryLog: this.db.getFormattedQueryLogs(5)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
status: 'error',
|
|
106
|
+
error: error.message,
|
|
107
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get top queries by execution time
|
|
113
|
+
*/
|
|
114
|
+
async getTopQueriesByTime(params) {
|
|
115
|
+
try {
|
|
116
|
+
const limit = params?.limit || 10;
|
|
117
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > 100) {
|
|
118
|
+
return { status: 'error', error: 'Limit must be a positive integer between 1 and 100' };
|
|
119
|
+
}
|
|
120
|
+
const query = `
|
|
121
|
+
SELECT
|
|
122
|
+
DIGEST_TEXT as query_pattern,
|
|
123
|
+
COUNT_STAR as execution_count,
|
|
124
|
+
ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
|
|
125
|
+
ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
|
|
126
|
+
ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
|
|
127
|
+
ROUND(SUM_LOCK_TIME / 1000000000000, 6) as total_lock_time_sec,
|
|
128
|
+
SUM_ROWS_EXAMINED as rows_examined,
|
|
129
|
+
SUM_ROWS_SENT as rows_sent,
|
|
130
|
+
SUM_ROWS_AFFECTED as rows_affected,
|
|
131
|
+
FIRST_SEEN,
|
|
132
|
+
LAST_SEEN
|
|
133
|
+
FROM performance_schema.events_statements_summary_by_digest
|
|
134
|
+
WHERE DIGEST_TEXT IS NOT NULL
|
|
135
|
+
ORDER BY SUM_TIMER_WAIT DESC
|
|
136
|
+
LIMIT ${limit}
|
|
137
|
+
`;
|
|
138
|
+
const results = await this.db.query(query);
|
|
139
|
+
return {
|
|
140
|
+
status: 'success',
|
|
141
|
+
data: results,
|
|
142
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
status: 'error',
|
|
148
|
+
error: error.message,
|
|
149
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get top queries by execution count
|
|
155
|
+
*/
|
|
156
|
+
async getTopQueriesByCount(params) {
|
|
157
|
+
try {
|
|
158
|
+
const limit = params?.limit || 10;
|
|
159
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > 100) {
|
|
160
|
+
return { status: 'error', error: 'Limit must be a positive integer between 1 and 100' };
|
|
161
|
+
}
|
|
162
|
+
const query = `
|
|
163
|
+
SELECT
|
|
164
|
+
DIGEST_TEXT as query_pattern,
|
|
165
|
+
COUNT_STAR as execution_count,
|
|
166
|
+
ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
|
|
167
|
+
ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
|
|
168
|
+
ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
|
|
169
|
+
SUM_ROWS_EXAMINED as rows_examined,
|
|
170
|
+
SUM_ROWS_SENT as rows_sent,
|
|
171
|
+
FIRST_SEEN,
|
|
172
|
+
LAST_SEEN
|
|
173
|
+
FROM performance_schema.events_statements_summary_by_digest
|
|
174
|
+
WHERE DIGEST_TEXT IS NOT NULL
|
|
175
|
+
ORDER BY COUNT_STAR DESC
|
|
176
|
+
LIMIT ${limit}
|
|
177
|
+
`;
|
|
178
|
+
const results = await this.db.query(query);
|
|
179
|
+
return {
|
|
180
|
+
status: 'success',
|
|
181
|
+
data: results,
|
|
182
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return {
|
|
187
|
+
status: 'error',
|
|
188
|
+
error: error.message,
|
|
189
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get slow queries
|
|
195
|
+
*/
|
|
196
|
+
async getSlowQueries(params) {
|
|
197
|
+
try {
|
|
198
|
+
const limit = params?.limit || 10;
|
|
199
|
+
const thresholdSec = params?.threshold_seconds || 1;
|
|
200
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > 100) {
|
|
201
|
+
return { status: 'error', error: 'Limit must be a positive integer between 1 and 100' };
|
|
202
|
+
}
|
|
203
|
+
if (typeof thresholdSec !== 'number' || thresholdSec <= 0) {
|
|
204
|
+
return { status: 'error', error: 'Threshold must be a positive number' };
|
|
205
|
+
}
|
|
206
|
+
const thresholdPico = thresholdSec * 1000000000000;
|
|
207
|
+
const query = `
|
|
208
|
+
SELECT
|
|
209
|
+
DIGEST_TEXT as query_pattern,
|
|
210
|
+
COUNT_STAR as execution_count,
|
|
211
|
+
ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
|
|
212
|
+
ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
|
|
213
|
+
ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
|
|
214
|
+
ROUND(SUM_LOCK_TIME / 1000000000000, 6) as total_lock_time_sec,
|
|
215
|
+
SUM_ROWS_EXAMINED as rows_examined,
|
|
216
|
+
SUM_ROWS_SENT as rows_sent,
|
|
217
|
+
SUM_NO_INDEX_USED as no_index_used_count,
|
|
218
|
+
FIRST_SEEN,
|
|
219
|
+
LAST_SEEN
|
|
220
|
+
FROM performance_schema.events_statements_summary_by_digest
|
|
221
|
+
WHERE DIGEST_TEXT IS NOT NULL AND AVG_TIMER_WAIT > ${thresholdPico}
|
|
222
|
+
ORDER BY AVG_TIMER_WAIT DESC
|
|
223
|
+
LIMIT ${limit}
|
|
224
|
+
`;
|
|
225
|
+
const results = await this.db.query(query);
|
|
226
|
+
return {
|
|
227
|
+
status: 'success',
|
|
228
|
+
data: results,
|
|
229
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
return {
|
|
234
|
+
status: 'error',
|
|
235
|
+
error: error.message,
|
|
236
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get table I/O statistics
|
|
242
|
+
*/
|
|
243
|
+
async getTableIOStats(params) {
|
|
244
|
+
try {
|
|
245
|
+
const limit = params?.limit || 20;
|
|
246
|
+
const schema = params?.table_schema;
|
|
247
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > 100) {
|
|
248
|
+
return { status: 'error', error: 'Limit must be a positive integer between 1 and 100' };
|
|
249
|
+
}
|
|
250
|
+
let query = `
|
|
251
|
+
SELECT
|
|
252
|
+
OBJECT_SCHEMA as table_schema,
|
|
253
|
+
OBJECT_NAME as table_name,
|
|
254
|
+
COUNT_READ as read_operations,
|
|
255
|
+
COUNT_WRITE as write_operations,
|
|
256
|
+
COUNT_FETCH as fetch_operations,
|
|
257
|
+
COUNT_INSERT as insert_operations,
|
|
258
|
+
COUNT_UPDATE as update_operations,
|
|
259
|
+
COUNT_DELETE as delete_operations,
|
|
260
|
+
ROUND(SUM_TIMER_READ / 1000000000000, 6) as total_read_time_sec,
|
|
261
|
+
ROUND(SUM_TIMER_WRITE / 1000000000000, 6) as total_write_time_sec,
|
|
262
|
+
ROUND(SUM_TIMER_FETCH / 1000000000000, 6) as total_fetch_time_sec
|
|
263
|
+
FROM performance_schema.table_io_waits_summary_by_table
|
|
264
|
+
WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
|
|
265
|
+
`;
|
|
266
|
+
if (schema) {
|
|
267
|
+
query += ` AND OBJECT_SCHEMA = '${schema.replace(/'/g, "''")}'`;
|
|
268
|
+
}
|
|
269
|
+
query += ` ORDER BY (COUNT_READ + COUNT_WRITE) DESC LIMIT ${limit}`;
|
|
270
|
+
const results = await this.db.query(query);
|
|
271
|
+
return {
|
|
272
|
+
status: 'success',
|
|
273
|
+
data: results,
|
|
274
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
return {
|
|
279
|
+
status: 'error',
|
|
280
|
+
error: error.message,
|
|
281
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get index usage statistics
|
|
287
|
+
*/
|
|
288
|
+
async getIndexUsageStats(params) {
|
|
289
|
+
try {
|
|
290
|
+
const limit = params?.limit || 20;
|
|
291
|
+
const schema = params?.table_schema;
|
|
292
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > 100) {
|
|
293
|
+
return { status: 'error', error: 'Limit must be a positive integer between 1 and 100' };
|
|
294
|
+
}
|
|
295
|
+
let query = `
|
|
296
|
+
SELECT
|
|
297
|
+
OBJECT_SCHEMA as table_schema,
|
|
298
|
+
OBJECT_NAME as table_name,
|
|
299
|
+
INDEX_NAME as index_name,
|
|
300
|
+
COUNT_STAR as usage_count,
|
|
301
|
+
COUNT_READ as read_count,
|
|
302
|
+
COUNT_WRITE as write_count,
|
|
303
|
+
COUNT_FETCH as fetch_count,
|
|
304
|
+
COUNT_INSERT as insert_count,
|
|
305
|
+
COUNT_UPDATE as update_count,
|
|
306
|
+
COUNT_DELETE as delete_count
|
|
307
|
+
FROM performance_schema.table_io_waits_summary_by_index_usage
|
|
308
|
+
WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
|
|
309
|
+
AND INDEX_NAME IS NOT NULL
|
|
310
|
+
`;
|
|
311
|
+
if (schema) {
|
|
312
|
+
query += ` AND OBJECT_SCHEMA = '${schema.replace(/'/g, "''")}'`;
|
|
313
|
+
}
|
|
314
|
+
query += ` ORDER BY COUNT_STAR DESC LIMIT ${limit}`;
|
|
315
|
+
const results = await this.db.query(query);
|
|
316
|
+
return {
|
|
317
|
+
status: 'success',
|
|
318
|
+
data: results,
|
|
319
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
return {
|
|
324
|
+
status: 'error',
|
|
325
|
+
error: error.message,
|
|
326
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get unused indexes
|
|
332
|
+
*/
|
|
333
|
+
async getUnusedIndexes(params) {
|
|
334
|
+
try {
|
|
335
|
+
const schema = params?.table_schema;
|
|
336
|
+
let query = `
|
|
337
|
+
SELECT
|
|
338
|
+
t.TABLE_SCHEMA as table_schema,
|
|
339
|
+
t.TABLE_NAME as table_name,
|
|
340
|
+
s.INDEX_NAME as index_name,
|
|
341
|
+
s.COLUMN_NAME as column_name,
|
|
342
|
+
s.SEQ_IN_INDEX as sequence_in_index,
|
|
343
|
+
s.NON_UNIQUE as is_non_unique
|
|
344
|
+
FROM information_schema.STATISTICS s
|
|
345
|
+
LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage p
|
|
346
|
+
ON s.TABLE_SCHEMA = p.OBJECT_SCHEMA
|
|
347
|
+
AND s.TABLE_NAME = p.OBJECT_NAME
|
|
348
|
+
AND s.INDEX_NAME = p.INDEX_NAME
|
|
349
|
+
JOIN information_schema.TABLES t
|
|
350
|
+
ON s.TABLE_SCHEMA = t.TABLE_SCHEMA
|
|
351
|
+
AND s.TABLE_NAME = t.TABLE_NAME
|
|
352
|
+
WHERE s.TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
|
|
353
|
+
AND s.INDEX_NAME != 'PRIMARY'
|
|
354
|
+
AND (p.INDEX_NAME IS NULL OR p.COUNT_STAR = 0)
|
|
355
|
+
AND t.TABLE_TYPE = 'BASE TABLE'
|
|
356
|
+
`;
|
|
357
|
+
if (schema) {
|
|
358
|
+
query += ` AND s.TABLE_SCHEMA = '${schema.replace(/'/g, "''")}'`;
|
|
359
|
+
}
|
|
360
|
+
query += ` ORDER BY s.TABLE_SCHEMA, s.TABLE_NAME, s.INDEX_NAME, s.SEQ_IN_INDEX`;
|
|
361
|
+
const results = await this.db.query(query);
|
|
362
|
+
return {
|
|
363
|
+
status: 'success',
|
|
364
|
+
data: results,
|
|
365
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
return {
|
|
370
|
+
status: 'error',
|
|
371
|
+
error: error.message,
|
|
372
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Get connection pool statistics
|
|
378
|
+
*/
|
|
379
|
+
async getConnectionPoolStats() {
|
|
380
|
+
try {
|
|
381
|
+
const statusQuery = `
|
|
382
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
|
383
|
+
'Threads_connected', 'Threads_running', 'Threads_created', 'Threads_cached',
|
|
384
|
+
'Connections', 'Max_used_connections', 'Max_used_connections_time',
|
|
385
|
+
'Aborted_connects', 'Aborted_clients', 'Connection_errors_max_connections'
|
|
386
|
+
)
|
|
387
|
+
`;
|
|
388
|
+
const variablesQuery = `
|
|
389
|
+
SHOW GLOBAL VARIABLES WHERE Variable_name IN (
|
|
390
|
+
'max_connections', 'thread_cache_size', 'connect_timeout',
|
|
391
|
+
'wait_timeout', 'interactive_timeout'
|
|
392
|
+
)
|
|
393
|
+
`;
|
|
394
|
+
const statusResult = await this.db.query(statusQuery);
|
|
395
|
+
const variablesResult = await this.db.query(variablesQuery);
|
|
396
|
+
const stats = {
|
|
397
|
+
current_status: {},
|
|
398
|
+
configuration: {},
|
|
399
|
+
health_indicators: {}
|
|
400
|
+
};
|
|
401
|
+
for (const row of statusResult) {
|
|
402
|
+
stats.current_status[row.Variable_name.toLowerCase()] = row.Value;
|
|
403
|
+
}
|
|
404
|
+
for (const row of variablesResult) {
|
|
405
|
+
stats.configuration[row.Variable_name.toLowerCase()] = row.Value;
|
|
406
|
+
}
|
|
407
|
+
// Calculate health indicators
|
|
408
|
+
const threadsConnected = parseInt(stats.current_status.threads_connected || '0');
|
|
409
|
+
const maxConnections = parseInt(stats.configuration.max_connections || '0');
|
|
410
|
+
const maxUsedConnections = parseInt(stats.current_status.max_used_connections || '0');
|
|
411
|
+
if (maxConnections > 0) {
|
|
412
|
+
stats.health_indicators.connection_usage_percentage = ((threadsConnected / maxConnections) * 100).toFixed(2) + '%';
|
|
413
|
+
stats.health_indicators.max_usage_percentage = ((maxUsedConnections / maxConnections) * 100).toFixed(2) + '%';
|
|
414
|
+
stats.health_indicators.available_connections = maxConnections - threadsConnected;
|
|
415
|
+
}
|
|
416
|
+
// Connection efficiency
|
|
417
|
+
const totalConnections = parseInt(stats.current_status.connections || '0');
|
|
418
|
+
const abortedConnects = parseInt(stats.current_status.aborted_connects || '0');
|
|
419
|
+
if (totalConnections > 0) {
|
|
420
|
+
stats.health_indicators.aborted_connection_percentage = ((abortedConnects / totalConnections) * 100).toFixed(4) + '%';
|
|
421
|
+
}
|
|
422
|
+
// Thread cache efficiency
|
|
423
|
+
const threadsCreated = parseInt(stats.current_status.threads_created || '0');
|
|
424
|
+
if (totalConnections > 0) {
|
|
425
|
+
stats.health_indicators.thread_cache_hit_rate = (((totalConnections - threadsCreated) / totalConnections) * 100).toFixed(2) + '%';
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
status: 'success',
|
|
429
|
+
data: stats,
|
|
430
|
+
queryLog: this.db.getFormattedQueryLogs(2)
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
return {
|
|
435
|
+
status: 'error',
|
|
436
|
+
error: error.message,
|
|
437
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get database health check
|
|
443
|
+
*/
|
|
444
|
+
async getDatabaseHealthCheck() {
|
|
445
|
+
try {
|
|
446
|
+
const health = {
|
|
447
|
+
overall_status: 'healthy',
|
|
448
|
+
checks: [],
|
|
449
|
+
warnings: [],
|
|
450
|
+
errors: []
|
|
451
|
+
};
|
|
452
|
+
// Check 1: Connection usage
|
|
453
|
+
const connQuery = `
|
|
454
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected', 'Max_used_connections')
|
|
455
|
+
`;
|
|
456
|
+
const maxConnQuery = `SHOW GLOBAL VARIABLES WHERE Variable_name = 'max_connections'`;
|
|
457
|
+
const connResult = await this.db.query(connQuery);
|
|
458
|
+
const maxConnResult = await this.db.query(maxConnQuery);
|
|
459
|
+
let threadsConnected = 0;
|
|
460
|
+
let maxUsedConnections = 0;
|
|
461
|
+
let maxConnections = 0;
|
|
462
|
+
for (const row of connResult) {
|
|
463
|
+
if (row.Variable_name === 'Threads_connected')
|
|
464
|
+
threadsConnected = parseInt(row.Value);
|
|
465
|
+
if (row.Variable_name === 'Max_used_connections')
|
|
466
|
+
maxUsedConnections = parseInt(row.Value);
|
|
467
|
+
}
|
|
468
|
+
for (const row of maxConnResult) {
|
|
469
|
+
maxConnections = parseInt(row.Value);
|
|
470
|
+
}
|
|
471
|
+
const connUsage = (threadsConnected / maxConnections) * 100;
|
|
472
|
+
const maxConnUsage = (maxUsedConnections / maxConnections) * 100;
|
|
473
|
+
health.checks.push({
|
|
474
|
+
name: 'Connection Usage',
|
|
475
|
+
status: connUsage < 80 ? 'healthy' : connUsage < 90 ? 'warning' : 'critical',
|
|
476
|
+
current: threadsConnected,
|
|
477
|
+
max: maxConnections,
|
|
478
|
+
usage_percentage: connUsage.toFixed(2) + '%'
|
|
479
|
+
});
|
|
480
|
+
if (connUsage >= 90) {
|
|
481
|
+
health.errors.push('Connection usage is critically high (>90%)');
|
|
482
|
+
health.overall_status = 'critical';
|
|
483
|
+
}
|
|
484
|
+
else if (connUsage >= 80) {
|
|
485
|
+
health.warnings.push('Connection usage is high (>80%)');
|
|
486
|
+
if (health.overall_status === 'healthy')
|
|
487
|
+
health.overall_status = 'warning';
|
|
488
|
+
}
|
|
489
|
+
// Check 2: Buffer pool hit ratio
|
|
490
|
+
const bufferQuery = `
|
|
491
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_reads')
|
|
492
|
+
`;
|
|
493
|
+
const bufferResult = await this.db.query(bufferQuery);
|
|
494
|
+
let readRequests = 0;
|
|
495
|
+
let reads = 0;
|
|
496
|
+
for (const row of bufferResult) {
|
|
497
|
+
if (row.Variable_name === 'Innodb_buffer_pool_read_requests')
|
|
498
|
+
readRequests = parseInt(row.Value);
|
|
499
|
+
if (row.Variable_name === 'Innodb_buffer_pool_reads')
|
|
500
|
+
reads = parseInt(row.Value);
|
|
501
|
+
}
|
|
502
|
+
const hitRatio = readRequests > 0 ? ((readRequests - reads) / readRequests) * 100 : 100;
|
|
503
|
+
health.checks.push({
|
|
504
|
+
name: 'Buffer Pool Hit Ratio',
|
|
505
|
+
status: hitRatio > 95 ? 'healthy' : hitRatio > 85 ? 'warning' : 'critical',
|
|
506
|
+
hit_ratio: hitRatio.toFixed(2) + '%'
|
|
507
|
+
});
|
|
508
|
+
if (hitRatio <= 85) {
|
|
509
|
+
health.errors.push('Buffer pool hit ratio is too low (<85%)');
|
|
510
|
+
health.overall_status = 'critical';
|
|
511
|
+
}
|
|
512
|
+
else if (hitRatio <= 95) {
|
|
513
|
+
health.warnings.push('Buffer pool hit ratio could be improved (<95%)');
|
|
514
|
+
if (health.overall_status === 'healthy')
|
|
515
|
+
health.overall_status = 'warning';
|
|
516
|
+
}
|
|
517
|
+
// Check 3: Aborted connections
|
|
518
|
+
const abortQuery = `
|
|
519
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN ('Connections', 'Aborted_connects')
|
|
520
|
+
`;
|
|
521
|
+
const abortResult = await this.db.query(abortQuery);
|
|
522
|
+
let totalConnections = 0;
|
|
523
|
+
let abortedConnects = 0;
|
|
524
|
+
for (const row of abortResult) {
|
|
525
|
+
if (row.Variable_name === 'Connections')
|
|
526
|
+
totalConnections = parseInt(row.Value);
|
|
527
|
+
if (row.Variable_name === 'Aborted_connects')
|
|
528
|
+
abortedConnects = parseInt(row.Value);
|
|
529
|
+
}
|
|
530
|
+
const abortRate = totalConnections > 0 ? (abortedConnects / totalConnections) * 100 : 0;
|
|
531
|
+
health.checks.push({
|
|
532
|
+
name: 'Aborted Connections',
|
|
533
|
+
status: abortRate < 1 ? 'healthy' : abortRate < 5 ? 'warning' : 'critical',
|
|
534
|
+
aborted: abortedConnects,
|
|
535
|
+
total: totalConnections,
|
|
536
|
+
abort_rate: abortRate.toFixed(4) + '%'
|
|
537
|
+
});
|
|
538
|
+
if (abortRate >= 5) {
|
|
539
|
+
health.errors.push('High rate of aborted connections (>5%)');
|
|
540
|
+
if (health.overall_status !== 'critical')
|
|
541
|
+
health.overall_status = 'warning';
|
|
542
|
+
}
|
|
543
|
+
else if (abortRate >= 1) {
|
|
544
|
+
health.warnings.push('Elevated rate of aborted connections (>1%)');
|
|
545
|
+
if (health.overall_status === 'healthy')
|
|
546
|
+
health.overall_status = 'warning';
|
|
547
|
+
}
|
|
548
|
+
// Check 4: Slow queries
|
|
549
|
+
const slowQuery = `
|
|
550
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN ('Questions', 'Slow_queries')
|
|
551
|
+
`;
|
|
552
|
+
const slowResult = await this.db.query(slowQuery);
|
|
553
|
+
let questions = 0;
|
|
554
|
+
let slowQueries = 0;
|
|
555
|
+
for (const row of slowResult) {
|
|
556
|
+
if (row.Variable_name === 'Questions')
|
|
557
|
+
questions = parseInt(row.Value);
|
|
558
|
+
if (row.Variable_name === 'Slow_queries')
|
|
559
|
+
slowQueries = parseInt(row.Value);
|
|
560
|
+
}
|
|
561
|
+
const slowQueryRate = questions > 0 ? (slowQueries / questions) * 100 : 0;
|
|
562
|
+
health.checks.push({
|
|
563
|
+
name: 'Slow Queries',
|
|
564
|
+
status: slowQueryRate < 1 ? 'healthy' : slowQueryRate < 5 ? 'warning' : 'critical',
|
|
565
|
+
slow_queries: slowQueries,
|
|
566
|
+
total_queries: questions,
|
|
567
|
+
slow_query_rate: slowQueryRate.toFixed(4) + '%'
|
|
568
|
+
});
|
|
569
|
+
if (slowQueryRate >= 5) {
|
|
570
|
+
health.warnings.push('High rate of slow queries (>5%)');
|
|
571
|
+
if (health.overall_status === 'healthy')
|
|
572
|
+
health.overall_status = 'warning';
|
|
573
|
+
}
|
|
574
|
+
else if (slowQueryRate >= 1) {
|
|
575
|
+
health.warnings.push('Elevated rate of slow queries (>1%)');
|
|
576
|
+
if (health.overall_status === 'healthy')
|
|
577
|
+
health.overall_status = 'warning';
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
status: 'success',
|
|
581
|
+
data: health,
|
|
582
|
+
queryLog: this.db.getFormattedQueryLogs(6)
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
return {
|
|
587
|
+
status: 'error',
|
|
588
|
+
error: error.message,
|
|
589
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Reset performance schema statistics
|
|
595
|
+
*/
|
|
596
|
+
async resetPerformanceStats() {
|
|
597
|
+
try {
|
|
598
|
+
// Truncate performance schema summary tables
|
|
599
|
+
const tables = [
|
|
600
|
+
'events_statements_summary_by_digest',
|
|
601
|
+
'events_statements_summary_global_by_event_name',
|
|
602
|
+
'table_io_waits_summary_by_table',
|
|
603
|
+
'table_io_waits_summary_by_index_usage'
|
|
604
|
+
];
|
|
605
|
+
for (const table of tables) {
|
|
606
|
+
await this.db.query(`TRUNCATE TABLE performance_schema.${table}`);
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
status: 'success',
|
|
610
|
+
message: 'Performance statistics reset successfully',
|
|
611
|
+
queryLog: this.db.getFormattedQueryLogs(tables.length)
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
return {
|
|
616
|
+
status: 'error',
|
|
617
|
+
error: error.message,
|
|
618
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
exports.PerformanceTools = PerformanceTools;
|