Files
MoShiro/backend/src/MoShiro.Backend.Analysis/Engine/MetricDef.cs
T
justme 83e5120377 refactor!: rename the project from Monitor to MoShiro
Namespaces, assemblies, projects and directories, the solution, binaries,
images, systemd units, config paths, environment variables, protocol method
names, the media type and the raw chunk magic all move to MoShiro/moshiro.
Existing data and deployments are not compatible and must be recreated.
Also fix the architecture tests after the solution layout change and make
the update tests independent of their order.
2026-10-07 01:49:04 +08:00

207 lines
7.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
/// <summary>长尾比:p99 / p50(从同一个直方图导出,所以窗口合并之后仍然精确)。</summary>
Tail,
}
/// <summary>算子:把一个流里的原始行变成观测值。算子都是纯函数,每个都有边界用例测试。</summary>
public abstract class MetricOp
{
/// <summary>算子的名称和参数,参与指标指纹;改变语义时必须同时改变它。</summary>
public abstract string Signature { get; }
public abstract void Evaluate(ChunkView view, RawStream stream, IObservationSink sink);
}
/// <summary>导出值:<c>指标id.统计量</c>,例如 <c>rtt.p95</c>。已发布的 id 不能改名,只能新增。</summary>
public sealed record MetricExport(string Id, Stat Stat, Unit Unit, double? Good = null, double? Poor = null);
/// <summary>一行声明 = 一个指标:输入(kind.stream)+ 算子 + 累加器 + 导出。</summary>
public sealed class MetricDef
{
public MetricDef(string id, AccKind kind, IReadOnlyList<string> from, MetricOp op, string label, string description, Better better, IReadOnlyList<MetricExport> 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; }
/// <summary><c>kind.stream</c>;<c>*</c> 表示所有具有该 stream 的 Kind。</summary>
public IReadOnlyList<string> From { get; }
public MetricOp Op { get; }
public string Label { get; }
public string Description { get; }
public Better Better { get; }
public IReadOnlyList<MetricExport> Exports { get; }
public int Version { get; }
/// <summary>由声明内容和算子签名计算得出,变化时触发回填。</summary>
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;
}
/// <summary>从累加器导出一个值;没有样本时返回 null。<paramref name="durationSeconds"/> 用于速率类导出。</summary>
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];
}
}
/// <summary>声明指标的简写。</summary>
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",
};
}