@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/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 (e.g., 'PT1H', 'P1D')"),
343
- }, async ({ resourceId, query, timespan }) => {
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(result, null, 2),
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 (e.g., 'FunctionAppLogs', 'traces', 'requests')"),
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
- }, async ({ resourceId, tableName, timespan, limit }) => {
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(result, null, 2),
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
- }, async ({ resourceId, searchText, tableName, timespan, limit }) => {
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(result, null, 2),
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 (0=Verbose, 1=Info, 2=Warning, 3=Error, 4=Critical)"),
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
- }, async ({ resourceId, functionName, timespan, severityLevel, limit }) => {
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(result, null, 2),
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
- }, async ({ resourceId, functionName, timespan, limit }) => {
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(result, null, 2),
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
- }, async ({ resourceId, functionName, timespan }) => {
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
- }, async ({ resourceId, functionName, timespan, limit }) => {
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(result, null, 2),
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
- console.error("log-analytics tools registered: 10 tools, 4 prompts");
585
- console.error("Log Analytics tools registered: 10 tools, 4 prompts");
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