< 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@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
86%
Covered lines: 63
Uncovered lines: 10
Coverable lines: 73
Total lines: 191
Line coverage: 86.3%
Branch coverage
91%
Covered branches: 33
Total branches: 36
Branch coverage: 91.6%
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@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/19/2025 - 02:25:56 Line coverage: 86.3% (63/73) Branch coverage: 91.6% (33/36) Total lines: 191 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 86.3% (63/73) Branch coverage: 88.8% (32/36) Total lines: 191 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 86.3% (63/73) Branch coverage: 91.6% (33/36) Total lines: 191 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c8330 10/13/2025 - 16:52:37 Line coverage: 79.4% (58/73) Branch coverage: 69.4% (25/36) Total lines: 191 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/19/2025 - 02:25:56 Line coverage: 86.3% (63/73) Branch coverage: 91.6% (33/36) Total lines: 191 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 86.3% (63/73) Branch coverage: 88.8% (32/36) Total lines: 191 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 86.3% (63/73) Branch coverage: 91.6% (33/36) Total lines: 191 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c8330

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%1212100%
get_Name()100%11100%
get_Tags()100%11100%
get_Logger()100%11100%
CheckAsync(...)87.5%171682.5%
ResolveDrive(...)75%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>
 7046    public DiskSpaceProbe(
 7047        string name,
 7048        string[] tags,
 7049        string? path = null,
 7050        double criticalPercent = CriticalThresholdDefault,
 7051        double warnPercent = WarnThresholdDefault,
 7052        Serilog.ILogger? logger = null)
 53    {
 7054        if (criticalPercent <= 0 || warnPercent <= 0 || warnPercent <= criticalPercent || warnPercent > 100)
 55        {
 456            throw new ArgumentException("Invalid threshold configuration. Must satisfy: 0 < critical < warn <= 100.");
 57        }
 58
 6659        Name = name;
 6660        Tags = tags;
 6661        _path = string.IsNullOrWhiteSpace(path) ? AppContext.BaseDirectory : path;
 6662        _criticalPercent = criticalPercent;
 6663        _warnPercent = warnPercent;
 6664        Logger = logger ?? Log.ForContext("HealthProbe", name).ForContext("Probe", name);
 6665    }
 66
 67    /// <summary>
 68    /// Probe name.
 69    /// </summary>
 8270    public string Name { get; }
 71    /// <summary>
 72    /// Probe tags used for filtering.
 73    /// </summary>
 274    public string[] Tags { get; }
 75
 76    /// <inheritdoc />
 10977    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
 987            var drive = ResolveDrive(_path);
 988            if (drive is null)
 89            {
 190                if (Logger.IsEnabled(LogEventLevel.Debug))
 91                {
 192                    Logger.Debug("DiskSpaceProbe {Probe} drive not found for path {Path}", Name, _path);
 93                }
 194                return Task.FromResult(new ProbeResult(ProbeStatus.Unhealthy, $"Drive not found for path '{_path}'."));
 95            }
 96
 897            if (!drive.IsReady)
 98            {
 099                return Task.FromResult(new ProbeResult(ProbeStatus.Unhealthy, $"Drive '{drive.Name}' is not ready."));
 100            }
 101
 8102            if (Logger.IsEnabled(LogEventLevel.Debug))
 103            {
 8104                Logger.Debug("DiskSpaceProbe {Probe} checking drive {Drive}", Name, drive.Name);
 8105                Logger.Debug("DiskSpaceProbe {Probe} drive is ready {Drive}", Name, drive.Name);
 106            }
 107
 8108            var total = drive.TotalSize; // bytes
 8109            var free = drive.AvailableFreeSpace; // bytes (user-available)
 8110            if (total <= 0)
 111            {
 0112                return Task.FromResult(new ProbeResult(ProbeStatus.Unhealthy, $"Drive '{drive.Name}' total size reported
 113            }
 114
 8115            var freePercent = (double)free / total * 100.0;
 8116            var status = freePercent < _criticalPercent
 8117                ? ProbeStatus.Unhealthy
 8118                : freePercent < _warnPercent
 8119                    ? ProbeStatus.Degraded
 8120                    : ProbeStatus.Healthy;
 121
 8122            if (Logger.IsEnabled(LogEventLevel.Debug))
 123            {
 8124                Logger.Debug("DiskSpaceProbe {Probe} free percent={Percent:F1}", Name, freePercent);
 125            }
 126
 8127            var data = new Dictionary<string, object>
 8128            {
 8129                ["path"] = _path,
 8130                ["driveName"] = drive.Name,
 8131                ["totalBytes"] = total,
 8132                ["freeBytes"] = free,
 8133                ["freePercent"] = Math.Round(freePercent, 2),
 8134                ["criticalPercent"] = _criticalPercent,
 8135                ["warnPercent"] = _warnPercent
 8136            };
 137
 8138            var desc = $"Free {FormatBytes(free)} of {FormatBytes(total)} ({freePercent:F2}% free)";
 139
 8140            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        }
 9151    }
 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        {
 9162            if (string.IsNullOrWhiteSpace(path))
 163            {
 0164                return null;
 165            }
 9166            var root = Path.GetPathRoot(path);
 9167            return string.IsNullOrEmpty(root) ? null : new DriveInfo(root);
 168        }
 0169        catch
 170        {
 0171            return null;
 172        }
 9173    }
 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    {
 16182        double len = bytes;
 16183        var order = 0;
 64184        while (len >= BytesPerKilobyte && order < sizes.Length - 1)
 185        {
 48186            order++;
 48187            len /= BytesPerKilobyte;
 188        }
 16189        return string.Create(CultureInfo.InvariantCulture, $"{len:0.##} {sizes[order]}");
 190    }
 191}