| | | 1 | | using AmbientServices.Extensions; |
| | | 2 | | using System; |
| | | 3 | | using System.Collections.Generic; |
| | | 4 | | using System.Linq; |
| | | 5 | | using System.Text; |
| | | 6 | | |
| | | 7 | | namespace 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> |
| | | 23 | | internal 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> |
| | | 455 | | internal class AggregatedAlert |
| | | 456 | | { |
| | | 457 | | private static string RenderSource(string? source) { return source ?? Status.DefaultSource; } |
| | | 458 | | 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 | | |
| | | 472 | | public float AverageRating => RatingSum / Sources.Count; |
| | | 473 | | |
| | | 474 | | public AggregatedAlert(StatusResults initialResults) |
| | | 475 | | { |
| | | 476 | | StatusAuditReport report = initialResults.Report ?? new StatusAuditReport(initialResults.Time, TimeSpan.Zero, nu |
| | | 477 | | CommonAlert = report.Alert; |
| | | 478 | | RatingSum = report.Alert?.Rating ?? StatusRating.Okay; |
| | | 479 | | Sources = new List<string> { |
| | | 480 | | RenderSource(initialResults.SourceSystem) |
| | | 481 | | }; |
| | | 482 | | Target = RenderTarget(initialResults.TargetSystem); |
| | | 483 | | TimeRange = new StatusResultsDateTimeRange(initialResults.Time); |
| | | 484 | | Report = report; |
| | | 485 | | AuditStartRange = new StatusResultsDateTimeRange(report.AuditStartTime); |
| | | 486 | | AuditDurationRange = new TimeSpanRange(report.AuditDuration); |
| | | 487 | | NextAuditTime = report.NextAuditTime; |
| | | 488 | | PropertyRanges = new List<StatusPropertyRange>(); |
| | | 489 | | } |
| | | 490 | | |
| | | 491 | | public AggregatedAlert(string? source, string? target, DateTime time, StatusAuditReport? initialReport) |
| | | 492 | | { |
| | | 493 | | StatusAuditReport report = initialReport ?? new StatusAuditReport(time, TimeSpan.Zero, null, StatusAuditAlert.No |
| | | 494 | | CommonAlert = report.Alert; |
| | | 495 | | RatingSum = report.Alert?.Rating ?? StatusRating.Okay; |
| | | 496 | | Sources = new List<string> { |
| | | 497 | | RenderSource(source) |
| | | 498 | | }; |
| | | 499 | | Target = RenderTarget(target); |
| | | 500 | | TimeRange = new StatusResultsDateTimeRange(time); |
| | | 501 | | Report = report; |
| | | 502 | | AuditStartRange = new StatusResultsDateTimeRange(report.AuditStartTime); |
| | | 503 | | AuditDurationRange = new TimeSpanRange(report.AuditDuration); |
| | | 504 | | NextAuditTime = report.NextAuditTime; |
| | | 505 | | PropertyRanges = new List<StatusPropertyRange>(); |
| | | 506 | | } |
| | | 507 | | |
| | | 508 | | public void Aggregate(StatusResults additionalResults) |
| | | 509 | | { |
| | | 510 | | StatusAuditReport report = additionalResults.Report ?? new StatusAuditReport(additionalResults.Time, TimeSpan.Ze |
| | | 511 | | if (CommonAlert != report.Alert) throw new InvalidOperationException("Only results with the same alert can be ag |
| | | 512 | | if (Target != RenderTarget(additionalResults.TargetSystem)) throw new InvalidOperationException("Only results wi |
| | | 513 | | RatingSum += report.Alert?.Rating ?? StatusRating.Okay; |
| | | 514 | | Sources.Add(RenderSource(additionalResults.SourceSystem)); |
| | | 515 | | TimeRange.AddSample(additionalResults.Time); |
| | | 516 | | AuditStartRange.AddSample(report.AuditStartTime); |
| | | 517 | | AuditDurationRange.AddSample(report.AuditDuration); |
| | | 518 | | NextAuditTime = (NextAuditTime == null) ? report.NextAuditTime : new DateTime(Math.Min(NextAuditTime.Value.Ticks |
| | | 519 | | PropertyRanges = new List<StatusPropertyRange>(); |
| | | 520 | | } |
| | | 521 | | |
| | | 522 | | public void Aggregate(string? source, string target, DateTime time, StatusAuditReport? additionalReport) |
| | | 523 | | { |
| | | 524 | | StatusAuditReport report = additionalReport ?? new StatusAuditReport(time, TimeSpan.Zero, null, StatusAuditAlert |
| | | 525 | | if (CommonAlert != report.Alert) throw new InvalidOperationException("Only results with the same alert can be ag |
| | | 526 | | if (Target != RenderTarget(target)) throw new InvalidOperationException("Only results with the same target can b |
| | | 527 | | RatingSum += report.Alert?.Rating ?? StatusRating.Okay; |
| | | 528 | | Sources.Add(RenderSource(source)); |
| | | 529 | | TimeRange.AddSample(time); |
| | | 530 | | AuditStartRange.AddSample(report.AuditStartTime); |
| | | 531 | | AuditDurationRange.AddSample(report.AuditDuration); |
| | | 532 | | NextAuditTime = (NextAuditTime == null) ? report.NextAuditTime : new DateTime(Math.Min(NextAuditTime.Value.Ticks |
| | | 533 | | PropertyRanges = new List<StatusPropertyRange>(); |
| | | 534 | | } |
| | | 535 | | |
| | | 536 | | internal bool CanBeAggregated(string target, StatusAuditReport? candidateReport) |
| | | 537 | | { |
| | | 538 | | return Target == RenderTarget(target) && candidateReport != null && CommonAlert == candidateReport.Alert; |
| | | 539 | | } |
| | | 540 | | |
| | | 541 | | internal string TerseSources |
| | | 542 | | { |
| | | 543 | | get |
| | | 544 | | { |
| | | 545 | | if (Sources.Count == 1) return Sources[0]; |
| | | 546 | | return "[" + Sources.Count.ToString(System.Globalization.CultureInfo.InvariantCulture) + "]"; |
| | | 547 | | } |
| | | 548 | | } |
| | | 549 | | internal string DetailsSources |
| | | 550 | | { |
| | | 551 | | get |
| | | 552 | | { |
| | | 553 | | if (Sources.Count == 1) return Sources[0] + " is"; |
| | | 554 | | return "[" + string.Join(",", Sources) + "] are"; |
| | | 555 | | } |
| | | 556 | | } |
| | | 557 | | } |
| | | 558 | | |
| | | 559 | | internal 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 | | |
| | | 588 | | internal class StatusResultsDateTimeRange |
| | | 589 | | { |
| | | 590 | | public DateTime Earliest { get; private set; } |
| | | 591 | | public DateTime Latest { get; private set; } |
| | | 592 | | |
| | 2 | 593 | | public StatusResultsDateTimeRange(DateTime time) |
| | | 594 | | { |
| | 2 | 595 | | Earliest = Latest = time; |
| | 2 | 596 | | } |
| | | 597 | | |
| | | 598 | | public void AddSample(DateTime time) |
| | | 599 | | { |
| | 2 | 600 | | if (time < Earliest) Earliest = time; |
| | 2 | 601 | | if (time > Latest) Latest = time; |
| | 2 | 602 | | } |
| | | 603 | | |
| | | 604 | | public string ToShortTimeString() |
| | | 605 | | { |
| | 2 | 606 | | return (Earliest == Latest) |
| | 2 | 607 | | ? Earliest.ToString("t", System.Globalization.CultureInfo.InvariantCulture) |
| | 2 | 608 | | : Earliest.ToString("t", System.Globalization.CultureInfo.InvariantCulture) + "-" + Latest.ToString("t", Sys |
| | | 609 | | } |
| | | 610 | | public string ToLongTimeString() |
| | | 611 | | { |
| | 2 | 612 | | return (Earliest == Latest) |
| | 2 | 613 | | ? Earliest.ToString("T", System.Globalization.CultureInfo.InvariantCulture) |
| | 2 | 614 | | : Earliest.ToString("T", System.Globalization.CultureInfo.InvariantCulture) + "-" + Latest.ToString("T", Sys |
| | | 615 | | } |
| | | 616 | | |
| | | 617 | | public override string ToString() |
| | | 618 | | { |
| | 2 | 619 | | return ToLongTimeString(); |
| | | 620 | | } |
| | | 621 | | } |
| | | 622 | | |
| | | 623 | | internal 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 | | } |