< Summary

Information
Class: AmbientServices.AggregatedAlert
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Status/StatusResultsOrganizer.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 60
Uncovered lines: 0
Coverable lines: 60
Total lines: 656
Line coverage: 100%
Branch coverage
100%
Covered branches: 44
Total branches: 44
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
RenderSource(...)100%22100%
RenderTarget(...)100%22100%
get_AverageRating()100%11100%
.ctor(...)100%44100%
.ctor(...)100%44100%
Aggregate(...)100%1212100%
Aggregate(...)100%1212100%
CanBeAggregated(...)100%44100%
get_TerseSources()100%22100%
get_DetailsSources()100%22100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Status/StatusResultsOrganizer.cs

#LineLine coverage
 1using AmbientServices.Extensions;
 2using System;
 3using System.Collections.Generic;
 4using System.Linq;
 5using System.Text;
 6
 7namespace AmbientServices;
 8
 9/// <summary>
 10/// A class used to rate and organize a tree of <see cref="StatusResults"/> such that single results are pushed up to pa
 11/// </summary>
 12/// <remarks>
 13/// <pitch>The summarization engine behind <see cref="StatusResults.GetSummaryAlerts"/>: it merges one or more results t
 14/// <pledge>
 15/// Merging honors the <see cref="StatusResults"/> naming rules: nodes are placed by target, results with matching targe
 16/// <see cref="ComputeOverallRatingAndSort"/> assigns each node a rating according to its <see cref="StatusNatureOfSyste
 17/// </pledge>
 18/// <plan>
 19/// A recursive mutable tree built by walking each added <see cref="StatusResults"/>, keyed on (target, source, nature) 
 20/// </plan>
 21/// <pin><see cref="StatusResults"/></pin>
 22/// </remarks>
 23internal class StatusResultsOrganizer
 24{
 25    private readonly IStatusThresholdsRegistry? _thresholds;
 26    private readonly List<StatusPropertyRange> _propertyRanges = new();
 27    private readonly List<StatusResultsOrganizer> _children = new();
 28
 29    /// <summary>
 30    /// Gets the most recent time from all the <see cref="StatusResults.Time"/> property of all descendant <see cref="St
 31    /// </summary>
 32    public DateTime MostRecentTime { get; private set; }
 33    /// <summary>
 34    /// Gets the overall rating for this branch of the tree, if one is available and has been determined.
 35    /// </summary>
 36    public float? OverallRating { get; private set; }
 37    /// <summary>
 38    /// Gets whether or not some of the descendant nodes indicate that status tests have not yet been completed.
 39    /// </summary>
 40    public bool SomeRatingsPending { get; private set; }
 41    /// <summary>
 42    /// Gets the source.
 43    /// </summary>
 44    public string? Source { get; private set; }
 45    /// <summary>
 46    /// Gets the target.
 47    /// </summary>
 48    public string Target { get; private set; }
 49    /// <summary>
 50    /// Gets the <see cref="StatusNatureOfSystem"/> for the corresponding <see cref="StatusResults"/>.
 51    /// </summary>
 52    public StatusNatureOfSystem NatureOfSystem { get; private set; }
 53    /// <summary>
 54    /// Gets a <see cref="StatusAuditReport"/> summarizing the status for this node in the tree.
 55    /// </summary>
 56    public StatusAuditReport? OverallReport { get; private set; }
 57    /// <summary>
 58    /// Gets an enumeration of <see cref="StatusPropertyRange"/>s indicating the range of the various properties for thi
 59    /// </summary>
 60    public IEnumerable<StatusPropertyRange> PropertyRanges => _propertyRanges;
 61    /// <summary>
 62    /// Gets the <see cref="StatusPropertyRange"/> for the property that has the worst rating according to the applied t
 63    /// </summary>
 64    internal StatusPropertyRange? WorstPropertyRange { get; private set; }
 65    /// <summary>
 66    /// Gets the <see cref="StatusAuditAlert"/> for the worst property range according to the applied thresholds.
 67    /// </summary>
 68    internal StatusAuditAlert? WorstPropertyAlert { get; private set; }
 69    /// <summary>
 70    /// Gets the rating used for sorting, which is the overall rating if there is one, or <see cref="StatusRating.Okay"/
 71    /// </summary>
 72    internal float SortRating => OverallRating ?? StatusRating.Okay;
 73    /// <summary>
 74    /// Gets the number of direct children to this node.
 75    /// </summary>
 76    public int ChildrenCount => _children.Count;
 77    /// <summary>
 78    /// Gets an enumeration of <see cref="StatusResultsOrganizer"/>s for the child nodes.
 79    /// </summary>
 80    public IEnumerable<StatusResultsOrganizer> Children => _children;
 81
 82    private StatusResultsOrganizer(StatusResults results, string? source, string localTarget)
 83    {
 84        MostRecentTime = results.Time;
 85        Source = results.SourceSystem ?? source;
 86        Target = localTarget;
 87        NatureOfSystem = results.NatureOfSystem;
 88        OverallReport = results.Report;
 89    }
 90
 91    public StatusResultsOrganizer(IStatusThresholdsRegistry? thresholds = null)
 92    {
 93        _thresholds = thresholds;
 94        Target = "/";
 95        NatureOfSystem = StatusNatureOfSystem.ChildrenHeterogeneous;
 96    }
 97
 98    public StatusResultsOrganizer(StatusResults results, string? source = null, IStatusThresholdsRegistry? thresholds = 
 99    {
 100        _thresholds = thresholds;
 101        Target = "/";
 102        NatureOfSystem = StatusNatureOfSystem.ChildrenHeterogeneous;
 103        Add(results, source);
 104    }
 105    /// <summary>
 106    /// Adds the specified <see cref="StatusResults"/> to this root node.
 107    /// </summary>
 108    /// <param name="results">The <see cref="StatusResults"/> for this node.</param>
 109    /// <param name="source">The source to assign for nodes that don't have a source assigned.</param>
 110    public void Add(StatusResults results, string? source = null)
 111    {
 112        Add(this, results, source);
 113    }
 114    /// <summary>
 115    /// Adds the specified <see cref="StatusResults"/> as a descendant of the specified root.
 116    /// The position in the tree where the results will be put is determined by the <see cref="StatusResults.TargetDispl
 117    /// with nodes that have the same target ending up as siblings to a common parent.
 118    /// </summary>
 119    /// <param name="root">The root <see cref="StatusResultsOrganizer"/> being added to.</param>
 120    /// <param name="results">The <see cref="StatusResults"/> being added.</param>
 121    /// <param name="source">A source, if one needs to be assigned.</param>
 122    /// <param name="target">A parent target for the specified node.</param>
 123    public void Add(StatusResultsOrganizer root, StatusResults results, string? source = null, string target = "")
 124    {
 125        // a leaf node?
 126        if (results.NatureOfSystem == StatusNatureOfSystem.Leaf)
 127        {
 128            // rate this leaf node if we can (we need the ratings here in order to collate and sort results in ComputeOv
 129            string localTarget = results.TargetSystem;
 130            StatusResultsOrganizer organizedResults = new(results, source, localTarget);
 131            StatusResultsOrganizer parent = this;
 132            string? localtarget = results.TargetSystem;
 133            // a child of the root?
 134            if (localtarget?.StartsWith('/') ?? false)
 135            {
 136                parent = root;
 137                localtarget = localtarget.Substring(1);
 138            }
 139            parent._children.Add(organizedResults);
 140            // merge in the properties
 141            organizedResults.MergeProperties(results);
 142        }
 143        else // the results instance we're adding is an inner node
 144        {
 145            StatusResultsOrganizer? match;
 146            // do the results specify a target?
 147            if (!string.IsNullOrEmpty(results.TargetSystem))
 148            {
 149                if (results.TargetSystem == "/")
 150                {
 151                    match = root;
 152                }
 153                else
 154                {
 155                    StatusResultsOrganizer parent = this;
 156                    string? localTarget = results.TargetSystem;
 157                    // a child of the root?
 158                    if (localTarget.StartsWith('/'))
 159                    {
 160                        parent = root;
 161                        localTarget = localTarget.Substring(1);
 162                    }
 163                    // is there NOT a matching node that's a child of the parent?
 164                    match = parent._children.Where(c => c.Target == localTarget && c.Source == source && c.NatureOfSyste
 165                    if (match == null)
 166                    { // we don't have a node to merge into, so build a new node now with the properties of these result
 167                        match = new StatusResultsOrganizer(results, source, localTarget);  // note that this new matchin
 168                        parent._children.Add(match);
 169                    }
 170                    System.Diagnostics.Debug.Assert(match.NatureOfSystem != StatusNatureOfSystem.Leaf);
 171                }
 172            }
 173            else // no target is specified, so we should merge these results directly into this node
 174            {
 175                match = this;
 176            }
 177
 178            // does the matching node have children?
 179            if (match.NatureOfSystem != StatusNatureOfSystem.Leaf)
 180            {
 181                DateTime? time = null;
 182                // merge the children of the specified results
 183                foreach (StatusResults child in results.Children)
 184                {
 185                    match.Add(root, child, results.SourceSystem ?? source, ComputeTarget(target, results.TargetSystem));
 186                    if (time == null || child.Time > time)
 187                    {
 188                        time = child.Time;
 189                    }
 190                }
 191                MostRecentTime = (time > results.Time) ? time.Value : results.Time;
 192            }
 193            else // the matching node is a leaf node (this is strange because the node we're adding is NOT a leaf node)
 194            {
 195                match.MostRecentTime = results.Time;
 196                // the node we're adding is NOT a leaf node, so it CANNOT have a report (StatusResults cannot have both 
 197                System.Diagnostics.Debug.Assert(results.Report == null);
 198            }
 199            // merge in the properties
 200            match.MergeProperties(results);
 201        }
 202    }
 203
 204    private void MergeProperties(StatusResults results)
 205    {
 206        // merge any attributes into the matching node
 207        foreach (StatusProperty property in results.Properties)
 208        {
 209            StatusPropertyRange? range = _propertyRanges.Where(ar => ar.Name == property.Name).FirstOrDefault();
 210            if (range != null)
 211            {
 212                range.Merge(property.Value);
 213            }
 214            else
 215            {
 216                _propertyRanges.Add(new StatusPropertyRange(property));
 217            }
 218        }
 219    }
 220
 221    /// <summary>
 222    /// Clamps the status rating into the standard range between <see cref="StatusRating.Catastrophic"/> and <see cref="
 223    /// </summary>
 224    /// <param name="rawRating">The raw range value.</param>
 225    /// <returns>A range value truncated to between <see cref="StatusRating.Catastrophic"/> and <see cref="StatusRating.
 226    internal static float ClampedRating(float rawRating)
 227    {
 228        if (float.IsNaN(rawRating)) return StatusRating.Okay;
 229        return rawRating < StatusRating.Catastrophic ? StatusRating.Catastrophic
 230            : (rawRating > StatusRating.Okay ? StatusRating.Okay : rawRating);
 231    }
 232    public void ComputeOverallRatingAndSort(string target = "")
 233    {
 234        float? worstRating = null;
 235        StatusRatingRange worstRatingRange = StatusRatingRange.Superlative + 1;
 236        bool childPending = false;
 237
 238        // keep track of the worst property rating
 239        StatusAuditAlert? worstAlert = null;
 240        StatusPropertyRange? worstAlertPropertyRange = null;
 241        foreach (StatusPropertyRange propertyRange in _propertyRanges)
 242        {
 243            string propertyPath = ComputeTarget(target, propertyRange.Name).TrimStart('/');
 244            // is there a thresholds to use to rate a property here or are there defaults?
 245            StatusPropertyThresholds? thresholds = (_thresholds ?? StatusPropertyThresholds.DefaultPropertyThresholds).G
 246            // is there a numeric value for which thresholds can be applied?
 247            float? minValue = null;
 248            if (!string.IsNullOrEmpty(propertyRange.MinValue))
 249            {
 250                float f;
 251                if (float.TryParse(propertyRange.MinValue, out f)) minValue = f;
 252            }
 253            float? maxValue = null;
 254            if (!string.IsNullOrEmpty(propertyRange.MaxValue))
 255            {
 256                float f;
 257                if (float.TryParse(propertyRange.MaxValue, out f)) maxValue = f;
 258            }
 259            // are there thresholds AND range values?
 260            if (thresholds != null && minValue != null && maxValue != null)
 261            {
 262                // rate based on the value and the thresholds--is this now the worst rating?
 263                StatusAuditAlert alert = thresholds.Rate(propertyRange.Name, minValue.Value, maxValue.Value);
 264                if (worstAlert is null || alert.Rating < worstAlert.Rating)
 265                {
 266                    worstAlert = alert;
 267                    worstAlertPropertyRange = propertyRange;
 268                }
 269            }
 270        }
 271        WorstPropertyAlert = worstAlert;
 272        WorstPropertyRange = worstAlertPropertyRange;
 273
 274        StatusAuditAlert? assignedAlert = OverallReport?.Alert;
 275        float? assignedRating = assignedAlert?.Rating;
 276        float? worstThresholdRating = WorstPropertyAlert?.Rating;
 277
 278        // the overall rating will depend on the type of system we're rating
 279        switch (NatureOfSystem)
 280        {
 281            case StatusNatureOfSystem.ChildrenIrrelevant:
 282                // there shouldn't be a report here--we don't care!
 283                System.Diagnostics.Debug.Assert(OverallReport == null);
 284                // let's rate the children anyway so we can add it to the report even if it doesn't affect the rating he
 285                foreach (StatusResultsOrganizer child in _children)
 286                {
 287                    // child not rated yet?
 288                    if (child.OverallRating == null) child.ComputeOverallRatingAndSort(ComputeTarget(target, child.Targe
 289                    // pending?
 290                    if (child.SomeRatingsPending) childPending = true;
 291                }
 292                OverallRating = StatusRating.Okay;
 293                break;
 294            case StatusNatureOfSystem.Leaf:
 295                // is there neither an explicitly-assigned rating nor a rating based on property thresholds?  bail out n
 296                if (assignedRating == null && worstThresholdRating == null) return;
 297                OverallRating = assignedRating;
 298                break;
 299            default:
 300            case StatusNatureOfSystem.ChildrenHeterogeneous:
 301                // find the worst child rating
 302                foreach (StatusResultsOrganizer child in _children)
 303                {
 304                    // child not rated yet?
 305                    if (child.OverallRating == null) child.ComputeOverallRatingAndSort(ComputeTarget(target, child.Targe
 306                    if (child.OverallRating != null)
 307                    {
 308                        // aggregate results for each child
 309                        float childRating = child.OverallRating.Value;
 310                        if (worstRating == null || childRating < worstRating)
 311                        {
 312                            worstRating = childRating;
 313                        }
 314                        StatusRatingRange childRatingRange = StatusRating.FindRange(childRating);
 315                        if (childRatingRange < worstRatingRange)
 316                        {
 317                            worstRatingRange = childRatingRange;
 318                        }
 319                    }
 320                    // pending?
 321                    if (child.SomeRatingsPending) childPending = true;
 322                }
 323                OverallRating = assignedRating = worstRating ?? StatusRating.Okay;
 324                break;
 325            case StatusNatureOfSystem.ChildrenHomogeneous:
 326                // compute both ways because we don't know up front what the distribution of status rating ranges is
 327                float ratingSum = 0.0f;
 328                float clampedRatingSum = 0.0f;
 329                int ratedChildCount = 0;
 330                // first count how many reports are in each clamped rating class
 331                int[] childrenWithRating = new int[StatusRating.Ranges];
 332                // make sure that if the number of clamped rating ranges is exactly three (we have to change the code he
 333                System.Diagnostics.Debug.Assert(ClampedRating(StatusRating.Catastrophic) - ClampedRating(StatusRating.Ok
 334                // check all the child groups
 335                foreach (StatusResultsOrganizer child in Children)
 336                {
 337                    // child not rated yet?
 338                    if (child.OverallRating == null) child.ComputeOverallRatingAndSort(ComputeTarget(target, child.Targe
 339                    if (child.OverallRating != null)
 340                    {
 341                        float childRating = child.OverallRating.Value;
 342                        float clampedRating = ClampedRating(childRating);
 343                        StatusRatingRange range = StatusRating.FindRange(clampedRating);
 344                        clampedRatingSum += clampedRating;
 345                        ratingSum += childRating;
 346                        ++ratedChildCount;
 347                        System.Diagnostics.Debug.Assert(range >= StatusRatingRange.Fail && range <= StatusRatingRange.Ok
 348                        ++childrenWithRating[(int)range];
 349                    }
 350                    // pending?
 351                    if (child.SomeRatingsPending) childPending = true;
 352                }
 353                float rating;
 354#pragma warning disable CA1508  // this check is explicitly to make sure that the subsequent condition is changed if the
 355                System.Diagnostics.Debug.Assert(StatusRating.Ranges == 5);
 356#pragma warning restore CA1508
 357                // are all of the ratings in the same range?
 358                if (childrenWithRating[(int)StatusRatingRange.Okay] == ratedChildCount || childrenWithRating[(int)Status
 359                {
 360                    // the rating is the average of all the children
 361                    rating = ratingSum / ratedChildCount;
 362                }
 363                else // we have ratings in more than one range, so the overall rating will be in the StatusRating.Alert 
 364                {
 365                    // the average clamped rating cannot be out of range because it's clamped, and it cannot be on a bou
 366                    System.Diagnostics.Debug.Assert(clampedRatingSum / ratedChildCount > -1.0f && clampedRatingSum / rat
 367                    rating = StatusRating.Fail + ((clampedRatingSum / ratedChildCount) + 1.0f) / 4.0f;
 368                }
 369                OverallRating = assignedRating = rating;
 370                break;
 371        }
 372        // is there a child that is pending (or is this node pending)?
 373        if (childPending || float.IsNaN(OverallReport?.Alert?.Rating ?? 0.0f)) SomeRatingsPending = true;
 374
 375        // only one child and it counts?
 376        if (_children.Count == 1 && NatureOfSystem != StatusNatureOfSystem.ChildrenIrrelevant)
 377        {
 378            // move everything from that child up into us
 379            StatusResultsOrganizer child = _children[0];
 380            _propertyRanges.Clear();
 381            _propertyRanges.AddRange(child.PropertyRanges);
 382            _children.Clear();
 383            _children.AddRange(child.Children);
 384            NatureOfSystem = child.NatureOfSystem;
 385            OverallRating = child.OverallRating;
 386            OverallReport = child.OverallReport;
 387            Source = child.Source ?? Source;
 388            Target = ComputeTarget(Target, child.Target);
 389            // note that the child's children should already be sorted
 390        }
 391        else if (_children.Count > 1) // sort the children (if any)
 392        {
 393            _children.Sort((a, b) => (a.OverallRating ?? StatusRating.Okay).CompareTo(b.OverallRating ?? StatusRating.Ok
 394        }
 395        // is the threshold rating worse than the assigned rating?
 396        if (worstThresholdRating < OverallRating)
 397        {
 398            System.Diagnostics.Debug.Assert(worstThresholdRating == WorstPropertyAlert?.Rating);
 399            OverallRating = worstThresholdRating;
 400            // was there no report before
 401            if (OverallReport == null)
 402            {
 403                // create a new report with the worst property value
 404                OverallReport = new StatusAuditReport(AmbientClock.UtcNow, TimeSpan.Zero, null, WorstPropertyAlert);
 405            }
 406            else // there was an existing report
 407            {
 408                // replace that report with a new one with the alert from the worst property rating
 409                OverallReport = new StatusAuditReport(OverallReport.AuditStartTime, OverallReport.AuditDuration, Overall
 410            }
 411        }
 412        // still no rating?  that's okay (literally)
 413        else if (OverallRating == null) OverallRating = StatusRating.Okay;
 414    }
 415
 416    private static string ComputeTarget(string parentTargetSystem, string childTarget)
 417    {
 418        if (childTarget.StartsWith('/') || string.IsNullOrEmpty(parentTargetSystem)) return childTarget;
 419        if (parentTargetSystem.EndsWith('/')) return parentTargetSystem + childTarget.TrimStart('.');
 420        return parentTargetSystem.TrimEnd('.') + "." + childTarget.TrimStart('.');
 421    }
 422
 423    public override string ToString()
 424    {
 425        StringBuilder output = new();
 426        if (Source == null && string.IsNullOrEmpty(Target.Trim('/')))
 427        {
 428            output.Append("Overall:");
 429        }
 430        else
 431        {
 432            if (Source != null)
 433            {
 434                output.Append(Source);
 435                output.Append("->");
 436            }
 437            output.Append(Target);
 438            output.Append(':');
 439        }
 440        if (OverallReport != null)
 441        {
 442            output.Append(OverallReport.ToString());
 443        }
 444        return output.ToString();
 445    }
 446}
 447
 448/// <summary>
 449/// A class that accumulates equivalent alerts from multiple sources for the same target so they can be reported as a si
 450/// </summary>
 451/// <remarks>
 452/// <pitch>Collapses "the same problem on N servers" into one notification entry: sources are collected while times, aud
 453/// <pledge>Only results that <see cref="CanBeAggregated"/> — same rendered target and an equivalent alert (see <see cre
 454/// </remarks>
 455internal class AggregatedAlert
 456{
 2457    private static string RenderSource(string? source) { return source ?? Status.DefaultSource; }
 2458    private static string RenderTarget(string? target) { return target ?? Status.DefaultTarget; }
 459
 460
 461    public StatusAuditAlert? CommonAlert { get; private set; }
 462    public float RatingSum { get; private set; }
 463    public List<string> Sources { get; private set; }
 464    public string Target { get; private set; }
 465    public List<StatusPropertyRange> PropertyRanges { get; private set; }
 466    public StatusResultsDateTimeRange TimeRange { get; private set; }
 467    public StatusAuditReport Report { get; private set; }
 468    public StatusResultsDateTimeRange AuditStartRange { get; private set; }
 469    public TimeSpanRange AuditDurationRange { get; private set; }
 470    public DateTime? NextAuditTime { get; private set; }
 471
 2472    public float AverageRating => RatingSum / Sources.Count;
 473
 2474    public AggregatedAlert(StatusResults initialResults)
 475    {
 2476        StatusAuditReport report = initialResults.Report ?? new StatusAuditReport(initialResults.Time, TimeSpan.Zero, nu
 2477        CommonAlert = report.Alert;
 2478        RatingSum = report.Alert?.Rating ?? StatusRating.Okay;
 2479        Sources = new List<string> {
 2480            RenderSource(initialResults.SourceSystem)
 2481        };
 2482        Target = RenderTarget(initialResults.TargetSystem);
 2483        TimeRange = new StatusResultsDateTimeRange(initialResults.Time);
 2484        Report = report;
 2485        AuditStartRange = new StatusResultsDateTimeRange(report.AuditStartTime);
 2486        AuditDurationRange = new TimeSpanRange(report.AuditDuration);
 2487        NextAuditTime = report.NextAuditTime;
 2488        PropertyRanges = new List<StatusPropertyRange>();
 2489    }
 490
 2491    public AggregatedAlert(string? source, string? target, DateTime time, StatusAuditReport? initialReport)
 492    {
 2493        StatusAuditReport report = initialReport ?? new StatusAuditReport(time, TimeSpan.Zero, null, StatusAuditAlert.No
 2494        CommonAlert = report.Alert;
 2495        RatingSum = report.Alert?.Rating ?? StatusRating.Okay;
 2496        Sources = new List<string> {
 2497            RenderSource(source)
 2498        };
 2499        Target = RenderTarget(target);
 2500        TimeRange = new StatusResultsDateTimeRange(time);
 2501        Report = report;
 2502        AuditStartRange = new StatusResultsDateTimeRange(report.AuditStartTime);
 2503        AuditDurationRange = new TimeSpanRange(report.AuditDuration);
 2504        NextAuditTime = report.NextAuditTime;
 2505        PropertyRanges = new List<StatusPropertyRange>();
 2506    }
 507
 508    public void Aggregate(StatusResults additionalResults)
 509    {
 2510        StatusAuditReport report = additionalResults.Report ?? new StatusAuditReport(additionalResults.Time, TimeSpan.Ze
 2511        if (CommonAlert != report.Alert) throw new InvalidOperationException("Only results with the same alert can be ag
 2512        if (Target != RenderTarget(additionalResults.TargetSystem)) throw new InvalidOperationException("Only results wi
 2513        RatingSum += report.Alert?.Rating ?? StatusRating.Okay;
 2514        Sources.Add(RenderSource(additionalResults.SourceSystem));
 2515        TimeRange.AddSample(additionalResults.Time);
 2516        AuditStartRange.AddSample(report.AuditStartTime);
 2517        AuditDurationRange.AddSample(report.AuditDuration);
 2518        NextAuditTime = (NextAuditTime == null) ? report.NextAuditTime : new DateTime(Math.Min(NextAuditTime.Value.Ticks
 2519        PropertyRanges = new List<StatusPropertyRange>();
 2520    }
 521
 522    public void Aggregate(string? source, string target, DateTime time, StatusAuditReport? additionalReport)
 523    {
 2524        StatusAuditReport report = additionalReport ?? new StatusAuditReport(time, TimeSpan.Zero, null, StatusAuditAlert
 2525        if (CommonAlert != report.Alert) throw new InvalidOperationException("Only results with the same alert can be ag
 2526        if (Target != RenderTarget(target)) throw new InvalidOperationException("Only results with the same target can b
 2527        RatingSum += report.Alert?.Rating ?? StatusRating.Okay;
 2528        Sources.Add(RenderSource(source));
 2529        TimeRange.AddSample(time);
 2530        AuditStartRange.AddSample(report.AuditStartTime);
 2531        AuditDurationRange.AddSample(report.AuditDuration);
 2532        NextAuditTime = (NextAuditTime == null) ? report.NextAuditTime : new DateTime(Math.Min(NextAuditTime.Value.Ticks
 2533        PropertyRanges = new List<StatusPropertyRange>();
 2534    }
 535
 536    internal bool CanBeAggregated(string target, StatusAuditReport? candidateReport)
 537    {
 2538        return Target == RenderTarget(target) && candidateReport != null && CommonAlert == candidateReport.Alert;
 539    }
 540
 541    internal string TerseSources
 542    {
 543        get
 544        {
 2545            if (Sources.Count == 1) return Sources[0];
 2546            return "[" + Sources.Count.ToString(System.Globalization.CultureInfo.InvariantCulture) + "]";
 547        }
 548    }
 549    internal string DetailsSources
 550    {
 551        get
 552        {
 2553            if (Sources.Count == 1) return Sources[0] + " is";
 2554            return "[" + string.Join(",", Sources) + "] are";
 555        }
 556    }
 557}
 558
 559internal class StatusPropertyRange
 560{
 561    public string Name { get; private set; }
 562    public string MinValue { get; private set; }
 563    public string MaxValue { get; private set; }
 564
 565    public StatusPropertyRange(StatusProperty property)
 566    {
 567        Name = property.Name;
 568        MinValue = property.Value;
 569        MaxValue = property.Value;
 570    }
 571    public void Merge(string propertyValue)
 572    {
 573        if (propertyValue.CompareNaturalInvariant(MinValue) < 0)
 574        {
 575            MinValue = propertyValue;
 576        }
 577        if (propertyValue.CompareNaturalInvariant(MaxValue) > 0)
 578        {
 579            MaxValue = propertyValue;
 580        }
 581    }
 582    public override string ToString()
 583    {
 584        return Name + "=" + MinValue + ((MinValue == MaxValue) ? "" : ("-" + MaxValue));
 585    }
 586}
 587
 588internal class StatusResultsDateTimeRange
 589{
 590    public DateTime Earliest { get; private set; }
 591    public DateTime Latest { get; private set; }
 592
 593    public StatusResultsDateTimeRange(DateTime time)
 594    {
 595        Earliest = Latest = time;
 596    }
 597
 598    public void AddSample(DateTime time)
 599    {
 600        if (time < Earliest) Earliest = time;
 601        if (time > Latest) Latest = time;
 602    }
 603
 604    public string ToShortTimeString()
 605    {
 606        return (Earliest == Latest)
 607            ? Earliest.ToString("t", System.Globalization.CultureInfo.InvariantCulture)
 608            : Earliest.ToString("t", System.Globalization.CultureInfo.InvariantCulture) + "-" + Latest.ToString("t", Sys
 609    }
 610    public string ToLongTimeString()
 611    {
 612        return (Earliest == Latest)
 613            ? Earliest.ToString("T", System.Globalization.CultureInfo.InvariantCulture)
 614            : Earliest.ToString("T", System.Globalization.CultureInfo.InvariantCulture) + "-" + Latest.ToString("T", Sys
 615    }
 616
 617    public override string ToString()
 618    {
 619        return ToLongTimeString();
 620    }
 621}
 622
 623internal class TimeSpanRange
 624{
 625    public TimeSpan Shortest { get; private set; }
 626    public TimeSpan Longest { get; private set; }
 627
 628    public TimeSpanRange(TimeSpan time)
 629    {
 630        Shortest = Longest = time;
 631    }
 632
 633    public void AddSample(TimeSpan time)
 634    {
 635        if (time < Shortest) Shortest = time;
 636        if (time > Longest) Longest = time;
 637    }
 638
 639    public string ToShortString()
 640    {
 641        return (Shortest == Longest)
 642            ? Shortest.ToShortHumanReadableString()
 643            : Shortest.ToShortHumanReadableString() + "-" + Longest.ToShortHumanReadableString();
 644    }
 645    public string ToLongString()
 646    {
 647        return (Shortest == Longest)
 648            ? Shortest.ToLongHumanReadableString()
 649            : Shortest.ToLongHumanReadableString() + "-" + Longest.ToLongHumanReadableString();
 650    }
 651
 652    public override string ToString()
 653    {
 654        return ToLongString();
 655    }
 656}