@mcp-consultant-tools/log-analytics 28.0.0 → 29.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/build/cli/commands/function-commands.d.ts +7 -0
  2. package/build/cli/commands/function-commands.d.ts.map +1 -0
  3. package/build/cli/commands/function-commands.js +120 -0
  4. package/build/cli/commands/function-commands.js.map +1 -0
  5. package/build/cli/commands/index.d.ts +10 -0
  6. package/build/cli/commands/index.d.ts.map +1 -0
  7. package/build/cli/commands/index.js +15 -0
  8. package/build/cli/commands/index.js.map +1 -0
  9. package/build/cli/commands/query-commands.d.ts +7 -0
  10. package/build/cli/commands/query-commands.d.ts.map +1 -0
  11. package/build/cli/commands/query-commands.js +486 -0
  12. package/build/cli/commands/query-commands.js.map +1 -0
  13. package/build/cli/commands/workspace-commands.d.ts +7 -0
  14. package/build/cli/commands/workspace-commands.d.ts.map +1 -0
  15. package/build/cli/commands/workspace-commands.js +47 -0
  16. package/build/cli/commands/workspace-commands.js.map +1 -0
  17. package/build/cli/output.d.ts +11 -0
  18. package/build/cli/output.d.ts.map +1 -0
  19. package/build/cli/output.js +10 -0
  20. package/build/cli/output.js.map +1 -0
  21. package/build/cli.d.ts +9 -0
  22. package/build/cli.d.ts.map +1 -0
  23. package/build/cli.js +27 -0
  24. package/build/cli.js.map +1 -0
  25. package/build/context-factory.d.ts +4 -0
  26. package/build/context-factory.d.ts.map +1 -0
  27. package/build/context-factory.js +45 -0
  28. package/build/context-factory.js.map +1 -0
  29. package/build/index.d.ts +14 -2
  30. package/build/index.d.ts.map +1 -1
  31. package/build/index.js +35 -1121
  32. package/build/index.js.map +1 -1
  33. package/build/prompts/index.d.ts +7 -0
  34. package/build/prompts/index.d.ts.map +1 -0
  35. package/build/prompts/index.js +6 -0
  36. package/build/prompts/index.js.map +1 -0
  37. package/build/prompts/templates.d.ts +3 -0
  38. package/build/prompts/templates.d.ts.map +1 -0
  39. package/build/prompts/templates.js +195 -0
  40. package/build/prompts/templates.js.map +1 -0
  41. package/build/services/index.d.ts +3 -0
  42. package/build/services/index.d.ts.map +1 -0
  43. package/build/services/index.js +2 -0
  44. package/build/services/index.js.map +1 -0
  45. package/build/services/log-analytics-service.d.ts +117 -0
  46. package/build/services/log-analytics-service.d.ts.map +1 -0
  47. package/build/services/log-analytics-service.js +419 -0
  48. package/build/services/log-analytics-service.js.map +1 -0
  49. package/build/tool-examples.d.ts +1 -8
  50. package/build/tool-examples.d.ts.map +1 -1
  51. package/build/tool-examples.js +1 -12
  52. package/build/tool-examples.js.map +1 -1
  53. package/build/tools/function-tools.d.ts +3 -0
  54. package/build/tools/function-tools.d.ts.map +1 -0
  55. package/build/tools/function-tools.js +536 -0
  56. package/build/tools/function-tools.js.map +1 -0
  57. package/build/tools/index.d.ts +9 -0
  58. package/build/tools/index.d.ts.map +1 -0
  59. package/build/tools/index.js +12 -0
  60. package/build/tools/index.js.map +1 -0
  61. package/build/tools/query-tools.d.ts +3 -0
  62. package/build/tools/query-tools.d.ts.map +1 -0
  63. package/build/tools/query-tools.js +105 -0
  64. package/build/tools/query-tools.js.map +1 -0
  65. package/build/tools/workspace-tools.d.ts +3 -0
  66. package/build/tools/workspace-tools.d.ts.map +1 -0
  67. package/build/tools/workspace-tools.js +53 -0
  68. package/build/tools/workspace-tools.js.map +1 -0
  69. package/build/types.d.ts +9 -0
  70. package/build/types.d.ts.map +1 -0
  71. package/build/types.js +2 -0
  72. package/build/types.js.map +1 -0
  73. package/package.json +5 -3
package/build/index.js CHANGED
@@ -1,17 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * @mcp-consultant-tools/log-analytics
4
+ *
5
+ * MCP server for Azure Log Analytics integration.
6
+ * Entry point: MCP server startup + backward-compatible registerLogAnalyticsTools().
7
+ */
2
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
9
  import { pathToFileURL } from "node:url";
4
10
  import { realpathSync } from "node:fs";
5
11
  import { createMcpServer, createEnvLoader } from "@mcp-consultant-tools/core";
