taurusdb-core 0.3.0 → 0.5.0-rc.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/LICENSE +21 -0
- package/README.md +3 -1
- package/dist/audit/jsonl-writer.d.ts +47 -0
- package/dist/audit/jsonl-writer.js +124 -0
- package/dist/auth/sql-profile-loader/env-source.js +10 -0
- package/dist/auth/sql-profile-loader/parsing.js +11 -2
- package/dist/auth/sql-profile-loader/runtime-override.js +10 -2
- package/dist/auth/sql-profile-loader/types.d.ts +5 -0
- package/dist/capability/probe.js +3 -1
- package/dist/cloud/auth.d.ts +8 -0
- package/dist/cloud/auth.js +61 -3
- package/dist/cloud/csms.d.ts +7 -0
- package/dist/cloud/csms.js +60 -0
- package/dist/cloud/keychain.d.ts +28 -0
- package/dist/cloud/keychain.js +200 -0
- package/dist/cloud/kms.d.ts +7 -0
- package/dist/cloud/kms.js +86 -0
- package/dist/config/env.js +20 -0
- package/dist/config/schema.d.ts +88 -0
- package/dist/config/schema.js +21 -0
- package/dist/context/datasource-resolver.js +7 -0
- package/dist/context/session-context.d.ts +7 -0
- package/dist/diagnostics/metrics-source.d.ts +3 -0
- package/dist/diagnostics/metrics-source.js +7 -2
- package/dist/diagnostics/replication-lag.d.ts +4 -0
- package/dist/diagnostics/replication-lag.js +138 -0
- package/dist/diagnostics/slow-sql-source/das-source.d.ts +3 -0
- package/dist/diagnostics/slow-sql-source/das-source.js +3 -0
- package/dist/diagnostics/slow-sql-source/factory.js +9 -2
- package/dist/diagnostics/slow-sql-source/taurus-api-source.d.ts +3 -0
- package/dist/diagnostics/slow-sql-source/taurus-api-source.js +3 -0
- package/dist/diagnostics/types.d.ts +5 -1
- package/dist/diagnostics/types.js +7 -7
- package/dist/engine/runtime.d.ts +2 -2
- package/dist/engine/runtime.js +8 -14
- package/dist/engine/types.d.ts +3 -2
- package/dist/engine.d.ts +4 -3
- package/dist/engine.js +44 -2
- package/dist/executor/bounded-sql.d.ts +1 -0
- package/dist/executor/bounded-sql.js +10 -0
- package/dist/executor/concurrency-limiter.d.ts +15 -0
- package/dist/executor/concurrency-limiter.js +67 -0
- package/dist/executor/connection-pool.d.ts +3 -2
- package/dist/executor/connection-pool.js +36 -24
- package/dist/executor/sql-executor.d.ts +3 -0
- package/dist/executor/sql-executor.js +52 -9
- package/dist/executor/types.d.ts +5 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.js +7 -1
- package/dist/safety/confirmation-store.d.ts +30 -7
- package/dist/safety/confirmation-store.js +154 -30
- package/dist/safety/guardrail.d.ts +3 -0
- package/dist/safety/guardrail.js +16 -6
- package/dist/safety/redaction.d.ts +6 -0
- package/dist/safety/redaction.js +47 -6
- package/dist/safety/sql-classifier.d.ts +1 -0
- package/dist/safety/sql-classifier.js +1 -0
- package/dist/safety/sql-validator.d.ts +1 -0
- package/dist/safety/sql-validator.js +24 -0
- package/dist/schema/adapters/mysql.js +3 -1
- package/dist/taurus/flashback.js +4 -10
- package/dist/taurus/recycle-bin.js +2 -8
- package/dist/utils/formatter.d.ts +6 -2
- package/dist/utils/formatter.js +7 -3
- package/dist/utils/logger.js +8 -0
- package/dist/utils/mysql-identifier.d.ts +1 -0
- package/dist/utils/mysql-identifier.js +9 -0
- package/package.json +14 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchHuaweiCloud } from "../cloud/auth.js";
|
|
1
|
+
import { fetchHuaweiCloud, getHuaweiCloudAuthFromConfig } from "../cloud/auth.js";
|
|
2
2
|
const TAURUS_CES_METRICS = {
|
|
3
3
|
cpu_util: "gaussdb_mysql001_cpu_util",
|
|
4
4
|
mem_util: "gaussdb_mysql002_mem_util",
|
|
@@ -132,6 +132,7 @@ export class CesMetricsSource {
|
|
|
132
132
|
accessKeyId;
|
|
133
133
|
secretAccessKey;
|
|
134
134
|
securityToken;
|
|
135
|
+
credentialProvider;
|
|
135
136
|
namespace;
|
|
136
137
|
instanceDimension;
|
|
137
138
|
nodeDimension;
|
|
@@ -149,6 +150,7 @@ export class CesMetricsSource {
|
|
|
149
150
|
this.accessKeyId = options.accessKeyId;
|
|
150
151
|
this.secretAccessKey = options.secretAccessKey;
|
|
151
152
|
this.securityToken = options.securityToken;
|
|
153
|
+
this.credentialProvider = options.credentialProvider;
|
|
152
154
|
this.namespace = options.namespace;
|
|
153
155
|
this.instanceDimension = options.instanceDimension;
|
|
154
156
|
this.nodeDimension = options.nodeDimension;
|
|
@@ -228,6 +230,7 @@ export class CesMetricsSource {
|
|
|
228
230
|
accessKeyId: this.accessKeyId,
|
|
229
231
|
secretAccessKey: this.secretAccessKey,
|
|
230
232
|
securityToken: this.securityToken,
|
|
233
|
+
credentialProvider: this.credentialProvider,
|
|
231
234
|
},
|
|
232
235
|
fetchImpl: (input, init) => this.fetchImpl(input, {
|
|
233
236
|
...init,
|
|
@@ -257,7 +260,8 @@ export function createMetricsSource(config) {
|
|
|
257
260
|
!ces.projectId ||
|
|
258
261
|
!ces.instanceId ||
|
|
259
262
|
(!ces.authToken &&
|
|
260
|
-
!(config.cloud?.accessKeyId && config.cloud?.secretAccessKey)
|
|
263
|
+
!(config.cloud?.accessKeyId && config.cloud?.secretAccessKey) &&
|
|
264
|
+
!config.cloud?.keychainService)) {
|
|
261
265
|
return undefined;
|
|
262
266
|
}
|
|
263
267
|
return new CesMetricsSource({
|
|
@@ -269,6 +273,7 @@ export function createMetricsSource(config) {
|
|
|
269
273
|
accessKeyId: config.cloud?.accessKeyId,
|
|
270
274
|
secretAccessKey: config.cloud?.secretAccessKey,
|
|
271
275
|
securityToken: config.cloud?.securityToken,
|
|
276
|
+
credentialProvider: getHuaweiCloudAuthFromConfig(config).credentialProvider,
|
|
272
277
|
namespace: ces.namespace,
|
|
273
278
|
instanceDimension: ces.instanceDimension,
|
|
274
279
|
nodeDimension: ces.nodeDimension,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SessionContext } from "../context/session-context.js";
|
|
2
|
+
import type { SqlExecutor } from "../executor/sql-executor.js";
|
|
3
|
+
import type { DiagnoseReplicationLagInput, DiagnosticResult } from "./types.js";
|
|
4
|
+
export declare function diagnoseReplicationLag(executor: SqlExecutor, input: DiagnoseReplicationLagInput, ctx: SessionContext): Promise<DiagnosticResult>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
function rowsAsObjects(result) {
|
|
2
|
+
return result.rows.map((row) => Object.fromEntries(result.columns.map((column, index) => [column.name, row[index]])));
|
|
3
|
+
}
|
|
4
|
+
function text(row, ...names) {
|
|
5
|
+
for (const name of names) {
|
|
6
|
+
const value = row[name];
|
|
7
|
+
if (value !== undefined && value !== null && String(value).trim()) {
|
|
8
|
+
return String(value).trim();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function number(row, ...names) {
|
|
14
|
+
const value = text(row, ...names);
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
20
|
+
}
|
|
21
|
+
async function readReplicationStatus(executor, ctx) {
|
|
22
|
+
try {
|
|
23
|
+
return rowsAsObjects(await executor.executeReadonly("SHOW REPLICA STATUS", ctx, {
|
|
24
|
+
maxRows: 20,
|
|
25
|
+
maxColumns: 80,
|
|
26
|
+
maxFieldChars: 512,
|
|
27
|
+
maxResultBytes: 262144,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return rowsAsObjects(await executor.executeReadonly("SHOW SLAVE STATUS", ctx, {
|
|
32
|
+
maxRows: 20,
|
|
33
|
+
maxColumns: 80,
|
|
34
|
+
maxFieldChars: 512,
|
|
35
|
+
maxResultBytes: 262144,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function diagnoseReplicationLag(executor, input, ctx) {
|
|
40
|
+
let rows;
|
|
41
|
+
try {
|
|
42
|
+
rows = await readReplicationStatus(executor, ctx);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
rows = [];
|
|
46
|
+
}
|
|
47
|
+
const filtered = rows.filter((row) => {
|
|
48
|
+
const channel = text(row, "Channel_Name");
|
|
49
|
+
const replicaId = text(row, "Server_Id", "Source_Server_Id", "Master_Server_Id");
|
|
50
|
+
return ((!input.channel || channel === input.channel) &&
|
|
51
|
+
(!input.replicaId || replicaId === input.replicaId));
|
|
52
|
+
});
|
|
53
|
+
const window = {
|
|
54
|
+
from: input.timeRange?.from,
|
|
55
|
+
to: input.timeRange?.to,
|
|
56
|
+
relative: input.timeRange?.relative,
|
|
57
|
+
};
|
|
58
|
+
if (filtered.length === 0) {
|
|
59
|
+
return {
|
|
60
|
+
tool: "diagnose_replication_lag",
|
|
61
|
+
status: "not_applicable",
|
|
62
|
+
severity: "info",
|
|
63
|
+
summary: "No replication channel was visible on the selected database endpoint.",
|
|
64
|
+
diagnosisWindow: window,
|
|
65
|
+
rootCauseCandidates: [],
|
|
66
|
+
keyFindings: [
|
|
67
|
+
"SHOW REPLICA STATUS and SHOW SLAVE STATUS returned no matching channel.",
|
|
68
|
+
],
|
|
69
|
+
evidence: [],
|
|
70
|
+
recommendedActions: [
|
|
71
|
+
"Confirm that the selected endpoint is a read replica and that the read-only SQL user can inspect replication status.",
|
|
72
|
+
],
|
|
73
|
+
limitations: [
|
|
74
|
+
"A primary or standalone endpoint legitimately has no replica status.",
|
|
75
|
+
"Cloud time-series replication metrics are not included in this data-plane-only result.",
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const findings = filtered.map((row) => {
|
|
80
|
+
const lag = number(row, "Seconds_Behind_Source", "Seconds_Behind_Master");
|
|
81
|
+
const io = text(row, "Replica_IO_Running", "Slave_IO_Running") ?? "unknown";
|
|
82
|
+
const sql = text(row, "Replica_SQL_Running", "Slave_SQL_Running") ?? "unknown";
|
|
83
|
+
const channel = text(row, "Channel_Name") ?? "default";
|
|
84
|
+
return { lag, io, sql, channel };
|
|
85
|
+
});
|
|
86
|
+
const maxLag = Math.max(...findings.map((item) => item.lag ?? 0));
|
|
87
|
+
const stopped = findings.some((item) => item.io.toLowerCase() === "no" || item.sql.toLowerCase() === "no");
|
|
88
|
+
const severity = stopped || maxLag >= 300 ? "high" : maxLag >= 30 ? "warning" : "info";
|
|
89
|
+
const status = stopped || maxLag >= 30 ? "inconclusive" : "ok";
|
|
90
|
+
return {
|
|
91
|
+
tool: "diagnose_replication_lag",
|
|
92
|
+
status,
|
|
93
|
+
severity,
|
|
94
|
+
summary: stopped
|
|
95
|
+
? "At least one replication worker is not running."
|
|
96
|
+
: `Observed maximum replication delay of ${maxLag} seconds.`,
|
|
97
|
+
diagnosisWindow: window,
|
|
98
|
+
rootCauseCandidates: [
|
|
99
|
+
...(stopped
|
|
100
|
+
? [{
|
|
101
|
+
code: "replication_worker_stopped",
|
|
102
|
+
title: "Replication worker stopped",
|
|
103
|
+
confidence: "high",
|
|
104
|
+
rationale: "Replica IO or SQL worker reports No.",
|
|
105
|
+
}]
|
|
106
|
+
: []),
|
|
107
|
+
...(maxLag >= 30
|
|
108
|
+
? [{
|
|
109
|
+
code: "replication_delay",
|
|
110
|
+
title: "Replica apply delay",
|
|
111
|
+
confidence: "high",
|
|
112
|
+
rationale: `Seconds behind source reached ${maxLag}.`,
|
|
113
|
+
}]
|
|
114
|
+
: []),
|
|
115
|
+
],
|
|
116
|
+
keyFindings: findings.map((item) => `Channel ${item.channel}: lag=${item.lag ?? "unknown"}s, io=${item.io}, sql=${item.sql}.`),
|
|
117
|
+
evidence: [{
|
|
118
|
+
source: "replication_status",
|
|
119
|
+
title: "Current replication status",
|
|
120
|
+
summary: `Collected ${findings.length} matching replication channel(s).`,
|
|
121
|
+
}],
|
|
122
|
+
recommendedActions: stopped
|
|
123
|
+
? [
|
|
124
|
+
"Inspect Last_IO_Error and Last_SQL_Error directly with a privileged DBA account.",
|
|
125
|
+
"Do not skip replication errors until the failed event and data consistency impact are understood.",
|
|
126
|
+
]
|
|
127
|
+
: maxLag >= 30
|
|
128
|
+
? [
|
|
129
|
+
"Check long transactions, write bursts, DDL, and replica resource saturation.",
|
|
130
|
+
"Compare lag over time in Cloud Eye before changing replica topology.",
|
|
131
|
+
]
|
|
132
|
+
: ["Continue monitoring the replication delay trend."],
|
|
133
|
+
limitations: [
|
|
134
|
+
"This result is a point-in-time data-plane snapshot.",
|
|
135
|
+
"Error text and relay-log SQL are intentionally not returned to the MCP client.",
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HuaweiCloudCredentialProvider } from "../../cloud/auth.js";
|
|
1
2
|
import type { SessionContext } from "../../context/session-context.js";
|
|
2
3
|
import type { FindTopSlowSqlInput } from "../types.js";
|
|
3
4
|
import type { ExternalSlowSqlSample, ResolveSlowSqlInput, SlowSqlSource } from "./types.js";
|
|
@@ -9,6 +10,7 @@ type DasSlowSqlSourceOptions = {
|
|
|
9
10
|
accessKeyId?: string;
|
|
10
11
|
secretAccessKey?: string;
|
|
11
12
|
securityToken?: string;
|
|
13
|
+
credentialProvider?: HuaweiCloudCredentialProvider;
|
|
12
14
|
datastoreType: "MySQL" | "TaurusDB";
|
|
13
15
|
requestTimeoutMs: number;
|
|
14
16
|
defaultLookbackMinutes: number;
|
|
@@ -24,6 +26,7 @@ export declare class DasSlowSqlSource implements SlowSqlSource {
|
|
|
24
26
|
private readonly accessKeyId?;
|
|
25
27
|
private readonly secretAccessKey?;
|
|
26
28
|
private readonly securityToken?;
|
|
29
|
+
private readonly credentialProvider?;
|
|
27
30
|
private readonly datastoreType;
|
|
28
31
|
private readonly requestTimeoutMs;
|
|
29
32
|
private readonly defaultLookbackMinutes;
|
|
@@ -9,6 +9,7 @@ export class DasSlowSqlSource {
|
|
|
9
9
|
accessKeyId;
|
|
10
10
|
secretAccessKey;
|
|
11
11
|
securityToken;
|
|
12
|
+
credentialProvider;
|
|
12
13
|
datastoreType;
|
|
13
14
|
requestTimeoutMs;
|
|
14
15
|
defaultLookbackMinutes;
|
|
@@ -23,6 +24,7 @@ export class DasSlowSqlSource {
|
|
|
23
24
|
this.accessKeyId = options.accessKeyId;
|
|
24
25
|
this.secretAccessKey = options.secretAccessKey;
|
|
25
26
|
this.securityToken = options.securityToken;
|
|
27
|
+
this.credentialProvider = options.credentialProvider;
|
|
26
28
|
this.datastoreType = options.datastoreType;
|
|
27
29
|
this.requestTimeoutMs = options.requestTimeoutMs;
|
|
28
30
|
this.defaultLookbackMinutes = options.defaultLookbackMinutes;
|
|
@@ -149,6 +151,7 @@ export class DasSlowSqlSource {
|
|
|
149
151
|
accessKeyId: this.accessKeyId,
|
|
150
152
|
secretAccessKey: this.secretAccessKey,
|
|
151
153
|
securityToken: this.securityToken,
|
|
154
|
+
credentialProvider: this.credentialProvider,
|
|
152
155
|
},
|
|
153
156
|
fetchImpl: (input, init) => this.fetchImpl(input, {
|
|
154
157
|
...init,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getHuaweiCloudAuthFromConfig } from "../../cloud/auth.js";
|
|
1
2
|
import { DasSlowSqlSource } from "./das-source.js";
|
|
2
3
|
import { TaurusApiSlowSqlSource } from "./taurus-api-source.js";
|
|
3
4
|
import { sortExternalSamples } from "./utils.js";
|
|
@@ -28,6 +29,7 @@ class CompositeSlowSqlSource {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
export function createSlowSqlSource(config) {
|
|
32
|
+
const credentialProvider = getHuaweiCloudAuthFromConfig(config).credentialProvider;
|
|
31
33
|
const sources = [];
|
|
32
34
|
const taurusApi = config.slowSqlSource?.taurusApi;
|
|
33
35
|
if (taurusApi?.enabled &&
|
|
@@ -36,7 +38,8 @@ export function createSlowSqlSource(config) {
|
|
|
36
38
|
taurusApi.instanceId &&
|
|
37
39
|
taurusApi.nodeId &&
|
|
38
40
|
(taurusApi.authToken ||
|
|
39
|
-
(config.cloud?.accessKeyId && config.cloud?.secretAccessKey)
|
|
41
|
+
(config.cloud?.accessKeyId && config.cloud?.secretAccessKey) ||
|
|
42
|
+
config.cloud?.keychainService)) {
|
|
40
43
|
sources.push(new TaurusApiSlowSqlSource({
|
|
41
44
|
endpoint: taurusApi.endpoint,
|
|
42
45
|
projectId: taurusApi.projectId,
|
|
@@ -46,6 +49,7 @@ export function createSlowSqlSource(config) {
|
|
|
46
49
|
accessKeyId: config.cloud?.accessKeyId,
|
|
47
50
|
secretAccessKey: config.cloud?.secretAccessKey,
|
|
48
51
|
securityToken: config.cloud?.securityToken,
|
|
52
|
+
credentialProvider,
|
|
49
53
|
language: taurusApi.language,
|
|
50
54
|
requestTimeoutMs: taurusApi.requestTimeoutMs,
|
|
51
55
|
defaultLookbackMinutes: taurusApi.defaultLookbackMinutes,
|
|
@@ -57,7 +61,9 @@ export function createSlowSqlSource(config) {
|
|
|
57
61
|
das.endpoint &&
|
|
58
62
|
das.projectId &&
|
|
59
63
|
das.instanceId &&
|
|
60
|
-
(das.authToken ||
|
|
64
|
+
(das.authToken ||
|
|
65
|
+
(config.cloud?.accessKeyId && config.cloud?.secretAccessKey) ||
|
|
66
|
+
config.cloud?.keychainService)) {
|
|
61
67
|
sources.push(new DasSlowSqlSource({
|
|
62
68
|
endpoint: das.endpoint,
|
|
63
69
|
projectId: das.projectId,
|
|
@@ -66,6 +72,7 @@ export function createSlowSqlSource(config) {
|
|
|
66
72
|
accessKeyId: config.cloud?.accessKeyId,
|
|
67
73
|
secretAccessKey: config.cloud?.secretAccessKey,
|
|
68
74
|
securityToken: config.cloud?.securityToken,
|
|
75
|
+
credentialProvider,
|
|
69
76
|
datastoreType: das.datastoreType,
|
|
70
77
|
requestTimeoutMs: das.requestTimeoutMs,
|
|
71
78
|
defaultLookbackMinutes: das.defaultLookbackMinutes,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HuaweiCloudCredentialProvider } from "../../cloud/auth.js";
|
|
1
2
|
import type { SessionContext } from "../../context/session-context.js";
|
|
2
3
|
import type { FindTopSlowSqlInput } from "../types.js";
|
|
3
4
|
import type { ExternalSlowSqlSample, ResolveSlowSqlInput, SlowSqlSource } from "./types.js";
|
|
@@ -10,6 +11,7 @@ type TaurusApiSlowSqlSourceOptions = {
|
|
|
10
11
|
accessKeyId?: string;
|
|
11
12
|
secretAccessKey?: string;
|
|
12
13
|
securityToken?: string;
|
|
14
|
+
credentialProvider?: HuaweiCloudCredentialProvider;
|
|
13
15
|
language: "en-us" | "zh-cn";
|
|
14
16
|
requestTimeoutMs: number;
|
|
15
17
|
defaultLookbackMinutes: number;
|
|
@@ -25,6 +27,7 @@ export declare class TaurusApiSlowSqlSource implements SlowSqlSource {
|
|
|
25
27
|
private readonly accessKeyId?;
|
|
26
28
|
private readonly secretAccessKey?;
|
|
27
29
|
private readonly securityToken?;
|
|
30
|
+
private readonly credentialProvider?;
|
|
28
31
|
private readonly language;
|
|
29
32
|
private readonly requestTimeoutMs;
|
|
30
33
|
private readonly defaultLookbackMinutes;
|
|
@@ -10,6 +10,7 @@ export class TaurusApiSlowSqlSource {
|
|
|
10
10
|
accessKeyId;
|
|
11
11
|
secretAccessKey;
|
|
12
12
|
securityToken;
|
|
13
|
+
credentialProvider;
|
|
13
14
|
language;
|
|
14
15
|
requestTimeoutMs;
|
|
15
16
|
defaultLookbackMinutes;
|
|
@@ -24,6 +25,7 @@ export class TaurusApiSlowSqlSource {
|
|
|
24
25
|
this.accessKeyId = options.accessKeyId;
|
|
25
26
|
this.secretAccessKey = options.secretAccessKey;
|
|
26
27
|
this.securityToken = options.securityToken;
|
|
28
|
+
this.credentialProvider = options.credentialProvider;
|
|
27
29
|
this.language = options.language;
|
|
28
30
|
this.requestTimeoutMs = options.requestTimeoutMs;
|
|
29
31
|
this.defaultLookbackMinutes = options.defaultLookbackMinutes;
|
|
@@ -128,6 +130,7 @@ export class TaurusApiSlowSqlSource {
|
|
|
128
130
|
accessKeyId: this.accessKeyId,
|
|
129
131
|
secretAccessKey: this.secretAccessKey,
|
|
130
132
|
securityToken: this.securityToken,
|
|
133
|
+
credentialProvider: this.credentialProvider,
|
|
131
134
|
},
|
|
132
135
|
fetchImpl: (input, init) => this.fetchImpl(input, {
|
|
133
136
|
...init,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DiagnosticToolName = "diagnose_service_latency" | "diagnose_db_hotspot" | "find_top_slow_sql" | "diagnose_slow_query" | "diagnose_connection_spike" | "diagnose_lock_contention" | "diagnose_storage_pressure";
|
|
1
|
+
export type DiagnosticToolName = "diagnose_service_latency" | "diagnose_db_hotspot" | "find_top_slow_sql" | "diagnose_slow_query" | "diagnose_connection_spike" | "diagnose_lock_contention" | "diagnose_replication_lag" | "diagnose_storage_pressure";
|
|
2
2
|
export type DiagnosticStatus = "ok" | "inconclusive" | "not_applicable";
|
|
3
3
|
export type DiagnosticSeverity = "info" | "warning" | "high" | "critical";
|
|
4
4
|
export type DiagnosticEvidenceLevel = "basic" | "standard" | "full";
|
|
@@ -46,6 +46,10 @@ export interface DiagnoseStoragePressureInput extends DiagnosticBaseInput {
|
|
|
46
46
|
scope?: "instance" | "database" | "table";
|
|
47
47
|
table?: string;
|
|
48
48
|
}
|
|
49
|
+
export interface DiagnoseReplicationLagInput extends DiagnosticBaseInput {
|
|
50
|
+
replicaId?: string;
|
|
51
|
+
channel?: string;
|
|
52
|
+
}
|
|
49
53
|
export interface DiagnosticRootCauseCandidate {
|
|
50
54
|
code: string;
|
|
51
55
|
title: string;
|
|
@@ -11,29 +11,29 @@ export function createPlaceholderDiagnosticResult(tool, input, options) {
|
|
|
11
11
|
},
|
|
12
12
|
rootCauseCandidates: [
|
|
13
13
|
{
|
|
14
|
-
code: `${tool}
|
|
14
|
+
code: `${tool}_insufficient_evidence`,
|
|
15
15
|
title: options.candidateTitle,
|
|
16
16
|
confidence: "low",
|
|
17
17
|
rationale: options.candidateRationale,
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
keyFindings: options.keyFindings ?? [
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"No conclusive live control-plane or data-plane evidence matched this diagnosis.",
|
|
22
|
+
"The result is intentionally inconclusive rather than inferring a root cause without evidence.",
|
|
23
23
|
],
|
|
24
24
|
suspiciousEntities: options.suspiciousEntities,
|
|
25
25
|
evidence: options.evidence ?? [
|
|
26
26
|
{
|
|
27
|
-
source: "
|
|
28
|
-
title: "
|
|
29
|
-
summary: "
|
|
27
|
+
source: "diagnostic_limitations",
|
|
28
|
+
title: "No conclusive evidence",
|
|
29
|
+
summary: "The configured collectors returned no evidence sufficient for a root-cause conclusion.",
|
|
30
30
|
},
|
|
31
31
|
],
|
|
32
32
|
recommendedActions: options.recommendedActions ?? [
|
|
33
33
|
"Implement the required collectors before relying on this tool for production diagnosis.",
|
|
34
34
|
],
|
|
35
35
|
limitations: options.limitations ?? [
|
|
36
|
-
"
|
|
36
|
+
"The configured datasource, permissions, time window, or cloud metric sources did not provide sufficient evidence.",
|
|
37
37
|
],
|
|
38
38
|
};
|
|
39
39
|
}
|
package/dist/engine/runtime.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SessionContext } from "../context/session-context.js";
|
|
2
2
|
import type { CancelResult, ExplainResult, MutationOptions, MutationResult, QueryResult, QueryStatus, ReadonlyOptions } from "../executor/sql-executor.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ConfirmationRequest, ConfirmationValidationResult } from "../safety/confirmation-store.js";
|
|
4
4
|
import type { GuardrailDecision } from "../safety/guardrail.js";
|
|
5
5
|
import { type FlashbackInput } from "../taurus/flashback.js";
|
|
6
6
|
import { type RestoreRecycleBinTableInput } from "../taurus/recycle-bin.js";
|
|
@@ -14,7 +14,7 @@ export declare function listRecycleBin(engine: TaurusDBEngine, ctx: SessionConte
|
|
|
14
14
|
export declare function restoreRecycleBinTable(engine: TaurusDBEngine, input: RestoreRecycleBinTableInput, ctx: SessionContext, opts?: MutationOptions): Promise<MutationResult>;
|
|
15
15
|
export declare function getQueryStatus(engine: TaurusDBEngine, queryId: string): Promise<QueryStatus>;
|
|
16
16
|
export declare function cancelQuery(engine: TaurusDBEngine, queryId: string): Promise<CancelResult>;
|
|
17
|
-
export declare function issueConfirmation(engine: TaurusDBEngine, input: IssueConfirmationInput): Promise<
|
|
17
|
+
export declare function issueConfirmation(engine: TaurusDBEngine, input: IssueConfirmationInput): Promise<ConfirmationRequest>;
|
|
18
18
|
export declare function validateConfirmation(engine: TaurusDBEngine, token: string, sql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
|
|
19
19
|
export declare function handleConfirmation(engine: TaurusDBEngine, decision: GuardrailDecision, ctx: SessionContext): Promise<ConfirmationOutcome>;
|
|
20
20
|
export declare function close(engine: TaurusDBEngine): Promise<void>;
|
package/dist/engine/runtime.js
CHANGED
|
@@ -3,6 +3,7 @@ import { InMemoryConfirmationStore } from "../safety/confirmation-store.js";
|
|
|
3
3
|
import { buildFlashbackSql, FlashbackNoViewError, flashbackReadonlyOptions, formatTimestamp, normalizeFlashbackWhereClause, resolveRelativeTimestampFromBase, resolveFlashbackTimestamp, } from "../taurus/flashback.js";
|
|
4
4
|
import { buildListRecycleBinSql, buildRestoreRecycleBinTableSql, recycleBinMutationOptions, recycleBinReadonlyOptions } from "../taurus/recycle-bin.js";
|
|
5
5
|
import { normalizeSql, sqlHash } from "../utils/hash.js";
|
|
6
|
+
import { quoteMysqlIdentifier } from "../utils/mysql-identifier.js";
|
|
6
7
|
function resolveConfirmationSql(input) {
|
|
7
8
|
const normalized = input.normalizedSql ?? (input.sql ? normalizeSql(input.sql) : undefined);
|
|
8
9
|
const hash = input.sqlHash ?? (normalized ? sqlHash(normalized) : undefined);
|
|
@@ -26,13 +27,6 @@ function hasSqlPattern(sql, pattern) {
|
|
|
26
27
|
return pattern.test(sql);
|
|
27
28
|
}
|
|
28
29
|
const NO_FLASHBACK_VIEW_PATTERN = /No view available for provided TIMESTAMP/i;
|
|
29
|
-
const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
30
|
-
function quoteIdentifier(identifier) {
|
|
31
|
-
if (!SIMPLE_IDENTIFIER_PATTERN.test(identifier)) {
|
|
32
|
-
throw new Error(`Invalid identifier: "${identifier}".`);
|
|
33
|
-
}
|
|
34
|
-
return `\`${identifier}\``;
|
|
35
|
-
}
|
|
36
30
|
function firstRowAsObject(result) {
|
|
37
31
|
if (result.rows.length === 0) {
|
|
38
32
|
return undefined;
|
|
@@ -91,7 +85,7 @@ async function buildFlashbackNoViewError(engine, ctx, input, database, requested
|
|
|
91
85
|
if (input.where?.trim()) {
|
|
92
86
|
try {
|
|
93
87
|
const where = normalizeFlashbackWhereClause(input.where);
|
|
94
|
-
const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${
|
|
88
|
+
const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${quoteMysqlIdentifier("updated_at")} FROM ${quoteMysqlIdentifier(database, "database")}.${quoteMysqlIdentifier(input.table, "table")} WHERE (${where}) LIMIT 1`, ctx, { maxRows: 1, maxColumns: 1, maxFieldChars: 128 });
|
|
95
89
|
const updatedAtRow = firstRowAsObject(updatedAtResult);
|
|
96
90
|
if (typeof updatedAtRow?.updated_at === "string") {
|
|
97
91
|
details.current_row_updated_at = updatedAtRow.updated_at;
|
|
@@ -341,7 +335,6 @@ export async function restoreRecycleBinTable(engine, input, ctx, opts) {
|
|
|
341
335
|
}
|
|
342
336
|
return engine.executor.executeMutation(buildRestoreRecycleBinTableSql(input), ctx, {
|
|
343
337
|
...recycleBinMutationOptions(opts),
|
|
344
|
-
allowReadonlyFallbackForMutations: true,
|
|
345
338
|
});
|
|
346
339
|
}
|
|
347
340
|
export async function getQueryStatus(engine, queryId) {
|
|
@@ -367,17 +360,18 @@ export async function handleConfirmation(engine, decision, ctx) {
|
|
|
367
360
|
if (!decision.requiresConfirmation) {
|
|
368
361
|
return { status: "confirmed" };
|
|
369
362
|
}
|
|
370
|
-
const
|
|
363
|
+
const approval = await engine.confirmationStore.issue({
|
|
371
364
|
sqlHash: decision.sqlHash,
|
|
372
365
|
normalizedSql: decision.normalizedSql,
|
|
373
366
|
context: ctx,
|
|
374
367
|
riskLevel: decision.riskLevel,
|
|
375
368
|
});
|
|
376
369
|
return {
|
|
377
|
-
status: "
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
370
|
+
status: "approval_required",
|
|
371
|
+
request: approval.request,
|
|
372
|
+
requestId: approval.requestId,
|
|
373
|
+
issuedAt: approval.issuedAt,
|
|
374
|
+
expiresAt: approval.expiresAt,
|
|
381
375
|
};
|
|
382
376
|
}
|
|
383
377
|
export async function close(engine) {
|
package/dist/engine/types.d.ts
CHANGED
|
@@ -31,8 +31,9 @@ export type IssueConfirmationInput = {
|
|
|
31
31
|
export type ConfirmationOutcome = {
|
|
32
32
|
status: "confirmed";
|
|
33
33
|
} | {
|
|
34
|
-
status: "
|
|
35
|
-
|
|
34
|
+
status: "approval_required";
|
|
35
|
+
request: string;
|
|
36
|
+
requestId: string;
|
|
36
37
|
issuedAt: number;
|
|
37
38
|
expiresAt: number;
|
|
38
39
|
};
|
package/dist/engine.d.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { type Config } from "./config/index.js";
|
|
|
6
6
|
import type { DatasourceResolveInput, DatasourceResolver, SessionContext } from "./context/session-context.js";
|
|
7
7
|
import { type ConnectionPool } from "./executor/connection-pool.js";
|
|
8
8
|
import { type CancelResult, type ExplainResult, type MutationOptions, type MutationResult, type QueryResult, type QueryStatus, type ReadonlyOptions, type SqlExecutor } from "./executor/sql-executor.js";
|
|
9
|
-
import { type ConfirmationStore, type
|
|
9
|
+
import { type ConfirmationStore, type ConfirmationRequest, type ConfirmationValidationResult } from "./safety/confirmation-store.js";
|
|
10
10
|
import { type Guardrail, type GuardrailDecision, type InspectInput } from "./safety/guardrail.js";
|
|
11
11
|
import { type DatabaseInfo, type SchemaIntrospector, type TableInfo, type TableSchema } from "./schema/introspector.js";
|
|
12
12
|
import { type FlashbackInput } from "./taurus/flashback.js";
|
|
13
13
|
import { type RestoreRecycleBinTableInput } from "./taurus/recycle-bin.js";
|
|
14
|
-
import { type DbHotspotResult, type DiagnoseDbHotspotInput, type DiagnoseServiceLatencyInput, type FindTopSlowSqlInput, type FindTopSlowSqlResult, type DiagnoseConnectionSpikeInput, type DiagnoseLockContentionInput, type DiagnoseSlowQueryInput, type DiagnoseStoragePressureInput, type DiagnosticResult, type ServiceLatencyResult } from "./diagnostics/types.js";
|
|
14
|
+
import { type DbHotspotResult, type DiagnoseDbHotspotInput, type DiagnoseServiceLatencyInput, type FindTopSlowSqlInput, type FindTopSlowSqlResult, type DiagnoseConnectionSpikeInput, type DiagnoseLockContentionInput, type DiagnoseSlowQueryInput, type DiagnoseStoragePressureInput, type DiagnoseReplicationLagInput, type DiagnosticResult, type ServiceLatencyResult } from "./diagnostics/types.js";
|
|
15
15
|
import { type SlowSqlSource } from "./diagnostics/slow-sql-source.js";
|
|
16
16
|
import { type MetricsSource } from "./diagnostics/metrics-source.js";
|
|
17
17
|
import type { ConfirmationOutcome, DataSourceInfo, EnhancedExplainResult, IssueConfirmationInput, ShowLockWaitsInput, ShowProcesslistInput, TaurusDBEngineCreateOptions, TaurusDBEngineDeps } from "./engine/types.js";
|
|
@@ -44,6 +44,7 @@ export declare class TaurusDBEngine {
|
|
|
44
44
|
listFeatures(ctx: SessionContext): Promise<FeatureMatrix>;
|
|
45
45
|
showProcesslist(input: ShowProcesslistInput, ctx: SessionContext): Promise<QueryResult>;
|
|
46
46
|
showLockWaits(input: ShowLockWaitsInput, ctx: SessionContext): Promise<QueryResult>;
|
|
47
|
+
diagnoseReplicationLag(input: DiagnoseReplicationLagInput, ctx: SessionContext): Promise<DiagnosticResult>;
|
|
47
48
|
findMetadataLockWaits(input: DiagnoseLockContentionInput, ctx: SessionContext): Promise<MetadataLockRow[]>;
|
|
48
49
|
findLatestDeadlock(ctx: SessionContext): Promise<DeadlockSummary | undefined>;
|
|
49
50
|
findStatementDigestSample(digestText: string, ctx: SessionContext): Promise<StatementDigestRow | undefined>;
|
|
@@ -70,7 +71,7 @@ export declare class TaurusDBEngine {
|
|
|
70
71
|
restoreRecycleBinTable(input: RestoreRecycleBinTableInput, ctx: SessionContext, opts?: MutationOptions): Promise<MutationResult>;
|
|
71
72
|
getQueryStatus(queryId: string): Promise<QueryStatus>;
|
|
72
73
|
cancelQuery(queryId: string): Promise<CancelResult>;
|
|
73
|
-
issueConfirmation(input: IssueConfirmationInput): Promise<
|
|
74
|
+
issueConfirmation(input: IssueConfirmationInput): Promise<ConfirmationRequest>;
|
|
74
75
|
validateConfirmation(token: string, sql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
|
|
75
76
|
handleConfirmation(decision: GuardrailDecision, ctx: SessionContext): Promise<ConfirmationOutcome>;
|
|
76
77
|
close(): Promise<void>;
|
package/dist/engine.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
1
2
|
import { createSqlProfileLoader, } from "./auth/sql-profile-loader.js";
|
|
2
3
|
import { createSecretResolver, } from "./auth/secret-resolver.js";
|
|
4
|
+
import { createHuaweiKmsSecretResolver } from "./cloud/kms.js";
|
|
5
|
+
import { createHuaweiCsmsSecretResolver } from "./cloud/csms.js";
|
|
3
6
|
import { createCapabilityProbe, } from "./capability/probe.js";
|
|
4
7
|
import { getConfig } from "./config/index.js";
|
|
5
8
|
import { createDatasourceResolver } from "./context/datasource-resolver.js";
|
|
@@ -10,11 +13,26 @@ import { createConfirmationStore, } from "./safety/confirmation-store.js";
|
|
|
10
13
|
import { createGuardrail, } from "./safety/guardrail.js";
|
|
11
14
|
import { createSchemaIntrospector, } from "./schema/introspector.js";
|
|
12
15
|
import { createMySqlSchemaAdapter } from "./schema/adapters/mysql.js";
|
|
16
|
+
import { diagnoseReplicationLag } from "./diagnostics/replication-lag.js";
|
|
13
17
|
import { createSlowSqlSource, } from "./diagnostics/slow-sql-source.js";
|
|
14
18
|
import { createMetricsSource, } from "./diagnostics/metrics-source.js";
|
|
15
19
|
import { findLatestDeadlock, findMetadataLockWaits, findStatementDigestCandidatesForSqlHints, findStatementDigestSample, findStatementDigestSampleForSql, findStatementWaitEvents, findStorageStatementDigests, findTableStorageStats, findTopStatementDigests, isPerformanceSchemaEnabled, showLockWaits, showProcesslist, } from "./engine/data-access.js";
|
|
16
20
|
import { diagnoseConnectionSpike, diagnoseDbHotspot, diagnoseLockContention, diagnoseServiceLatency, diagnoseSlowQuery, diagnoseStoragePressure, findTopSlowSql, } from "./engine/diagnostics.js";
|
|
17
21
|
import { cancelQuery, close, executeMutation, executeReadonly, explain, explainEnhanced, flashbackQuery, getQueryStatus, handleConfirmation, issueConfirmation, listRecycleBin, restoreRecycleBinTable, validateConfirmation, } from "./engine/runtime.js";
|
|
22
|
+
async function readMutationApprovalSecret(filePath) {
|
|
23
|
+
const info = await lstat(filePath);
|
|
24
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
25
|
+
throw new Error("Mutation approval secret must be a regular, non-symlink file.");
|
|
26
|
+
}
|
|
27
|
+
if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
|
|
28
|
+
throw new Error("Mutation approval secret file must not be accessible by group or other users.");
|
|
29
|
+
}
|
|
30
|
+
const secret = (await readFile(filePath, "utf8")).trim();
|
|
31
|
+
if (Buffer.byteLength(secret, "utf8") < 32) {
|
|
32
|
+
throw new Error("Mutation approval secret must contain at least 32 bytes.");
|
|
33
|
+
}
|
|
34
|
+
return secret;
|
|
35
|
+
}
|
|
18
36
|
function toDataSourceInfo(profile, defaultDatasource) {
|
|
19
37
|
return {
|
|
20
38
|
name: profile.name,
|
|
@@ -56,7 +74,14 @@ export class TaurusDBEngine {
|
|
|
56
74
|
static async create(options = {}) {
|
|
57
75
|
const config = options.config ?? getConfig();
|
|
58
76
|
const profileLoader = options.profileLoader ?? createSqlProfileLoader({ config });
|
|
59
|
-
const secretResolver = options.secretResolver ??
|
|
77
|
+
const secretResolver = options.secretResolver ??
|
|
78
|
+
createSecretResolver({
|
|
79
|
+
uriResolvers: {
|
|
80
|
+
"hw-csms": createHuaweiCsmsSecretResolver({ config }),
|
|
81
|
+
"hw-kms": createHuaweiKmsSecretResolver({ config }),
|
|
82
|
+
"hw-kms-file": createHuaweiKmsSecretResolver({ config }),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
60
85
|
const datasourceResolver = options.datasourceResolver ??
|
|
61
86
|
createDatasourceResolver({
|
|
62
87
|
config,
|
|
@@ -80,9 +105,23 @@ export class TaurusDBEngine {
|
|
|
80
105
|
const executor = options.executor ??
|
|
81
106
|
createSqlExecutor({
|
|
82
107
|
connectionPool,
|
|
108
|
+
maxConcurrentQueries: config.limits.maxConcurrentQueries,
|
|
109
|
+
maxQueuedQueries: config.limits.maxQueuedQueries,
|
|
110
|
+
queueTimeoutMs: config.limits.queueTimeoutMs,
|
|
83
111
|
});
|
|
84
112
|
const guardrail = options.guardrail ?? createGuardrail();
|
|
85
|
-
|
|
113
|
+
let approvalSecret;
|
|
114
|
+
if (config.security.approvalSecretPath) {
|
|
115
|
+
approvalSecret = await readMutationApprovalSecret(config.security.approvalSecretPath);
|
|
116
|
+
}
|
|
117
|
+
if (config.security.mutationsEnabled && !approvalSecret && !options.confirmationStore) {
|
|
118
|
+
throw new Error("Mutations require TAURUSDB_MUTATION_APPROVAL_SECRET_FILE with at least 32 bytes.");
|
|
119
|
+
}
|
|
120
|
+
const confirmationStore = options.confirmationStore ??
|
|
121
|
+
createConfirmationStore({
|
|
122
|
+
approvalSecret,
|
|
123
|
+
ttlSeconds: config.security.approvalTtlSeconds,
|
|
124
|
+
});
|
|
86
125
|
const capabilityProbe = options.capabilityProbe ??
|
|
87
126
|
createCapabilityProbe({
|
|
88
127
|
connectionPool,
|
|
@@ -146,6 +185,9 @@ export class TaurusDBEngine {
|
|
|
146
185
|
async showLockWaits(input, ctx) {
|
|
147
186
|
return showLockWaits(this, input, ctx);
|
|
148
187
|
}
|
|
188
|
+
async diagnoseReplicationLag(input, ctx) {
|
|
189
|
+
return diagnoseReplicationLag(this.executor, input, ctx);
|
|
190
|
+
}
|
|
149
191
|
async findMetadataLockWaits(input, ctx) {
|
|
150
192
|
return findMetadataLockWaits(this, input, ctx);
|
|
151
193
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildServerBoundedReadonlySql(sql: string, maxRows: number): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function buildServerBoundedReadonlySql(sql, maxRows) {
|
|
2
|
+
const normalized = sql.trim().replace(/;\s*$/, "");
|
|
3
|
+
if (!/^(?:select|with)\b/i.test(normalized)) {
|
|
4
|
+
return normalized;
|
|
5
|
+
}
|
|
6
|
+
if (!Number.isInteger(maxRows) || maxRows <= 0) {
|
|
7
|
+
throw new Error("maxRows must be a positive integer.");
|
|
8
|
+
}
|
|
9
|
+
return `SELECT * FROM (${normalized}) AS __taurus_mcp_bounded LIMIT ${maxRows + 1}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class QueryConcurrencyError extends Error {
|
|
2
|
+
readonly code = "SERVER_BUSY";
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class QueryConcurrencyLimiter {
|
|
6
|
+
private readonly maxConcurrent;
|
|
7
|
+
private readonly maxQueued;
|
|
8
|
+
private readonly queueTimeoutMs;
|
|
9
|
+
private active;
|
|
10
|
+
private readonly queue;
|
|
11
|
+
constructor(maxConcurrent: number, maxQueued: number, queueTimeoutMs: number);
|
|
12
|
+
run<T>(operation: () => Promise<T>): Promise<T>;
|
|
13
|
+
private acquire;
|
|
14
|
+
private releaseFactory;
|
|
15
|
+
}
|