}                                                                                                                                                               // 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);
            }
        }
Example #3
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;
            }
        }