using System.Security.Cryptography; using System.Text; using MoShiro.Protocol; namespace MoShiro.Backend.Analysis; public enum Unit { None, Ms, Pct, Count, PerMin, PerHour, Mbps, Packets, Score, } public enum Better { None, Lower, Higher, } public enum Stat { P50, P90, P95, P99, Mean, Max, Min, Pct, Count, PerMin, PerHour, P999, /// 长尾比:p99 / p50(从同一个直方图导出,所以窗口合并之后仍然精确)。 Tail, } /// 算子:把一个流里的原始行变成观测值。算子都是纯函数,每个都有边界用例测试。 public abstract class MetricOp { /// 算子的名称和参数,参与指标指纹;改变语义时必须同时改变它。 public abstract string Signature { get; } public abstract void Evaluate(ChunkView view, RawStream stream, IObservationSink sink); } /// 导出值:指标id.统计量,例如 rtt.p95。已发布的 id 不能改名,只能新增。 public sealed record MetricExport(string Id, Stat Stat, Unit Unit, double? Good = null, double? Poor = null); /// 一行声明 = 一个指标:输入(kind.stream)+ 算子 + 累加器 + 导出。 public sealed class MetricDef { public MetricDef(string id, AccKind kind, IReadOnlyList from, MetricOp op, string label, string description, Better better, IReadOnlyList exports, int version = 1) { Id = id; Kind = kind; From = from; Op = op; Label = label; Description = description; Better = better; Exports = exports; Version = version; Fingerprint = ComputeFingerprint(); } public string Id { get; } public AccKind Kind { get; } /// kind.stream;* 表示所有具有该 stream 的 Kind。 public IReadOnlyList From { get; } public MetricOp Op { get; } public string Label { get; } public string Description { get; } public Better Better { get; } public IReadOnlyList Exports { get; } public int Version { get; } /// 由声明内容和算子签名计算得出,变化时触发回填。 public string Fingerprint { get; } public bool Matches(string kind, string stream) { foreach (string source in From) { int dot = source.IndexOf('.', StringComparison.Ordinal); if (source.AsSpan(dot + 1).SequenceEqual(stream) && (source.AsSpan(0, dot).SequenceEqual("*") || source.AsSpan(0, dot).SequenceEqual(kind))) { return true; } } return false; } /// 从累加器导出一个值;没有样本时返回 null。 用于速率类导出。 public static double? Export(Accumulator acc, MetricExport export, double durationSeconds) { switch (acc) { case HistogramAcc h: return export.Stat switch { Stat.P50 => h.Quantile(0.5), Stat.P90 => h.Quantile(0.9), Stat.P95 => h.Quantile(0.95), Stat.P99 => h.Quantile(0.99), Stat.P999 => h.Quantile(0.999), Stat.Tail => h.Quantile(0.5) is > 0 and double p50 && h.Quantile(0.99) is { } p99 ? p99 / p50 : null, Stat.Mean => h.Mean, Stat.Max => h.Count == 0 ? null : h.Max, Stat.Min => h.Count == 0 ? null : h.Min, _ => null, }; case RatioAcc r: return export.Stat == Stat.Pct ? r.Percent : null; case CounterAcc c: return export.Stat switch { Stat.Count => c.Value, Stat.PerMin => durationSeconds > 0 ? c.Value / (durationSeconds / 60.0) : null, Stat.PerHour => durationSeconds > 0 ? c.Value / (durationSeconds / 3600.0) : null, _ => null, }; case MaxAcc m: return m.Result; case MinAcc n: return n.Result; case SumAcc s: return export.Stat == Stat.Mean ? s.Mean : s.Count == 0 ? null : s.Sum; case RunLengthAcc rl: return export.Stat switch { Stat.Max => rl.Runs == 0 ? null : rl.Max, Stat.Mean => rl.Mean, _ => null, }; default: return null; } } private string ComputeFingerprint() { var text = new StringBuilder() .Append('v').Append(Version).Append('|').Append(Id).Append('|').Append(Kind).Append('|') .AppendJoin(',', From).Append('|').Append(Op.Signature).Append('|'); foreach (MetricExport e in Exports) { text.Append(e.Id).Append(':').Append(e.Stat).Append(';'); } return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(text.ToString())))[..16]; } } /// 声明指标的简写。 public static class Metric { public static MetricDef Histogram(string id, string[] from, MetricOp op, string label, string description, Unit unit, Stat[] exports, Better better = Better.Lower, double? good = null, double? poor = null) => new(id, AccKind.Histogram, from, op, label, description, better, [.. exports.Select(s => s == Stat.Tail ? new MetricExport(id + "." + StatName(s), s, Unit.None) : new MetricExport(id + "." + StatName(s), s, unit, good, poor))]); public static MetricDef Ratio(string id, string[] from, MetricOp op, string label, string description, Better better = Better.Lower, double? good = null, double? poor = null, string? exportId = null) => new(id, AccKind.Ratio, from, op, label, description, better, [new MetricExport(exportId ?? id, Stat.Pct, Unit.Pct, good, poor)]); public static MetricDef Counter(string id, string[] from, MetricOp op, string label, string description, MetricExport export, Better better = Better.Lower) => new(id, AccKind.Counter, from, op, label, description, better, [export]); public static MetricDef Max(string id, string[] from, MetricOp op, string label, string description, Unit unit, string? exportId = null, Better better = Better.Lower, double? good = null, double? poor = null) => new(id, AccKind.Max, from, op, label, description, better, [new MetricExport(exportId ?? id, Stat.Max, unit, good, poor)]); public static MetricDef RunLength(string id, string[] from, MetricOp op, string label, string description, params MetricExport[] exports) => new(id, AccKind.RunLength, from, op, label, description, Better.Lower, exports); public static string StatName(Stat stat) => stat switch { Stat.P50 => "p50", Stat.P90 => "p90", Stat.P95 => "p95", Stat.P99 => "p99", Stat.Mean => "mean", Stat.Max => "max", Stat.Min => "min", Stat.Pct => "pct", Stat.Count => "count", Stat.PerMin => "per_min", Stat.P999 => "p999", Stat.Tail => "tail", _ => "per_h", }; }