public void StatusRatingUtilityFunctions()
        {
            float             previousValue = float.MinValue;
            StatusRatingRange previousRange = StatusRatingRange.Pending;

            foreach (StatusRatingRange range in Enum.GetValues(typeof(StatusRatingRange)).Cast <StatusRatingRange>())
            {
                float lowerBound = StatusRating.GetRangeLowerBound(range);
                Assert.IsTrue(float.IsNaN(lowerBound) || lowerBound > previousValue);
                if (!float.IsNaN(lowerBound))
                {
                    previousValue = lowerBound;
                }
                Assert.AreEqual(previousRange, StatusRating.FindRange(lowerBound));
                previousRange = float.IsNaN(lowerBound) ? StatusRatingRange.Fail : range;
                float upperBound = StatusRating.GetRangeUpperBound(range);
                Assert.AreEqual(range, StatusRating.FindRange(upperBound));
                string rangeSymbol = StatusRating.GetRangeSymbol(range);
                Assert.IsFalse(string.IsNullOrEmpty(rangeSymbol));
                Assert.IsFalse(string.IsNullOrEmpty(StatusRating.GetRangeBackgroundColor(upperBound)));
                Assert.IsFalse(string.IsNullOrEmpty(StatusRating.GetRangeForegroundColor(upperBound)));
                Assert.IsFalse(string.IsNullOrEmpty(StatusRating.GetRatingRgbForegroundColor(upperBound)));
            }
            Assert.AreEqual(StatusRating.Okay, StatusResultsOrganizer.ClampedRating(StatusRating.Pending));
            Assert.AreEqual(StatusRating.Catastrophic, StatusResultsOrganizer.ClampedRating(StatusRating.Catastrophic - 1));
            Assert.AreEqual(StatusRating.Okay, StatusResultsOrganizer.ClampedRating(StatusRating.Superlative + 1));
            Assert.AreEqual(StatusRating.GetRatingRgbForegroundColor(StatusRating.Catastrophic), StatusRating.GetRatingRgbForegroundColor(StatusRating.Catastrophic - 1));
            Assert.AreNotEqual(StatusRating.GetRatingRgbForegroundColor(StatusRating.Superlative), StatusRating.GetRatingRgbForegroundColor(StatusRating.Superlative + 1));
        }
        }                                                                                                                                                               // 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>
 /// Constructs a status notification writer with the specified notification time.
 /// </summary>
 /// <param name="notificationTime">The notification time.  Defaults to the current time.</param>
 public StatusNotificationWriter(DateTime?notificationTime = null)
 {
     _tabLevel = 1;
     _currentSectionRatingRange = (StatusRatingRange)(-1);
     _details          = new StringBuilder();
     _terse            = new StringBuilder();
     _notificationTime = notificationTime ?? AmbientClock.UtcNow;
 }
        /// <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();
        }
        /// <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 override Task Audit(StatusResultsBuilder statusBuilder, CancellationToken cancel = default(CancellationToken))
        {
            statusBuilder.NatureOfSystem = StatusNatureOfSystem.ChildrenIrrelevant;
            StatusRatingRange currentAuditRating = (StatusRatingRange)(_auditNumber++ % (int)EnumExtensions.MaxEnumValue <StatusRatingRange>());
            float             rating             = (StatusRating.GetRangeUpperBound(currentAuditRating) + StatusRating.GetRangeLowerBound(currentAuditRating)) / 2;

            if (rating <= StatusRating.Fail)
            {
                statusBuilder.AddFailure("FailCode", "Fail", "The system has failed!", StatusRating.Fail - rating);
            }
            else if (rating <= StatusRating.Alert)
            {
                statusBuilder.AddAlert("AlertCode", "Alert", "The system has alerted!", StatusRating.Alert - rating);
            }
            else if (rating <= StatusRating.Okay)
            {
                statusBuilder.AddOkay("OkayCode", "Okay", "The system is okay", StatusRating.Okay - rating);
            }
            else
            {
                statusBuilder.AddOkay("SuperCode", "Superlative", "The system is superlative", StatusRating.Superlative - rating);
            }
            return(Task.CompletedTask);
        }
 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('>');
 }
Example #9
0
 /// <summary>
 /// Returns a background color for the specified rating range.
 /// </summary>
 /// <param name="ratingRange">A <see cref="StatusRatingRange"/> indicating the range, presumably returned by <see cref="FindRange"/>.</param>
 /// <returns>A string identifying the color associated with the rating range.</returns>
 public static string GetRangeBackgroundColor(StatusRatingRange ratingRange)
 {
     return(RangeBackgroundColors[(int)ratingRange]);
 }
Example #10
0
 /// <summary>
 /// Returns a string indicating the name of the specified range.
 /// </summary>
 /// <param name="ratingRange">A <see cref="StatusRatingRange"/> indicating the range, presumably returned by <see cref="FindRange"/>.</param>
 /// <returns>A string containing the name of the specified range.</returns>
 public static string GetRangeName(StatusRatingRange ratingRange)
 {
     return(RangeNames[(int)ratingRange]);
 }
Example #11
0
 /// <summary>
 /// Gets the <see cref="float"/> for the upper bound for the specified range.
 /// The returned value is the lower bound for the next range.
 /// Passing this value to <see cref="FindRange"/> will return the specified range.
 /// </summary>
 /// <param name="ratingRange">The <see cref="StatusRatingRange"/> to get the upper bound for.</param>
 /// <returns>The upper bound for the specified range.</returns>
 public static float GetRangeUpperBound(StatusRatingRange ratingRange)
 {
     return((ratingRange == 0) ? float.NaN : RangeValues[(int)ratingRange + 1]);
 }
Example #12
0
 /// <summary>
 /// Gets the single-character symbol for the specified status rating range.
 /// </summary>
 /// <param name="ratingRange">A <see cref="StatusRatingRange"/> indicating the range, the same that would be returned by <see cref="FindRange"/> for values in the specified range.</param>
 /// <returns>A single character representing the specified rating range.</returns>
 public static string GetRangeSymbol(StatusRatingRange ratingRange)
 {
     return(RangeSymbols[(int)ratingRange]);
 }
Example #13
0
        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;
            }
        }