Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 21 additions & 3 deletions src/AppCommon/Commands/AzureServiceBusCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,34 @@ public static Command CreateCommand()
IsRequired = false
};

var regionArg = new Option<string>(
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<string>("--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
Expand All @@ -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);
});

Expand All @@ -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);
}

Expand Down
10 changes: 3 additions & 7 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
<Project>

<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="AWSSDK.CloudWatch" Version="4.0.4.6" />
<PackageVersion Include="AWSSDK.SecurityToken" Version="4.0.2.9" />
<PackageVersion Include="AWSSDK.SQS" Version="4.0.1.11" />
<PackageVersion Include="Azure.Identity" Version="1.14.0" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
<PackageVersion Include="Azure.Monitor.Query" Version="1.6.0" />
<PackageVersion Include="Azure.Monitor.Query.Metrics" Version="1.0.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="ICSharpCode.Decompiler" Version="8.2.0.7535" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.3" />
Expand All @@ -29,9 +27,7 @@
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.Threading.RateLimiting" Version="8.0.0" />
</ItemGroup>

<ItemGroup Label="Pinned transitive dependencies">
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
</ItemGroup>

</Project>
</Project>
70 changes: 24 additions & 46 deletions src/Query/AzureServiceBus/AzureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> loginExceptions = [];
readonly Action<string> log;
Expand All @@ -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<string> log = null)
public AzureClient(string resourceId, string serviceBusDomain, string region, string metricsDomain, Action<string> 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();
}
Expand All @@ -51,7 +40,6 @@ IEnumerable<TokenCredential> 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
Expand All @@ -64,10 +52,7 @@ IEnumerable<TokenCredential> CreateCredentials()
/// <summary>
/// Doesn't change the last successful `current` method but restores all options as possibilities if it doesn't work
/// </summary>
public void ResetConnectionQueue()
{
connectionQueue = new Queue<AuthenticatedClientSet>(connections);
}
public void ResetConnectionQueue() => connectionQueue = new Queue<AuthenticatedClientSet>(connections);

async Task<T> GetDataWithCurrentCredentials<T>(GetDataDelegate<T> getData, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -116,33 +101,33 @@ bool NextCredentials()
}
}

public Task<IReadOnlyList<MetricValue>> GetMetrics(string queueName, DateOnly startTime, DateOnly endTime, CancellationToken cancellationToken = default)
{
return GetDataWithCurrentCredentials(async token =>
public Task<IList<MetricValue>> 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"))
{
// Azure exception message has a lot of information including exact resource group name
throw new QueryException(QueryFailureReason.InvalidEnvironment, reqFailed.Message);
}
}, cancellationToken);
}

public Task<string[]> GetQueueNames(CancellationToken cancellationToken = default)
{
Expand All @@ -162,31 +147,24 @@ public Task<string[]> 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<T> GetDataDelegate<T>(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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Query/Particular.ThroughputQuery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PackageReference Include="AWSSDK.SQS" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Messaging.ServiceBus" />
<PackageReference Include="Azure.Monitor.Query" />
<PackageReference Include="Azure.Monitor.Query.Metrics" />
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="Npgsql" />
<PackageReference Include="System.Threading.RateLimiting" />
Expand Down