6
- import { LogAnalyticsService } from "./LogAnalyticsService.js";
7
- import { z } from 'zod';
8
- import { formatTableAsMarkdown, analyzeLogs, analyzeFunctionLogs, analyzeFunctionErrors, analyzeFunctionStats, generateRecommendations, filterColumns, resolveColumnPreset, } from './utils/loganalytics-formatters.js';
9
- import { descWithExamples, KQL_EXAMPLES, TIMESPAN_EXAMPLES, COLUMN_PRESET_EXAMPLES, TABLE_NAME_EXAMPLES, APP_NAME_EXAMPLES, OUTPUT_FORMAT_EXAMPLES, SEVERITY_EXAMPLES, } from './tool-examples.js';
10
- export function registerLogAnalyticsTools(server, loganalyticsService) {
11
- let service = loganalyticsService || null;
12
- function getLogAnalyticsService() {
12
+ import { LogAnalyticsService } from './services/log-analytics-service.js';
13
+ import { registerAllTools } from './tools/index.js';
14
+ import { registerAllPrompts } from './prompts/index.js';
15
+ /**
16
+ * Build a ServiceContext from environment variables (lazy service initialization).
17
+ */
18
+ function createServiceContext() {
19
+ let service = null;
20
+ function getService() {
13
21
  if (!service) {
14
- const missingConfig = [];
15
22
  let resources = [];
16
23
  if (process.env.LOGANALYTICS_RESOURCES) {
17
24
  try {
@@ -30,10 +37,7 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
30
37
  }];
31
38
  }
32
39
  else {
33
- missingConfig.push("LOGANALYTICS_RESOURCES or LOGANALYTICS_WORKSPACE_ID");
34
- }
35
- if (missingConfig.length > 0) {
36
- throw new Error(`Missing Log Analytics configuration: ${missingConfig.join(", ")}`);
40
+ throw new Error("Missing Log Analytics configuration: LOGANALYTICS_RESOURCES or LOGANALYTICS_WORKSPACE_ID");
37
41
  }
38
42
  const config = {
39
43
  resources,
@@ -47,1122 +51,32 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
47
51
  }
48
52
  return service;
49
53
  }
50
- // ========================================
51
- // PROMPTS
52
- // ========================================
53
- server.prompt("la-workspace-summary", "Generate a comprehensive health summary for a Log Analytics workspace", {
54
- resourceId: z.string().describe("Resource ID"),
55
- timespan: z.string().optional().describe("Time range (default: PT1H)"),
56
- }, async ({ resourceId, timespan }) => {
57
- try {
58
- const service = getLogAnalyticsService();
59
- const timespanValue = timespan || 'PT1H';
60
- // Get recent errors from FunctionAppLogs
61
- const errorsResult = await service.getFunctionErrors(resourceId, undefined, timespanValue, 50);
62
- const statsResult = await service.getFunctionStats(resourceId, undefined, timespanValue);
63
- // Format results
64
- const errorsTable = errorsResult.tables[0] ? formatTableAsMarkdown(errorsResult.tables[0]) : '*No errors*';
65
- const statsTable = statsResult.tables[0] ? formatTableAsMarkdown(statsResult.tables[0]) : '*No statistics*';
66
- // Analyze
67
- const errorsAnalysis = analyzeFunctionErrors(errorsResult.tables[0]);
68
- const statsAnalysis = analyzeFunctionStats(statsResult.tables[0]);
69
- const report = `# Log Analytics Workspace Health Summary\n\n` +
70
- `**Resource**: ${resourceId}\n` +
71
- `**Time Range**: ${timespanValue}\n` +
72
- `**Generated**: ${new Date().toISOString()}\n\n` +
73
- `## Function Statistics\n\n${statsTable}\n\n` +
74
- `### Key Insights\n${statsAnalysis}\n\n` +
75
- `## Recent Errors\n\n${errorsTable}\n\n` +
76
- `### Error Analysis\n${errorsAnalysis}\n\n` +
77
- `## Recommendations\n\n` +
78
- generateRecommendations({
79
- errorCount: errorsResult.tables[0]?.rows.length || 0,
80
- }).join('\n');
81
- return {
82
- messages: [
83
- {
84
- role: "assistant",
85
- content: {
86
- type: "text",
87
- text: report,
88
- },
89
- },
90
- ],
91
- };
92
- }
93
- catch (error) {
94
- console.error("Error generating workspace summary:", error);
95
- return {
96
- messages: [
97
- {
98
- role: "assistant",
99
- content: {
100
- type: "text",
101
- text: `Failed to generate workspace summary: ${error.message}`,
102
- },
103
- },
104
- ],
105
- };
106
- }
107
- });
108
- server.prompt("la-fn-troubleshooting", "Generate a comprehensive troubleshooting guide for an Azure Function", {
109
- resourceId: z.string().describe("Resource ID"),
110
- functionName: z.string().describe("Function name to analyze"),
111
- timespan: z.string().optional().describe("Time range (default: PT1H)"),
112
- }, async ({ resourceId, functionName, timespan }) => {
113
- try {
114
- const service = getLogAnalyticsService();
115
- const timespanValue = timespan || 'PT1H';
116
- // Get comprehensive data
117
- const logsResult = await service.getFunctionLogs(resourceId, functionName, timespanValue, undefined, 100);
118
- const errorsResult = await service.getFunctionErrors(resourceId, functionName, timespanValue, 50);
119
- const statsResult = await service.getFunctionStats(resourceId, functionName, timespanValue);
120
- const invocationsResult = await service.getFunctionInvocations(resourceId, functionName, timespanValue, 50);
121
- // Format results
122
- const logsTable = logsResult.tables[0] ? formatTableAsMarkdown(logsResult.tables[0]) : '*No logs*';
123
- const errorsTable = errorsResult.tables[0] ? formatTableAsMarkdown(errorsResult.tables[0]) : '*No errors*';
124
- const statsTable = statsResult.tables[0] ? formatTableAsMarkdown(statsResult.tables[0]) : '*No statistics*';
125
- const invocationsTable = invocationsResult.tables[0] ? formatTableAsMarkdown(invocationsResult.tables[0]) : '*No invocations*';
126
- // Analyze
127
- const logsAnalysis = analyzeFunctionLogs(logsResult.tables[0]);
128
- const errorsAnalysis = analyzeFunctionErrors(errorsResult.tables[0]);
129
- const statsAnalysis = analyzeFunctionStats(statsResult.tables[0]);
130
- const report = `# Azure Function Troubleshooting Guide\n\n` +
131
- `**Function**: ${functionName}\n` +
132
- `**Resource**: ${resourceId}\n` +
133
- `**Time Range**: ${timespanValue}\n` +
134
- `**Generated**: ${new Date().toISOString()}\n\n` +
135
- `## Executive Summary\n\n${statsTable}\n\n` +
136
- `### Statistics Insights\n${statsAnalysis}\n\n` +
137
- `## Error Analysis\n\n${errorsTable}\n\n` +
138
- `### Error Insights\n${errorsAnalysis}\n\n` +
139
- `## Recent Logs\n\n${logsTable}\n\n` +
140
- `### Log Insights\n${logsAnalysis}\n\n` +
141
- `## Recent Invocations\n\n${invocationsTable}\n\n` +
142
- `## Recommendations\n\n` +
143
- generateRecommendations({
144
- errorCount: errorsResult.tables[0]?.rows.length || 0,
145
- }).join('\n');
146
- return {
147
- messages: [
148
- {
149
- role: "assistant",
150
- content: {
151
- type: "text",
152
- text: report,
153
- },
154
- },
155
- ],
156
- };
157
- }
158
- catch (error) {
159
- console.error("Error generating function troubleshooting guide:", error);
160
- return {
161
- messages: [
162
- {
163
- role: "assistant",
164
- content: {
165
- type: "text",
166
- text: `Failed to generate troubleshooting guide: ${error.message}`,
167
- },
168
- },
169
- ],
170
- };
171
- }
172
- });
173
- server.prompt("la-fn-performance", "Generate a performance analysis report for Azure Functions", {
174
- resourceId: z.string().describe("Resource ID"),
175
- functionName: z.string().optional().describe("Function name (optional, analyzes all if not specified)"),
176
- timespan: z.string().optional().describe("Time range (default: PT1H)"),
177
- }, async ({ resourceId, functionName, timespan }) => {
178
- try {
179
- const service = getLogAnalyticsService();
180
- const timespanValue = timespan || 'PT1H';
181
- // Get performance data
182
- const statsResult = await service.getFunctionStats(resourceId, functionName, timespanValue);
183
- const invocationsResult = await service.getFunctionInvocations(resourceId, functionName, timespanValue, 100);
184
- // Format results
185
- const statsTable = statsResult.tables[0] ? formatTableAsMarkdown(statsResult.tables[0]) : '*No statistics*';
186
- const invocationsTable = invocationsResult.tables[0] ? formatTableAsMarkdown(invocationsResult.tables[0]) : '*No invocations*';
187
- // Analyze
188
- const statsAnalysis = analyzeFunctionStats(statsResult.tables[0]);
189
- const report = `# Azure Function Performance Report\n\n` +
190
- `**Function**: ${functionName || 'All Functions'}\n` +
191
- `**Resource**: ${resourceId}\n` +
192
- `**Time Range**: ${timespanValue}\n` +
193
- `**Generated**: ${new Date().toISOString()}\n\n` +
194
- `## Execution Statistics\n\n${statsTable}\n\n` +
195
- `### Performance Insights\n${statsAnalysis}\n\n` +
196
- `## Recent Invocations\n\n${invocationsTable}\n\n` +
197
- `## Recommendations\n\n` +
198
- `- Monitor success rates and investigate functions below 95%\n` +
199
- `- Review invocation patterns for optimization opportunities\n` +
200
- `- Consider implementing retry logic for transient failures\n`;
201
- return {
202
- messages: [
203
- {
204
- role: "assistant",
205
- content: {
206
- type: "text",
207
- text: report,
208
- },
209
- },
210
- ],
211
- };
212
- }
213
- catch (error) {
214
- console.error("Error generating performance report:", error);
215
- return {
216
- messages: [
217
- {
218
- role: "assistant",
219
- content: {
220
- type: "text",
221
- text: `Failed to generate performance report: ${error.message}`,
222
- },
223
- },
224
- ],
225
- };
226
- }
227
- });
228
- server.prompt("la-logs-report", "Generate a formatted logs report with insights and analysis", {
229
- resourceId: z.string().describe("Resource ID"),
230
- tableName: z.string().describe("Table name to query"),
231
- timespan: z.string().optional().describe("Time range (default: PT1H)"),
232
- limit: z.string().optional().describe("Maximum number of logs (default: 100)"),
233
- }, async ({ resourceId, tableName, timespan, limit }) => {
234
- try {
235
- const service = getLogAnalyticsService();
236
- const timespanValue = timespan || 'PT1H';
237
- const limitValue = limit ? parseInt(limit, 10) : 100;
238
- // Get logs
239
- const logsResult = await service.getRecentEvents(resourceId, tableName, timespanValue, limitValue);
240
- // Format results
241
- const logsTable = logsResult.tables[0] ? formatTableAsMarkdown(logsResult.tables[0]) : '*No logs*';
242
- // Analyze
243
- const analysis = analyzeLogs(logsResult.tables[0], tableName);
244
- const report = `# Log Analytics Report\n\n` +
245
- `**Table**: ${tableName}\n` +
246
- `**Resource**: ${resourceId}\n` +
247
- `**Time Range**: ${timespanValue}\n` +
248
- `**Limit**: ${limitValue}\n` +
249
- `**Generated**: ${new Date().toISOString()}\n\n` +
250
- `## Log Entries\n\n${logsTable}\n\n` +
251
- `### Analysis\n${analysis}\n\n` +
252
- `## Recommendations\n\n` +
253
- `- Review log patterns for anomalies\n` +
254
- `- Investigate any error or warning entries\n` +
255
- `- Consider adjusting log retention policies\n`;
256
- return {
257
- messages: [
258
- {
259
- role: "assistant",
260
- content: {
261
- type: "text",
262
- text: report,
263
- },
264
- },
265
- ],
266
- };
267
- }
268
- catch (error) {
269
- console.error("Error generating logs report:", error);
270
- return {
271
- messages: [
272
- {
273
- role: "assistant",
274
- content: {
275
- type: "text",
276
- text: `Failed to generate logs report: ${error.message}`,
277
- },
278
- },
279
- ],
280
- };
281
- }
282
- });
283
- // ========================================
284
- // TOOLS
285
- // ========================================
286
- server.tool("la-list-workspaces", "List all configured Log Analytics workspaces (active and inactive)", {}, async () => {
287
- try {
288
- const service = getLogAnalyticsService();
289
- const resources = service.getAllResources();
290
- return {
291
- content: [
292
- {
293
- type: "text",
294
- text: JSON.stringify(resources, null, 2),
295
- },
296
- ],
297
- };
298
- }
299
- catch (error) {
300
- console.error("Error listing Log Analytics workspaces:", error);
301
- return {
302
- content: [
303
- {
304
- type: "text",
305
- text: `Failed to list workspaces: ${error.message}`,
306
- },
307
- ],
308
- isError: true
309
- };
310
- }
311
- });
312
- server.tool("la-get-metadata", "Get schema metadata (tables and columns) for a Log Analytics workspace", {
313
- resourceId: z.string().describe("Resource ID (use la-list-workspaces to find IDs)"),
314
- }, async ({ resourceId }) => {
315
- try {
316
- const service = getLogAnalyticsService();
317
- const metadata = await service.getMetadata(resourceId);
318
- return {
319
- content: [
320
- {
321
- type: "text",
322
- text: JSON.stringify(metadata, null, 2),
323
- },
324
- ],
325
- };
326
- }
327
- catch (error) {
328
- console.error("Error getting Log Analytics metadata:", error);
329
- return {
330
- content: [
331
- {
332
- type: "text",
333
- text: `Failed to get metadata: ${error.message}`,
334
- },
335
- ],
336
- isError: true
337
- };
338
- }
339
- });
340
- server.tool("la-execute-query", "Execute a custom KQL query against Log Analytics workspace", {
341
- resourceId: z.string().describe("Resource ID"),
342
- query: z.string().describe(descWithExamples("KQL query string", KQL_EXAMPLES)),
343
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
344
- columnPreset: z.enum(["minimal", "investigation", "full"]).optional()
345
- .describe(descWithExamples("Column preset for filtering results. 'minimal' reduces token count significantly", COLUMN_PRESET_EXAMPLES)),
346
- columns: z.array(z.string()).optional()
347
- .describe("Custom columns to include (overrides columnPreset). E.g., ['TimeGenerated', 'Message']"),
348
- outputFormat: z.enum(["json", "markdown"]).optional()
349
- .describe(descWithExamples("Output format (default: json)", OUTPUT_FORMAT_EXAMPLES)),
350
- }, async ({ resourceId, query, timespan, columnPreset, columns, outputFormat }) => {
351
- try {
352
- const service = getLogAnalyticsService();
353
- const result = await service.executeQuery(resourceId, query, timespan);
354
- // Apply column filtering
355
- const columnsToInclude = resolveColumnPreset(columnPreset, columns);
356
- const filteredTables = result.tables.map((t) => filterColumns(t, columnsToInclude));
357
- const filteredResult = { ...result, tables: filteredTables };
358
- // Format output
359
- if (outputFormat === 'markdown' && filteredTables.length > 0) {
360
- const markdown = filteredTables.map((t) => formatTableAsMarkdown(t)).join('\n\n');
361
- return {
362
- content: [{ type: "text", text: markdown }],
363
- };
364
- }
365
- return {
366
- content: [
367
- {
368
- type: "text",
369
- text: JSON.stringify(filteredResult, null, 2),
370
- },
371
- ],
372
- };
373
- }
374
- catch (error) {
375
- console.error("Error executing Log Analytics query:", error);
376
- return {
377
- content: [
378
- {
379
- type: "text",
380
- text: `Failed to execute query: ${error.message}`,
381
- },
382
- ],
383
- isError: true
384
- };
385
- }
386
- });
387
- server.tool("la-test-access", "Test access to a Log Analytics workspace by executing a simple query", {
388
- resourceId: z.string().describe("Resource ID"),
389
- }, async ({ resourceId }) => {
390
- try {
391
- const service = getLogAnalyticsService();
392
- const result = await service.testWorkspaceAccess(resourceId);
393
- return {
394
- content: [
395
- {
396
- type: "text",
397
- text: JSON.stringify(result, null, 2),
398
- },
399
- ],
400
- };
401
- }
402
- catch (error) {
403
- console.error("Error testing workspace access:", error);
404
- return {
405
- content: [
406
- {
407
- type: "text",
408
- text: `Failed to test workspace access: ${error.message}`,
409
- },
410
- ],
411
- isError: true
412
- };
413
- }
414
- });
415
- server.tool("la-get-recent-events", "Get recent events from a specific Log Analytics table", {
416
- resourceId: z.string().describe("Resource ID"),
417
- tableName: z.string().describe(descWithExamples("Table name to query", TABLE_NAME_EXAMPLES)),
418
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
419
- limit: z.number().optional().describe("Maximum number of results (default: 100)"),
420
- columnPreset: z.enum(["minimal", "investigation", "full"]).optional()
421
- .describe(descWithExamples("Column preset for filtering results", COLUMN_PRESET_EXAMPLES)),
422
- columns: z.array(z.string()).optional()
423
- .describe("Custom columns to include (overrides columnPreset)"),
424
- outputFormat: z.enum(["json", "markdown"]).optional()
425
- .describe(descWithExamples("Output format (default: json)", OUTPUT_FORMAT_EXAMPLES)),
426
- }, async ({ resourceId, tableName, timespan, limit, columnPreset, columns, outputFormat }) => {
427
- try {
428
- const service = getLogAnalyticsService();
429
- const result = await service.getRecentEvents(resourceId, tableName, timespan || 'PT1H', limit || 100);
430
- // Apply column filtering
431
- const columnsToInclude = resolveColumnPreset(columnPreset, columns);
432
- const filteredTables = result.tables.map((t) => filterColumns(t, columnsToInclude));
433
- const filteredResult = { ...result, tables: filteredTables };
434
- // Format output
435
- if (outputFormat === 'markdown' && filteredTables.length > 0) {
436
- const markdown = filteredTables.map((t) => formatTableAsMarkdown(t)).join('\n\n');
437
- return {
438
- content: [{ type: "text", text: markdown }],
439
- };
440
- }
441
- return {
442
- content: [
443
- {
444
- type: "text",
445
- text: JSON.stringify(filteredResult, null, 2),
446
- },
447
- ],
448
- };
449
- }
450
- catch (error) {
451
- console.error("Error getting recent events:", error);
452
- return {
453
- content: [
454
- {
455
- type: "text",
456
- text: `Failed to get recent events: ${error.message}`,
457
- },
458
- ],
459
- isError: true
460
- };
461
- }
462
- });
463
- server.tool("la-search-logs", "Search logs by text content across tables or a specific table", {
464
- resourceId: z.string().describe("Resource ID"),
465
- searchText: z.string().describe("Text to search for (case-insensitive)"),
466
- tableName: z.string().optional().describe(descWithExamples("Table name to search in (optional, searches all if not specified)", TABLE_NAME_EXAMPLES)),
467
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
468
- limit: z.number().optional().describe("Maximum number of results (default: 100)"),
469
- columnPreset: z.enum(["minimal", "investigation", "full"]).optional()
470
- .describe(descWithExamples("Column preset for filtering results", COLUMN_PRESET_EXAMPLES)),
471
- columns: z.array(z.string()).optional()
472
- .describe("Custom columns to include (overrides columnPreset)"),
473
- outputFormat: z.enum(["json", "markdown"]).optional()
474
- .describe(descWithExamples("Output format (default: json)", OUTPUT_FORMAT_EXAMPLES)),
475
- }, async ({ resourceId, searchText, tableName, timespan, limit, columnPreset, columns, outputFormat }) => {
476
- try {
477
- const service = getLogAnalyticsService();
478
- const result = await service.searchLogs(resourceId, searchText, tableName, timespan || 'PT1H', limit || 100);
479
- // Apply column filtering
480
- const columnsToInclude = resolveColumnPreset(columnPreset, columns);
481
- const filteredTables = result.tables.map((t) => filterColumns(t, columnsToInclude));
482
- const filteredResult = { ...result, tables: filteredTables };
483
- // Format output
484
- if (outputFormat === 'markdown' && filteredTables.length > 0) {
485
- const markdown = filteredTables.map((t) => formatTableAsMarkdown(t)).join('\n\n');
486
- return {
487
- content: [{ type: "text", text: markdown }],
488
- };
489
- }
490
- return {
491
- content: [
492
- {
493
- type: "text",
494
- text: JSON.stringify(filteredResult, null, 2),
495
- },
496
- ],
497
- };
498
- }
499
- catch (error) {
500
- console.error("Error searching logs:", error);
501
- return {
502
- content: [
503
- {
504
- type: "text",
505
- text: `Failed to search logs: ${error.message}`,
506
- },
507
- ],
508
- isError: true
509
- };
510
- }
511
- });
512
- server.tool("la-get-fn-logs", "Get Azure Function logs from FunctionAppLogs table with optional filtering", {
513
- resourceId: z.string().describe("Resource ID"),
514
- functionName: z.string().optional().describe(descWithExamples("Function name to filter by (optional)", APP_NAME_EXAMPLES)),
515
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
516
- severityLevel: z.number().optional().describe(descWithExamples("Minimum severity level", SEVERITY_EXAMPLES)),
517
- limit: z.number().optional().describe("Maximum number of results (default: 100)"),
518
- columnPreset: z.enum(["minimal", "investigation", "full"]).optional()
519
- .describe(descWithExamples("Column preset for filtering results", COLUMN_PRESET_EXAMPLES)),
520
- columns: z.array(z.string()).optional()
521
- .describe("Custom columns to include (overrides columnPreset)"),
522
- outputFormat: z.enum(["json", "markdown"]).optional()
523
- .describe(descWithExamples("Output format (default: json)", OUTPUT_FORMAT_EXAMPLES)),
524
- }, async ({ resourceId, functionName, timespan, severityLevel, limit, columnPreset, columns, outputFormat }) => {
525
- try {
526
- const service = getLogAnalyticsService();
527
- const result = await service.getFunctionLogs(resourceId, functionName, timespan || 'PT1H', severityLevel, limit || 100);
528
- // Apply column filtering
529
- const columnsToInclude = resolveColumnPreset(columnPreset, columns);
530
- const filteredTables = result.tables.map((t) => filterColumns(t, columnsToInclude));
531
- const filteredResult = { ...result, tables: filteredTables };
532
- // Format output
533
- if (outputFormat === 'markdown' && filteredTables.length > 0) {
534
- const markdown = filteredTables.map((t) => formatTableAsMarkdown(t)).join('\n\n');
535
- return {
536
- content: [{ type: "text", text: markdown }],
537
- };
538
- }
539
- return {
540
- content: [
541
- {
542
- type: "text",
543
- text: JSON.stringify(filteredResult, null, 2),
544
- },
545
- ],
546
- };
547
- }
548
- catch (error) {
549
- console.error("Error getting function logs:", error);
550
- return {
551
- content: [
552
- {
553
- type: "text",
554
- text: `Failed to get function logs: ${error.message}`,
555
- },
556
- ],
557
- isError: true
558
- };
559
- }
560
- });
561
- server.tool("la-get-fn-errors", "Get Azure Function error logs with exception details", {
562
- resourceId: z.string().describe("Resource ID"),
563
- functionName: z.string().optional().describe(descWithExamples("Function name to filter by (optional)", APP_NAME_EXAMPLES)),
564
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
565
- limit: z.number().optional().describe("Maximum number of results (default: 100)"),
566
- columnPreset: z.enum(["minimal", "investigation", "full"]).optional()
567
- .describe(descWithExamples("Column preset for filtering results", COLUMN_PRESET_EXAMPLES)),
568
- columns: z.array(z.string()).optional()
569
- .describe("Custom columns to include (overrides columnPreset)"),
570
- outputFormat: z.enum(["json", "markdown"]).optional()
571
- .describe(descWithExamples("Output format (default: json)", OUTPUT_FORMAT_EXAMPLES)),
572
- }, async ({ resourceId, functionName, timespan, limit, columnPreset, columns, outputFormat }) => {
573
- try {
574
- const service = getLogAnalyticsService();
575
- const result = await service.getFunctionErrors(resourceId, functionName, timespan || 'PT1H', limit || 100);
576
- // Apply column filtering
577
- const columnsToInclude = resolveColumnPreset(columnPreset, columns);
578
- const filteredTables = result.tables.map((t) => filterColumns(t, columnsToInclude));
579
- const filteredResult = { ...result, tables: filteredTables };
580
- // Format output
581
- if (outputFormat === 'markdown' && filteredTables.length > 0) {
582
- const markdown = filteredTables.map((t) => formatTableAsMarkdown(t)).join('\n\n');
583
- return {
584
- content: [{ type: "text", text: markdown }],
585
- };
586
- }
587
- return {
588
- content: [
589
- {
590
- type: "text",
591
- text: JSON.stringify(filteredResult, null, 2),
592
- },
593
- ],
594
- };
595
- }
596
- catch (error) {
597
- console.error("Error getting function errors:", error);
598
- return {
599
- content: [
600
- {
601
- type: "text",
602
- text: `Failed to get function errors: ${error.message}`,
603
- },
604
- ],
605
- isError: true
606
- };
607
- }
608
- });
609
- server.tool("la-get-fn-stats", "Get execution statistics for Azure Functions (count, success rate, errors). Returns aggregated data - no column filtering needed.", {
610
- resourceId: z.string().describe("Resource ID"),
611
- functionName: z.string().optional().describe(descWithExamples("Function name (optional, returns stats for all functions if not specified)", APP_NAME_EXAMPLES)),
612
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
613
- outputFormat: z.enum(["json", "markdown"]).optional()
614
- .describe(descWithExamples("Output format (default: json)", OUTPUT_FORMAT_EXAMPLES)),
615
- }, async ({ resourceId, functionName, timespan, outputFormat }) => {
616
- try {
617
- const service = getLogAnalyticsService();
618
- const result = await service.getFunctionStats(resourceId, functionName, timespan || 'PT1H');
619
- // Format output
620
- if (outputFormat === 'markdown' && result.tables && result.tables.length > 0) {
621
- const markdown = result.tables.map((t) => formatTableAsMarkdown(t)).join('\n\n');
622
- return {
623
- content: [{ type: "text", text: markdown }],
624
- };
625
- }
626
- return {
627
- content: [
628
- {
629
- type: "text",
630
- text: JSON.stringify(result, null, 2),
631
- },
632
- ],
633
- };
634
- }
635
- catch (error) {
636
- console.error("Error getting function stats:", error);
637
- return {
638
- content: [
639
- {
640
- type: "text",
641
- text: `Failed to get function stats: ${error.message}`,
642
- },
643
- ],
644
- isError: true
645
- };
646
- }
647
- });
648
- server.tool("la-get-fn-invocations", "Get Azure Function invocation history from requests/traces tables", {
649
- resourceId: z.string().describe("Resource ID"),
650
- functionName: z.string().optional().describe(descWithExamples("Function name to filter by (optional)", APP_NAME_EXAMPLES)),
651
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
652
- limit: z.number().optional().describe("Maximum number of results (default: 100)"),
653
- columnPreset: z.enum(["minimal", "investigation", "full"]).optional()
654
- .describe(descWithExamples("Column preset for filtering results", COLUMN_PRESET_EXAMPLES)),
655
- columns: z.array(z.string()).optional()
656
- .describe("Custom columns to include (overrides columnPreset)"),
657
- outputFormat: z.enum(["json", "markdown"]).optional()
658
- .describe(descWithExamples("Output format (default: json)", OUTPUT_FORMAT_EXAMPLES)),
659
- }, async ({ resourceId, functionName, timespan, limit, columnPreset, columns, outputFormat }) => {
660
- try {
661
- const service = getLogAnalyticsService();
662
- const result = await service.getFunctionInvocations(resourceId, functionName, timespan || 'PT1H', limit || 100);
663
- // Apply column filtering
664
- const columnsToInclude = resolveColumnPreset(columnPreset, columns);
665
- const filteredTables = result.tables.map((t) => filterColumns(t, columnsToInclude));
666
- const filteredResult = { ...result, tables: filteredTables };
667
- // Format output
668
- if (outputFormat === 'markdown' && filteredTables.length > 0) {
669
- const markdown = filteredTables.map((t) => formatTableAsMarkdown(t)).join('\n\n');
670
- return {
671
- content: [{ type: "text", text: markdown }],
672
- };
673
- }
674
- return {
675
- content: [
676
- {
677
- type: "text",
678
- text: JSON.stringify(filteredResult, null, 2),
679
- },
680
- ],
681
- };
682
- }
683
- catch (error) {
684
- console.error("Error getting function invocations:", error);
685
- return {
686
- content: [
687
- {
688
- type: "text",
689
- text: `Failed to get function invocations: ${error.message}`,
690
- },
691
- ],
692
- isError: true
693
- };
694
- }
695
- });
696
- // ========================================
697
- // NEW INVESTIGATION TOOLS
698
- // ========================================
699
- server.tool("la-get-error-summary", "Get aggregated error summary by type - ideal for starting investigations. Returns counts, first/last seen, and sample messages.", {
700
- resourceId: z.string().describe("Resource ID"),
701
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
702
- tableName: z.enum(["AppExceptions", "AppTraces", "FunctionAppLogs"]).optional()
703
- .describe("Table to analyze (default: AppExceptions)"),
704
- minCount: z.number().optional().describe("Minimum error count to include (default: 1)"),
705
- deduplicateRetries: z.boolean().optional()
706
- .describe("Group by OperationId to deduplicate retry attempts (default: true). Shows UniqueErrors count and total RetryCount."),
707
- outputFormat: z.enum(["json", "markdown"]).optional()
708
- .describe(descWithExamples("Output format (default: markdown for readability)", OUTPUT_FORMAT_EXAMPLES)),
709
- }, async ({ resourceId, timespan, tableName, minCount, deduplicateRetries, outputFormat }) => {
710
- try {
711
- const service = getLogAnalyticsService();
712
- const table = tableName || 'AppExceptions';
713
- const timespanValue = timespan || 'PT1H';
714
- const minCountValue = minCount || 1;
715
- const dedupe = deduplicateRetries !== false; // default true
716
- // Build aggregation query based on table type
717
- let query;
718
- if (table === 'AppExceptions') {
719
- if (dedupe) {
720
- // Two-stage aggregation: first by OperationId to dedupe retries, then by error type
721
- query = `
722
- AppExceptions
723
- | summarize
724
- RetryCount = count(),
725
- FirstSeen = min(TimeGenerated),
726
- LastSeen = max(TimeGenerated),
727
- SampleMessage = take_any(OuterMessage)
728
- by OperationId, ExceptionType, AppRoleName
729
- | summarize
730
- UniqueErrors = count(),
731
- TotalRetries = sum(RetryCount),
732
- FirstSeen = min(FirstSeen),
733
- LastSeen = max(LastSeen),
734
- SampleMessage = take_any(SampleMessage)
735
- by ExceptionType, AppRoleName
736
- | where UniqueErrors >= ${minCountValue}
737
- | order by UniqueErrors desc
738
- `;
739
- }
740
- else {
741
- query = `
742
- AppExceptions
743
- | summarize
744
- Count = count(),
745
- FirstSeen = min(TimeGenerated),
746
- LastSeen = max(TimeGenerated),
747
- SampleMessage = take_any(OuterMessage)
748
- by ExceptionType, AppRoleName
749
- | where Count >= ${minCountValue}
750
- | order by Count desc
751
- `;
752
- }
753
- }
754
- else if (table === 'AppTraces') {
755
- if (dedupe) {
756
- query = `
757
- AppTraces
758
- | where SeverityLevel >= 3
759
- | summarize
760
- RetryCount = count(),
761
- FirstSeen = min(TimeGenerated),
762
- LastSeen = max(TimeGenerated),
763
- SampleMessage = take_any(Message)
764
- by OperationId, AppRoleName, SeverityLevel
765
- | summarize
766
- UniqueErrors = count(),
767
- TotalRetries = sum(RetryCount),
768
- FirstSeen = min(FirstSeen),
769
- LastSeen = max(LastSeen),
770
- SampleMessage = take_any(SampleMessage)
771
- by AppRoleName, SeverityLevel
772
- | where UniqueErrors >= ${minCountValue}
773
- | order by UniqueErrors desc
774
- `;
775
- }
776
- else {
777
- query = `
778
- AppTraces
779
- | where SeverityLevel >= 3
780
- | summarize
781
- Count = count(),
782
- FirstSeen = min(TimeGenerated),
783
- LastSeen = max(TimeGenerated),
784
- SampleMessage = take_any(Message)
785
- by AppRoleName, SeverityLevel
786
- | where Count >= ${minCountValue}
787
- | order by Count desc
788
- `;
789
- }
790
- }
791
- else {
792
- // FunctionAppLogs - no OperationId, use InvocationId instead
793
- if (dedupe) {
794
- query = `
795
- FunctionAppLogs
796
- | where ExceptionDetails != ''
797
- | summarize
798
- RetryCount = count(),
799
- FirstSeen = min(TimeGenerated),
800
- LastSeen = max(TimeGenerated),
801
- SampleMessage = take_any(Message)
802
- by InvocationId, FunctionName
803
- | summarize
804
- UniqueErrors = count(),
805
- TotalRetries = sum(RetryCount),
806
- FirstSeen = min(FirstSeen),
807
- LastSeen = max(LastSeen),
808
- SampleMessage = take_any(SampleMessage)
809
- by FunctionName
810
- | where UniqueErrors >= ${minCountValue}
811
- | order by UniqueErrors desc
812
- `;
813
- }
814
- else {
815
- query = `
816
- FunctionAppLogs
817
- | where ExceptionDetails != ''
818
- | summarize
819
- Count = count(),
820
- FirstSeen = min(TimeGenerated),
821
- LastSeen = max(TimeGenerated),
822
- SampleMessage = take_any(Message)
823
- by FunctionName
824
- | where Count >= ${minCountValue}
825
- | order by Count desc
826
- `;
827
- }
828
- }
829
- const result = await service.executeQuery(resourceId, query, timespanValue);
830
- // Default to markdown for this aggregation tool
831
- const format = outputFormat || 'markdown';
832
- if (format === 'markdown' && result.tables && result.tables.length > 0) {
833
- const dedupeNote = dedupe ? ' (deduplicated by OperationId)' : '';
834
- const markdown = `## Error Summary (${table})${dedupeNote}\n\n**Time range:** ${timespanValue}\n\n` +
835
- result.tables.map((t) => formatTableAsMarkdown(t)).join('\n\n');
836
- return {
837
- content: [{ type: "text", text: markdown }],
838
- };
839
- }
840
- return {
841
- content: [
842
- {
843
- type: "text",
844
- text: JSON.stringify(result, null, 2),
845
- },
846
- ],
847
- };
848
- }
849
- catch (error) {
850
- console.error("Error getting error summary:", error);
851
- return {
852
- content: [
853
- {
854
- type: "text",
855
- text: `Failed to get error summary: ${error.message}`,
856
- },
857
- ],
858
- isError: true
859
- };
860
- }
861
- });
862
- server.tool("la-investigate-app", "Combined investigation tool: searches AppTraces + AppExceptions, returns summary + recent error details. Best starting point for app investigations.", {
863
- resourceId: z.string().describe("Resource ID"),
864
- appNamePattern: z.string().optional().describe(descWithExamples("Filter by app name (searches AppRoleName). Partial match supported.", APP_NAME_EXAMPLES)),
865
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT1H)", TIMESPAN_EXAMPLES)),
866
- includeDetails: z.boolean().optional().describe("Include recent error details (default: true)"),
867
- detailsLimit: z.number().optional().describe("Max recent errors to include (default: 20)"),
868
- deduplicateRetries: z.boolean().optional()
869
- .describe("Group by OperationId to deduplicate retry attempts (default: true)"),
870
- }, async ({ resourceId, appNamePattern, timespan, includeDetails, detailsLimit, deduplicateRetries }) => {
871
- try {
872
- const service = getLogAnalyticsService();
873
- const timespanValue = timespan || 'PT1H';
874
- const showDetails = includeDetails !== false;
875
- const limit = detailsLimit || 20;
876
- const dedupe = deduplicateRetries !== false; // default true
877
- const appFilter = appNamePattern
878
- ? `| where AppRoleName contains "${appNamePattern}"`
879
- : '';
880
- // Query 1: Exception summary (with optional deduplication)
881
- const exceptionSummaryQuery = dedupe ? `
882
- AppExceptions
883
- ${appFilter}
884
- | summarize
885
- RetryCount = count(),
886
- FirstSeen = min(TimeGenerated),
887
- LastSeen = max(TimeGenerated)
888
- by OperationId, ExceptionType, AppRoleName
889
- | summarize
890
- UniqueErrors = count(),
891
- TotalRetries = sum(RetryCount),
892
- FirstSeen = min(FirstSeen),
893
- LastSeen = max(LastSeen)
894
- by ExceptionType, AppRoleName
895
- | order by UniqueErrors desc
896
- | take 20
897
- ` : `
898
- AppExceptions
899
- ${appFilter}
900
- | summarize
901
- Count = count(),
902
- FirstSeen = min(TimeGenerated),
903
- LastSeen = max(TimeGenerated)
904
- by ExceptionType, AppRoleName
905
- | order by Count desc
906
- | take 20
907
- `;
908
- // Query 2: Trace severity distribution (with optional deduplication)
909
- const traceSeverityQuery = dedupe ? `
910
- AppTraces
911
- ${appFilter}
912
- | summarize RetryCount = count() by OperationId, SeverityLevel, AppRoleName
913
- | summarize UniqueTraces = count(), TotalCount = sum(RetryCount) by SeverityLevel, AppRoleName
914
- | order by SeverityLevel desc
915
- ` : `
916
- AppTraces
917
- ${appFilter}
918
- | summarize Count = count() by SeverityLevel, AppRoleName
919
- | order by SeverityLevel desc
920
- `;
921
- // Query 3: Recent errors - deduplicated by showing one per OperationId
922
- const recentErrorsQuery = showDetails ? (dedupe ? `
923
- AppExceptions
924
- ${appFilter}
925
- | summarize
926
- TimeGenerated = max(TimeGenerated),
927
- RetryCount = count(),
928
- OuterMessage = take_any(OuterMessage)
929
- by OperationId, AppRoleName, ExceptionType
930
- | project TimeGenerated, AppRoleName, ExceptionType, OuterMessage, RetryCount
931
- | order by TimeGenerated desc
932
- | take ${limit}
933
- ` : `
934
- AppExceptions
935
- ${appFilter}
936
- | project TimeGenerated, AppRoleName, ExceptionType, OuterMessage
937
- | order by TimeGenerated desc
938
- | take ${limit}
939
- `) : null;
940
- // Execute queries
941
- const [exceptionSummary, traceSeverity, recentErrors] = await Promise.all([
942
- service.executeQuery(resourceId, exceptionSummaryQuery, timespanValue),
943
- service.executeQuery(resourceId, traceSeverityQuery, timespanValue),
944
- recentErrorsQuery ? service.executeQuery(resourceId, recentErrorsQuery, timespanValue) : null,
945
- ]);
946
- // Build markdown report
947
- let markdown = `# App Investigation Report\n\n`;
948
- markdown += `**Filter:** ${appNamePattern || '(all apps)'}\n`;
949
- markdown += `**Time range:** ${timespanValue}\n`;
950
- markdown += dedupe ? `**Deduplication:** enabled (grouped by OperationId)\n\n` : '\n';
951
- markdown += `## Exception Summary\n\n`;
952
- if (exceptionSummary.tables && exceptionSummary.tables.length > 0 && exceptionSummary.tables[0].rows.length > 0) {
953
- markdown += formatTableAsMarkdown(exceptionSummary.tables[0]);
954
- }
955
- else {
956
- markdown += '*No exceptions found*';
957
- }
958
- markdown += '\n\n';
959
- markdown += `## Trace Severity Distribution\n\n`;
960
- if (traceSeverity.tables && traceSeverity.tables.length > 0 && traceSeverity.tables[0].rows.length > 0) {
961
- markdown += formatTableAsMarkdown(traceSeverity.tables[0]);
962
- }
963
- else {
964
- markdown += '*No traces found*';
965
- }
966
- markdown += '\n\n';
967
- if (showDetails && recentErrors) {
968
- markdown += `## Recent Errors (${limit} max)\n\n`;
969
- if (recentErrors.tables && recentErrors.tables.length > 0 && recentErrors.tables[0].rows.length > 0) {
970
- markdown += formatTableAsMarkdown(recentErrors.tables[0]);
971
- }
972
- else {
973
- markdown += '*No recent errors*';
974
- }
975
- }
976
- return {
977
- content: [{ type: "text", text: markdown }],
978
- };
979
- }
980
- catch (error) {
981
- console.error("Error investigating app:", error);
982
- return {
983
- content: [
984
- {
985
- type: "text",
986
- text: `Failed to investigate app: ${error.message}`,
987
- },
988
- ],
989
- isError: true
990
- };
991
- }
992
- });
993
- server.tool("la-investigate-sync", "Investigate SmartConnectorCloud (SCC) sync failures. Only useful for clients using SmartConnectorCloud. Auto-derives sync app name from workspace ID (log-{env}-{client}-... → func-{env}-{client}-sc-sync-...). Best tool for SCC sync debugging.", {
994
- resourceId: z.string().describe("Resource ID (e.g., 'log-dev-rtpi-uks-01'). Environment and client are auto-extracted."),
995
- timespan: z.string().optional().describe(descWithExamples("Time range (default: PT8H - typical work day)", TIMESPAN_EXAMPLES)),
996
- includeDetails: z.boolean().optional().describe("Include recent error details (default: true)"),
997
- detailsLimit: z.number().optional().describe("Max recent errors to include (default: 10)"),
998
- }, async ({ resourceId, timespan, includeDetails, detailsLimit }) => {
999
- try {
1000
- const service = getLogAnalyticsService();
1001
- const timespanValue = timespan || 'PT8H';
1002
- const showDetails = includeDetails !== false;
1003
- const limit = detailsLimit || 10;
1004
- // Parse resourceId to extract environment and client
1005
- // Pattern: log-{environment}-{client}-...
1006
- const match = resourceId.match(/^log-([^-]+)-([^-]+)/);
1007
- if (!match) {
1008
- return {
1009
- content: [{
1010
- type: "text",
1011
- text: `Could not parse environment/client from resourceId '${resourceId}'. Expected format: log-{environment}-{client}-...`,
1012
- }],
1013
- isError: true,
1014
- };
1015
- }
1016
- const environment = match[1];
1017
- const client = match[2];
1018
- const syncAppPattern = `func-${environment}-${client}-sc-sync`;
1019
- // Query 1: Error summary by FunctionName (deduplicated)
1020
- // FunctionName is in Properties.AzureFunctions_FunctionName
1021
- const errorsByFunctionQuery = `
1022
- AppExceptions
1023
- | where AppRoleName contains "${syncAppPattern}"
1024
- | extend FunctionName = tostring(Properties.AzureFunctions_FunctionName)
1025
- | summarize
1026
- RetryCount = count(),
1027
- FirstSeen = min(TimeGenerated),
1028
- LastSeen = max(TimeGenerated),
1029
- SampleMessage = take_any(OuterMessage)
1030
- by OperationId, FunctionName, ExceptionType
1031
- | summarize
1032
- UniqueErrors = count(),
1033
- TotalRetries = sum(RetryCount),
1034
- FirstSeen = min(FirstSeen),
1035
- LastSeen = max(LastSeen),
1036
- SampleMessage = take_any(SampleMessage)
1037
- by FunctionName, ExceptionType
1038
- | order by UniqueErrors desc
1039
- `;
1040
- // Query 2: Error category summary (Dataverse, ServiceBus, SQL, etc.)
1041
- const errorCategoryQuery = `
1042
- AppExceptions
1043
- | where AppRoleName contains "${syncAppPattern}"
1044
- | extend ErrorCategory = case(
1045
- ExceptionType contains "FaultException" or ExceptionType contains "OrganizationService", "Dataverse",
1046
- ExceptionType contains "ServiceBus", "ServiceBus",
1047
- ExceptionType contains "Sql", "Database",
1048
- ExceptionType contains "Timeout", "Timeout",
1049
- ExceptionType contains "Socket" or ExceptionType contains "Http", "Network",
1050
- "Other"
1051
- )
1052
- | summarize
1053
- RetryCount = count(),
1054
- UniqueOps = dcount(OperationId)
1055
- by ErrorCategory
1056
- | order by UniqueOps desc
1057
- `;
1058
- // Query 3: Recent errors with details (deduplicated)
1059
- const recentErrorsQuery = showDetails ? `
1060
- AppExceptions
1061
- | where AppRoleName contains "${syncAppPattern}"
1062
- | extend FunctionName = tostring(Properties.AzureFunctions_FunctionName)
1063
- | summarize
1064
- TimeGenerated = max(TimeGenerated),
1065
- RetryCount = count(),
1066
- OuterMessage = take_any(OuterMessage)
1067
- by OperationId, FunctionName, ExceptionType
1068
- | project TimeGenerated, FunctionName, ExceptionType, OuterMessage, RetryCount
1069
- | order by TimeGenerated desc
1070
- | take ${limit}
1071
- ` : null;
1072
- // Query 4: Severity 3 (Error) traces for additional context
1073
- const errorTracesQuery = `
1074
- AppTraces
1075
- | where AppRoleName contains "${syncAppPattern}"
1076
- | where SeverityLevel >= 3
1077
- | summarize
1078
- RetryCount = count()
1079
- by OperationId, Message
1080
- | summarize
1081
- UniqueErrors = count(),
1082
- TotalCount = sum(RetryCount)
1083
- by Message
1084
- | order by UniqueErrors desc
1085
- | take 10
1086
- `;
1087
- // Execute queries in parallel
1088
- const [errorsByFunction, errorCategory, recentErrors, errorTraces] = await Promise.all([
1089
- service.executeQuery(resourceId, errorsByFunctionQuery, timespanValue),
1090
- service.executeQuery(resourceId, errorCategoryQuery, timespanValue),
1091
- recentErrorsQuery ? service.executeQuery(resourceId, recentErrorsQuery, timespanValue) : null,
1092
- service.executeQuery(resourceId, errorTracesQuery, timespanValue),
1093
- ]);
1094
- // Build markdown report
1095
- let markdown = `# SmartConnector Sync Investigation\n\n`;
1096
- markdown += `**Environment:** ${environment}\n`;
1097
- markdown += `**Client:** ${client}\n`;
1098
- markdown += `**Sync App:** ${syncAppPattern}-*\n`;
1099
- markdown += `**Time range:** ${timespanValue}\n`;
1100
- markdown += `**Deduplication:** enabled (grouped by OperationId)\n\n`;
1101
- // Error category summary (quick overview)
1102
- markdown += `## Error Categories\n\n`;
1103
- if (errorCategory.tables?.[0]?.rows?.length > 0) {
1104
- markdown += formatTableAsMarkdown(errorCategory.tables[0]);
1105
- }
1106
- else {
1107
- markdown += '*No errors found* ✅';
1108
- }
1109
- markdown += '\n\n';
1110
- // Errors by function (which sync operations are failing?)
1111
- markdown += `## Errors by Sync Operation\n\n`;
1112
- if (errorsByFunction.tables?.[0]?.rows?.length > 0) {
1113
- markdown += formatTableAsMarkdown(errorsByFunction.tables[0]);
1114
- }
1115
- else {
1116
- markdown += '*No errors found* ✅';
1117
- }
1118
- markdown += '\n\n';
1119
- // Error traces (additional context from logs)
1120
- markdown += `## Error Traces (Severity 3+)\n\n`;
1121
- if (errorTraces.tables?.[0]?.rows?.length > 0) {
1122
- markdown += formatTableAsMarkdown(errorTraces.tables[0]);
1123
- }
1124
- else {
1125
- markdown += '*No error traces found*';
1126
- }
1127
- markdown += '\n\n';
1128
- // Recent errors with details
1129
- if (showDetails && recentErrors) {
1130
- markdown += `## Recent Errors (${limit} max)\n\n`;
1131
- if (recentErrors.tables?.[0]?.rows?.length > 0) {
1132
- markdown += formatTableAsMarkdown(recentErrors.tables[0]);
1133
- }
1134
- else {
1135
- markdown += '*No recent errors*';
1136
- }
1137
- }
1138
- return {
1139
- content: [{ type: "text", text: markdown }],
1140
- };
1141
- }
1142
- catch (error) {
1143
- console.error("Error investigating sync:", error);
1144
- return {
1145
- content: [
1146
- {
1147
- type: "text",
1148
- text: `Failed to investigate sync: ${error.message}`,
1149
- },
1150
- ],
1151
- isError: true
1152
- };
1153
- }
1154
- });
1155
- console.error("Log Analytics tools registered: 13 tools, 4 prompts");
54
+ return {
55
+ get logAnalytics() { return getService(); },
56
+ };
57
+ }
58
+ /**
59
+ * Register Log Analytics tools and prompts to an MCP server.
60
+ * Backward-compatible API for the meta package.
61
+ */
62
+ export function registerLogAnalyticsTools(server) {
63
+ const ctx = createServiceContext();
64
+ registerAllTools(server, ctx);
65
+ registerAllPrompts(server, ctx);
1156
66
  }
1157
- // CLI entry point (standalone execution)
1158
- // Uses realpathSync to resolve symlinks created by npx
67
+ // Backward-compatible exports
68
+ export { LogAnalyticsService } from './services/log-analytics-service.js';
69
+ /**
70
+ * Standalone CLI server (when run directly)
71
+ * Uses realpathSync to resolve symlinks created by npx
72
+ */
1159
73
  if (import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
1160
74
  const loadEnv = createEnvLoader();
1161
75
  loadEnv();
1162
76
  const server = createMcpServer({
1163
77
  name: "mcp-log-analytics",
1164
78
  version: "1.0.0",
1165
- capabilities: { tools: {}, prompts: {} }
79
+ capabilities: { tools: {}, prompts: {} },
1166
80
  });
1167
81
  registerLogAnalyticsTools(server);
1168
82
  const transport = new StdioServerTransport();