} // string.IsNullOrEmpty ensures response is not null /// <summary> /// Computes a summary status report based on <see cref="Properties"/>, <see cref="NatureOfSystem"/>, and <see cref="Children"/>, when a node-level report is not available. /// </summary> /// <param name="includeHtmlTag">Whether or not to include the html and body tags.</param> /// <param name="ignoreRatingsBetterThan">A value indicating which reports to completely ignore.</param> /// <param name="ignorePendingRatings">Whether or not to ignore pending ratings.</param> /// <param name="notificationTimeZone">An optional <see cref="TimeZoneInfo"/> that will be used to convert the notification time. If not specified, UTC will be used.</param> /// <returns>A <see cref="StatusAuditAlert"/> summarizing the overall state of the system.</returns> public StatusAuditAlert GetSummaryAlerts(bool includeHtmlTag, float ignoreRatingsBetterThan, bool ignorePendingRatings, TimeZoneInfo?notificationTimeZone = null) { DateTime start = AmbientClock.UtcNow; StatusResultsOrganizer organized = new StatusResultsOrganizer(this); organized.ComputeOverallRatingAndSort(); DateTime notificationTime = TimeZoneInfo.ConvertTimeFromUtc(organized.MostRecentTime, notificationTimeZone ?? TimeZoneInfo.Utc); StatusNotificationWriter writer = new StatusNotificationWriter(notificationTime); // build HTML style and header for the indicated rating and rating range float overallRating = organized.SortRating; if (includeHtmlTag) { writer.EnterHtmlAndBody(overallRating); } writer.EnterStatusRange(overallRating); StatusRatingRange ratingRange = StatusRating.FindRange(overallRating); // filter irrelevant top-level reports AggregatedAlert?aggregatedAlert = null; foreach (StatusResultsOrganizer child in organized.Children) { // use the specified child rating, or okay if one is not specified float childRating = child.SortRating; // is this one better than the cutoff? stop now because all the subsequent reports are better than this one! (because they're sorted by rating) if (childRating > ignoreRatingsBetterThan || (ignorePendingRatings && float.IsNaN(childRating))) { break; } StatusRatingRange childRatingRange = StatusRating.FindRange(childRating); if (childRatingRange != ratingRange) { if (aggregatedAlert != null) { writer.WriteAggregatedAlert(aggregatedAlert); aggregatedAlert = null; } writer.LeaveStatusRange(); writer.EnterStatusRange(childRating); ratingRange = childRatingRange; } Aggregate(ref aggregatedAlert, writer, start, child, ignoreRatingsBetterThan); } if (aggregatedAlert != null) { writer.WriteAggregatedAlert(aggregatedAlert); aggregatedAlert = null; } writer.LeaveStatusRange(); if (includeHtmlTag) { writer.LeaveBodyAndHtml(); } StatusAuditAlert alert = new StatusAuditAlert(overallRating, string.Empty, writer.Terse, writer.Details); return(alert); }
private void Aggregate(ref AggregatedAlert?aggregatedAlert, StatusNotificationWriter writer, DateTime start, StatusResultsOrganizer child, float ignoreRatingsBetterThan) { // does this child have children? if (child.ChildrenCount > 0) { System.Diagnostics.Debug.Assert(child.NatureOfSystem == StatusNatureOfSystem.ChildrenIrrelevant || child.ChildrenCount > 1); // recurse for this level too RenderTargetedResults(writer, start, child, ignoreRatingsBetterThan); } else { // can we aggregate this one? if (aggregatedAlert != null && aggregatedAlert.CanBeAggregated(child.Target, child.OverallReport)) { aggregatedAlert.Aggregate(child.Source, child.Target, child.MostRecentTime, child.OverallReport); } else // this one can't be aggregated with previous ones, so we need to flush the previously-aggregated alerts and start a new aggregation { if (aggregatedAlert != null) { writer.WriteAggregatedAlert(aggregatedAlert); } aggregatedAlert = new AggregatedAlert(child.Source, child.Target, child.MostRecentTime, child.OverallReport); } } }
private void RenderTargetedResults(StatusNotificationWriter writer, DateTime start, StatusResultsOrganizer results, float ignoreRatingsBetterThan) { float rating = results.SortRating; writer.EnterTarget(results.Target, rating); AggregatedAlert?aggregatedAlert = null; foreach (StatusResultsOrganizer child in results.Children) { // is this one better than the cutoff? stop now because all the subsequent reports are better than this one! if (child.OverallRating > ignoreRatingsBetterThan) { break; } Aggregate(ref aggregatedAlert, writer, start, child, ignoreRatingsBetterThan); } if (aggregatedAlert != null) { writer.WriteAggregatedAlert(aggregatedAlert); } writer.LeaveTarget(); }
public void ComputeOverallRatingAndSort(string target = "") { float? worstRating = null; StatusRatingRange worstRatingRange = StatusRatingRange.Superlative + 1; bool childPending = false; // keep track of the worst property rating StatusAuditAlert? worstAlert = null; StatusPropertyRange?worstAlertPropertyRange = null; foreach (StatusPropertyRange propertyRange in _propertyRanges) { string propertyPath = ComputeTarget(target, propertyRange.Name).TrimStart('/'); // is there a thresholds to use to rate a property here or are there defaults? StatusPropertyThresholds?thresholds = (_thresholds ?? StatusPropertyThresholds.DefaultPropertyThresholds).GetThresholds(propertyPath); // is there a numeric value for which thresholds can be applied? float?minValue = null; if (!string.IsNullOrEmpty(propertyRange.MinValue)) { float f; if (float.TryParse(propertyRange.MinValue, out f)) { minValue = f; } } float?maxValue = null; if (!string.IsNullOrEmpty(propertyRange.MaxValue)) { float f; if (float.TryParse(propertyRange.MaxValue, out f)) { maxValue = f; } } // are there thresholds AND range values? if (thresholds != null && minValue != null && maxValue != null) { // rate based on the value and the thresholds--is this now the worst rating? StatusAuditAlert alert = thresholds.Rate(propertyRange.Name, minValue.Value, maxValue.Value); if (Object.ReferenceEquals(worstAlert, null) || alert.Rating < worstAlert.Rating) { worstAlert = alert; worstAlertPropertyRange = propertyRange; } } } WorstPropertyAlert = worstAlert; WorstPropertyRange = worstAlertPropertyRange; StatusAuditAlert?assignedAlert = OverallReport?.Alert; float? assignedRating = assignedAlert?.Rating; float? worstThresholdRating = WorstPropertyAlert?.Rating; // the overall rating will depend on the type of system we're rating switch (NatureOfSystem) { case StatusNatureOfSystem.ChildrenIrrelevant: // there shouldn't be a report here--we don't care! System.Diagnostics.Debug.Assert(OverallReport == null); // let's rate the children anyway so we can add it to the report even if it doesn't affect the rating here foreach (StatusResultsOrganizer child in _children) { // child not rated yet? if (child.OverallRating == null) { child.ComputeOverallRatingAndSort(ComputeTarget(target, child.Target)); } // pending? if (child.SomeRatingsPending) { childPending = true; } } OverallRating = StatusRating.Okay; break; case StatusNatureOfSystem.Leaf: // is there neither an explicitly-assigned rating nor a rating based on property thresholds? bail out now without setting a new overall report if (assignedRating == null && worstThresholdRating == null) { return; } OverallRating = assignedRating; break; default: case StatusNatureOfSystem.ChildrenHeterogenous: // find the worst child rating foreach (StatusResultsOrganizer child in _children) { // child not rated yet? if (child.OverallRating == null) { child.ComputeOverallRatingAndSort(ComputeTarget(target, child.Target)); } if (child.OverallRating != null) { // aggregate results for each child float childRating = child.OverallRating.Value; if (worstRating == null || childRating < worstRating) { worstRating = childRating; } StatusRatingRange childRatingRange = StatusRating.FindRange(childRating); if (childRatingRange < worstRatingRange) { worstRatingRange = childRatingRange; } } // pending? if (child.SomeRatingsPending) { childPending = true; } } OverallRating = assignedRating = worstRating ?? StatusRating.Okay; break; case StatusNatureOfSystem.ChildrenHomogenous: // compute both ways because we don't know up front what the distribution of status rating ranges is float ratingSum = 0.0f; float clampedRatingSum = 0.0f; int ratedChildCount = 0; // first count how many reports are in each clamped rating class int[] childrenWithRating = new int[StatusRating.Ranges]; // make sure that if the number of clamped rating ranges is exactly three (we have to change the code here if this changes) System.Diagnostics.Debug.Assert(ClampedRating(StatusRating.Catastrophic) - ClampedRating(StatusRating.Okay) <= 2); // check all the child groups foreach (StatusResultsOrganizer child in Children) { // child not rated yet? if (child.OverallRating == null) { child.ComputeOverallRatingAndSort(ComputeTarget(target, child.Target)); } if (child.OverallRating != null) { float childRating = child.OverallRating.Value; float clampedRating = ClampedRating(childRating); StatusRatingRange range = StatusRating.FindRange(clampedRating); clampedRatingSum += clampedRating; ratingSum += childRating; ++ratedChildCount; System.Diagnostics.Debug.Assert(range >= StatusRatingRange.Fail && range <= StatusRatingRange.Okay); ++childrenWithRating[(int)range]; } // pending? if (child.SomeRatingsPending) { childPending = true; } } float rating; #pragma warning disable CA1508 // this check is explicitly to make sure that the subsequent condition is changed if the number of ranges changes System.Diagnostics.Debug.Assert(StatusRating.Ranges == 5); #pragma warning restore CA1508 // are all of the ratings in the same range? if (childrenWithRating[(int)StatusRatingRange.Okay] == ratedChildCount || childrenWithRating[(int)StatusRatingRange.Alert] == ratedChildCount || childrenWithRating[(int)StatusRatingRange.Fail] == ratedChildCount) { // the rating is the average of all the children rating = ratingSum / ratedChildCount; } else // we have ratings in more than one range, so the overall rating will be in the StatusRating.Alert range { // the average clamped rating cannot be out of range because it's clamped, and it cannot be on a boundary because one of the children was not in the same range with the others! System.Diagnostics.Debug.Assert(clampedRatingSum / ratedChildCount > -1.0f && clampedRatingSum / ratedChildCount < 3.0f); rating = StatusRating.Fail + ((clampedRatingSum / ratedChildCount) + 1.0f) / 4.0f; } OverallRating = assignedRating = rating; break; } // is there a child that is pending (or is this node pending)? if (childPending || float.IsNaN(OverallReport?.Alert?.Rating ?? 0.0f)) { SomeRatingsPending = true; } // only one child and it counts? if (_children.Count == 1 && NatureOfSystem != StatusNatureOfSystem.ChildrenIrrelevant) { // move everything from that child up into us StatusResultsOrganizer child = _children[0]; _propertyRanges.Clear(); _propertyRanges.AddRange(child.PropertyRanges); _children.Clear(); _children.AddRange(child.Children); NatureOfSystem = child.NatureOfSystem; OverallRating = child.OverallRating; OverallReport = child.OverallReport; Source = child.Source ?? Source; Target = ComputeTarget(Target, child.Target); // note that the child's children should already be sorted } else if (_children.Count > 1) // sort the children (if any) { _children.Sort((a, b) => (a.OverallRating ?? StatusRating.Okay).CompareTo(b.OverallRating ?? StatusRating.Okay)); } // is the threshold rating worse than the assigned rating? if (worstThresholdRating < OverallRating) { System.Diagnostics.Debug.Assert(worstThresholdRating == WorstPropertyAlert?.Rating); OverallRating = worstThresholdRating; // was there no report before if (OverallReport == null) { // create a new report with the worst property value OverallReport = new StatusAuditReport(AmbientClock.UtcNow, TimeSpan.Zero, null, WorstPropertyAlert); } else // there was an existing report { // replace that report with a new one with the alert from the worst property rating OverallReport = new StatusAuditReport(OverallReport.AuditStartTime, OverallReport.AuditDuration, OverallReport.NextAuditTime, WorstPropertyAlert); } } // still no rating? that's okay (literally) else if (OverallRating == null) { OverallRating = StatusRating.Okay; } }
/// <summary> /// Adds the specified <see cref="StatusResults"/> as a descendant of the specified root. /// The position in the tree where the results will be put is determined by the <see cref="StatusResults.TargetDisplayName"/> property, /// with nodes that have the same target ending up as siblings to a common parent. /// </summary> /// <param name="root">The root <see cref="StatusResultsOrganizer"/> being added to.</param> /// <param name="results">The <see cref="StatusResults"/> being added.</param> /// <param name="source">A source, if one needs to be assigned.</param> /// <param name="target">A parent target for the specified node.</param> public void Add(StatusResultsOrganizer root, StatusResults results, string?source = null, string target = "") { // a leaf node? if (results.NatureOfSystem == StatusNatureOfSystem.Leaf) { // rate this leaf node if we can (we need the ratings here in order to collate and sort results in ComputeOverallRatingAndSort) string localTarget = results.TargetSystem; StatusResultsOrganizer organizedResults = new StatusResultsOrganizer(results, source, localTarget); StatusResultsOrganizer parent = this; string?localtarget = results.TargetSystem; // a child of the root? if (localtarget?.StartsWith("/", StringComparison.Ordinal) ?? false) { parent = root; localtarget = localtarget.Substring(1); } parent._children.Add(organizedResults); // merge in the properties organizedResults.MergeProperties(results); } else // the results instance we're adding is an inner node { StatusResultsOrganizer?match; // do the results specify a target? if (!string.IsNullOrEmpty(results.TargetSystem)) { if (results.TargetSystem == "/") { match = root; } else { StatusResultsOrganizer parent = this; string?localTarget = results.TargetSystem; // a child of the root? if (localTarget.StartsWith("/", StringComparison.Ordinal)) { parent = root; localTarget = localTarget.Substring(1); } // is there NOT a matching node that's a child of the parent? match = parent._children.Where(c => c.Target == localTarget && c.Source == source && c.NatureOfSystem == results.NatureOfSystem).FirstOrDefault(); // note that the matching node cannot be a leaf in this case because the NatureOfSystem had to match the results and we already checked results for that if (match == null) { // we don't have a node to merge into, so build a new node now with the properties of these results match = new StatusResultsOrganizer(results, source, localTarget); // note that this new matching node cannot be a leaf in this case because the NatureOfSystem had to match the results and we already checked results for that parent._children.Add(match); } System.Diagnostics.Debug.Assert(match.NatureOfSystem != StatusNatureOfSystem.Leaf); } } else // no target is specified, so we should merge these results directly into this node { match = this; } // does the matching node have children? if (match.NatureOfSystem != StatusNatureOfSystem.Leaf) { DateTime?time = null; // merge the children of the specified results foreach (StatusResults child in results.Children) { match.Add(root, child, results.SourceSystem ?? source, ComputeTarget(target, results.TargetSystem)); if (time == null || child.Time > time) { time = child.Time; } } MostRecentTime = (time > results.Time) ? time.Value : results.Time; } else // the matching node is a leaf node (this is strange because the node we're adding is NOT a leaf node) { match.MostRecentTime = results.Time; // the node we're adding is NOT a leaf node, so it CANNOT have a report (StatusResults cannot have both children and a report, though we can here once we rate parent nodes based on their children) System.Diagnostics.Debug.Assert(results.Report == null); } // merge in the properties match.MergeProperties(results); } }