feat(cli): table output formatter for audit events (#23 M8)
This commit is contained in:
@@ -29,19 +29,16 @@ public sealed class JsonLinesAuditFormatter : IAuditFormatter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an <see cref="IAuditFormatter"/> for a given <c>--format</c> value. The
|
||||
/// table formatter is filled in by Bundle C; until then <c>--format table</c> falls back
|
||||
/// to JSONL with a one-time notice so the flag is wired but not silently broken.
|
||||
/// Resolves an <see cref="IAuditFormatter"/> for a given <c>--format</c> value:
|
||||
/// <c>table</c> renders a column-aligned text table (<see cref="TableAuditFormatter"/>),
|
||||
/// any other value (including <c>json</c>) renders JSONL.
|
||||
/// </summary>
|
||||
public static class AuditFormatterFactory
|
||||
{
|
||||
public static IAuditFormatter Create(string format, TextWriter notices)
|
||||
{
|
||||
if (string.Equals(format, "table", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
notices.WriteLine("note: 'table' output is not yet available; using json. (Bundle C)");
|
||||
return new JsonLinesAuditFormatter();
|
||||
}
|
||||
return new TableAuditFormatter();
|
||||
|
||||
return new JsonLinesAuditFormatter();
|
||||
}
|
||||
|
||||
96
src/ScadaLink.CLI/Commands/TableAuditFormatter.cs
Normal file
96
src/ScadaLink.CLI/Commands/TableAuditFormatter.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ScadaLink.CLI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable table formatter for <c>audit query --format table</c> (Audit Log
|
||||
/// #23 M8-T6). Renders each fetched page as a column-aligned text table with a fixed
|
||||
/// column set (<see cref="Columns"/>). Long free-text fields (Target, Actor) are
|
||||
/// truncated with an ellipsis so columns stay aligned regardless of payload size.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A header row is emitted once per page (matching the streamable, page-at-a-time
|
||||
/// contract of <see cref="IAuditFormatter"/>). An empty page emits the header only,
|
||||
/// so the column shape is visible even with zero results.
|
||||
/// </remarks>
|
||||
public sealed class TableAuditFormatter : IAuditFormatter
|
||||
{
|
||||
/// <summary>JSON property name (camelCase, as the server serializes it) → column header.</summary>
|
||||
private static readonly (string Property, string Header, int MaxWidth)[] Columns =
|
||||
{
|
||||
("occurredAtUtc", "OccurredAtUtc", 24),
|
||||
("channel", "Channel", 14),
|
||||
("kind", "Kind", 18),
|
||||
("status", "Status", 12),
|
||||
("target", "Target", 32),
|
||||
("actor", "Actor", 20),
|
||||
("durationMs", "DurationMs", 10),
|
||||
("httpStatus", "HttpStatus", 10),
|
||||
};
|
||||
|
||||
public void WritePage(IReadOnlyList<JsonElement> events, TextWriter output)
|
||||
{
|
||||
// Build every cell first so column widths account for the actual data.
|
||||
var rows = new List<string[]>(events.Count);
|
||||
foreach (var evt in events)
|
||||
{
|
||||
var cells = new string[Columns.Length];
|
||||
for (var i = 0; i < Columns.Length; i++)
|
||||
cells[i] = Truncate(CellValue(evt, Columns[i].Property), Columns[i].MaxWidth);
|
||||
rows.Add(cells);
|
||||
}
|
||||
|
||||
var widths = new int[Columns.Length];
|
||||
for (var i = 0; i < Columns.Length; i++)
|
||||
widths[i] = Columns[i].Header.Length;
|
||||
foreach (var row in rows)
|
||||
for (var i = 0; i < Columns.Length; i++)
|
||||
widths[i] = Math.Max(widths[i], row[i].Length);
|
||||
|
||||
WriteRow(output, Columns.Select(c => c.Header).ToArray(), widths);
|
||||
foreach (var row in rows)
|
||||
WriteRow(output, row, widths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a cell value for <paramref name="property"/> from an audit event.
|
||||
/// A missing property or a JSON <c>null</c> renders as an empty string (never
|
||||
/// the literal text "null").
|
||||
/// </summary>
|
||||
private static string CellValue(JsonElement evt, string property)
|
||||
{
|
||||
if (evt.ValueKind != JsonValueKind.Object
|
||||
|| !evt.TryGetProperty(property, out var value)
|
||||
|| value.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return value.ValueKind == JsonValueKind.String
|
||||
? value.GetString() ?? string.Empty
|
||||
: value.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates <paramref name="value"/> to <paramref name="maxWidth"/> characters,
|
||||
/// replacing the tail with a single-character ellipsis so the column stays aligned.
|
||||
/// </summary>
|
||||
private static string Truncate(string value, int maxWidth)
|
||||
{
|
||||
if (maxWidth <= 0 || value.Length <= maxWidth)
|
||||
return value;
|
||||
if (maxWidth == 1)
|
||||
return "…";
|
||||
return value.Substring(0, maxWidth - 1) + "…";
|
||||
}
|
||||
|
||||
private static void WriteRow(TextWriter output, IReadOnlyList<string> cells, int[] widths)
|
||||
{
|
||||
for (var i = 0; i < cells.Count; i++)
|
||||
{
|
||||
// Last column is not padded — avoids trailing whitespace at line end.
|
||||
output.Write(i == cells.Count - 1 ? cells[i] : cells[i].PadRight(widths[i] + 2));
|
||||
}
|
||||
output.WriteLine();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user