< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Health.DiskSpaceProbe
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Health/DiskSpaceProbe.cs
Tag: Kestrun/Kestrun@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
79%
Covered lines: 58
Uncovered lines: 15
Coverable lines: 73
Total lines: 191
Line coverage: 79.4%
Branch coverage
69%
Covered branches: 25
Total branches: 36
Branch coverage: 69.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/13/2025 - 16:52:37 Line coverage: 79.4% (58/73) Branch coverage: 69.4% (25/36) Total lines: 191 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e 10/13/2025 - 16:52:37 Line coverage: 79.4% (58/73) Branch coverage: 69.4% (25/36) Total lines: 191 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)66.66%121293.75%
get_Name()100%11100%
get_Tags()100%210%
get_Logger()100%11100%
CheckAsync(...)68.75%201675%
ResolveDrive(...)50%5457.14%
FormatBytes(...)100%44100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Health/DiskSpaceProbe.cs

#LineLine coverage
 1using System.Globalization;
 2using Serilog;
 3using Serilog.Events;
 4
 5namespace Kestrun.Health;
 6
 7/// <summary>
 8/// Probe that reports free disk space for a target drive / mount point.
 9/// </summary>
 10/// <remarks>
 11/// By default it inspects the drive containing the current process executable (AppContext.BaseDirectory).
 12/// Status mapping (unless overridden):
 13///  Healthy: free percent greater-or-equal to warn threshold (default 10%)
 14///  Degraded: free percent between critical and warn thresholds (default 5% - 10%)
 15///  Unhealthy: free percent below critical threshold (default under 5%)
 16/// On error (e.g., drive missing) the probe returns Unhealthy with the exception message.
 17/// </remarks>
 18public sealed class DiskSpaceProbe : IProbe
 19{
 20    /// <summary>
 21    /// Number of bytes per kilobyte (1024).
 22    /// </summary>
 23    private const double BytesPerKilobyte = 1024.0;
 24    /// <summary>
 25    /// Units for formatting byte sizes.
 26    /// </summary>
 127    private static readonly string[] sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
 28
 29    private const double CriticalThresholdDefault = 5.0;
 30    private const double WarnThresholdDefault = 10.0;
 31
 32    private readonly string _path;
 33    private readonly double _criticalPercent;
 34    private readonly double _warnPercent;
 35
 36    /// <summary>
 37    /// Creates a new <see cref="DiskSpaceProbe"/>.
 38    /// </summary>
 39    /// <param name="name">Probe name (e.g., "disk").</param>
 40    /// <param name="tags">Probe tags (e.g., ["live"], ["ready"]).</param>
 41    /// <param name="path">Directory path whose containing drive should be measured. Defaults to AppContext.BaseDirector
 42    /// <param name="criticalPercent">Below this free percentage the probe is Unhealthy. Default 5.</param>
 43    /// <param name="warnPercent">Below this free percentage (but above critical) the probe is Degraded. Default 10.</pa
 44    /// <param name="logger">Optional logger; if null a context logger is created.</param>
 45    /// <exception cref="ArgumentException">Thrown when thresholds are invalid.</exception>
 5746    public DiskSpaceProbe(
 5747        string name,
 5748        string[] tags,
 5749        string? path = null,
 5750        double criticalPercent = CriticalThresholdDefault,
 5751        double warnPercent = WarnThresholdDefault,
 5752        Serilog.ILogger? logger = null)
 53    {
 5754        if (criticalPercent <= 0 || warnPercent <= 0 || warnPercent <= criticalPercent || warnPercent > 100)
 55        {
 056            throw new ArgumentException("Invalid threshold configuration. Must satisfy: 0 < critical < warn <= 100.");
 57        }
 58
 5759        Name = name;
 5760        Tags = tags;
 5761        _path = string.IsNullOrWhiteSpace(path) ? AppContext.BaseDirectory : path!;
 5762        _criticalPercent = criticalPercent;
 5763        _warnPercent = warnPercent;
 5764        Logger = logger ?? Log.ForContext("HealthProbe", name).ForContext("Probe", name);
 5765    }
 66
 67    /// <summary>
 68    /// Probe name.
 69    /// </summary>
 6370    public string Name { get; }
 71    /// <summary>
 72    /// Probe tags used for filtering.
 73    /// </summary>
 074    public string[] Tags { get; }
 75
 76    /// <inheritdoc />
 7277    public Serilog.ILogger Logger { get; init; }
 78
 79    /// <summary>
 80    /// Executes the disk space check.
 81    /// </summary>
 82    public Task<ProbeResult> CheckAsync(CancellationToken ct = default)
 83    {
 84        try
 85        {
 86            // Resolve drive info
 387            var drive = ResolveDrive(_path);
 388            if (drive is null)
 89            {
 090                if (Logger.IsEnabled(LogEventLevel.Debug))
 91                {
 092                    Logger.Debug("DiskSpaceProbe {Probe} drive not found for path {Path}", Name, _path);
 93                }
 094                return Task.FromResult(new ProbeResult(ProbeStatus.Unhealthy, $"Drive not found for path '{_path}'."));
 95            }
 96
 397            if (!drive.IsReady)
 98            {
 099                return Task.FromResult(new ProbeResult(ProbeStatus.Unhealthy, $"Drive '{drive.Name}' is not ready."));
 100            }
 101
 3102            if (Logger.IsEnabled(LogEventLevel.Debug))
 103            {
 3104                Logger.Debug("DiskSpaceProbe {Probe} checking drive {Drive}", Name, drive.Name);
 3105                Logger.Debug("DiskSpaceProbe {Probe} drive is ready {Drive}", Name, drive.Name);
 106            }
 107
 3108            var total = drive.TotalSize; // bytes
 3109            var free = drive.AvailableFreeSpace; // bytes (user-available)
 3110            if (total <= 0)
 111            {
 0112                return Task.FromResult(new ProbeResult(ProbeStatus.Unhealthy, $"Drive '{drive.Name}' total size reported
 113            }
 114
 3115            var freePercent = (double)free / total * 100.0;
 3116            var status = freePercent < _criticalPercent
 3117                ? ProbeStatus.Unhealthy
 3118                : freePercent < _warnPercent
 3119                    ? ProbeStatus.Degraded
 3120                    : ProbeStatus.Healthy;
 121
 3122            if (Logger.IsEnabled(LogEventLevel.Debug))
 123            {
 3124                Logger.Debug("DiskSpaceProbe {Probe} free percent={Percent:F1}", Name, freePercent);
 125            }
 126
 3127            var data = new Dictionary<string, object>
 3128            {
 3129                ["path"] = _path,
 3130                ["driveName"] = drive.Name,
 3131                ["totalBytes"] = total,
 3132                ["freeBytes"] = free,
 3133                ["freePercent"] = Math.Round(freePercent, 2),
 3134                ["criticalPercent"] = _criticalPercent,
 3135                ["warnPercent"] = _warnPercent
 3136            };
 137
 3138            var desc = $"Free {FormatBytes(free)} of {FormatBytes(total)} ({freePercent:F2}% free)";
 139
 3140            return Task.FromResult(new ProbeResult(status, desc, data));
 141        }
 0142        catch (OperationCanceledException) when (ct.IsCancellationRequested)
 143        {
 0144            return Task.FromResult(new ProbeResult(ProbeStatus.Degraded, "Canceled", new Dictionary<string, object> { ["
 145        }
 0146        catch (Exception ex)
 147        {
 0148            Logger.Error(ex, "DiskSpaceProbe {Probe} failed", Name);
 0149            return Task.FromResult(new ProbeResult(ProbeStatus.Unhealthy, ex.Message));
 150        }
 3151    }
 152
 153    /// <summary>
 154    /// Resolves the <see cref="DriveInfo"/> for the given path.
 155    /// </summary>
 156    /// <param name="path">The path to resolve.</param>
 157    /// <returns>The <see cref="DriveInfo"/> if found; otherwise, null.</returns>
 158    private static DriveInfo? ResolveDrive(string path)
 159    {
 160        try
 161        {
 3162            if (string.IsNullOrWhiteSpace(path))
 163            {
 0164                return null;
 165            }
 3166            var root = Path.GetPathRoot(path);
 3167            return string.IsNullOrEmpty(root) ? null : new DriveInfo(root);
 168        }
 0169        catch
 170        {
 0171            return null;
 172        }
 3173    }
 174
 175    /// <summary>
 176    /// Formats a byte count into a human-readable string using binary (1024) units.
 177    /// </summary>
 178    /// <param name="bytes">The number of bytes.</param>
 179    /// <returns>A human-readable string representation of the byte count.</returns>
 180    private static string FormatBytes(long bytes)
 181    {
 6182        double len = bytes;
 6183        var order = 0;
 24184        while (len >= BytesPerKilobyte && order < sizes.Length - 1)
 185        {
 18186            order++;
 18187            len /= BytesPerKilobyte;
 188        }
 6189        return string.Create(CultureInfo.InvariantCulture, $"{len:0.##} {sizes[order]}");
 190    }
 191}