Skip to content

Commit 923263e

Browse files
committed
Address comments
1 parent 2ef0ded commit 923263e

File tree

2 files changed

+223
-41
lines changed

2 files changed

+223
-41
lines changed

src/common/atlas/performanceAdvisorUtils.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export enum PerformanceAdvisorOperation {
99
SCHEMA_SUGGESTIONS = "schemaSuggestions",
1010
}
1111

12-
interface SuggestedIndex {
12+
export interface SuggestedIndex {
1313
avgObjSize?: number;
1414
id?: string;
1515
impact?: Array<string>;
@@ -20,29 +20,29 @@ interface SuggestedIndex {
2020

2121
interface SuggestedIndexesResponse {
2222
content: {
23-
suggestedIndexes?: SuggestedIndex[];
23+
suggestedIndexes?: Array<SuggestedIndex>;
2424
};
2525
}
2626

2727
interface DropIndexesResponse {
2828
content: {
29-
hiddenIndexes?: DropIndexSuggestion[];
30-
redundantIndexes?: DropIndexSuggestion[];
31-
unusedIndexes?: DropIndexSuggestion[];
29+
hiddenIndexes?: Array<DropIndexSuggestion>;
30+
redundantIndexes?: Array<DropIndexSuggestion>;
31+
unusedIndexes?: Array<DropIndexSuggestion>;
3232
};
3333
}
3434

3535
interface SchemaAdviceResponse {
3636
content: {
37-
recommendations?: SchemaRecommendation[];
37+
recommendations?: Array<SchemaRecommendation>;
3838
};
3939
}
4040

4141
interface SlowQueriesResponse {
42-
slowQueries?: SlowQueryLog[];
42+
slowQueries?: Array<SlowQueryLog>;
4343
}
4444

45-
interface DropIndexSuggestion {
45+
export interface DropIndexSuggestion {
4646
accessCount?: number;
4747
index?: Array<{ [key: string]: 1 | -1 }>;
4848
name?: string;
@@ -52,7 +52,7 @@ interface DropIndexSuggestion {
5252
sizeBytes?: number;
5353
}
5454

55-
type SchemaTriggerType =
55+
export type SchemaTriggerType =
5656
| "PERCENT_QUERIES_USE_LOOKUP"
5757
| "NUMBER_OF_QUERIES_USE_LOOKUP"
5858
| "DOCS_CONTAIN_UNBOUNDED_ARRAY"
@@ -61,6 +61,16 @@ type SchemaTriggerType =
6161
| "NUM_INDEXES"
6262
| "QUERIES_CONTAIN_CASE_INSENSITIVE_REGEX";
6363

64+
export const SCHEMA_TRIGGER_DESCRIPTIONS: Record<SchemaTriggerType, string> = {
65+
PERCENT_QUERIES_USE_LOOKUP: "High percentage of queries (>50%) use $lookup operations",
66+
NUMBER_OF_QUERIES_USE_LOOKUP: "High number of queries (>100) use $lookup operations",
67+
DOCS_CONTAIN_UNBOUNDED_ARRAY: "Arrays with over 10000 entries detected in the collection(s)",
68+
NUMBER_OF_NAMESPACES: "Too many namespaces (collections) in the database (>100)",
69+
DOC_SIZE_TOO_LARGE: "Documents larger than 2 MB found in the collection(s)",
70+
NUM_INDEXES: "More than 30 indexes detected in the collection(s) scanned",
71+
QUERIES_CONTAIN_CASE_INSENSITIVE_REGEX: "Queries use case-insensitive regular expressions",
72+
};
73+
6474
type SchemaRecommedationType =
6575
| "REDUCE_LOOKUP_OPS"
6676
| "AVOID_UNBOUNDED_ARRAY"
@@ -70,7 +80,17 @@ type SchemaRecommedationType =
7080
| "OPTIMIZE_CASE_INSENSITIVE_REGEX_QUERIES"
7181
| "OPTIMIZE_TEXT_QUERIES";
7282

73-
interface SchemaRecommendation {
83+
export const SCHEMA_RECOMMENDATION_DESCRIPTIONS: Record<SchemaRecommedationType, string> = {
84+
REDUCE_LOOKUP_OPS: "Reduce the use of $lookup operations",
85+
AVOID_UNBOUNDED_ARRAY: "Avoid using unbounded arrays in documents",
86+
REDUCE_DOCUMENT_SIZE: "Reduce the size of documents",
87+
REMOVE_UNNECESSARY_INDEXES: "Remove unnecessary indexes",
88+
REDUCE_NUMBER_OF_NAMESPACES: "Reduce the number of collections in the database",
89+
OPTIMIZE_CASE_INSENSITIVE_REGEX_QUERIES: "Optimize case-insensitive regex queries",
90+
OPTIMIZE_TEXT_QUERIES: "Optimize text search queries",
91+
};
92+
93+
export interface SchemaRecommendation {
7494
affectedNamespaces?: Array<{
7595
namespace?: string | null;
7696
triggers?: Array<{
@@ -96,7 +116,7 @@ interface SlowQueryLogMetrics {
96116
responseLength?: number;
97117
}
98118

99-
interface SlowQueryLog {
119+
export interface SlowQueryLog {
100120
line?: string;
101121
metrics?: SlowQueryLogMetrics;
102122
namespace?: string;
@@ -204,9 +224,9 @@ export async function getSlowQueries(
204224
apiClient: ApiClient,
205225
projectId: string,
206226
clusterName: string,
207-
since?: number,
227+
since?: Date,
208228
processId?: string,
209-
namespaces?: string[]
229+
namespaces?: Array<string>
210230
): Promise<{ slowQueryLogs: Array<SlowQueryLog> }> {
211231
try {
212232
// If processId is not provided, get it from inspecting the cluster
@@ -222,7 +242,7 @@ export async function getSlowQueries(
222242
processId: actualProcessId,
223243
},
224244
query: {
225-
...(since && { since: Number(since) }),
245+
...(since && { since: since.getTime() }),
226246
...(namespaces && { namespaces: namespaces }),
227247
},
228248
},

src/tools/atlas/read/listPerformanceAdvisor.ts

Lines changed: 189 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@ import { z } from "zod";
22
import { AtlasToolBase } from "../atlasTool.js";
33
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import type { OperationType, ToolArgs } from "../../tool.js";
5+
import { formatUntrustedData } from "../../tool.js";
56
import {
67
getSuggestedIndexes,
78
getDropIndexSuggestions,
89
getSchemaAdvice,
910
getSlowQueries,
1011
PerformanceAdvisorOperation,
1112
type PerformanceAdvisorData,
13+
type SuggestedIndex,
14+
type DropIndexSuggestion,
15+
type SlowQueryLog,
16+
type SchemaRecommendation,
17+
SCHEMA_RECOMMENDATION_DESCRIPTIONS,
18+
SCHEMA_TRIGGER_DESCRIPTIONS,
19+
type SchemaTriggerType,
1220
} from "../../../common/atlas/performanceAdvisorUtils.js";
1321

1422
export class ListPerformanceAdvisorTool extends AtlasToolBase {
@@ -20,12 +28,120 @@ export class ListPerformanceAdvisorTool extends AtlasToolBase {
2028
clusterName: z.string().describe("Atlas cluster name to list performance advisor recommendations"),
2129
operations: z
2230
.array(z.nativeEnum(PerformanceAdvisorOperation))
31+
.default(Object.values(PerformanceAdvisorOperation))
2332
.describe("Operations to list performance advisor recommendations"),
24-
since: z.number().describe("Date to list slow query logs since").optional(),
33+
since: z.date().describe("Date to list slow query logs since").optional(),
2534
processId: z.string().describe("Process ID to list slow query logs").optional(),
2635
namespaces: z.array(z.string()).describe("Namespaces to list slow query logs").optional(),
2736
};
2837

38+
private formatSuggestedIndexesTable(suggestedIndexes: Array<SuggestedIndex>): string {
39+
if (suggestedIndexes.length === 0) return "No suggested indexes found.";
40+
41+
const rows = suggestedIndexes
42+
.map((index, i) => {
43+
const namespace = index.namespace ?? "N/A";
44+
const weight = index.weight ?? "N/A";
45+
const avgObjSize = index.avgObjSize ?? "N/A";
46+
const indexKeys = index.index ? index.index.map((key) => Object.keys(key)[0]).join(", ") : "N/A";
47+
return `${i + 1} | ${namespace} | ${weight} | ${avgObjSize} | ${indexKeys}`;
48+
})
49+
.join("\n");
50+
51+
return `# | Namespace | Weight | Avg Obj Size | Index Keys
52+
---|-----------|--------|--------------|------------
53+
${rows}`;
54+
}
55+
56+
private formatDropIndexesTable(dropIndexSuggestions: {
57+
hiddenIndexes: Array<DropIndexSuggestion>;
58+
redundantIndexes: Array<DropIndexSuggestion>;
59+
unusedIndexes: Array<DropIndexSuggestion>;
60+
}): string {
61+
const allIndexes = [
62+
...dropIndexSuggestions.hiddenIndexes.map((idx) => ({ ...idx, type: "Hidden" })),
63+
...dropIndexSuggestions.redundantIndexes.map((idx) => ({ ...idx, type: "Redundant" })),
64+
...dropIndexSuggestions.unusedIndexes.map((idx) => ({ ...idx, type: "Unused" })),
65+
];
66+
67+
if (allIndexes.length === 0) return "No drop index suggestions found.";
68+
69+
const rows = allIndexes
70+
.map((index, i) => {
71+
const name = index.name ?? "N/A";
72+
const namespace = index.namespace ?? "N/A";
73+
const type = index.type ?? "N/A";
74+
const sizeBytes = index.sizeBytes ?? "N/A";
75+
const accessCount = index.accessCount ?? "N/A";
76+
return `${i + 1} | ${name} | ${namespace} | ${type} | ${sizeBytes} | ${accessCount}`;
77+
})
78+
.join("\n");
79+
80+
return `# | Index Name | Namespace | Type | Size (bytes) | Access Count
81+
---|------------|-----------|------|--------------|-------------
82+
${rows}`;
83+
}
84+
85+
private formatSlowQueriesTable(slowQueryLogs: Array<SlowQueryLog>): string {
86+
if (slowQueryLogs.length === 0) return "No slow query logs found.";
87+
88+
const rows = slowQueryLogs
89+
.map((log, i) => {
90+
const namespace = log.namespace ?? "N/A";
91+
const opType = log.opType ?? "N/A";
92+
const executionTime = log.metrics?.operationExecutionTime ?? "N/A";
93+
const docsExamined = log.metrics?.docsExamined ?? "N/A";
94+
const docsReturned = log.metrics?.docsReturned ?? "N/A";
95+
return `${i + 1} | ${namespace} | ${opType} | ${executionTime}ms | ${docsExamined} | ${docsReturned}`;
96+
})
97+
.join("\n");
98+
99+
return `# | Namespace | Operation | Execution Time | Docs Examined | Docs Returned
100+
---|-----------|-----------|---------------|---------------|---------------
101+
${rows}`;
102+
}
103+
104+
private getTriggerDescription(triggerType: SchemaTriggerType | undefined): string {
105+
if (!triggerType) return "N/A";
106+
return SCHEMA_TRIGGER_DESCRIPTIONS[triggerType] ?? triggerType;
107+
}
108+
109+
private getNamespaceTriggerDescriptions(namespace: {
110+
triggers?: Array<{ triggerType?: SchemaTriggerType }>;
111+
}): string {
112+
if (!namespace.triggers) return "N/A";
113+
114+
return namespace.triggers.map((trigger) => this.getTriggerDescription(trigger.triggerType)).join(", ");
115+
}
116+
117+
private getTriggerDescriptions(suggestion: SchemaRecommendation): string {
118+
if (!suggestion.affectedNamespaces) return "N/A";
119+
120+
return suggestion.affectedNamespaces
121+
.map((namespace) => this.getNamespaceTriggerDescriptions(namespace))
122+
.join(", ");
123+
}
124+
125+
private formatSchemaSuggestionsTable(schemaSuggestions: Array<SchemaRecommendation>): string {
126+
if (schemaSuggestions.length === 0) return "No schema suggestions found.";
127+
128+
const rows = schemaSuggestions
129+
.map((suggestion: SchemaRecommendation, i) => {
130+
const recommendation = suggestion.recommendation
131+
? (SCHEMA_RECOMMENDATION_DESCRIPTIONS[suggestion.recommendation] ?? suggestion.recommendation)
132+
: "N/A";
133+
const description = suggestion.description ?? "N/A";
134+
const triggeredBy = this.getTriggerDescriptions(suggestion);
135+
const affectedNamespaces = suggestion.affectedNamespaces?.length ?? 0;
136+
return `${i + 1} | ${recommendation} | ${description} | ${triggeredBy} | ${affectedNamespaces} namespaces`;
137+
})
138+
.join("\n");
139+
140+
return `# | Recommendation | Description | Triggered By | Affected Namespaces
141+
---|---------------|-------------|----------|-------------------
142+
${rows}`;
143+
}
144+
29145
protected async execute({
30146
projectId,
31147
clusterName,
@@ -41,41 +157,46 @@ export class ListPerformanceAdvisorTool extends AtlasToolBase {
41157
schemaSuggestions: [],
42158
};
43159

44-
// If operations is empty, get all performance advisor recommendations
45-
// Otherwise, get only the specified operations
46-
const operationsToExecute = operations.length === 0 ? Object.values(PerformanceAdvisorOperation) : operations;
47-
48160
try {
49-
if (operationsToExecute.includes(PerformanceAdvisorOperation.SUGGESTED_INDEXES)) {
50-
const { suggestedIndexes } = await getSuggestedIndexes(this.session.apiClient, projectId, clusterName);
51-
data.suggestedIndexes = suggestedIndexes;
161+
const performanceAdvisorPromises = [];
162+
163+
if (operations.includes(PerformanceAdvisorOperation.SUGGESTED_INDEXES)) {
164+
performanceAdvisorPromises.push(
165+
getSuggestedIndexes(this.session.apiClient, projectId, clusterName).then(({ suggestedIndexes }) => {
166+
data.suggestedIndexes = suggestedIndexes;
167+
})
168+
);
52169
}
53170

54-
if (operationsToExecute.includes(PerformanceAdvisorOperation.DROP_INDEX_SUGGESTIONS)) {
55-
const { hiddenIndexes, redundantIndexes, unusedIndexes } = await getDropIndexSuggestions(
56-
this.session.apiClient,
57-
projectId,
58-
clusterName
171+
if (operations.includes(PerformanceAdvisorOperation.DROP_INDEX_SUGGESTIONS)) {
172+
performanceAdvisorPromises.push(
173+
getDropIndexSuggestions(this.session.apiClient, projectId, clusterName).then(
174+
({ hiddenIndexes, redundantIndexes, unusedIndexes }) => {
175+
data.dropIndexSuggestions = { hiddenIndexes, redundantIndexes, unusedIndexes };
176+
}
177+
)
59178
);
60-
data.dropIndexSuggestions = { hiddenIndexes, redundantIndexes, unusedIndexes };
61179
}
62180

63-
if (operationsToExecute.includes(PerformanceAdvisorOperation.SLOW_QUERY_LOGS)) {
64-
const { slowQueryLogs } = await getSlowQueries(
65-
this.session.apiClient,
66-
projectId,
67-
clusterName,
68-
since,
69-
processId,
70-
namespaces
181+
if (operations.includes(PerformanceAdvisorOperation.SLOW_QUERY_LOGS)) {
182+
performanceAdvisorPromises.push(
183+
getSlowQueries(this.session.apiClient, projectId, clusterName, since, processId, namespaces).then(
184+
({ slowQueryLogs }) => {
185+
data.slowQueryLogs = slowQueryLogs;
186+
}
187+
)
71188
);
72-
data.slowQueryLogs = slowQueryLogs;
73189
}
74190

75-
if (operationsToExecute.includes(PerformanceAdvisorOperation.SCHEMA_SUGGESTIONS)) {
76-
const { recommendations } = await getSchemaAdvice(this.session.apiClient, projectId, clusterName);
77-
data.schemaSuggestions = recommendations;
191+
if (operations.includes(PerformanceAdvisorOperation.SCHEMA_SUGGESTIONS)) {
192+
performanceAdvisorPromises.push(
193+
getSchemaAdvice(this.session.apiClient, projectId, clusterName).then(({ recommendations }) => {
194+
data.schemaSuggestions = recommendations;
195+
})
196+
);
78197
}
198+
199+
await Promise.all(performanceAdvisorPromises);
79200
} catch (error) {
80201
return {
81202
content: [
@@ -87,8 +208,49 @@ export class ListPerformanceAdvisorTool extends AtlasToolBase {
87208
};
88209
}
89210

211+
// Format the data as tables
212+
let formattedOutput = "";
213+
let totalItems = 0;
214+
215+
if (data.suggestedIndexes.length > 0) {
216+
const suggestedIndexesTable = this.formatSuggestedIndexesTable(data.suggestedIndexes);
217+
formattedOutput += `\n## Suggested Indexes\n${suggestedIndexesTable}\n`;
218+
totalItems += data.suggestedIndexes.length;
219+
}
220+
221+
if (
222+
data.dropIndexSuggestions.hiddenIndexes.length > 0 ||
223+
data.dropIndexSuggestions.redundantIndexes.length > 0 ||
224+
data.dropIndexSuggestions.unusedIndexes.length > 0
225+
) {
226+
const dropIndexesTable = this.formatDropIndexesTable(data.dropIndexSuggestions);
227+
formattedOutput += `\n## Drop Index Suggestions\n${dropIndexesTable}\n`;
228+
totalItems +=
229+
data.dropIndexSuggestions.hiddenIndexes.length +
230+
data.dropIndexSuggestions.redundantIndexes.length +
231+
data.dropIndexSuggestions.unusedIndexes.length;
232+
}
233+
234+
if (data.slowQueryLogs.length > 0) {
235+
const slowQueriesTable = this.formatSlowQueriesTable(data.slowQueryLogs);
236+
formattedOutput += `\n## Slow Query Logs\n${slowQueriesTable}\n`;
237+
totalItems += data.slowQueryLogs.length;
238+
}
239+
240+
if (data.schemaSuggestions.length > 0) {
241+
const schemaTable = this.formatSchemaSuggestionsTable(data.schemaSuggestions);
242+
formattedOutput += `\n## Schema Suggestions\n${schemaTable}\n`;
243+
totalItems += data.schemaSuggestions.length;
244+
}
245+
246+
if (totalItems === 0) {
247+
return {
248+
content: [{ type: "text", text: "No performance advisor recommendations found." }],
249+
};
250+
}
251+
90252
return {
91-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
253+
content: formatUntrustedData(`Found ${totalItems} performance advisor recommendations`, formattedOutput),
92254
};
93255
}
94256
}

0 commit comments

Comments
 (0)