chore: initial project import
This commit is contained in:
commit
3fc4e5ca53
347 files changed
+56172
No files matched your search
@@ -0,0 +1,128 @@
|
||||
namespace Monitor.Protocol;
|
||||
|
||||
[Flags]
|
||||
public enum Scope
|
||||
{
|
||||
Line = 1,
|
||||
Target = 2,
|
||||
}
|
||||
|
||||
public enum ColumnType
|
||||
{
|
||||
U8,
|
||||
U16,
|
||||
U32,
|
||||
U64,
|
||||
I32,
|
||||
I64,
|
||||
}
|
||||
|
||||
public enum ColumnEnc
|
||||
{
|
||||
Plain,
|
||||
Delta,
|
||||
}
|
||||
|
||||
/// <summary>时间列的时钟归属:Probe、Agent 各自的单调时钟(微秒),或所在一侧的本地时钟。</summary>
|
||||
public enum ColumnRole
|
||||
{
|
||||
None,
|
||||
TsProbe,
|
||||
TsAgent,
|
||||
TsLocal,
|
||||
}
|
||||
|
||||
public sealed record ColumnSpec(string Name, ColumnType Type, ColumnEnc Enc = ColumnEnc.Plain, bool Nullable = false, ColumnRole Role = ColumnRole.None);
|
||||
|
||||
/// <summary>数据面上的一侧:Probe 永远是拨号方,Agent 永远是监听方。</summary>
|
||||
public enum Side
|
||||
{
|
||||
Probe,
|
||||
Agent,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一个记录流(一张表)。<see cref="Driver"/> 是行的主导方:行由它发出的包或事件决定,另一侧的列按 <see cref="Key"/>(例如 <c>seq</c>)补充,
|
||||
/// 缺失就是缺失(回显缺 t2/t3 表示上行丢失,单向数据流缺 t_recv 表示丢失)。没有 <see cref="Key"/> 的流只有主导方一侧的列,原样透传。
|
||||
/// 每一列属于哪一侧由它的时钟决定(<c>TsProbe</c> 属于 Probe,<c>TsAgent</c> 属于 Agent),其他列属于主导方。
|
||||
/// <see cref="Union"/> 流是例外:两侧各自记录完整的行(行里有一列标明是哪一侧),合并时直接拼接,不连接。
|
||||
/// 这一条声明同时决定了两端各记录什么、Agent 怎么合并,新增一种记录流不需要写合并代码。
|
||||
/// </summary>
|
||||
public sealed record StreamSpec(string Name, IReadOnlyList<ColumnSpec> Columns, Side Driver = Side.Probe, string? Key = null, bool Union = false)
|
||||
{
|
||||
public Side OwnerOf(ColumnSpec column) => column.Role switch
|
||||
{
|
||||
ColumnRole.TsProbe => Side.Probe,
|
||||
ColumnRole.TsAgent => Side.Agent,
|
||||
_ => Driver,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 该侧记录的列(不含连接键:键两侧都有,由行的编号表示)。<see cref="Union"/> 流两侧各自记录完整的行(例如各自套接字的内核采样),
|
||||
/// 所以每一侧拥有所有列。
|
||||
/// </summary>
|
||||
public IReadOnlyList<ColumnSpec> ColumnsOf(Side side) =>
|
||||
[.. Columns.Where(c => c.Name != Key && (Union || OwnerOf(c) == side))];
|
||||
}
|
||||
|
||||
/// <summary>积木组合(只描述,不含代码;节点按这个描述组装执行图)。</summary>
|
||||
public sealed record BlockSet(string Transport, string Pacer, string Payload, string Behavior, IReadOnlyList<string> Samplers);
|
||||
|
||||
/// <summary>
|
||||
/// 采集方法声明:积木组合 + 参数 + 记录的列 + 带宽预算。
|
||||
/// 这一条声明同时被 Probe/Agent(组装执行)、Backend(校验与目录)、Analysis(按列名读数据)使用。
|
||||
/// </summary>
|
||||
public sealed record KindSpec(
|
||||
string Id,
|
||||
string Title,
|
||||
string Description,
|
||||
Scope Scopes,
|
||||
BlockSet Blocks,
|
||||
IReadOnlyList<ParamSpec> Params,
|
||||
IReadOnlyList<StreamSpec> Streams,
|
||||
Func<ParamValues, long> BudgetBitsPerSecond)
|
||||
{
|
||||
public StreamSpec? FindStream(string name)
|
||||
{
|
||||
foreach (StreamSpec stream in Streams)
|
||||
{
|
||||
if (stream.Name == name)
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Col
|
||||
{
|
||||
public static ColumnSpec U8(string name, bool nullable = false) => new(name, ColumnType.U8, ColumnEnc.Plain, nullable);
|
||||
|
||||
public static ColumnSpec U16(string name, bool nullable = false) => new(name, ColumnType.U16, ColumnEnc.Plain, nullable);
|
||||
|
||||
public static ColumnSpec U32(string name, bool nullable = false) => new(name, ColumnType.U32, ColumnEnc.Plain, nullable);
|
||||
|
||||
public static ColumnSpec U64(string name, ColumnEnc enc = ColumnEnc.Plain) => new(name, ColumnType.U64, enc);
|
||||
|
||||
/// <summary>单调递增的序号:差分编码。</summary>
|
||||
public static ColumnSpec Seq(string name) => new(name, ColumnType.U32, ColumnEnc.Delta);
|
||||
|
||||
public static ColumnSpec TsProbe(string name, bool nullable = false) => new(name, ColumnType.I64, ColumnEnc.Delta, nullable, ColumnRole.TsProbe);
|
||||
|
||||
public static ColumnSpec TsAgent(string name, bool nullable = false) => new(name, ColumnType.I64, ColumnEnc.Delta, nullable, ColumnRole.TsAgent);
|
||||
|
||||
public static ColumnSpec TsLocal(string name) => new(name, ColumnType.I64, ColumnEnc.Delta, false, ColumnRole.TsLocal);
|
||||
}
|
||||
|
||||
/// <summary>带宽预算的估算函数(bit/s)。</summary>
|
||||
public static class Bandwidth
|
||||
{
|
||||
private const int UdpOverheadBytes = 28;
|
||||
private const int TcpOverheadBytes = 52;
|
||||
|
||||
public static long UdpEcho(long rateHz, long sizeBytes) => rateHz * ((sizeBytes + UdpOverheadBytes) * 8) * 2;
|
||||
|
||||
public static long TcpEcho(long rateHz, long sizeBytes) => rateHz * ((sizeBytes + TcpOverheadBytes) * 8) * 2;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
namespace Monitor.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Kind 目录。新增一种采集方法 = 在这里加一个静态字段并放进 <see cref="All"/>;
|
||||
/// 参数一律用 <see cref="P"/> 声明默认值和上下限。
|
||||
/// </summary>
|
||||
public static class Kinds
|
||||
{
|
||||
private static readonly string[] s_directions = ["fwd", "rev", "bidir"];
|
||||
|
||||
private static readonly StreamSpec s_echoMsg = new(
|
||||
"msg",
|
||||
[
|
||||
Col.Seq("seq"), Col.U16("size"),
|
||||
Col.TsProbe("t1"), Col.TsAgent("t2", nullable: true), Col.TsAgent("t3", nullable: true), Col.TsProbe("t4", nullable: true),
|
||||
],
|
||||
Side.Probe,
|
||||
"seq");
|
||||
|
||||
private static readonly StreamSpec s_tcpInfo = new(
|
||||
"tcp_info",
|
||||
[
|
||||
Col.TsLocal("ts"), Col.U8("side"), Col.U32("srtt_us"), Col.U32("rttvar_us"), Col.U32("cwnd"),
|
||||
Col.U32("retrans_total"), Col.U32("segs_out"), Col.U64("bytes_acked"),
|
||||
],
|
||||
Union: true);
|
||||
|
||||
public static readonly KindSpec UdpEcho = new(
|
||||
"udp_echo",
|
||||
"UDP echo",
|
||||
"Fixed-rate UDP echo with four timestamps per packet.",
|
||||
Scope.Line,
|
||||
new BlockSet("udp", "fixed", "fixed", "echo", []),
|
||||
[
|
||||
P.Int("rate_hz", 10, 1, 200, "Hz"),
|
||||
P.Int("size", 64, 44, 1400, "B"),
|
||||
P.Int("port", 7443, 1, 65535),
|
||||
],
|
||||
[s_echoMsg],
|
||||
p => Bandwidth.UdpEcho(p.GetInt("rate_hz"), p.GetInt("size")));
|
||||
|
||||
public static readonly KindSpec UdpTrain = new(
|
||||
"udp_train",
|
||||
"UDP train",
|
||||
"Fixed-rate or laddered one-way UDP stream (Agent to Probe) for loss and jitter under load.",
|
||||
Scope.Line,
|
||||
new BlockSet("udp", "ladder", "fixed", "sink", []),
|
||||
[
|
||||
P.Number("mbps", 10, 0.1, 1000, "Mbit/s"),
|
||||
P.Int("size", 1250, 200, 1400, "B"),
|
||||
P.Int("duration_s", 10, 1, 120, "s"),
|
||||
P.Enum("direction", "rev", s_directions),
|
||||
P.Int("port", 7443, 1, 65535),
|
||||
],
|
||||
[
|
||||
new StreamSpec("pkt", [Col.Seq("seq"), Col.U16("size"), Col.TsAgent("t_send"), Col.TsProbe("t_recv", nullable: true)], Side.Agent, "seq"),
|
||||
new StreamSpec("step", [Col.U16("index"), Col.U32("mbps"), Col.TsProbe("start_ts"), Col.TsProbe("end_ts")]),
|
||||
],
|
||||
p => (long)(p.GetNumber("mbps") * 1_000_000));
|
||||
|
||||
public static readonly KindSpec TcpEcho = new(
|
||||
"tcp_echo",
|
||||
"TCP echo",
|
||||
"Small TCP messages echoed with four timestamps, plus periodic kernel tcp_info samples.",
|
||||
Scope.Line,
|
||||
new BlockSet("tcp", "fixed", "uniform", "echo", ["tcp_info"]),
|
||||
[
|
||||
P.Int("rate_hz", 20, 1, 200, "Hz"),
|
||||
P.Int("min", 32, 16, 1400, "B"),
|
||||
P.Int("max", 64, 16, 1400, "B"),
|
||||
P.Int("tcp_info_ms", 100, 10, 1000, "ms"),
|
||||
P.Int("port", 7444, 1, 65535),
|
||||
P.Bool("tls", false),
|
||||
],
|
||||
[s_echoMsg, s_tcpInfo],
|
||||
p => Bandwidth.TcpEcho(p.GetInt("rate_hz"), p.GetInt("max")));
|
||||
|
||||
public static readonly KindSpec TcpBulk = new(
|
||||
"tcp_bulk",
|
||||
"TCP bulk",
|
||||
"Bulk TCP transfer on several streams with periodic byte counters and tcp_info.",
|
||||
Scope.Line,
|
||||
new BlockSet("tcp", "fixed", "fixed", "source", ["tcp_info"]),
|
||||
[
|
||||
P.Int("streams", 4, 1, 16),
|
||||
P.Int("duration_s", 4, 1, 120, "s"),
|
||||
P.Enum("direction", "rev", s_directions),
|
||||
P.Int("sample_ms", 100, 20, 1000, "ms"),
|
||||
P.Int("port", 7444, 1, 65535),
|
||||
P.Bool("tls", true),
|
||||
],
|
||||
[
|
||||
new StreamSpec("bytes", [Col.TsProbe("ts"), Col.U8("conn"), Col.U64("bytes")]),
|
||||
s_tcpInfo,
|
||||
],
|
||||
_ => 0);
|
||||
|
||||
public static readonly KindSpec Connect = new(
|
||||
"connect",
|
||||
"Connect",
|
||||
"Repeated TCP and TLS connection establishment with per-phase timings and outcome.",
|
||||
Scope.Line | Scope.Target,
|
||||
new BlockSet("tcp", "fixed", "fixed", "connect", []),
|
||||
[
|
||||
P.Int("per_minute", 6, 1, 60),
|
||||
P.Bool("tls", true),
|
||||
P.Int("timeout_ms", 5000, 500, 30000, "ms"),
|
||||
P.Int("port", 7444, 1, 65535),
|
||||
],
|
||||
[
|
||||
new StreamSpec(
|
||||
"conn",
|
||||
[Col.Seq("attempt"), Col.TsProbe("ts"), Col.U32("tcp_us", nullable: true), Col.U32("tls_us", nullable: true), Col.U8("outcome")]),
|
||||
],
|
||||
p => p.GetInt("per_minute") * 4000 / 60);
|
||||
|
||||
public static readonly KindSpec Hold = new(
|
||||
"hold",
|
||||
"Hold",
|
||||
"A long-lived TLS connection with periodic heartbeats; records resets and lifetime.",
|
||||
Scope.Line,
|
||||
new BlockSet("tls", "fixed", "fixed", "hold", []),
|
||||
[
|
||||
P.Int("heartbeat_s", 30, 5, 300, "s"),
|
||||
P.Int("port", 7444, 1, 65535),
|
||||
],
|
||||
[
|
||||
new StreamSpec("hold", [Col.TsProbe("ts"), Col.U8("event"), Col.U32("lifetime_ms")]),
|
||||
],
|
||||
p => 2000 / Math.Max(1, p.GetInt("heartbeat_s")) * 8);
|
||||
|
||||
public static readonly KindSpec IcmpEcho = new(
|
||||
"icmp_echo",
|
||||
"ICMP echo",
|
||||
"Probe or Agent side ICMP echo to a target address.",
|
||||
Scope.Target,
|
||||
new BlockSet("icmp", "fixed", "fixed", "echo", []),
|
||||
[
|
||||
P.Int("rate_hz", 1, 1, 20, "Hz"),
|
||||
P.Int("size", 64, 44, 1400, "B"),
|
||||
],
|
||||
[
|
||||
new StreamSpec("msg", [Col.Seq("seq"), Col.U16("size"), Col.TsLocal("t1"), new ColumnSpec("t4", ColumnType.I64, ColumnEnc.Delta, true, ColumnRole.TsLocal)]),
|
||||
],
|
||||
p => Bandwidth.UdpEcho(p.GetInt("rate_hz"), p.GetInt("size")));
|
||||
|
||||
public static readonly IReadOnlyList<KindSpec> All = [UdpEcho, UdpTrain, TcpEcho, TcpBulk, Connect, Hold, IcmpEcho];
|
||||
|
||||
public static KindSpec? Find(string id)
|
||||
{
|
||||
foreach (KindSpec kind in All)
|
||||
{
|
||||
if (kind.Id == id)
|
||||
{
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Monitor.Protocol;
|
||||
|
||||
public enum ParamType
|
||||
{
|
||||
Int,
|
||||
Number,
|
||||
Bool,
|
||||
Enum,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 参数定义:类型、默认值、上下限。节点与 Backend 使用同一套校验,禁止写没有上下限的参数。
|
||||
/// </summary>
|
||||
public abstract record ParamSpec(string Name, string Description, string? Unit)
|
||||
{
|
||||
public abstract ParamType Type { get; }
|
||||
|
||||
/// <summary>用于目录展示的默认值。</summary>
|
||||
public abstract JsonElement DefaultValue { get; }
|
||||
|
||||
/// <summary>校验并规范化一个参数值;失败时返回错误说明。</summary>
|
||||
public abstract bool TryNormalize(JsonElement input, out JsonElement normalized, out string error);
|
||||
|
||||
protected static JsonElement Of(long value) => JsonSerializer.SerializeToElement(value, ParamJson.Default.Int64);
|
||||
|
||||
protected static JsonElement Of(double value) => JsonSerializer.SerializeToElement(value, ParamJson.Default.Double);
|
||||
|
||||
protected static JsonElement Of(bool value) => JsonSerializer.SerializeToElement(value, ParamJson.Default.Boolean);
|
||||
|
||||
protected static JsonElement Of(string value) => JsonSerializer.SerializeToElement(value, ParamJson.Default.String);
|
||||
}
|
||||
|
||||
public sealed record IntParam(string Name, long Default, long Min, long Max, string? Unit = null, string Description = "")
|
||||
: ParamSpec(Name, Description, Unit)
|
||||
{
|
||||
public override ParamType Type => ParamType.Int;
|
||||
|
||||
public override JsonElement DefaultValue => Of(Default);
|
||||
|
||||
public override bool TryNormalize(JsonElement input, out JsonElement normalized, out string error)
|
||||
{
|
||||
normalized = default;
|
||||
error = "";
|
||||
if (input.ValueKind != JsonValueKind.Number || !input.TryGetInt64(out long value))
|
||||
{
|
||||
error = "must be an integer";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value < Min || value > Max)
|
||||
{
|
||||
error = $"must be between {Min.ToString(CultureInfo.InvariantCulture)} and {Max.ToString(CultureInfo.InvariantCulture)}";
|
||||
return false;
|
||||
}
|
||||
|
||||
normalized = Of(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record NumberParam(string Name, double Default, double Min, double Max, string? Unit = null, string Description = "")
|
||||
: ParamSpec(Name, Description, Unit)
|
||||
{
|
||||
public override ParamType Type => ParamType.Number;
|
||||
|
||||
public override JsonElement DefaultValue => Of(Default);
|
||||
|
||||
public override bool TryNormalize(JsonElement input, out JsonElement normalized, out string error)
|
||||
{
|
||||
normalized = default;
|
||||
error = "";
|
||||
if (input.ValueKind != JsonValueKind.Number || !input.TryGetDouble(out double value) || !double.IsFinite(value))
|
||||
{
|
||||
error = "must be a number";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value < Min || value > Max)
|
||||
{
|
||||
error = $"must be between {Min.ToString(CultureInfo.InvariantCulture)} and {Max.ToString(CultureInfo.InvariantCulture)}";
|
||||
return false;
|
||||
}
|
||||
|
||||
normalized = Of(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record BoolParam(string Name, bool Default, string Description = "")
|
||||
: ParamSpec(Name, Description, null)
|
||||
{
|
||||
public override ParamType Type => ParamType.Bool;
|
||||
|
||||
public override JsonElement DefaultValue => Of(Default);
|
||||
|
||||
public override bool TryNormalize(JsonElement input, out JsonElement normalized, out string error)
|
||||
{
|
||||
normalized = default;
|
||||
error = "";
|
||||
if (input.ValueKind is not (JsonValueKind.True or JsonValueKind.False))
|
||||
{
|
||||
error = "must be a boolean";
|
||||
return false;
|
||||
}
|
||||
|
||||
normalized = Of(input.GetBoolean());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record EnumParam(string Name, string Default, IReadOnlyList<string> Values, string Description = "")
|
||||
: ParamSpec(Name, Description, null)
|
||||
{
|
||||
public override ParamType Type => ParamType.Enum;
|
||||
|
||||
public override JsonElement DefaultValue => Of(Default);
|
||||
|
||||
public override bool TryNormalize(JsonElement input, out JsonElement normalized, out string error)
|
||||
{
|
||||
normalized = default;
|
||||
error = "";
|
||||
string? value = input.ValueKind == JsonValueKind.String ? input.GetString() : null;
|
||||
if (value is null || !Values.Contains(value))
|
||||
{
|
||||
error = "must be one of: " + string.Join(", ", Values);
|
||||
return false;
|
||||
}
|
||||
|
||||
normalized = Of(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>参数声明的简写。</summary>
|
||||
public static class P
|
||||
{
|
||||
public static ParamSpec Int(string name, long @default, long min, long max, string? unit = null, string description = "") =>
|
||||
new IntParam(name, @default, min, max, unit, description);
|
||||
|
||||
public static ParamSpec Number(string name, double @default, double min, double max, string? unit = null, string description = "") =>
|
||||
new NumberParam(name, @default, min, max, unit, description);
|
||||
|
||||
public static ParamSpec Bool(string name, bool @default, string description = "") => new BoolParam(name, @default, description);
|
||||
|
||||
public static ParamSpec Enum(string name, string @default, string[] values, string description = "") =>
|
||||
new EnumParam(name, @default, values, description);
|
||||
}
|
||||
|
||||
[System.Text.Json.Serialization.JsonSerializable(typeof(long))]
|
||||
[System.Text.Json.Serialization.JsonSerializable(typeof(double))]
|
||||
[System.Text.Json.Serialization.JsonSerializable(typeof(bool))]
|
||||
[System.Text.Json.Serialization.JsonSerializable(typeof(string))]
|
||||
internal sealed partial class ParamJson : System.Text.Json.Serialization.JsonSerializerContext;
|
||||
|
||||
/// <summary>已经校验并补全了默认值的参数。</summary>
|
||||
public sealed class ParamValues
|
||||
{
|
||||
private readonly Dictionary<string, JsonElement> _values;
|
||||
|
||||
public ParamValues(Dictionary<string, JsonElement> values)
|
||||
{
|
||||
_values = values;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, JsonElement> Values => _values;
|
||||
|
||||
public long GetInt(string name) => _values[name].GetInt64();
|
||||
|
||||
public double GetNumber(string name) => _values[name].GetDouble();
|
||||
|
||||
public bool GetBool(string name) => _values[name].GetBoolean();
|
||||
|
||||
public string GetString(string name) => _values[name].GetString()!;
|
||||
}
|
||||
|
||||
public sealed record ParamResolution(ParamValues? Values, IReadOnlyDictionary<string, string> Errors)
|
||||
{
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
}
|
||||
|
||||
public static class ParamResolver
|
||||
{
|
||||
/// <summary>未知参数视为错误;缺失的参数取默认值。</summary>
|
||||
public static ParamResolution Resolve(IReadOnlyList<ParamSpec> specs, IReadOnlyDictionary<string, JsonElement>? input)
|
||||
{
|
||||
var errors = new Dictionary<string, string>();
|
||||
var values = new Dictionary<string, JsonElement>(specs.Count);
|
||||
foreach (ParamSpec spec in specs)
|
||||
{
|
||||
if (input is not null && input.TryGetValue(spec.Name, out JsonElement raw))
|
||||
{
|
||||
if (spec.TryNormalize(raw, out JsonElement normalized, out string error))
|
||||
{
|
||||
values[spec.Name] = normalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors[spec.Name] = error;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
values[spec.Name] = spec.DefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (input is not null)
|
||||
{
|
||||
foreach (string key in input.Keys)
|
||||
{
|
||||
if (!values.ContainsKey(key) && !errors.ContainsKey(key))
|
||||
{
|
||||
errors[key] = "is not a parameter of this kind";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ParamResolution(errors.Count == 0 ? new ParamValues(values) : null, errors);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user