diff --git a/src/AppCommon/Commands/AzureServiceBusCommand.cs b/src/AppCommon/Commands/AzureServiceBusCommand.cs index 19a87865..2ce7d8f2 100644 --- a/src/AppCommon/Commands/AzureServiceBusCommand.cs +++ b/src/AppCommon/Commands/AzureServiceBusCommand.cs @@ -23,16 +23,34 @@ public static Command CreateCommand() IsRequired = false }; + var regionArg = new Option( + name: "--region", + description: "The Azure region where the Service Bus namespace is located, which is listed as the location in the Properties page in the Azure Portal.") + { + IsRequired = true + }; + + var metricsDomainArg = new Option("--metricsDomain", + description: "The Azure Monitor Metrics domain. Defaults to 'metrics.monitor.azure.com' and only must be specified for Azure customers using non-standard domains like government cloud customers.") + { + IsRequired = false + }; + serviceBusDomainArg.SetDefaultValue("servicebus.windows.net"); + metricsDomainArg.SetDefaultValue("metrics.monitor.azure.com"); command.AddOption(resourceIdArg); command.AddOption(serviceBusDomainArg); + command.AddOption(regionArg); + command.AddOption(metricsDomainArg); command.SetHandler(async context => { var shared = SharedOptions.Parse(context); var resourceId = context.ParseResult.GetValueForOption(resourceIdArg); var serviceBusDomain = context.ParseResult.GetValueForOption(serviceBusDomainArg); + var region = context.ParseResult.GetValueForOption(regionArg); + var metricsDomain = context.ParseResult.GetValueForOption(metricsDomainArg); var cancellationToken = context.GetCancellationToken(); #if DEBUG @@ -44,7 +62,7 @@ public static Command CreateCommand() } #endif - var runner = new AzureServiceBusCommand(shared, resourceId, serviceBusDomain); + var runner = new AzureServiceBusCommand(shared, resourceId, serviceBusDomain, region, metricsDomain); await runner.Run(cancellationToken); }); @@ -55,10 +73,10 @@ public static Command CreateCommand() string[] queueNames; - public AzureServiceBusCommand(SharedOptions shared, string resourceId, string serviceBusDomain) + public AzureServiceBusCommand(SharedOptions shared, string resourceId, string serviceBusDomain, string region, string metricsDomain) : base(shared) { - azure = new AzureClient(resourceId, serviceBusDomain, Out.WriteLine); + azure = new AzureClient(resourceId, serviceBusDomain, region, metricsDomain, Out.WriteLine); RunInfo.Add("AzureServiceBusNamespace", azure.FullyQualifiedNamespace); } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 6ab03e50..4e1633fd 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,17 +1,15 @@ - true true - - + - + @@ -29,9 +27,7 @@ - - - + \ No newline at end of file diff --git a/src/Query/AzureServiceBus/AzureClient.cs b/src/Query/AzureServiceBus/AzureClient.cs index 1f9fce4e..501d9a8b 100644 --- a/src/Query/AzureServiceBus/AzureClient.cs +++ b/src/Query/AzureServiceBus/AzureClient.cs @@ -8,12 +8,12 @@ using Azure.Core; using Azure.Identity; using Azure.Messaging.ServiceBus.Administration; - using Azure.Monitor.Query; - using Azure.Monitor.Query.Models; + using Azure.Monitor.Query.Metrics; + using Azure.Monitor.Query.Metrics.Models; public class AzureClient { - readonly string resourceId; + readonly ResourceIdentifier resourceId; readonly AuthenticatedClientSet[] connections; readonly List loginExceptions = []; readonly Action log; @@ -22,26 +22,15 @@ public class AzureClient AuthenticatedClientSet currentClients; public string FullyQualifiedNamespace { get; } - public string SubscriptionId { get; } - public string ResourceGroup { get; } - - public AzureClient(string resourceId, string serviceBusDomain, Action log = null) + public AzureClient(string resourceId, string serviceBusDomain, string region, string metricsDomain, Action log = null) { - this.resourceId = resourceId; + this.resourceId = ResourceIdentifier.Parse(resourceId); this.log = log ?? new(msg => { }); - var resourceIdentifier = ResourceIdentifier.Parse(resourceId); - - ResourceGroup = resourceIdentifier.ResourceGroupName; - - SubscriptionId = resourceIdentifier.SubscriptionId; - - FullyQualifiedNamespace = $"{resourceIdentifier.Name}.{serviceBusDomain}"; + FullyQualifiedNamespace = $"{this.resourceId.Name}.{serviceBusDomain}"; - connections = CreateCredentials() - .Select(c => new AuthenticatedClientSet(c, FullyQualifiedNamespace)) - .ToArray(); + connections = [.. CreateCredentials().Select(c => new AuthenticatedClientSet(c, FullyQualifiedNamespace, region, metricsDomain))]; ResetConnectionQueue(); } @@ -51,7 +40,6 @@ IEnumerable CreateCredentials() yield return new AzureCliCredential(); yield return new AzurePowerShellCredential(); yield return new EnvironmentCredential(); - yield return new SharedTokenCacheCredential(); yield return new VisualStudioCredential(); // Don't really need this one to take 100s * 4 tries to finally time out @@ -64,10 +52,7 @@ IEnumerable CreateCredentials() /// /// Doesn't change the last successful `current` method but restores all options as possibilities if it doesn't work /// - public void ResetConnectionQueue() - { - connectionQueue = new Queue(connections); - } + public void ResetConnectionQueue() => connectionQueue = new Queue(connections); async Task GetDataWithCurrentCredentials(GetDataDelegate getData, CancellationToken cancellationToken) { @@ -116,25 +101,26 @@ bool NextCredentials() } } - public Task> GetMetrics(string queueName, DateOnly startTime, DateOnly endTime, CancellationToken cancellationToken = default) - { - return GetDataWithCurrentCredentials(async token => + public Task> GetMetrics(string queueName, DateOnly startTime, DateOnly endTime, CancellationToken cancellationToken = default) => + GetDataWithCurrentCredentials(async token => { try { - var response = await currentClients.Metrics.QueryResourceAsync(resourceId, + var response = await currentClients.Metrics.QueryResourcesAsync( + [resourceId], ["CompleteMessage"], - new MetricsQueryOptions + "Microsoft.ServiceBus/Namespaces", + new MetricsQueryResourcesOptions { Filter = $"EntityName eq '{queueName}'", - TimeRange = new QueryTimeRange(startTime.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc), endTime.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc)), + StartTime = startTime.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc), + EndTime = endTime.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc), Granularity = TimeSpan.FromDays(1) }, token).ConfigureAwait(false); // Yeah, it's buried deep - var metricValues = response.Value.Metrics.FirstOrDefault()?.TimeSeries.FirstOrDefault()?.Values; - return metricValues; + return response.Value.Values.FirstOrDefault()?.Metrics.FirstOrDefault()?.TimeSeries.FirstOrDefault()?.Values ?? []; } catch (Azure.RequestFailedException reqFailed) when (reqFailed.Message.Contains("ResourceGroupNotFound")) { @@ -142,7 +128,6 @@ public Task> GetMetrics(string queueName, DateOnly st throw new QueryException(QueryFailureReason.InvalidEnvironment, reqFailed.Message); } }, cancellationToken); - } public Task GetQueueNames(CancellationToken cancellationToken = default) { @@ -162,31 +147,24 @@ public Task GetQueueNames(CancellationToken cancellationToken = defaul }, cancellationToken); } - static bool IsAuthenticationException(Exception x) - { - return x is CredentialUnavailableException or AuthenticationFailedException or UnauthorizedAccessException; - } + static bool IsAuthenticationException(Exception x) => + x is CredentialUnavailableException or AuthenticationFailedException or UnauthorizedAccessException; delegate Task GetDataDelegate(CancellationToken cancellationToken); class AuthenticatedClientSet { public string Name { get; } - public MetricsQueryClient Metrics { get; } + public MetricsClient Metrics { get; } public ServiceBusAdministrationClient ServiceBus { get; } - public AuthenticatedClientSet(TokenCredential credentials, string fullyQualifiedNamespace) + public AuthenticatedClientSet(TokenCredential credentials, string fullyQualifiedNamespace, string region, string metricsDomain) { - var managementUrl = "https://management.azure.com"; - - if (!fullyQualifiedNamespace.EndsWith("servicebus.windows.net", StringComparison.OrdinalIgnoreCase)) - { - var reversedParts = fullyQualifiedNamespace.Split('.', StringSplitOptions.RemoveEmptyEntries).Reverse().ToArray(); - managementUrl = $"https://management.{reversedParts[1]}.{reversedParts[0]}"; - } + var metricsUrl = $"https://{region}.{metricsDomain}"; + var audience = $"https://{metricsDomain}"; Name = credentials.GetType().Name; - Metrics = new MetricsQueryClient(new Uri(managementUrl), credentials, new MetricsQueryClientOptions { Audience = new MetricsQueryAudience(managementUrl) }); + Metrics = new MetricsClient(new Uri(metricsUrl), credentials, new MetricsClientOptions { Audience = new MetricsClientAudience(audience) }); ServiceBus = new ServiceBusAdministrationClient(fullyQualifiedNamespace, credentials); } } diff --git a/src/Query/Particular.ThroughputQuery.csproj b/src/Query/Particular.ThroughputQuery.csproj index ce412316..1a638aa1 100644 --- a/src/Query/Particular.ThroughputQuery.csproj +++ b/src/Query/Particular.ThroughputQuery.csproj @@ -10,7 +10,7 @@ - +