| | | 1 | | using System.Text; |
| | | 2 | | |
| | | 3 | | namespace Kestrun.Health; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Produces a concise, human-readable text representation of a <see cref="HealthReport"/>. |
| | | 7 | | /// Intended for terminals, logs, or lightweight probes where structured formats (JSON/YAML/XML) |
| | | 8 | | /// are unnecessary. |
| | | 9 | | /// </summary> |
| | | 10 | | public static class HealthReportTextFormatter |
| | | 11 | | { |
| | | 12 | | /// <summary> |
| | | 13 | | /// Formats a <see cref="HealthReport"/> into a concise plain text representation. Includes probe data when <paramre |
| | | 14 | | /// </summary> |
| | | 15 | | /// <param name="report">The health report to format.</param> |
| | | 16 | | /// <param name="includeData">When true (default) emits per-probe data key/value lines.</param> |
| | | 17 | | /// <returns>A multi-line string suitable for console or log output.</returns> |
| | | 18 | | public static string Format(HealthReport report, bool includeData = true) |
| | | 19 | | { |
| | 1 | 20 | | ArgumentNullException.ThrowIfNull(report); |
| | 1 | 21 | | var sb = new StringBuilder(); |
| | 1 | 22 | | _ = sb.AppendLine($"Status: {report.StatusText}"); |
| | 1 | 23 | | _ = sb.AppendLine($"GeneratedAt: {report.GeneratedAt:O}"); |
| | 1 | 24 | | var s = report.Summary; |
| | 1 | 25 | | _ = sb.AppendLine($"Summary: total={s.Total} healthy={s.Healthy} degraded={s.Degraded} unhealthy={s.Unhealthy}") |
| | 1 | 26 | | if (report.AppliedTags is { Count: > 0 }) |
| | | 27 | | { |
| | 0 | 28 | | _ = sb.AppendLine($"Tags: {string.Join(",", report.AppliedTags)}"); |
| | | 29 | | } |
| | 1 | 30 | | _ = sb.AppendLine("Probes:"); |
| | 6 | 31 | | foreach (var p in report.Probes) |
| | | 32 | | { |
| | 2 | 33 | | _ = sb.Append(" - "); |
| | 2 | 34 | | _ = sb.Append($"name={p.Name} status={p.StatusText} duration={FormatDuration(p.Duration)}"); |
| | 2 | 35 | | if (!string.IsNullOrWhiteSpace(p.Description)) |
| | | 36 | | { |
| | 2 | 37 | | _ = sb.Append($" desc=\"{Escape(p.Description!)}\""); |
| | | 38 | | } |
| | 2 | 39 | | if (!string.IsNullOrWhiteSpace(p.Error)) |
| | | 40 | | { |
| | 0 | 41 | | _ = sb.Append($" error=\"{Escape(p.Error!)}\""); |
| | | 42 | | } |
| | 2 | 43 | | _ = sb.AppendLine(); |
| | 2 | 44 | | if (includeData && p.Data is { Count: > 0 }) |
| | | 45 | | { |
| | 8 | 46 | | foreach (var kvp in p.Data) |
| | | 47 | | { |
| | 2 | 48 | | _ = sb.AppendLine($" {kvp.Key}={RenderValue(kvp.Value)}"); |
| | | 49 | | } |
| | | 50 | | } |
| | | 51 | | } |
| | 1 | 52 | | return sb.ToString(); |
| | | 53 | | } |
| | | 54 | | |
| | | 55 | | private static string RenderValue(object? value) |
| | | 56 | | { |
| | 2 | 57 | | return value is null |
| | 2 | 58 | | ? "<null>" |
| | 2 | 59 | | : value switch |
| | 2 | 60 | | { |
| | 0 | 61 | | string s => '"' + Escape(s) + '"', |
| | 0 | 62 | | DateTime dt => dt.ToString("O"), |
| | 0 | 63 | | DateTimeOffset dto => dto.ToString("O"), |
| | 0 | 64 | | TimeSpan ts => ts.ToString(), |
| | 2 | 65 | | _ => value.ToString() ?? string.Empty |
| | 2 | 66 | | }; |
| | | 67 | | } |
| | | 68 | | |
| | 2 | 69 | | private static string Escape(string input) => input.Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"); |
| | | 70 | | |
| | | 71 | | private static string FormatDuration(TimeSpan duration) |
| | | 72 | | { |
| | 2 | 73 | | return duration.TotalMilliseconds < 1 |
| | 2 | 74 | | ? "<1ms" |
| | 2 | 75 | | : duration.TotalMilliseconds < 1000 |
| | 2 | 76 | | ? ((int)duration.TotalMilliseconds) + "ms" |
| | 2 | 77 | | : duration.TotalSeconds < 60 ? duration.TotalSeconds.ToString("0.###") + "s" : duration.ToString(); |
| | | 78 | | } |
| | | 79 | | } |