| | | 1 | | using AmbientServices.Extensions; |
| | | 2 | | using AmbientServices.Utilities; |
| | | 3 | | using System; |
| | | 4 | | using System.Collections.Concurrent; |
| | | 5 | | using System.Collections.Generic; |
| | | 6 | | using System.Linq; |
| | | 7 | | |
| | | 8 | | namespace AmbientServices; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// An enum identifying the nature of the status threshold. |
| | | 12 | | /// </summary> |
| | | 13 | | public enum StatusThresholdNature |
| | | 14 | | { |
| | | 15 | | /// <summary> |
| | | 16 | | /// Low values for this threshold are good. |
| | | 17 | | /// </summary> |
| | | 18 | | LowIsGood, |
| | | 19 | | /// <summary> |
| | | 20 | | /// High values for this threshold are good. |
| | | 21 | | /// </summary> |
| | | 22 | | HighIsGood, |
| | | 23 | | } |
| | | 24 | | /// <summary> |
| | | 25 | | /// An immutable class that holds status information about property threshold values at which status ratings should tran |
| | | 26 | | /// Thresholds only apply to <see cref="StatusProperty"/>s whose values are numeric, and those property values must be c |
| | | 27 | | /// The static <see cref="DefaultPropertyThresholds"/> property provides access to the thresholds for all currently-load |
| | | 28 | | /// </summary> |
| | | 29 | | /// <remarks> |
| | | 30 | | /// <pitch>Turns a measured number (free disk space, queue depth, latency) into a <see cref="StatusRating"/>: three opti |
| | | 31 | | /// <pledge> |
| | | 32 | | /// The three thresholds must be monotonic; their ordering determines whether low or high values are good (the explicit |
| | | 33 | | /// <see cref="Rate(string,float,float)"/> is pure: it returns a <see cref="StatusAuditAlert"/> whose rating is position |
| | | 34 | | /// </pledge> |
| | | 35 | | /// <plan> |
| | | 36 | | /// Rating is a threshold ladder walked in the good-to-bad direction indicated by the nature, with linear interpolation |
| | | 37 | | /// </plan> |
| | | 38 | | /// </remarks> |
| | | 39 | | public class StatusPropertyThresholds |
| | | 40 | | { |
| | | 41 | | private static ConcurrentDictionary<string, StatusPropertyThresholds> _thresholds = InitializeThresholds(); |
| | | 42 | | private static readonly DefaultStatusThresholds _thresholdsAccessor = new(_thresholds); |
| | | 43 | | /// <summary> |
| | | 44 | | /// Gets a <see cref="IStatusThresholdsRegistry"/> containing the default status thresholds (those assigned via <see |
| | | 45 | | /// </summary> |
| | | 46 | | public static IStatusThresholdsRegistry DefaultPropertyThresholds => _thresholdsAccessor; |
| | | 47 | | |
| | | 48 | | private static ConcurrentDictionary<string, StatusPropertyThresholds> InitializeThresholds() |
| | | 49 | | { |
| | | 50 | | _thresholds = new ConcurrentDictionary<string, StatusPropertyThresholds>(); |
| | | 51 | | // hook into all subsequent assembly loads so we can register their thresholds |
| | | 52 | | AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad; |
| | | 53 | | // register the thresholds from all currently-loaded assemblies |
| | | 54 | | foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) |
| | | 55 | | { |
| | | 56 | | RegisterAssemblyThresholds(assembly); |
| | | 57 | | } |
| | | 58 | | return _thresholds; |
| | | 59 | | } |
| | | 60 | | private static void CurrentDomain_AssemblyLoad(object? sender, AssemblyLoadEventArgs args) |
| | | 61 | | { |
| | | 62 | | RegisterAssemblyThresholds(args.LoadedAssembly); |
| | | 63 | | } |
| | | 64 | | private static void RegisterAssemblyThresholds(System.Reflection.Assembly assembly) |
| | | 65 | | { |
| | | 66 | | // does the loaded assembly refer to this one (if it doesn't, it can't have the stuff we're looking for) |
| | | 67 | | if (assembly.DoesAssemblyReferDirectlyToAssembly(System.Reflection.Assembly.GetExecutingAssembly())) |
| | | 68 | | { |
| | | 69 | | // loop through all the types looking for types that are not abstract, inherit from StatusNode (directly or |
| | | 70 | | foreach (Type type in assembly.GetLoadableTypes()) |
| | | 71 | | { |
| | | 72 | | if (Status.IsTestableStatusCheckerClass(type)) |
| | | 73 | | { |
| | | 74 | | foreach (KeyValuePair<string, StatusPropertyThresholds> kvp in GetThresholds(type)) |
| | | 75 | | { |
| | | 76 | | _thresholds.TryAdd(kvp.Key.ToUpperInvariant(), kvp.Value); |
| | | 77 | | } |
| | | 78 | | } |
| | | 79 | | } |
| | | 80 | | } |
| | | 81 | | } |
| | | 82 | | private static IEnumerable<KeyValuePair<string, StatusPropertyThresholds>> GetThresholds(Type type) |
| | | 83 | | { |
| | | 84 | | object[] attributes = type.GetCustomAttributes(typeof(DefaultPropertyThresholdsAttribute), true); |
| | | 85 | | if (attributes != null && attributes.Length > 0) |
| | | 86 | | { |
| | | 87 | | foreach (DefaultPropertyThresholdsAttribute checkerThresholds in attributes.Where(o => o.GetType() == typeof |
| | | 88 | | { |
| | | 89 | | if (checkerThresholds.DeferToType != null) |
| | | 90 | | { |
| | | 91 | | foreach (KeyValuePair<string, StatusPropertyThresholds> kvp in GetThresholds(checkerThresholds.Defer |
| | | 92 | | { |
| | | 93 | | yield return new KeyValuePair<string, StatusPropertyThresholds>(checkerThresholds.PropertyPath + |
| | | 94 | | } |
| | | 95 | | } |
| | | 96 | | else if (checkerThresholds.Thresholds != null) |
| | | 97 | | { |
| | | 98 | | yield return new KeyValuePair<string, StatusPropertyThresholds>(checkerThresholds.PropertyPath, chec |
| | | 99 | | } |
| | | 100 | | } |
| | | 101 | | } |
| | | 102 | | } |
| | | 103 | | |
| | | 104 | | /// <summary> |
| | | 105 | | /// Constructs a <see cref="StatusPropertyThresholds"/> instance with the specified thresholds. |
| | | 106 | | /// </summary> |
| | | 107 | | /// <param name="nature">A <see cref="StatusThresholdNature"/> indicating whether or not low values are good. Only |
| | | 108 | | /// <param name="failVsAlertThreshold">The threshold which divides failures from alerts (at this value it counts as |
| | | 109 | | /// <param name="alertVsOkayThreshold">The threshold which divides alerts from okays (at this value it counts as an |
| | | 110 | | /// <param name="okayVsSuperlativeThreshold">The threshold which divides okays from superlatives (at this value it c |
| | | 111 | | public StatusPropertyThresholds(float? failVsAlertThreshold, float? alertVsOkayThreshold, float? okayVsSuperlativeTh |
| | | 112 | | { |
| | | 113 | | StatusThresholdNature? computedNature = null; |
| | | 114 | | if (failVsAlertThreshold > alertVsOkayThreshold || alertVsOkayThreshold > okayVsSuperlativeThreshold || failVsAl |
| | | 115 | | if (failVsAlertThreshold < alertVsOkayThreshold || alertVsOkayThreshold < okayVsSuperlativeThreshold || failVsAl |
| | | 116 | | { |
| | | 117 | | if (computedNature != null) throw new ArgumentException("The threshold values must be listed in either ascen |
| | | 118 | | computedNature = StatusThresholdNature.HighIsGood; |
| | | 119 | | } |
| | | 120 | | Nature = computedNature ?? nature; |
| | | 121 | | FailVsAlertThreshold = failVsAlertThreshold; |
| | | 122 | | AlertVsOkayThreshold = alertVsOkayThreshold; |
| | | 123 | | OkayVsSuperlativeThreshold = okayVsSuperlativeThreshold; |
| | | 124 | | } |
| | | 125 | | |
| | | 126 | | /// <summary> |
| | | 127 | | /// Gets a <see cref="StatusThresholdNature"/> indicating whether low values are good or not. |
| | | 128 | | /// </summary> |
| | | 129 | | public StatusThresholdNature Nature { get; } |
| | | 130 | | /// <summary> |
| | | 131 | | /// Gets the threshold value which divides failures from alerts. When the measured value is exactly equal to this v |
| | | 132 | | /// </summary> |
| | | 133 | | public float? FailVsAlertThreshold { get; } |
| | | 134 | | /// <summary> |
| | | 135 | | /// Gets the threshold value which divides alerts from okay. When the measured value is exactly equal to this value |
| | | 136 | | /// </summary> |
| | | 137 | | public float? AlertVsOkayThreshold { get; } |
| | | 138 | | /// <summary> |
| | | 139 | | /// Gets the threshold value which divides okays from superlatives. When the measured value is exactly equal to thi |
| | | 140 | | /// </summary> |
| | | 141 | | public float? OkayVsSuperlativeThreshold { get; } |
| | | 142 | | |
| | | 143 | | /// <summary> |
| | | 144 | | /// Rates the value based on the thresholds and gets a <see cref="StatusAuditAlert"/> indicating the status of the v |
| | | 145 | | /// </summary> |
| | | 146 | | /// <param name="propertyName">The name of the property being rated.</param> |
| | | 147 | | /// <param name="value">The value to be rated.</param> |
| | | 148 | | /// <returns>A <see cref="StatusAuditAlert"/> indicating the status relative to the thresholds.</returns> |
| | | 149 | | public StatusAuditAlert Rate(string propertyName, float value) |
| | | 150 | | { |
| | | 151 | | return Rate(propertyName, value, value); |
| | | 152 | | } |
| | | 153 | | /// <summary> |
| | | 154 | | /// Rates the value based on the thresholds and gets a <see cref="StatusAuditAlert"/> indicating the status of the v |
| | | 155 | | /// </summary> |
| | | 156 | | /// <param name="propertyName">The name of the property being rated.</param> |
| | | 157 | | /// <param name="lowValue">The low value of the range to be rated.</param> |
| | | 158 | | /// <param name="highValue">The high value of the range to be rated.</param> |
| | | 159 | | /// <returns>A <see cref="StatusAuditAlert"/> indicating the status relative to the thresholds.</returns> |
| | | 160 | | public StatusAuditAlert Rate(string propertyName, float lowValue, float highValue) |
| | | 161 | | { |
| | | 162 | | // if (lowValue < 0) throw new ArgumentOutOfRangeException(nameof(lowValue), "Status rating values must not b |
| | | 163 | | // if (highValue < 0) throw new ArgumentOutOfRangeException(nameof(highValue), "Status rating values must not |
| | | 164 | | string code = propertyName + ".Threshold"; |
| | | 165 | | string tersePrefix; |
| | | 166 | | string detailedPrefix = (lowValue == highValue) |
| | | 167 | | ? (propertyName + " is " + lowValue.ToSi(4) + " which is ") |
| | | 168 | | : (propertyName + " is between " + lowValue.ToSi(4) + " and " + highValue.ToSi(4) + " which is "); |
| | | 169 | | if (Nature == StatusThresholdNature.LowIsGood) |
| | | 170 | | { |
| | | 171 | | float value = highValue; |
| | | 172 | | tersePrefix = propertyName + ":" + value.ToSi(1); |
| | | 173 | | if (value < OkayVsSuperlativeThreshold) |
| | | 174 | | { |
| | | 175 | | float seriousness = LowIsGoodImportance(0.0f, OkayVsSuperlativeThreshold.Value, value); |
| | | 176 | | float rating = StatusRating.Superlative - seriousness; |
| | | 177 | | return new StatusAuditAlert(rating, code, tersePrefix + "<" + OkayVsSuperlativeThreshold.Value.ToSi(1), |
| | | 178 | | } |
| | | 179 | | else if (value < AlertVsOkayThreshold) |
| | | 180 | | { |
| | | 181 | | float lowThreshold = OkayVsSuperlativeThreshold ?? 0.0f; |
| | | 182 | | float seriousness = LowIsGoodImportance(lowThreshold, AlertVsOkayThreshold.Value, value); |
| | | 183 | | float rating = StatusRating.Okay - seriousness; |
| | | 184 | | return new StatusAuditAlert(rating, code, tersePrefix + ">=" + lowThreshold.ToSi(1), detailedPrefix + "i |
| | | 185 | | } |
| | | 186 | | else if (value < FailVsAlertThreshold) |
| | | 187 | | { |
| | | 188 | | float lowThreshold = AlertVsOkayThreshold ?? OkayVsSuperlativeThreshold ?? 0.0f; |
| | | 189 | | float seriousness = LowIsGoodImportance(lowThreshold, FailVsAlertThreshold.Value, value); |
| | | 190 | | float rating = StatusRating.Alert - seriousness; |
| | | 191 | | return new StatusAuditAlert(rating, code, tersePrefix + ">=" + lowThreshold.ToSi(1), detailedPrefix + "i |
| | | 192 | | } |
| | | 193 | | else |
| | | 194 | | { |
| | | 195 | | float lowThreshold = FailVsAlertThreshold ?? AlertVsOkayThreshold ?? OkayVsSuperlativeThreshold ?? 0.0f; |
| | | 196 | | float seriousness = LowIsGoodImportance(lowThreshold, float.MaxValue, value); |
| | | 197 | | float rating = StatusRating.Fail - seriousness; |
| | | 198 | | return new StatusAuditAlert(rating, code, tersePrefix + ">=" + lowThreshold.ToSi(1), detailedPrefix + "a |
| | | 199 | | } |
| | | 200 | | } |
| | | 201 | | else |
| | | 202 | | { |
| | | 203 | | float value = lowValue; |
| | | 204 | | tersePrefix = propertyName + ":" + value.ToSi(1); |
| | | 205 | | if (value > OkayVsSuperlativeThreshold) |
| | | 206 | | { |
| | | 207 | | float seriousness = HighIsGoodImportance(OkayVsSuperlativeThreshold.Value, float.MaxValue, value); |
| | | 208 | | float rating = StatusRating.Superlative - seriousness; |
| | | 209 | | return new StatusAuditAlert(rating, code, tersePrefix + ">" + OkayVsSuperlativeThreshold.Value.ToSi(1), |
| | | 210 | | } |
| | | 211 | | if (value > AlertVsOkayThreshold) |
| | | 212 | | { |
| | | 213 | | float highThreshold = OkayVsSuperlativeThreshold ?? float.MaxValue; |
| | | 214 | | float seriousness = HighIsGoodImportance(AlertVsOkayThreshold.Value, highThreshold, value); |
| | | 215 | | float rating = StatusRating.Okay - seriousness; |
| | | 216 | | return new StatusAuditAlert(rating, code, tersePrefix + "<=" + highThreshold.ToSi(1), detailedPrefix + " |
| | | 217 | | } |
| | | 218 | | else if (value > FailVsAlertThreshold) |
| | | 219 | | { |
| | | 220 | | float highThreshold = AlertVsOkayThreshold ?? OkayVsSuperlativeThreshold ?? float.MaxValue; |
| | | 221 | | float seriousness = HighIsGoodImportance(FailVsAlertThreshold.Value, highThreshold, value); |
| | | 222 | | float rating = StatusRating.Alert - seriousness; |
| | | 223 | | return new StatusAuditAlert(rating, code, tersePrefix + "<=" + highThreshold.ToSi(1), detailedPrefix + " |
| | | 224 | | } |
| | | 225 | | else |
| | | 226 | | { |
| | | 227 | | float highThreshold = FailVsAlertThreshold ?? AlertVsOkayThreshold ?? OkayVsSuperlativeThreshold ?? floa |
| | | 228 | | float seriousness = HighIsGoodImportance(0.0f, highThreshold, value); |
| | | 229 | | float rating = StatusRating.Fail - seriousness; |
| | | 230 | | return new StatusAuditAlert(rating, code, tersePrefix + "<=" + highThreshold.ToSi(1), detailedPrefix + " |
| | | 231 | | } |
| | | 232 | | } |
| | | 233 | | } |
| | | 234 | | private static float LowIsGoodImportance(float low, float high, float value) |
| | | 235 | | { |
| | | 236 | | if (value <= low) return 0.0f; |
| | | 237 | | if (value >= high) return 1.0f; |
| | | 238 | | return (float)((value - low) / (high - low)); |
| | | 239 | | } |
| | | 240 | | private static float HighIsGoodImportance(float low, float high, float value) |
| | | 241 | | { |
| | | 242 | | return 1.0f - LowIsGoodImportance(low, high, value); |
| | | 243 | | } |
| | | 244 | | } |
| | | 245 | | |
| | | 246 | | internal class DefaultStatusThresholds : IStatusThresholdsRegistry |
| | | 247 | | { |
| | | 248 | | private readonly ConcurrentDictionary<string, StatusPropertyThresholds> _thresholds; |
| | | 249 | | |
| | 2 | 250 | | public DefaultStatusThresholds(ConcurrentDictionary<string, StatusPropertyThresholds> thresholds) |
| | | 251 | | { |
| | 2 | 252 | | _thresholds = thresholds; |
| | 2 | 253 | | } |
| | | 254 | | |
| | | 255 | | public StatusPropertyThresholds? GetThresholds(string path) |
| | | 256 | | { |
| | | 257 | | StatusPropertyThresholds? value; |
| | 2 | 258 | | if (!_thresholds.TryGetValue(path.ToUpperInvariant(), out value)) return null; |
| | 2 | 259 | | return value; |
| | | 260 | | } |
| | | 261 | | } |
| | | 262 | | /// <summary> |
| | | 263 | | /// An attribute class that identifies the default property thresholds for a status test. |
| | | 264 | | /// </summary> |
| | | 265 | | /// <remarks> |
| | | 266 | | /// <pitch>The declarative way for a checker or auditor to ship sensible default thresholds with its code: decorate the |
| | | 267 | | /// <pledge> |
| | | 268 | | /// Attributes on testable checker classes are gathered into <see cref="StatusPropertyThresholds.DefaultPropertyThreshol |
| | | 269 | | /// The <see cref="DeferToType"/> form composes: it imports the thresholds declared on another type, prefixing each with |
| | | 270 | | /// </pledge> |
| | | 271 | | /// <priority> |
| | | 272 | | /// 1. Working thresholds shipped with the code over correct ones supplied at deployment: a checker declares its own def |
| | | 273 | | /// 2. Living beside the code it rates over living in one central table: thresholds are declared on the checker class an |
| | | 274 | | /// </priority> |
| | | 275 | | /// </remarks> |
| | | 276 | | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] |
| | | 277 | | public sealed class DefaultPropertyThresholdsAttribute : Attribute |
| | | 278 | | { |
| | | 279 | | |
| | | 280 | | /// <summary> |
| | | 281 | | /// Constructs a default property thresholds instance by looking at a different type whose threshold attribute will |
| | | 282 | | /// </summary> |
| | | 283 | | /// <param name="propertyPath">The path to the indicated node.</param> |
| | | 284 | | /// <param name="deferToType">The <see cref="Type"/> that will be added at the specified node path.</param> |
| | | 285 | | public DefaultPropertyThresholdsAttribute(string propertyPath, Type deferToType) |
| | | 286 | | { |
| | | 287 | | PropertyPath = propertyPath; |
| | | 288 | | Thresholds = null; |
| | | 289 | | DeferToType = deferToType; |
| | | 290 | | } |
| | | 291 | | /// <summary> |
| | | 292 | | /// Constructs a default property thresholds attribute instance using the specified parameters. |
| | | 293 | | /// Note that attribute parameters cannot take nullable values, so we use <see cref="float.NaN"/> instead to indicat |
| | | 294 | | /// </summary> |
| | | 295 | | /// <param name="propertyPath">The path to the property with a default threshold.</param> |
| | | 296 | | /// <param name="failVsAlertThreshold">The first value that is a failure instead of an alert. <see cref="float.NaN" |
| | | 297 | | /// <param name="alertVsOkayThreshold">The first value that is an alert instead of okay. <see cref="float.NaN"/> if |
| | | 298 | | /// <param name="okayVsSuperlativeThreshold">The first value that is okay instead of superlative. <see cref="float. |
| | | 299 | | /// <param name="thresholdNature">A <see cref="StatusThresholdNature"/> indicating whether low values are good or ba |
| | | 300 | | public DefaultPropertyThresholdsAttribute(string propertyPath, float failVsAlertThreshold = float.NaN, float alertVs |
| | | 301 | | { |
| | | 302 | | PropertyPath = propertyPath; |
| | | 303 | | Thresholds = new StatusPropertyThresholds(float.IsNaN(failVsAlertThreshold) ? null : failVsAlertThreshold, float |
| | | 304 | | DeferToType = null; |
| | | 305 | | } |
| | | 306 | | /// <summary> |
| | | 307 | | /// Gets the name of the property the thresholds apply to. |
| | | 308 | | /// </summary> |
| | | 309 | | public string PropertyPath { get; } |
| | | 310 | | /// <summary> |
| | | 311 | | /// Gets the <see cref="StatusPropertyThresholds"/> for the corresponding property. |
| | | 312 | | /// May be null if deferred to another type but that type has no attribute thresholds. |
| | | 313 | | /// </summary> |
| | | 314 | | public StatusPropertyThresholds? Thresholds { get; } |
| | | 315 | | /// <summary> |
| | | 316 | | /// A type to defer to for default property thresholds. Thresholds attached to that type will be added with the pro |
| | | 317 | | /// </summary> |
| | | 318 | | public Type? DeferToType { get; } |
| | | 319 | | /// <summary> |
| | | 320 | | /// Gets the status rating threshold that distinguishes failures from alerts. |
| | | 321 | | /// </summary> |
| | | 322 | | public float FailVsAlertThreshold => Thresholds?.FailVsAlertThreshold ?? float.NaN; |
| | | 323 | | /// <summary> |
| | | 324 | | /// Gets the status rating threshold that distinguishes alerts from okay. |
| | | 325 | | /// </summary> |
| | | 326 | | public float AlertVsOkayThreshold => Thresholds?.AlertVsOkayThreshold ?? float.NaN; |
| | | 327 | | /// <summary> |
| | | 328 | | /// Gets the status rating threshold that distinguishes okay from superlative. |
| | | 329 | | /// </summary> |
| | | 330 | | public float OkayVsSuperlativeThreshold => Thresholds?.OkayVsSuperlativeThreshold ?? float.NaN; |
| | | 331 | | /// <summary> |
| | | 332 | | /// Gets the status rating threshold that distinguishes okay from superlative. |
| | | 333 | | /// </summary> |
| | | 334 | | public StatusThresholdNature ThresholdNature => Thresholds?.Nature ?? StatusThresholdNature.HighIsGood; |
| | | 335 | | } |
| | | 336 | | /// <summary> |
| | | 337 | | /// An interface that abstracts the querying of thresholds used to rate system statuses. |
| | | 338 | | /// </summary> |
| | | 339 | | /// <remarks> |
| | | 340 | | /// <pitch>The seam for overriding how property values are rated: supply your own registry to summarization to replace t |
| | | 341 | | /// <pledge>A pure lookup from a dotted target-system property path to its <see cref="StatusPropertyThresholds"/>; retur |
| | | 342 | | /// </remarks> |
| | | 343 | | public interface IStatusThresholdsRegistry |
| | | 344 | | { |
| | | 345 | | /// <summary> |
| | | 346 | | /// Gets the <see cref="StatusPropertyThresholds"/> instance for the specified path, or null if no overriding thresh |
| | | 347 | | /// </summary> |
| | | 348 | | /// <param name="path">The target system path whose thresholds are desired.</param> |
| | | 349 | | /// <returns>A <see cref="StatusPropertyThresholds"/> instance containing the status rating thresholds, or null if t |
| | | 350 | | StatusPropertyThresholds? GetThresholds(string path); |
| | | 351 | | } |