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",
};
}