From dfa6f14bd0ddfc43fb778ef430670081e5e3d423 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Mon, 15 Jun 2026 17:22:23 +0800 Subject: [PATCH 1/4] Switch to Azure.ResourceManager.Monitor package --- src/Directory.Packages.props | 2 +- .../AzureQuery.cs | 107 ++++-------------- .../ServiceControl.Transports.ASBS.csproj | 2 +- 3 files changed, 26 insertions(+), 85 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 3801670993..4eb280acbd 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -7,7 +7,7 @@ - + diff --git a/src/ServiceControl.Transports.ASBS/AzureQuery.cs b/src/ServiceControl.Transports.ASBS/AzureQuery.cs index ef2ac43465..8eeb42ad96 100644 --- a/src/ServiceControl.Transports.ASBS/AzureQuery.cs +++ b/src/ServiceControl.Transports.ASBS/AzureQuery.cs @@ -12,9 +12,9 @@ namespace ServiceControl.Transports.ASBS; using Azure.Core; using Azure.Core.Pipeline; using Azure.Identity; -using Azure.Monitor.Query.Metrics; -using Azure.Monitor.Query.Metrics.Models; using Azure.ResourceManager; +using Azure.ResourceManager.Monitor; +using Azure.ResourceManager.Monitor.Models; using Azure.ResourceManager.Resources; using Azure.ResourceManager.ServiceBus; using BrokerThroughput; @@ -34,8 +34,6 @@ public class AzureQuery(ILogger logger, TimeProvider timeProvider, T TokenCredential? credential; ResourceIdentifier? resourceId; ArmEnvironment armEnvironment; - MetricsClientAudience metricsClientAudience; - MetricsClient? metricsClient; protected override void InitializeCore(ReadOnlyDictionary settings) { @@ -108,7 +106,7 @@ protected override void InitializeCore(ReadOnlyDictionary settin Diagnostics.AppendLine("Client secret set"); } - (armEnvironment, metricsClientAudience) = GetEnvironment(); + armEnvironment = GetEnvironment(); if (managementUrl == null) { @@ -147,26 +145,26 @@ protected override void InitializeCore(ReadOnlyDictionary settin return; - (ArmEnvironment armEnvironment, MetricsClientAudience metricsClientAudience) GetEnvironment() + ArmEnvironment GetEnvironment() { if (managementUrlParsed == null || managementUrlParsed == ArmEnvironment.AzurePublicCloud.Endpoint) { - return (ArmEnvironment.AzurePublicCloud, MetricsClientAudience.AzurePublicCloud); + return ArmEnvironment.AzurePublicCloud; } if (managementUrlParsed == ArmEnvironment.AzureChina.Endpoint) { - return (ArmEnvironment.AzureChina, MetricsClientAudience.AzureChina); + return ArmEnvironment.AzureChina; } if (managementUrlParsed == ArmEnvironment.AzureGermany.Endpoint) { - return (ArmEnvironment.AzureGermany, MetricsClientAudience.AzurePublicCloud); + return ArmEnvironment.AzureGermany; } if (managementUrlParsed == ArmEnvironment.AzureGovernment.Endpoint) { - return (ArmEnvironment.AzureGovernment, MetricsClientAudience.AzureGovernment); + return ArmEnvironment.AzureGovernment; } string options = string.Join(", ", @@ -176,7 +174,7 @@ protected override void InitializeCore(ReadOnlyDictionary settin }.Select(environment => $"\"{environment.Endpoint}\"")); InitialiseErrors.Add($"Management url configuration is invalid, available options are {options}"); - return (ArmEnvironment.AzurePublicCloud, MetricsClientAudience.AzurePublicCloud); + return ArmEnvironment.AzurePublicCloud; } } @@ -241,85 +239,28 @@ public override async IAsyncEnumerable GetThroughputPerDay(IBro } } - async Task InitializeMetricsClient(CancellationToken cancellationToken = default) - { - if (resourceId is null || armClient is null || credential is null) - { - throw new InvalidOperationException("AzureQuery has not been initialized correctly."); - } - - var serviceBusNamespaceResource = await armClient - .GetServiceBusNamespaceResource(resourceId).GetAsync(cancellationToken) - ?? throw new Exception($"Could not find an Azure Service Bus namespace with resource Id: \"{resourceId}\""); - - // Determine the region of the namespace - var regionName = serviceBusNamespaceResource.Value.Data.Location.Name; - - // Build the regional Azure Monitor Metrics endpoint from the audience - var metricsEndpoint = BuildMetricsEndpoint(metricsClientAudience, regionName); - - // CreateNewOnMetadataUpdateAttribute the MetricsClient for this namespace - return new MetricsClient( - metricsEndpoint, - credential!, - new MetricsClientOptions - { - Audience = metricsClientAudience, - Transport = new HttpClientTransport( - new HttpClient(new SocketsHttpHandler - { - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2) - })) - }); - } - - static Uri BuildMetricsEndpoint(MetricsClientAudience audience, string regionName) - { - var region = regionName.ToLowerInvariant(); - var builder = new UriBuilder(audience.ToString()); - builder.Host = $"{region}.{builder.Host}"; - return builder.Uri; - } - - async Task> GetMetrics(string queueName, DateOnly startTime, DateOnly endTime, + async Task> GetMetrics(string queueName, DateOnly startTime, DateOnly endTime, CancellationToken cancellationToken = default) { - metricsClient ??= await InitializeMetricsClient(cancellationToken); - - var response = await metricsClient.QueryResourcesAsync( - [resourceId!], - [CompleteMessageMetricName], - MicrosoftServicebusNamespacesMetricsNamespace, - new MetricsQueryResourcesOptions - { - Filter = $"EntityName eq '{queueName}'", - TimeRange = new MetricsQueryTimeRange(startTime.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc), endTime.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc)), - Granularity = TimeSpan.FromDays(1) - }, - cancellationToken); - - var metricQueryResult = response.Value.Values.SingleOrDefault(mr => mr.Namespace == MicrosoftServicebusNamespacesMetricsNamespace); - - if (metricQueryResult is null) + var options = new ArmResourceGetMonitorMetricsOptions() { - throw new Exception($"No metrics query results returned for {MicrosoftServicebusNamespacesMetricsNamespace}"); - } + Metricnames = CompleteMessageMetricName, + Timespan = $"{startTime:o}/{endTime:o}", + Filter = $"EntityName eq '{queueName}'", + Interval = TimeSpan.FromDays(1), + Metricnamespace = MicrosoftServicebusNamespacesMetricsNamespace + }; - var metricResult = metricQueryResult.GetMetricByName(CompleteMessageMetricName); - - if (metricResult.Error.Message is not null) + await foreach (var metric in armClient.GetMonitorMetricsAsync(resourceId, options, cancellationToken)) { - throw new Exception($"Metrics query result for '{metricResult.Name}' failed: {metricResult.Error.Message}"); - } - - var timeSeries = metricResult.TimeSeries.SingleOrDefault(); - - if (timeSeries is null) - { - throw new Exception($"Metrics query result for '{metricResult.Name}' contained no time series"); + foreach (var timeSeries in metric.Timeseries) + { + return timeSeries.Data; + } } - return timeSeries.Values.AsReadOnly(); + // TODO: Better error handling + throw new Exception("No data returned from metrics query"); } public override async IAsyncEnumerable GetQueueNames( diff --git a/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj b/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj index a0404ac4a0..40b6d86250 100644 --- a/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj +++ b/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj @@ -12,8 +12,8 @@ - + From 08afe4158de63f928a158300be976ba4a32b69f7 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Tue, 16 Jun 2026 10:59:38 +0800 Subject: [PATCH 2/4] Query should range from start of day to end of day --- src/ServiceControl.Transports.ASBS/AzureQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl.Transports.ASBS/AzureQuery.cs b/src/ServiceControl.Transports.ASBS/AzureQuery.cs index 8eeb42ad96..d6eac95db2 100644 --- a/src/ServiceControl.Transports.ASBS/AzureQuery.cs +++ b/src/ServiceControl.Transports.ASBS/AzureQuery.cs @@ -245,7 +245,7 @@ async Task> GetMetrics(string queueName, DateO var options = new ArmResourceGetMonitorMetricsOptions() { Metricnames = CompleteMessageMetricName, - Timespan = $"{startTime:o}/{endTime:o}", + Timespan = $"{startTime.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc):o}/{endTime.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc):o}", Filter = $"EntityName eq '{queueName}'", Interval = TimeSpan.FromDays(1), Metricnamespace = MicrosoftServicebusNamespacesMetricsNamespace From 3db90dfcc7490df4b499b56991b38301f21c4199 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Tue, 16 Jun 2026 10:59:50 +0800 Subject: [PATCH 3/4] Better error handling --- .../AzureQuery.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/ServiceControl.Transports.ASBS/AzureQuery.cs b/src/ServiceControl.Transports.ASBS/AzureQuery.cs index d6eac95db2..b33d3054f8 100644 --- a/src/ServiceControl.Transports.ASBS/AzureQuery.cs +++ b/src/ServiceControl.Transports.ASBS/AzureQuery.cs @@ -251,16 +251,15 @@ async Task> GetMetrics(string queueName, DateO Metricnamespace = MicrosoftServicebusNamespacesMetricsNamespace }; - await foreach (var metric in armClient.GetMonitorMetricsAsync(resourceId, options, cancellationToken)) - { - foreach (var timeSeries in metric.Timeseries) - { - return timeSeries.Data; - } - } + var response = armClient.GetMonitorMetricsAsync(resourceId, options, cancellationToken); + + var metric = await response.SingleOrDefaultAsync(m => m.Name.Value == CompleteMessageMetricName, cancellationToken) + ?? throw new Exception($"Metric {CompleteMessageMetricName} not found for {queueName}"); + + var timeSeries = metric.Timeseries.SingleOrDefault() + ?? throw new Exception($"Metric {metric.Name.Value} for {queueName} contains no time series"); - // TODO: Better error handling - throw new Exception("No data returned from metrics query"); + return timeSeries.Data; } public override async IAsyncEnumerable GetQueueNames( From fd517c21892d297a742dbd2be2f1fe3c024e2b8f Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Tue, 16 Jun 2026 11:05:10 +0800 Subject: [PATCH 4/4] Package References in alphabetical order --- .../ServiceControl.Transports.ASBS.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj b/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj index 40b6d86250..bd61cf9af1 100644 --- a/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj +++ b/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj @@ -12,8 +12,8 @@ - +