Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageVersion Include="Azure.Monitor.Ingestion" Version="1.2.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.6.0" />
<PackageVersion Include="Azure.Monitor.Query.Logs" Version="1.0.0" />
<PackageVersion Include="Azure.Monitor.Query.Metrics" Version="1.0.0" />
<PackageVersion Include="Azure.ResourceManager" Version="1.14.0" />
<PackageVersion Include="Azure.ResourceManager.ApplicationInsights" Version="1.1.0" />
<PackageVersion Include="Azure.ResourceManager.AppContainers" Version="1.5.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: Features Added
description: Added the `azmcp monitor metrics batchquery` command for querying Azure Monitor metrics across multiple resources in a single request.
30 changes: 30 additions & 0 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -3296,6 +3296,36 @@ azmcp monitor metrics query --subscription <subscription> \
--end-time "2024-01-01T23:59:59Z" \
--interval "PT1H" \
--aggregation "Average"

# Query Azure Monitor metrics for multiple resources in a single batch request
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp monitor metrics batchquery --subscription <subscription> \
--resources <resources> \
--metric-namespace <metric-namespace> \
--metric-names <metric-names> \
[--resource-group <resource-group>] \
[--resource-type <resource-type>] \
[--start-time <start-time>] \
[--end-time <end-time>] \
[--interval <interval>] \
[--aggregation <aggregation>] \
[--filter <filter>] \
[--order-by <order-by>] \
[--top <top>] \
[--max-buckets <max-buckets>]

