} // 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); }
/// <summary> /// Writes a notification for the specified <see cref="AggregatedAlert"/>. /// </summary> /// <param name="aggregatedAlert">An <see cref="AggregatedAlert"/> that should be written to the notification.</param> public void WriteAggregatedAlert(AggregatedAlert aggregatedAlert) { if (_tabLevel <= 1) { throw new InvalidOperationException("A status range must be entered before aggregated alerts can be written!"); } StatusAuditAlert auditAlert = aggregatedAlert.CommonAlert ?? StatusAuditAlert.None; StatusRatingRange range = StatusRating.FindRange(auditAlert.Rating); string rangeName = StatusRating.GetRangeName(range); string rangeColor = StatusRating.GetRangeForegroundColor(auditAlert.Rating); string rgbColor = StatusRating.GetRatingRgbForegroundColor(aggregatedAlert.RatingSum / aggregatedAlert.Sources.Count); List <StatusPropertyRange> propertyRanges = aggregatedAlert.PropertyRanges; OpenHeader(_details, _terse, _tabLevel, (StatusRatingRange)(-1), rgbColor); _terse.Append(aggregatedAlert.Target + ": " + aggregatedAlert.TerseSources + "->"); #if RAWRATINGS _details.Append("[RATING=" + auditAlert.Rating.ToString(DebugRatingFloatFormat) + "] "); #endif _details.Append(aggregatedAlert.Target + ": " + aggregatedAlert.DetailsSources + " reporting: "); // multi-line alert details or properties to list? if ((aggregatedAlert.CommonAlert?.Details != null && (aggregatedAlert.CommonAlert.Details.Contains("<br/>", StringComparison.Ordinal) || aggregatedAlert.CommonAlert.Details.Contains("</div>", StringComparison.Ordinal))) || (propertyRanges != null && propertyRanges.Count > 0)) { CloseHeader(_details, _terse, _tabLevel); EnterTabLevel(); OpenHeader(_details, _terse, _tabLevel, (StatusRatingRange)(-1), rgbColor); _terse.Append(RenderTerse(auditAlert.Terse).Replace("\n", "\n" + new string(' ', _tabLevel), StringComparison.Ordinal)); _details.AppendLine(RenderDetails(auditAlert.Details)); CloseHeader(_details, _terse, _tabLevel); // are there properties? if (propertyRanges != null && propertyRanges.Count > 0) { _details.Append(" because"); foreach (StatusPropertyRange propertyRange in aggregatedAlert.PropertyRanges) { OpenHeader(_details, _terse, _tabLevel, (StatusRatingRange)(-1), rgbColor); _terse.Append(propertyRange.ToString()); _details.AppendLine(propertyRange.ToString()); CloseHeader(_details, _terse, _tabLevel); } } LeaveTabLevel(); } else { _terse.Append(RenderTerse(auditAlert.Terse)); _details.Append(' '); _details.Append(RenderDetails(auditAlert.Details)); CloseHeader(_details, _terse, _tabLevel); } }
/// <summary> /// Enters a target section. Should be matched by a subsequent call to <see cref="LeaveTarget"/>. /// </summary> /// <param name="target">The name of the target owning this section.</param> /// <param name="rating">The overall status rating for this target.</param> public void EnterTarget(string target, float rating) { if (_tabLevel < 2) { throw new InvalidOperationException("A status range must be entered before a target is!"); } string rgbColor = StatusRating.GetRatingRgbForegroundColor(rating); StatusRatingRange range = StatusRating.FindRange(rating); OpenHeader(_details, _terse, _tabLevel, (StatusRatingRange)(-1), rgbColor); _terse.Append(target); _details.Append(target); CloseHeader(_details, _terse, _tabLevel); EnterTabLevel(); }
private static void OpenHeader(StringBuilder details, StringBuilder terse, int tabLevel, StatusRatingRange range = (StatusRatingRange)(-1), string?color = null) { if (terse != null) { terse.Append(new string(' ', tabLevel - 1)); if (range != (StatusRatingRange)(-1)) { string symbolPrefix = StatusRating.GetRangeSymbol(range) + " "; terse.Append(symbolPrefix); } } details.Append('<'); details.Append(RenderLevelString(tabLevel)); if (!string.IsNullOrEmpty(color)) { details.Append(" style=\"color:" + color + "\""); } details.Append('>'); }
/// <summary> /// Gets a string representation of this object. /// </summary> /// <returns>A string representation of this object.</returns> public override string ToString() { StringBuilder output = new StringBuilder(); output.Append('@'); output.Append(_auditStartTime.ToShortTimeString().Replace(" ", "", StringComparison.Ordinal)); if (_alert != null) { output.Append(':'); output.Append(StatusRating.GetRangeName(_alert.Rating)); if (!string.IsNullOrEmpty(_alert.Terse)) { output.Append('('); output.Append(_alert.Terse); output.Append(')'); } } return(output.ToString()); }
/// <summary> /// Enters the html and body elements, with a backround color set if the overall rating is specified. /// Should be matched my a subsequent to <see cref="LeaveBodyAndHtml"/>. /// </summary> /// <param name="overallRating">The overall rating to use for the background color.</param> public void EnterHtmlAndBody(float?overallRating = null) { _details.Append( @"<html> <head>"); if (overallRating != null) { string backgroundColor = StatusRating.GetRangeBackgroundColor(overallRating.Value); _details.Append(@" <style>"); _details.Append(StatusRating.StyleDefinition); _details.Append(@" body{background-color:" + backgroundColor + @";} </style>"); } _details.Append(@" </head> <body> "); }
/// <summary> /// Enters a status range section for the range indicated by the rating. /// Should be matched by a subsequent call to <see cref="LeaveStatusRange"/>. /// </summary> /// <param name="rating">The status rating.</param> public void EnterStatusRange(float rating) { StatusRatingRange range = StatusRating.FindRange(rating); string rangeName = StatusRating.GetRangeName(range); string rangeColor = StatusRating.GetRangeForegroundColor(range); if (_tabLevel > 1) { throw new InvalidOperationException("All targets and status ranges must be closed before entering a new status range!"); } _details.Append("<div class=\""); #pragma warning disable CA1308 // this is to convert the range name from the C# style casing (Pascal) to the HTML style casing (kebab) _details.Append(rangeName.ToLowerInvariant()); #pragma warning restore CA1308 _details.Append("-range\">"); OpenHeader(_details, _terse, _tabLevel, range, rangeColor); _currentSectionRatingRange = range; _terse.Append(rangeName.ToUpperInvariant()); _details.Append(StatusRating.GetRangeSymbol(range)); _details.Append(' '); _details.Append(rangeName); if (_notificationTime != null) { _terse.Append(" @"); _terse.Append(_notificationTime.Value.ToShortTimeString()); _details.Append(" at "); _details.Append(_notificationTime.Value.ToLongTimeString()); _notificationTime = null; } CloseHeader(_details, _terse, _tabLevel); EnterTabLevel(); }
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; } }