feat(cli): flow-debug logs column and --show-logs

This commit is contained in:
Joseph Doherty
2026-09-09 12:03:42 -04:00
parent a45857ddd6
commit c6529fa996
4 changed files with 154 additions and 13 deletions
@@ -37,8 +37,14 @@ internal static class FlowDebugTableProjection
/// renderer's own fallbacks still apply.
/// </summary>
/// <param name="json">The raw success JSON body from the management API.</param>
/// <param name="showLogs">
/// When true, prints each step's <c>ScriptLog</c> lines and (on a failed step) the first
/// three lines of its exception detail as additional indented <c>note</c> rows immediately
/// following that step's row (step-logs design 2026-09-09, CLI Task 14). The <c>logs</c>
/// column (a count of attached log lines) is always present, independent of this flag.
/// </param>
/// <returns>Compact JSON suitable for table rendering.</returns>
internal static string ProjectEvents(string json)
internal static string ProjectEvents(string json, bool showLogs = false)
{
JsonDocument doc;
try
@@ -62,7 +68,7 @@ internal static class FlowDebugTableProjection
var truncated = TryGetPropertyCI(root, "truncated", out var t) && t.ValueKind == JsonValueKind.True;
var rows = ProjectSteps(events, truncated);
var rows = ProjectSteps(events, truncated, showLogs);
if (rows == null)
{
// The event array did not deserialize as canonical audit events (a future wire
@@ -103,7 +109,7 @@ internal static class FlowDebugTableProjection
/// Builds the flow graph from the canonical events and projects one row per step. Returns
/// null when the array is not a list of canonical audit events.
/// </summary>
private static JsonArray? ProjectSteps(JsonElement events, bool truncated)
private static JsonArray? ProjectSteps(JsonElement events, bool truncated, bool showLogs)
{
List<AuditEvent>? list;
try
@@ -148,12 +154,47 @@ internal static class FlowDebugTableProjection
["lifecycle"] = lifecycle is { Count: > 0 } ? $"{lifecycle.Count} {(lifecycle.Count == 1 ? "row" : "rows")}" : null,
["correlation"] = step.CorrelationId?.ToString("D"),
["error"] = step.ErrorMessage,
["logs"] = step.Logs?.Count ?? 0,
});
if (!showLogs)
continue;
if (step.Logs is { Count: > 0 } logs)
{
foreach (var line in logs)
rows.Add(new JsonObject { ["note"] = JsonValue.Create(FormatLogLine(line)) });
}
if (!string.IsNullOrWhiteSpace(step.ErrorDetail))
{
foreach (var line in FormatErrorDetailLines(step.ErrorDetail))
rows.Add(new JsonObject { ["note"] = JsonValue.Create(line) });
}
}
return rows;
}
/// <summary>Formats one <see cref="FlowLogLine"/> as <c> HH:mm:ss.fff LEVEL source message</c> (UTC, level upper-cased and padded to 5 chars).</summary>
private static string FormatLogLine(FlowLogLine line)
{
var time = line.OccurredAtUtc.UtcDateTime.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
var level = (line.Level ?? "info").ToUpperInvariant().PadRight(5);
return $" {time} {level} {line.Source} {line.Message}";
}
/// <summary>
/// Formats the first three lines of an exception's <c>ToString()</c> detail — the first
/// prefixed <c> exception: </c>, the rest indented four spaces.
/// </summary>
private static IEnumerable<string> FormatErrorDetailLines(string errorDetail)
{
var lines = errorDetail.Replace("\r\n", "\n").Split('\n');
for (var i = 0; i < Math.Min(3, lines.Length); i++)
yield return i == 0 ? $" exception: {lines[i]}" : $" {lines[i]}";
}
/// <summary>Projects one canonical audit event (camelCase JSON) to its compact row (fallback path).</summary>
private static JsonNode ProjectEvent(JsonElement evt)
{