@mcp-consultant-tools/log-analytics 20.0.0-beta.3 → 28.0.0-beta.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/build/LogAnalyticsService.d.ts.map +1 -1
- package/build/LogAnalyticsService.js +20 -11
- package/build/LogAnalyticsService.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +603 -33
- package/build/index.js.map +1 -1
- package/build/tool-examples.d.ts +44 -0
- package/build/tool-examples.d.ts.map +1 -0
- package/build/tool-examples.js +94 -0
- package/build/tool-examples.js.map +1 -0
- package/build/utils/loganalytics-formatters.d.ts +28 -0
- package/build/utils/loganalytics-formatters.d.ts.map +1 -1
- package/build/utils/loganalytics-formatters.js +72 -0
- package/build/utils/loganalytics-formatters.js.map +1 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import { realpathSync } from "node:fs";
|
|
|
5
5
|
import { createMcpServer, createEnvLoader } from "@mcp-consultant-tools/core";
|
|
6
6
|
import { LogAnalyticsService } from "./LogAnalyticsService.js";
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
-
import { formatTableAsMarkdown, analyzeLogs, analyzeFunctionLogs, analyzeFunctionErrors, analyzeFunctionStats, generateRecommendations } from './utils/loganalytics-formatters.js';
|
|
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';
|
|
9
10
|
export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
10
11
|
let service = loganalyticsService || null;
|
|
11
12
|
function getLogAnalyticsService() {
|
|
@@ -338,17 +339,34 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
|
338
339
|
});
|
|
339
340
|
server.tool("loganalytics-execute-query", "Execute a custom KQL query against Log Analytics workspace", {
|
|
340
341
|
resourceId: z.string().describe("Resource ID"),
|
|
341
|
-
query: z.string().describe("KQL query string"),
|
|
342
|
-
timespan: z.string().optional().describe("Time range (
|
|
343
|
-
|
|
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 }) => {
|
|
344
351
|
try {
|
|
345
352
|
const service = getLogAnalyticsService();
|
|
346
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
|
+
}
|
|
347
365
|
return {
|
|
348
366
|
content: [
|
|
349
367
|
{
|
|
350
368
|
type: "text",
|
|
351
|
-
text: JSON.stringify(
|
|
369
|
+
text: JSON.stringify(filteredResult, null, 2),
|
|
352
370
|
},
|
|
353
371
|
],
|
|
354
372
|
};
|
|
@@ -396,18 +414,35 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
|
396
414
|
});
|
|
397
415
|
server.tool("loganalytics-get-recent-events", "Get recent events from a specific Log Analytics table", {
|
|
398
416
|
resourceId: z.string().describe("Resource ID"),
|
|
399
|
-
tableName: z.string().describe("Table name
|
|
400
|
-
timespan: z.string().optional().describe("Time range (default: PT1H)"),
|
|
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)),
|
|
401
419
|
limit: z.number().optional().describe("Maximum number of results (default: 100)"),
|
|
402
|
-
|
|
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 }) => {
|
|
403
427
|
try {
|
|
404
428
|
const service = getLogAnalyticsService();
|
|
405
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
|
+
}
|
|
406
441
|
return {
|
|
407
442
|
content: [
|
|
408
443
|
{
|
|
409
444
|
type: "text",
|
|
410
|
-
text: JSON.stringify(
|
|
445
|
+
text: JSON.stringify(filteredResult, null, 2),
|
|
411
446
|
},
|
|
412
447
|
],
|
|
413
448
|
};
|
|
@@ -427,19 +462,36 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
|
427
462
|
});
|
|
428
463
|
server.tool("loganalytics-search-logs", "Search logs by text content across tables or a specific table", {
|
|
429
464
|
resourceId: z.string().describe("Resource ID"),
|
|
430
|
-
searchText: z.string().describe("Text to search for"),
|
|
431
|
-
tableName: z.string().optional().describe("Table name to search in (optional, searches all if not specified)"),
|
|
432
|
-
timespan: z.string().optional().describe("Time range (default: PT1H)"),
|
|
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)),
|
|
433
468
|
limit: z.number().optional().describe("Maximum number of results (default: 100)"),
|
|
434
|
-
|
|
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 }) => {
|
|
435
476
|
try {
|
|
436
477
|
const service = getLogAnalyticsService();
|
|
437
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
|
+
}
|
|
438
490
|
return {
|
|
439
491
|
content: [
|
|
440
492
|
{
|
|
441
493
|
type: "text",
|
|
442
|
-
text: JSON.stringify(
|
|
494
|
+
text: JSON.stringify(filteredResult, null, 2),
|
|
443
495
|
},
|
|
444
496
|
],
|
|
445
497
|
};
|
|
@@ -459,19 +511,36 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
|
459
511
|
});
|
|
460
512
|
server.tool("loganalytics-get-function-logs", "Get Azure Function logs from FunctionAppLogs table with optional filtering", {
|
|
461
513
|
resourceId: z.string().describe("Resource ID"),
|
|
462
|
-
functionName: z.string().optional().describe("Function name to filter by (optional)"),
|
|
463
|
-
timespan: z.string().optional().describe("Time range (default: PT1H)"),
|
|
464
|
-
severityLevel: z.number().optional().describe("Minimum severity level
|
|
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)),
|
|
465
517
|
limit: z.number().optional().describe("Maximum number of results (default: 100)"),
|
|
466
|
-
|
|
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 }) => {
|
|
467
525
|
try {
|
|
468
526
|
const service = getLogAnalyticsService();
|
|
469
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
|
+
}
|
|
470
539
|
return {
|
|
471
540
|
content: [
|
|
472
541
|
{
|
|
473
542
|
type: "text",
|
|
474
|
-
text: JSON.stringify(
|
|
543
|
+
text: JSON.stringify(filteredResult, null, 2),
|
|
475
544
|
},
|
|
476
545
|
],
|
|
477
546
|
};
|
|
@@ -491,18 +560,35 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
|
491
560
|
});
|
|
492
561
|
server.tool("loganalytics-get-function-errors", "Get Azure Function error logs with exception details", {
|
|
493
562
|
resourceId: z.string().describe("Resource ID"),
|
|
494
|
-
functionName: z.string().optional().describe("Function name to filter by (optional)"),
|
|
495
|
-
timespan: z.string().optional().describe("Time range (default: PT1H)"),
|
|
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)),
|
|
496
565
|
limit: z.number().optional().describe("Maximum number of results (default: 100)"),
|
|
497
|
-
|
|
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 }) => {
|
|
498
573
|
try {
|
|
499
574
|
const service = getLogAnalyticsService();
|
|
500
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
|
+
}
|
|
501
587
|
return {
|
|
502
588
|
content: [
|
|
503
589
|
{
|
|
504
590
|
type: "text",
|
|
505
|
-
text: JSON.stringify(
|
|
591
|
+
text: JSON.stringify(filteredResult, null, 2),
|
|
506
592
|
},
|
|
507
593
|
],
|
|
508
594
|
};
|
|
@@ -520,14 +606,23 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
|
520
606
|
};
|
|
521
607
|
}
|
|
522
608
|
});
|
|
523
|
-
server.tool("loganalytics-get-function-stats", "Get execution statistics for Azure Functions (count, success rate, errors)", {
|
|
609
|
+
server.tool("loganalytics-get-function-stats", "Get execution statistics for Azure Functions (count, success rate, errors). Returns aggregated data - no column filtering needed.", {
|
|
524
610
|
resourceId: z.string().describe("Resource ID"),
|
|
525
|
-
functionName: z.string().optional().describe("Function name (optional, returns stats for all functions if not specified)"),
|
|
526
|
-
timespan: z.string().optional().describe("Time range (default: PT1H)"),
|
|
527
|
-
|
|
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 }) => {
|
|
528
616
|
try {
|
|
529
617
|
const service = getLogAnalyticsService();
|
|
530
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
|
+
}
|
|
531
626
|
return {
|
|
532
627
|
content: [
|
|
533
628
|
{
|
|
@@ -552,18 +647,35 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
|
552
647
|
});
|
|
553
648
|
server.tool("loganalytics-get-function-invocations", "Get Azure Function invocation history from requests/traces tables", {
|
|
554
649
|
resourceId: z.string().describe("Resource ID"),
|
|
555
|
-
functionName: z.string().optional().describe("Function name to filter by (optional)"),
|
|
556
|
-
timespan: z.string().optional().describe("Time range (default: PT1H)"),
|
|
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)),
|
|
557
652
|
limit: z.number().optional().describe("Maximum number of results (default: 100)"),
|
|
558
|
-
|
|
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 }) => {
|
|
559
660
|
try {
|
|
560
661
|
const service = getLogAnalyticsService();
|
|
561
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
|
+
}
|
|
562
674
|
return {
|
|
563
675
|
content: [
|
|
564
676
|
{
|
|
565
677
|
type: "text",
|
|
566
|
-
text: JSON.stringify(
|
|
678
|
+
text: JSON.stringify(filteredResult, null, 2),
|
|
567
679
|
},
|
|
568
680
|
],
|
|
569
681
|
};
|
|
@@ -581,8 +693,466 @@ export function registerLogAnalyticsTools(server, loganalyticsService) {
|
|
|
581
693
|
};
|
|
582
694
|
}
|
|
583
695
|
});
|
|
584
|
-
|
|
585
|
-
|
|
696
|
+
// ========================================
|
|
697
|
+
// NEW INVESTIGATION TOOLS
|
|
698
|
+
// ========================================
|
|
699
|
+
server.tool("loganalytics-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("loganalytics-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("loganalytics-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");
|
|
586
1156
|
}
|
|
587
1157
|
// CLI entry point (standalone execution)
|
|
588
1158
|
// Uses realpathSync to resolve symlinks created by npx
|