Skip to content

Commit b79f2ae

Browse files
kyurkchyanclaude
andcommitted
Add monitor-queues command for live queue statistics
Introduces a new command that displays Service Bus queue statistics in a live-updating console table using Rx.NET and Spectre.Console. Features: - Real-time polling with configurable refresh interval (min 1 second) - Queue name filtering with wildcards (*, ?) or contains matching - Color-coded counts: red for DLQ > 0, yellow for active > 1000 - Only updates display when counts change (DistinctUntilChanged) - Graceful Ctrl+C shutdown Also adds CLAUDE.md for project context and updates documentation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 655342e commit b79f2ae

10 files changed

Lines changed: 455 additions & 7 deletions

File tree

CLAUDE.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build & Run
6+
7+
```bash
8+
dotnet build
9+
dotnet run -- <command> [options]
10+
dotnet run -- --help
11+
```
12+
13+
## Architecture
14+
15+
.NET 10 CLI tool for Azure Service Bus operations using CommandLineParser for argument parsing and DefaultAzureCredential for authentication.
16+
17+
### Project Structure
18+
19+
```
20+
ServiceBusToolset/
21+
├── Options/ # CLI options with [Verb] and [Option] attributes
22+
├── Commands/ # Command implementations
23+
├── Services/ # Business logic and external integrations
24+
└── Models/ # Data models
25+
```
26+
27+
### Adding a New Command
28+
29+
1. **Create Options class** in `Options/`:
30+
- Use `[Verb("command-name", HelpText = "...")]`
31+
- Use `[Option('c', "long-name", Required = true/false, HelpText = "...")]`
32+
- Add `Validate()` method returning `string?` (null = valid)
33+
34+
2. **Create Command class** in `Commands/`:
35+
- Implement `ICommand<TOptions>` interface
36+
- Inherit `BaseCommand<TOptions>` for DLQ operations (provides `CreateDlqReceiver`, `GetEntityDescription`)
37+
- Use primary constructor pattern
38+
- Handle exceptions: `AuthenticationFailedException`, `ServiceBusException`, `OperationCanceledException`
39+
40+
3. **Register in Program.cs**:
41+
- Add options type to `ParseArguments<...>()`
42+
- Add `MapResult` handler that creates command and calls `ExecuteAsync`
43+
44+
### Key Services
45+
46+
- `IServiceBusClientFactory` - Creates `ServiceBusClient` and `ServiceBusAdministrationClient`
47+
- `IConsoleOutput` - Console output abstraction (Info, Success, Warning, Error, Verbose, Table)
48+
- `IDlqCategoryAnalyzer` - Groups DLQ messages by Label and DeadLetterReason
49+
- `IAppInsightsService` - Queries Application Insights for diagnostics
50+
51+
### Dependencies
52+
53+
- `CommandLineParser` - CLI argument parsing with verbs
54+
- `Spectre.Console` - Rich console output and tables
55+
- `System.Reactive` - Rx.NET for reactive streams (monitor-queues)
56+
- `Azure.Identity` - DefaultAzureCredential (requires `az login` for local dev)

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ dotnet build
1515

1616
## Commands
1717

18-
| Command | Description |
19-
|--------------------------------------|-------------------------------------------------------------------|
20-
| [purge-dlq](docs/purge-dlq.md) | Purge messages from a dead letter queue |
21-
| [resubmit-dlq](docs/resubmit-dlq.md) | Resubmit messages from a dead letter queue back to the main queue |
22-
| [dump-dlq](docs/dump-dlq.md) | Export DLQ messages to a JSON file |
23-
| [diagnose-dlq](docs/diagnose-dlq.md) | Diagnose DLQ messages using Application Insights telemetry |
18+
| Command | Description |
19+
|------------------------------------------|-------------------------------------------------------------------|
20+
| [purge-dlq](docs/purge-dlq.md) | Purge messages from a dead letter queue |
21+
| [resubmit-dlq](docs/resubmit-dlq.md) | Resubmit messages from a dead letter queue back to the main queue |
22+
| [dump-dlq](docs/dump-dlq.md) | Export DLQ messages to a JSON file |
23+
| [diagnose-dlq](docs/diagnose-dlq.md) | Diagnose DLQ messages using Application Insights telemetry |
24+
| [monitor-queues](docs/monitor-queues.md) | Monitor queue statistics in a live-updating console table |
2425

2526
## Quick Start
2627

