Skip to content

Commit 017c895

Browse files
kyurkchyanclaude
andcommitted
Improve diagnose-dlq with batch queries and better output
- Use batch queries to App Insights (100 operation IDs per request) instead of querying one message at a time - Group diagnostic output by message Subject for better visibility - Group exceptions by type only, showing sample message for each - Add fallback to outerMessage when innermostMessage is empty - Increase default max-messages from 100 to 1000 - Enable text wrapping in console tables for full message display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4c237ab commit 017c895

5 files changed

Lines changed: 241 additions & 138 deletions

File tree

ServiceBusToolset/Commands/DiagnoseDlqCommand.cs

Lines changed: 125 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,13 @@ private async Task<List<DiagnosticResult>> DiagnoseMessagesAsync(
229229
DiagnoseDlqOptions options,
230230
CancellationToken cancellationToken)
231231
{
232-
var results = new List<DiagnosticResult>();
233-
var diagnosed = 0;
232+
// Extract operation IDs and build mapping to messages
233+
var messagesByOperationId = new Dictionary<string, ServiceBusReceivedMessage>();
234+
var operations = new List<(string OperationId, DateTimeOffset EnqueuedTime)>();
234235
var skipped = 0;
235236

236237
foreach (var message in messages)
237238
{
238-
cancellationToken.ThrowIfCancellationRequested();
239-
240239
var operationId = ExtractOperationId(message);
241240
if (string.IsNullOrEmpty(operationId))
242241
{
@@ -245,33 +244,43 @@ private async Task<List<DiagnosticResult>> DiagnoseMessagesAsync(
245244
continue;
246245
}
247246

248-
try
247+
// Handle duplicate operation IDs by keeping the first one
248+
if (!messagesByOperationId.ContainsKey(operationId))
249249
{
250-
var result = await appInsightsService.DiagnoseMessageAsync(operationId,
251-
message.EnqueuedTime,
252-
cancellationToken);
250+
messagesByOperationId[operationId] = message;
251+
operations.Add((operationId, message.EnqueuedTime));
252+
}
253+
}
254+
255+
if (operations.Count == 0)
256+
{
257+
Output.Warning($"No messages with valid operation IDs found (skipped {skipped})");
258+
return [];
259+
}
260+
261+
Output.Info($"Querying Application Insights for {operations.Count} messages (skipped {skipped} without operation ID)...");
262+
263+
// Batch query Application Insights
264+
var diagnosticResults = await appInsightsService.DiagnoseBatchAsync(operations,
265+
(current, total) => Output.Progress($"Querying App Insights batch {current}/{total}..."),
266+
cancellationToken);
267+
Console.WriteLine();
253268

254-
// Enrich with message info
269+
// Enrich results with message info
270+
var results = new List<DiagnosticResult>();
271+
foreach (var (operationId, result) in diagnosticResults)
272+
{
273+
if (messagesByOperationId.TryGetValue(operationId, out var message))
274+
{
255275
result.MessageId = message.MessageId;
256276
result.Subject = message.Subject;
257277
result.DeadLetterReason = message.DeadLetterReason;
258278
result.Body = TryDecodeBody(message);
259-
260279
results.Add(result);
261-
diagnosed++;
262-
263-
Output.Progress($"Diagnosed {diagnosed}/{messages.Count} messages (skipped {skipped})...");
264-
}
265-
catch (Exception ex)
266-
{
267-
Output.Verbose($"Error diagnosing message {message.MessageId}: {ex.Message}", options.Verbose);
268-
skipped++;
269280
}
270281
}
271282

272-
Console.WriteLine();
273-
Output.Info($"Diagnosed {diagnosed} messages, skipped {skipped} (no operation ID or query error)");
274-
283+
Output.Info($"Diagnosed {results.Count} messages");
275284
return results;
276285
}
277286

@@ -338,7 +347,7 @@ private async Task OutputResultsAsync(
338347

339348
Output.Success($"Found telemetry for {resultsWithTelemetry.Count} of {results.Count} messages");
340349

341-
// Print summary to console
350+
// Print summary to console - grouped by Subject
342351
PrintDiagnosticSummary(resultsWithTelemetry);
343352

344353
// Write to file if specified
@@ -353,65 +362,99 @@ private async Task OutputResultsAsync(
353362
private void PrintDiagnosticSummary(List<DiagnosticResult> results)
354363
{
355364
Output.Info("");
356-
Output.Info("Diagnostic Summary:");
357-
Output.Info("===================");
358-
359-
// Group exceptions by type/message
360-
var exceptionGroups = results
361-
.SelectMany(r => r.Exceptions)
362-
.GroupBy(e => new
363-
{
364-
e.ExceptionType,
365-
e.InnermostMessage
366-
})
367-
.OrderByDescending(g => g.Count())
368-
.Take(10);
369-
370-
if (exceptionGroups.Any())
365+
Output.Info("Diagnostic Summary by Message Type:");
366+
Output.Info("====================================");
367+
368+
// Group by Subject (message type)
369+
var groupedBySubject = results
370+
.GroupBy(r => r.Subject ?? "(none)")
371+
.OrderByDescending(g => g.Count());
372+
373+
foreach (var subjectGroup in groupedBySubject)
371374
{
375+
var messageCount = subjectGroup.Count();
376+
var totalExceptions = subjectGroup.Sum(r => r.Exceptions.Count);
377+
372378
Output.Info("");
373-
Output.Info("Top Exceptions:");
374-
var headers = new[]
379+
Output.Info($"[{subjectGroup.Key}] - {messageCount} messages, {totalExceptions} exceptions");
380+
Output.Info(new string('-', 60));
381+
382+
// Get exceptions for this subject, grouped by type only
383+
var exceptionGroups = subjectGroup
384+
.SelectMany(r => r.Exceptions)
385+
.GroupBy(e => e.ExceptionType ?? "(unknown)")
386+
.OrderByDescending(g => g.Count())
387+
.Take(5)
388+
.ToList();
389+
390+
if (exceptionGroups.Count > 0)
375391
{
376-
"Count",
377-
"Type",
378-
"Message"
379-
};
380-
var rows = exceptionGroups.Select(g => new[]
392+
var headers = new[]
393+
{
394+
"Count",
395+
"Exception Type",
396+
"Sample Message"
397+
};
398+
var rows = exceptionGroups.Select(g => new[]
399+
{
400+
g.Count().ToString(),
401+
g.Key,
402+
GetExceptionMessage(g.First())
403+
});
404+
Output.Table(headers, rows);
405+
}
406+
else
381407
{
382-
g.Count().ToString(),
383-
TruncateString(g.Key.ExceptionType ?? "(unknown)", 40),
384-
TruncateString(g.Key.InnermostMessage ?? "(no message)", 60)
385-
});
386-
Output.Table(headers, rows);
408+
Output.Info(" No exceptions found (check traces/dependencies in output file)");
409+
}
410+
411+
// Show failed dependencies if any
412+
var dependencyGroups = subjectGroup
413+
.SelectMany(r => r.FailedDependencies)
414+
.GroupBy(d => new
415+
{
416+
d.Type,
417+
d.Target
418+
})
419+
.OrderByDescending(g => g.Count())
420+
.Take(3);
421+
422+
if (dependencyGroups.Any())
423+
{
424+
Output.Info("");
425+
Output.Info(" Failed Dependencies:");
426+
foreach (var dep in dependencyGroups)
427+
{
428+
Output.Info($" - [{dep.Count()}x] {dep.Key.Type}: {TruncateString(dep.Key.Target ?? "", 40)}");
429+
}
430+
}
387431
}
388432

389-
// Group failed dependencies by target
390-
var dependencyGroups = results
391-
.SelectMany(r => r.FailedDependencies)
392-
.GroupBy(d => new
393-
{
394-
d.Type,
395-
d.Target
396-
})
397-
.OrderByDescending(g => g.Count())
398-
.Take(5);
433+
// Overall summary
434+
Output.Info("");
435+
Output.Info("Overall Top Exceptions:");
436+
Output.Info("=======================");
399437

400-
if (dependencyGroups.Any())
438+
var allExceptions = results
439+
.SelectMany(r => r.Exceptions)
440+
.GroupBy(e => e.ExceptionType ?? "(unknown)")
441+
.OrderByDescending(g => g.Count())
442+
.Take(10)
443+
.ToList();
444+
445+
if (allExceptions.Count > 0)
401446
{
402-
Output.Info("");
403-
Output.Info("Failed Dependencies:");
404447
var headers = new[]
405448
{
406449
"Count",
407450
"Type",
408-
"Target"
451+
"Sample Message"
409452
};
410-
var rows = dependencyGroups.Select(g => new[]
453+
var rows = allExceptions.Select(g => new[]
411454
{
412455
g.Count().ToString(),
413-
g.Key.Type ?? "(unknown)",
414-
TruncateString(g.Key.Target ?? "(unknown)", 50)
456+
g.Key,
457+
GetExceptionMessage(g.First())
415458
});
416459
Output.Table(headers, rows);
417460
}
@@ -554,4 +597,20 @@ private static string TruncateString(string value, int maxLength)
554597

555598
return value.Length <= maxLength ? value : value[..(maxLength - 3)] + "...";
556599
}
600+
601+
private static string GetExceptionMessage(ExceptionInfo ex)
602+
{
603+
// Prefer innermostMessage, fall back to outerMessage
604+
if (!string.IsNullOrWhiteSpace(ex.InnermostMessage))
605+
{
606+
return ex.InnermostMessage;
607+
}
608+
609+
if (!string.IsNullOrWhiteSpace(ex.OuterMessage))
610+
{
611+
return ex.OuterMessage;
612+
}
613+
614+
return "(no message)";
615+
}
557616
}

ServiceBusToolset/Options/DiagnoseDlqOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class DiagnoseDlqOptions
4444
public DateTime? BeforeEnqueueTime { get; set; }
4545

4646
[Option("max-messages",
47-
Default = 100,
47+
Default = 1000,
4848
HelpText = "Maximum number of messages to diagnose")]
4949
public int MaxMessages { get; set; }
5050

0 commit comments

Comments
 (0)