# Query CPU metrics across multiple storage accounts at once
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp monitor metrics batchquery --subscription <subscription> \
--resources "storageacct1,storageacct2,storageacct3" \
--resource-group <resource-group> \
--resource-type "Microsoft.Storage/storageAccounts" \
--metric-namespace "Microsoft.Storage/storageAccounts" \
--metric-names "Transactions" \
--start-time "2024-01-01T00:00:00Z" \
--end-time "2024-01-01T23:59:59Z" \
--interval "PT1H" \
--aggregation "Total"
```

#### Web Tests (Availability Tests)
Expand Down
3 changes: 3 additions & 0 deletions servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,9 @@ The `Interaction` column describes whether a prompt can invoke its tool immediat
| monitor_instrumentation_send-enhancement-select | Submit enhancement selection keys <enhancement_keys> for Azure Monitor instrumentation session <session_id> after enhancement options are presented | investigation-required |
| monitor_instrumentation_send-enhancement-select | Continue instrumentation enhancement flow by sending selected keys <enhancement_keys> to session <session_id> | none |
| monitor_instrumentation_send-enhancement-select | Send chosen enhancement option keys <enhancement_keys> to Azure Monitor instrumentation onboarding session <session_id> | none |
| monitor_metrics_batchquery | Get the <metric_name> metric for storage accounts <resource_name_1>, <resource_name_2>, and <resource_name_3> over the last <time_period> | none |
| monitor_metrics_batchquery | Compare <metric_name> across resources <resource_name_1> and <resource_name_2> in resource group <resource_group> for the last <time_period> | none |
| monitor_metrics_batchquery | Query <aggregation_type> <metric_name> for multiple <resource_type> resources <resource_name_1>, <resource_name_2> in one request | none |
| monitor_metrics_definitions | Get metric definitions for <resource_type> <resource_name> from the namespace | none |
| monitor_metrics_definitions | Show me all available metrics and their definitions for storage account <account_name> | none |
| monitor_metrics_definitions | What metric definitions are available for the Application Insights resource <resource_name> | none |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<ItemGroup>
<PackageReference Include="Azure.Core" />
<PackageReference Include="Azure.Monitor.Query.Logs" />
<PackageReference Include="Azure.Monitor.Query.Metrics" />
<PackageReference Include="Azure.ResourceManager" />
<PackageReference Include="Azure.ResourceManager.ApplicationInsights" />
<PackageReference Include="Azure.ResourceManager.CloudHealth" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Core.Commands.Subscription;
using Azure.Mcp.Core.Services.Azure.Subscription;
using Azure.Mcp.Tools.Monitor.Models;
using Azure.Mcp.Tools.Monitor.Options.Metrics;
using Azure.Mcp.Tools.Monitor.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Models.Command;

namespace Azure.Mcp.Tools.Monitor.Commands.Metrics;

/// <summary>
/// Command for querying Azure Monitor metrics across multiple resources in a single batch request
/// </summary>
[CommandMetadata(
Id = "6c1b0f5f-04c1-4b2e-8f0b-0d6f4f7cba2e",
Name = "batchquery",
Title = "Query Azure Monitor Metrics for Multiple Resources",
Description = "Query Azure Monitor metrics for multiple resources in a single batch request. Returns time series data for the specified metrics, grouped by resource. All resources must belong to the same subscription, Azure region, and resource type.",
Destructive = false,
Comment on lines +19 to +24
Idempotent = true,
OpenWorld = false,
ReadOnly = true,
Secret = false,
LocalRequired = false)]
public sealed class MetricsBatchQueryCommand(ILogger<MetricsBatchQueryCommand> logger, IMonitorMetricsService metricsService, ISubscriptionResolver subscriptionResolver)
: SubscriptionCommand<MetricsBatchQueryOptions, MetricsBatchQueryCommand.MetricsBatchQueryCommandResult>(subscriptionResolver)
{
private readonly ILogger<MetricsBatchQueryCommand> _logger = logger;
private readonly IMonitorMetricsService _metricsService = metricsService;

public override void ValidateOptions(MetricsBatchQueryOptions options, ValidationResult validationResult)
{
base.ValidateOptions(options, validationResult);

if (string.IsNullOrWhiteSpace(options.Resources))
{
validationResult.Errors.Add($"Invalid format for '--resources'. Provide a comma-separated list of resource names or resource IDs to query (e.g. resource1,resource2).");
}
else
{
string[] resources = [.. options.Resources.Split(',').Select(t => t.Trim())];
if (resources.Length == 0 || resources.Any(s => string.IsNullOrWhiteSpace(s)))
{
validationResult.Errors.Add($"Invalid format for '--resources'. Provide a comma-separated list of resource names or resource IDs to query (e.g. resource1,resource2).");
}
}

if (string.IsNullOrWhiteSpace(options.MetricNames))
{
validationResult.Errors.Add($"Invalid format for '--metric-names'. Provide a comma-separated list of metric names to query (e.g. CPU,memory).");
}
else
{
string[] metricNames = [.. options.MetricNames.Split(',').Select(t => t.Trim())];

if (metricNames.Length == 0 || metricNames.Any(s => string.IsNullOrWhiteSpace(s)))
{
validationResult.Errors.Add($"Invalid format for '--metric-names'. Provide a comma-separated list of metric names to query (e.g. CPU,memory).");
}
}
}

public override void PostBindOptions(MetricsBatchQueryOptions options)
{
base.PostBindOptions(options);
options.StartTime ??= DateTime.UtcNow.AddHours(-24).ToString("o"); // Default to 24 hours ago if not specified
options.EndTime ??= DateTime.UtcNow.ToString("o"); // Default to now if not specified
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, MetricsBatchQueryOptions options, CancellationToken cancellationToken)
{
try
{
string[] resources = [.. options.Resources.Split(',').Select(t => t.Trim())];
string[] metricNames = [.. options.MetricNames.Split(',').Select(t => t.Trim())];

var results = await _metricsService.QueryMetricsBatchAsync(
Comment on lines +75 to +82
options.Subscription!,
options.ResourceGroup,
options.ResourceType,
resources,
options.MetricNamespace,
metricNames,
options.StartTime,
options.EndTime,
options.Interval,
options.Aggregation,
options.Filter,
options.OrderBy,
options.Top,
options.Tenant,
options.RetryPolicy,
cancellationToken);

// Validate bucket count limit
if (results?.Count > 0)
{
int maxBuckets = options.MaxBuckets ?? 50; // Use provided value or default to 50

foreach (var resourceResult in results)
{
foreach (var metric in resourceResult.Metrics)
{
foreach (var timeSeries in metric.TimeSeries)
{
// Check each bucket array for exceeding the limit
var bucketCounts = new[]
{
timeSeries.AvgBuckets?.Length ?? 0,
timeSeries.MinBuckets?.Length ?? 0,
timeSeries.MaxBuckets?.Length ?? 0,
timeSeries.TotalBuckets?.Length ?? 0,
timeSeries.CountBuckets?.Length ?? 0
};

int maxBucketCount = bucketCounts.Max();

if (maxBucketCount > maxBuckets)
{
string errorMessage = $"Time series for metric '{metric.Name}' on resource '{resourceResult.ResourceId}' contains {maxBucketCount} time buckets, " +
$"which exceeds the maximum allowed limit of {maxBuckets}. " +
$"To resolve this issue, either query a smaller time range, " +
$"increase the interval size (e.g., use PT1H instead of PT5M), " +
$"or increase the --max-buckets parameter.";

context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = errorMessage;

_logger.LogWarning("Bucket limit exceeded. ResourceId: {ResourceId}, MetricName: {MetricName}, BucketCount: {BucketCount}, MaxBuckets: {MaxBuckets}",
resourceResult.ResourceId, metric.Name, maxBucketCount, maxBuckets);

return context.Response;
}
}
}
}
}

// Set results
context.Response.Results = ResponseResult.Create(new(results ?? []), MonitorJsonContext.Default.MetricsBatchQueryCommandResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error querying batch metrics. ResourceGroup: {ResourceGroup}, ResourceType: {ResourceType}, Resources: {Resources}, MetricNames: {MetricNames}.",
options.ResourceGroup, options.ResourceType, options.Resources, options.MetricNames);
HandleException(context, ex);
}

return context.Response;
}

// Strongly-typed result records
public sealed record MetricsBatchQueryCommandResult(List<ResourceMetricsResult> Results);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ namespace Azure.Mcp.Tools.Monitor.Commands;
[JsonSerializable(typeof(HealthModelSummary))]
[JsonSerializable(typeof(List<HealthModelSummary>))]
[JsonSerializable(typeof(List<JsonNode>))]
[JsonSerializable(typeof(MetricsBatchQueryCommand.MetricsBatchQueryCommandResult))]
[JsonSerializable(typeof(MetricsDefinitionsCommand.MetricsDefinitionsCommandResult))]
[JsonSerializable(typeof(MetricsDefinitionsCommand.MetricsDefinitionsCommandResult))]
[JsonSerializable(typeof(MetricsQueryCommand.MetricsQueryCommandResult))]
[JsonSerializable(typeof(MetricsQueryCommand.MetricsQueryCommandResult))]
[JsonSerializable(typeof(Azure.Mcp.Tools.Monitor.Models.ResourceMetricsResult))]
[JsonSerializable(typeof(List<Azure.Mcp.Tools.Monitor.Models.ResourceMetricsResult>))]
[JsonSerializable(typeof(TableListCommand.TableListCommandResult))]
[JsonSerializable(typeof(TableTypeListCommand.TableTypeListCommandResult))]
[JsonSerializable(typeof(WebTestsCreateOrUpdateCommand.WebTestsCreateOrUpdateCommandResult))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.Mcp.Tools.Monitor.Models;

/// <summary>
/// Represents the compact metric results for a single resource returned from a batch metrics query
/// </summary>
public class ResourceMetricsResult
{
/// <summary>
/// The resource ID the metrics were queried for
/// </summary>
[JsonPropertyName("resourceId")]
public string ResourceId { get; set; } = string.Empty;

/// <summary>
/// The compact metric results for this resource
/// </summary>
[JsonPropertyName("metrics")]
public List<MetricResult> Metrics { get; set; } = [];
}
2 changes: 2 additions & 0 deletions tools/Azure.Mcp.Tools.Monitor/src/MonitorSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<HealthModelGetCommand>();

services.AddSingleton<MetricsQueryCommand>();
services.AddSingleton<MetricsBatchQueryCommand>();
services.AddSingleton<MetricsDefinitionsCommand>();

services.AddSingleton<ActivityLogListCommand>();
Expand Down Expand Up @@ -151,6 +152,7 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
monitor.AddSubGroup(metrics);

metrics.AddCommand<MetricsQueryCommand>(serviceProvider);
metrics.AddCommand<MetricsBatchQueryCommand>(serviceProvider);
metrics.AddCommand<MetricsDefinitionsCommand>(serviceProvider);
Comment on lines 154 to 156

var activityLog = new CommandGroup("activitylog", "Azure Monitor activity log operations - Commands for querying and analyzing activity logs for Azure resources.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Options;
using Microsoft.Mcp.Core.Options;

namespace Azure.Mcp.Tools.Monitor.Options.Metrics;

/// <summary>
/// Options for querying metrics for multiple resources in a single batch request
/// </summary>
public sealed class MetricsBatchQueryOptions : ISubscriptionOption
{
/// <summary>
/// The resources to query metrics for (required)
/// </summary>
[Option(Description = "Comma-separated list of resource names or full resource IDs to query metrics for (up to 50 resources). " +
"All resources must belong to the same subscription, Azure region, and resource type.")]
public required string Resources { get; set; }

/// <summary>
/// The resource type (optional, e.g., 'Microsoft.Storage/storageAccounts')
/// </summary>
[Option(Description = "The Azure resource type (e.g., 'Microsoft.Storage/storageAccounts', 'Microsoft.Compute/virtualMachines'), applied to all resources. If not specified, will attempt to infer from each resource name.")]
public string? ResourceType { get; set; }

[Option(Description = OptionDescriptions.ResourceGroup)]
public string? ResourceGroup { get; set; }

[Option(Description = OptionDescriptions.Tenant)]
public string? Tenant { get; set; }

[Option(Description = OptionDescriptions.Subscription)]
public string? Subscription { get; set; }

/// <summary>
/// The names of metrics to query
/// </summary>
[Option(Description = "The names of metrics to query (comma-separated).")]
public required string MetricNames { get; set; }

[Option(Description = MonitorOptionDescriptions.MetricNamespace)]
public required string MetricNamespace { get; set; }

/// <summary>
/// Start time for the query in ISO format
/// </summary>
[Option(Description = "The start time for the query in ISO format (e.g., 2023-01-01T00:00:00Z). Defaults to 24 hours ago.")]
public string? StartTime { get; set; }

/// <summary>
/// End time for the query in ISO format
/// </summary>
[Option(Description = "The end time for the query in ISO format (e.g., 2023-01-01T00:00:00Z). Defaults to now.")]
public string? EndTime { get; set; }

/// <summary>
/// Time interval for the query
/// </summary>
[Option(Description = "The time interval for data points (e.g., PT1H for 1 hour, PT5M for 5 minutes).")]
public string? Interval { get; set; }

/// <summary>
/// Aggregation type(s) for the metrics (Average, Maximum, Minimum, Total, Count)
/// </summary>
[Option(Description = "The aggregation type(s) to use (comma-separated, e.g., Average,Maximum).")]
public string? Aggregation { get; set; }

/// <summary>
/// OData filter for the query
/// </summary>
[Option(Description = "OData filter to apply to the metrics query.")]
public string? Filter { get; set; }

/// <summary>
/// The aggregation to use for sorting results and the direction of the sort. Only valid when '--filter' is specified.
/// </summary>
[Option(Description = "The aggregation to use for sorting results and the direction of the sort (e.g., 'total asc'). Only valid when '--filter' is specified.")]
public string? OrderBy { get; set; }

/// <summary>
/// The maximum number of records to retrieve per resource. Only valid when '--filter' is specified.
/// </summary>
[Option(Description = "The maximum number of time series to retrieve per resource per metric. Only valid when '--filter' is specified. Defaults to 10.")]
public int? Top { get; set; }

/// <summary>
/// The maximum number of time buckets to return per metric time series. Defaults to 50.
/// </summary>
[Option(Description = "The maximum number of time buckets to return per metric time series. Defaults to 50.", DefaultValue = 50)]
public int? MaxBuckets { get; set; }

[OptionContainer(Prefix = "retry")]
public RetryPolicyOptions? RetryPolicy { get; set; }
}
Loading
Loading