@@ -46,6 +47,12 @@ dotnet run -- dump-dlq -n mynamespace.servicebus.windows.net -q myqueue -o dlq-m
4647
# Diagnose DLQ messages using Application Insights
4748
dotnet run -- diagnose-dlq -n mynamespace.servicebus.windows.net -q myqueue \
4849
-a "/subscriptions/.../resourceGroups/.../providers/microsoft.insights/components/my-app-insights"
50+
51+
# Monitor all queues with live-updating table
52+
dotnet run -- monitor-queues -n mynamespace.servicebus.windows.net
53+
54+
# Monitor queues matching a pattern with 10-second refresh
55+
dotnet run -- monitor-queues -n mynamespace.servicebus.windows.net -f "order-*" -r 10
4956
```
5057

5158
## Authentication
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System.Reactive.Linq;
2+
using Azure.Identity;
3+
using Azure.Messaging.ServiceBus;
4+
using ServiceBusToolset.Models;
5+
using ServiceBusToolset.Options;
6+
using ServiceBusToolset.Services;
7+
using Spectre.Console;
8+
9+
namespace ServiceBusToolset.Commands;
10+
11+
public class MonitorQueuesCommand(IQueueMonitorService monitorService, IConsoleOutput output) : ICommand<MonitorQueuesOptions>
12+
{
13+
public async Task<int> ExecuteAsync(MonitorQueuesOptions options, CancellationToken cancellationToken = default)
14+
{
15+
var validationError = options.Validate();
16+
if (validationError != null)
17+
{
18+
output.Error(validationError);
19+
return 1;
20+
}
21+
22+
try
23+
{
24+
output.Info($"Connecting to Service Bus namespace: {options.Namespace}");
25+
if (!string.IsNullOrEmpty(options.Filter))
26+
{
27+
output.Info($"Filter: {options.Filter}");
28+
}
29+
30+
output.Info($"Refresh interval: {options.RefreshInterval} seconds");
31+
output.Info("Press Ctrl+C to stop monitoring.");
32+
output.Info("");
33+
34+
var refreshInterval = TimeSpan.FromSeconds(options.RefreshInterval);
35+
36+
await monitorService
37+
.ObserveQueues(options.Namespace,
38+
options.Filter,
39+
refreshInterval,
40+
cancellationToken)
41+
.ForEachAsync(stats =>
42+
{
43+
Console.Clear();
44+
var table = CreateTable(stats);
45+
AnsiConsole.Write(table);
46+
47+
if (options.Verbose)
48+
{
49+
output.Verbose($"Updated at {DateTimeOffset.Now:HH:mm:ss} - {stats.Count} queues", options.Verbose);
50+
}
51+
},
52+
cancellationToken);
53+
54+
return 0;
55+
}
56+
catch (AuthenticationFailedException ex)
57+
{
58+
output.Error($"Authentication failed: {ex.Message}");
59+
output.Error("Ensure you are logged in with 'az login' or have valid environment credentials.");
60+
return 1;
61+
}
62+
catch (ServiceBusException ex)
63+
{
64+
output.Error($"Service Bus error: {ex.Message}");
65+
output.Verbose($"Reason: {ex.Reason}", options.Verbose);
66+
return 1;
67+
}
68+
catch (OperationCanceledException)
69+
{
70+
output.Info("");
71+
output.Info("Monitoring stopped.");
72+
return 0;
73+
}
74+
}
75+
76+
private static Table CreateTable(IReadOnlyList<QueueStatistics> statistics)
77+
{
78+
var table = new Table()
79+
.Border(TableBorder.Rounded)
80+
.Title("[bold blue]Service Bus Queue Monitor[/]")
81+
.AddColumn(new TableColumn("[bold]Queue Name[/]").LeftAligned())
82+
.AddColumn(new TableColumn("[bold]Active[/]").RightAligned())
83+
.AddColumn(new TableColumn("[bold]DLQ[/]").RightAligned())
84+
.AddColumn(new TableColumn("[bold]Scheduled[/]").RightAligned());
85+
86+
long totalActive = 0;
87+
long totalDlq = 0;
88+
long totalScheduled = 0;
89+
90+
foreach (var stat in statistics)
91+
{
92+
var activeStyle = stat.ActiveMessageCount > 1000 ? "[yellow]" : "[white]";
93+
var dlqStyle = stat.DeadLetterMessageCount > 0 ? "[red]" : "[white]";
94+
95+
table.AddRow(stat.Name,
96+
$"{activeStyle}{stat.ActiveMessageCount:N0}[/]",
97+
$"{dlqStyle}{stat.DeadLetterMessageCount:N0}[/]",
98+
$"[white]{stat.ScheduledMessageCount:N0}[/]");
99+
100+
totalActive += stat.ActiveMessageCount;
101+
totalDlq += stat.DeadLetterMessageCount;
102+
totalScheduled += stat.ScheduledMessageCount;
103+
}
104+
105+
if (statistics.Count > 0)
106+
{
107+
table.AddEmptyRow();
108+
109+
var totalActiveStyle = totalActive > 1000 ? "[bold yellow]" : "[bold white]";
110+
var totalDlqStyle = totalDlq > 0 ? "[bold red]" : "[bold white]";
111+
112+
table.AddRow("[bold]TOTAL[/]",
113+
$"{totalActiveStyle}{totalActive:N0}[/]",
114+
$"{totalDlqStyle}{totalDlq:N0}[/]",
115+
$"[bold white]{totalScheduled:N0}[/]");
116+
}
117+
118+
var timestamp = statistics.Count > 0 ? statistics[0].UpdatedAt : DateTimeOffset.Now;
119+
table.Caption($"Last updated: {timestamp:HH:mm:ss}");
120+
121+
return table;
122+
}
123+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace ServiceBusToolset.Models;
2+
3+
public record QueueStatistics(string Name,
4+
long ActiveMessageCount,
5+
long DeadLetterMessageCount,
6+
long ScheduledMessageCount,
7+
DateTimeOffset UpdatedAt)
8+
{
9+
public bool HasSameCountsAs(QueueStatistics other) =>
10+
Name == other.Name &&
11+
ActiveMessageCount == other.ActiveMessageCount &&
12+
DeadLetterMessageCount == other.DeadLetterMessageCount &&
13+
ScheduledMessageCount == other.ScheduledMessageCount;
14+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using CommandLine;
2+
3+
namespace ServiceBusToolset.Options;
4+
5+
[Verb("monitor-queues", HelpText = "Monitor Service Bus queue statistics in a live-updating console table")]
6+
public class MonitorQueuesOptions
7+
{
8+
[Option('n',
9+
"namespace",
10+
Required = true,
11+
HelpText = "Fully qualified Service Bus namespace (e.g., mynamespace.servicebus.windows.net)")]
12+
public required string Namespace { get; set; }
13+
14+
[Option('f',
15+
"filter",
16+
HelpText = "Queue name filter (wildcards * and ? supported, or contains match)")]
17+
public string? Filter { get; set; }
18+
19+
[Option('r',
20+
"refresh-interval",
21+
Default = 5,
22+
HelpText = "Refresh interval in seconds (minimum: 1)")]
23+
public int RefreshInterval { get; set; }
24+
25+
[Option('v',
26+
"verbose",
27+
Default = false,
28+
HelpText = "Enable verbose output")]
29+
public bool Verbose { get; set; }
30+
31+
public string? Validate() => RefreshInterval < 1 ? "Refresh interval must be at least 1 second." : null;
32+
}

ServiceBusToolset/Program.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
var output = new ConsoleOutput();
88
var categoryAnalyzer = new DlqCategoryAnalyzer();
99
var appInsightsService = new AppInsightsService();
10+
var queueMonitorService = new QueueMonitorService(clientFactory);
1011

1112
using var cts = new CancellationTokenSource();
1213
Console.CancelKeyPress += (_, e) =>
@@ -15,7 +16,7 @@
1516
cts.Cancel();
1617
};
1718

18-
return await Parser.Default.ParseArguments<PurgeDlqOptions, ResubmitDlqOptions, DumpDlqOptions, DiagnoseDlqOptions>(args)
19+
return await Parser.Default.ParseArguments<PurgeDlqOptions, ResubmitDlqOptions, DumpDlqOptions, DiagnoseDlqOptions, MonitorQueuesOptions>(args)
1920
.MapResult((PurgeDlqOptions opts) =>
2021
{
2122
var command = new PurgeDlqCommand(clientFactory, output, categoryAnalyzer);
@@ -39,6 +40,11 @@
3940
appInsightsService);
4041
return command.ExecuteAsync(opts, cts.Token);
4142
},
43+
(MonitorQueuesOptions opts) =>
44+
{
45+
var command = new MonitorQueuesCommand(queueMonitorService, output);
46+
return command.ExecuteAsync(opts, cts.Token);
47+
},
4248
errors => Task.FromResult(HandleParseErrors(errors)));
4349

4450
static int HandleParseErrors(IEnumerable<Error> errors)

ServiceBusToolset/ServiceBusToolset.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<PackageReference Include="Azure.Monitor.Query" Version="1.5.0"/>
1414
<PackageReference Include="CommandLineParser" Version="2.9.1"/>
1515
<PackageReference Include="Spectre.Console" Version="0.54.0"/>
16+
<PackageReference Include="System.Reactive" Version="6.0.1"/>
1617
</ItemGroup>
1718

1819
</Project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using ServiceBusToolset.Models;
2+
3+
namespace ServiceBusToolset.Services;
4+
5+
public interface IQueueMonitorService
6+
{
7+
IObservable<IReadOnlyList<QueueStatistics>> ObserveQueues(
8+
string fullyQualifiedNamespace,
9+
string? queueFilter,
10+
TimeSpan refreshInterval,
11+
CancellationToken cancellationToken);
12+
}

0 commit comments

Comments
 (0)