shared: add independent IPv4 and IPv6 collection [CI]
ci/woodpecker/pr/verify Pipeline was successful
ci/woodpecker/pr/web-build Pipeline was successful

This commit is contained in:
justme committed 2026-10-07 11:44:01 +08:00
1 parent c974a7dddb
commit 0eda0600cc
60 files changed
+699 -137

No files matched your search

@@ -87,7 +87,7 @@ public static class AgentEndpoints
private static async Task<IResult> PatchAsync(HttpContext http, AgentService agents, AgentPatchRequest request, string id, CancellationToken ct)
{
Result<AgentView> result = await agents.PatchAsync(
new NodeId(id), new AgentPatch { Name = request.Name, Labels = request.Labels, PublicHost = request.PublicHost, PublicVisible = request.PublicVisible }, Concurrency.ExpectedRowVersion(http), ct);
new NodeId(id), new AgentPatch { Name = request.Name, Labels = request.Labels, PublicHost = request.PublicHost, Ipv6Host = request.Ipv6Host, PublicVisible = request.PublicVisible }, Concurrency.ExpectedRowVersion(http), ct);
if (!result.TryGet(out AgentView? view, out FleetError? error))
{
return Problems.From(http, error);
@@ -109,7 +109,7 @@ public static class TaskEndpoints
return Problems.From(http, lineError);
}
Result<TestRecord> result = await tests.StartAsync(line.Key.Agent, line.Key.Probe, new StartTestRequest(request.Plan ?? "", request.Params), "api", ct);
Result<TestRecord> result = await tests.StartAsync(line.Key.Agent, line.Key.Probe, new StartTestRequest(request.Plan ?? "", request.Params), "api", line.IpVersion, ct);
if (!result.TryGet(out TestRecord? test, out FleetError? error))
{
return Problems.From(http, error);
@@ -124,10 +124,12 @@ public static class TaskEndpoints
// 只给 Agent id(并且有多个 Probe)时列出这个 Agent 上所有 Probe 的测试
NodeId agent;
ProbeId? probe = null;
string? ipVersion = null;
if (LineKey.TryParse(line_id, out LineKey key))
{
agent = key.Agent;
probe = key.Probe;
ipVersion = key.IpVersion;
}
else
{
@@ -135,7 +137,7 @@ public static class TaskEndpoints
}
return TypedResults.Json(
new PageDto<TestDto>([.. (await tests.ListAsync(agent, probe, Math.Clamp(limit ?? 50, 1, 500), ct)).Select(Mappers.ToDto)], null),
new PageDto<TestDto>([.. (await tests.ListAsync(agent, probe, Math.Clamp(limit ?? 50, 1, 500), ipVersion, ct)).Select(Mappers.ToDto)], null),
ApiJsonContext.Default.PageDtoTestDto);
}
@@ -31,34 +31,34 @@ public static class Mappers
public static OverviewLineDto ToDto(OverviewLine line) =>
new(
line.Line.Key.Id, line.Line.Agent.Agent.Id.Value, line.Line.Probe.Id, line.Line.Probe.Name, line.Line.Agent.Agent.Name, line.Line.Agent.Agent.Labels,
line.Line.Agent.Online, line.Line.Connected, line.Line.Agent.EndpointChanges, ToDto(line.Summary), Summary(line.LastTest));
line.Line.Agent.Online, line.Line.Connected, line.Line.Agent.EndpointChanges, ToDto(line.Summary), Summary(line.LastTest), line.Line.IpVersion);
public static HostDto ToDto(HostSnapshot h) =>
new(h.CpuPercent, h.MemUsed, h.MemTotal, h.DiskUsed, h.DiskTotal, h.NetRxTotal, h.NetTxTotal, h.NetRxBps, h.NetTxBps, h.NetLinkMbps, h.UptimeSeconds, Time(h.AtUtcMs));
public static ConnectedProbeDto ToDto(ConnectedProbeInfo p) => new(p.Id, p.Version, p.Remote, Time(p.SinceMs));
public static ConnectedProbeDto ToDto(ConnectedProbeInfo p) => new(p.Id, p.Version, p.Remote, Time(p.SinceMs), p.IpVersion);
public static ProbeDto ToDto(ProbeView v) =>
new(
v.Probe.Id, v.Probe.Name, Time(v.Probe.CreatedAtMs), Time(v.Probe.RevokedAtMs), v.Revoked, v.Online,
[.. v.Connections.Select(c => new ProbeConnectionDto(c.AgentId.Value, c.AgentName, c.AgentOnline, c.Version, c.Remote, Time(c.SinceMs)))]);
[.. v.Connections.Select(c => new ProbeConnectionDto(c.AgentId.Value, c.AgentName, c.AgentOnline, c.Version, c.Remote, Time(c.SinceMs), c.IpVersion))]);
public static SeriesDto ToDto(SeriesResult r, bool live) =>
new(
r.Line.Id, r.Line.Agent.Value, r.Line.Probe.Value, Time(r.Range.FromMs), Time(r.Range.ToMs), Steps.Format(r.Range.StepMs), r.Plan.Name, live && r.Range.IsRelative, r.Plan.Degraded,
r.T, r.Values, ToDto(r.Summary), new EdgePartialDto(r.Range.FirstPartial, r.Range.LastPartial),
[.. r.Gaps.Select(g => new GapDto(Time(g.FromMs), Time(g.ToMs), g.Reason))], [.. r.Events.Select(ToDto)], r.Versions);
[.. r.Gaps.Select(g => new GapDto(Time(g.FromMs), Time(g.ToMs), g.Reason))], [.. r.Events.Select(ToDto)], r.Versions, r.Line.IpVersion);
public static TestDto ToDto(TestDetail detail)
{
TestRecord t = detail.Test;
return new TestDto(
t.Id.Value, t.AgentId.Value, t.ProbeId.Value, t.Plan, detail.PhaseKinds, t.Status, t.Phase, t.ErrorCode, t.Trigger,
Time(t.RequestedAtMs), Time(t.StartedAtMs), Time(t.FinishedAtMs), detail.Result is null ? null : ToDto(detail.Result));
Time(t.RequestedAtMs), Time(t.StartedAtMs), Time(t.FinishedAtMs), detail.Result is null ? null : ToDto(detail.Result), t.IpVersion);
}
public static TestDto ToDto(TestRecord t) =>
new(t.Id.Value, t.AgentId.Value, t.ProbeId.Value, t.Plan, [.. (Plans.Find(t.Plan)?.Phases ?? []).Select(p => p.Kind)], t.Status, t.Phase, t.ErrorCode, t.Trigger, Time(t.RequestedAtMs), Time(t.StartedAtMs), Time(t.FinishedAtMs), null);
new(t.Id.Value, t.AgentId.Value, t.ProbeId.Value, t.Plan, [.. (Plans.Find(t.Plan)?.Phases ?? []).Select(p => p.Kind)], t.Status, t.Phase, t.ErrorCode, t.Trigger, Time(t.RequestedAtMs), Time(t.StartedAtMs), Time(t.FinishedAtMs), null, t.IpVersion);
public static TestResultDto ToDto(TestResultView r) =>
new(
@@ -103,7 +103,7 @@ public static class Mappers
: null;
return new AgentDto(
a.Id.Value, a.Name, StatusName(a.Status), view.Online, view.ProbeConnected, [.. view.Probes.Select(ToDto)], view.Host is null ? null : ToDto(view.Host), a.PublicHost, a.ProbeTcpPort, a.ProbeUdpPorts, a.Labels, a.PublicVisible,
a.Kinds, a.AgentVersion, limits, a.TaskVersion, a.AppliedTaskVersion, rejected, heartbeat, view.EndpointChanges, Time(a.CreatedAtMs), Time(a.LastSeenAtMs));
a.Kinds, a.AgentVersion, limits, a.TaskVersion, a.AppliedTaskVersion, rejected, heartbeat, view.EndpointChanges, Time(a.CreatedAtMs), Time(a.LastSeenAtMs), a.Ipv6Host, a.Ipv6Supported);
}
public static TaskDto ToDto(TaskRecord t) =>
@@ -37,10 +37,12 @@ public sealed record AgentDto(
HeartbeatDto? Heartbeat,
int EndpointChanges,
DateTimeOffset CreatedAt,
DateTimeOffset? LastSeenAt);
DateTimeOffset? LastSeenAt,
string Ipv6Host = "",
bool Ipv6Supported = false);
/// <summary>merge-patch:缺省或为 null 的字段不修改。</summary>
public sealed record AgentPatchRequest(string? Name, IReadOnlyList<string>? Labels, string? PublicHost, bool? PublicVisible);
public sealed record AgentPatchRequest(string? Name, IReadOnlyList<string>? Labels, string? PublicHost, bool? PublicVisible, string? Ipv6Host = null);
public sealed record JoinTokenDto(string Id, string? Note, IReadOnlyList<string> Labels, bool AutoApprove, int MaxUses, int Uses, DateTimeOffset ExpiresAt, DateTimeOffset? RevokedAt, DateTimeOffset CreatedAt);
@@ -52,10 +54,10 @@ public sealed record InstallCommandsDto(string Linux, string Macos, string Windo
public sealed record CreatedJoinTokenDto(JoinTokenDto Token, string Plaintext, InstallCommandsDto InstallCommands);
/// <summary>Agent 眼中连着的一个 Probe(Agent 上报)。</summary>
public sealed record ConnectedProbeDto(string ProbeId, string? Version, string? Remote, DateTimeOffset ConnectedSince);
public sealed record ConnectedProbeDto(string ProbeId, string? Version, string? Remote, DateTimeOffset ConnectedSince, string IpVersion = "ipv4");
/// <summary>某个 Probe 现在连着的一个 Agent。</summary>
public sealed record ProbeConnectionDto(string AgentId, string AgentName, bool AgentOnline, string? Version, string? Remote, DateTimeOffset ConnectedSince);
public sealed record ProbeConnectionDto(string AgentId, string AgentName, bool AgentOnline, string? Version, string? Remote, DateTimeOffset ConnectedSince, string IpVersion = "ipv4");
public sealed record ProbeDto(string Id, string Name, DateTimeOffset CreatedAt, DateTimeOffset? RevokedAt, bool Revoked, bool Online, IReadOnlyList<ProbeConnectionDto> Connections);
@@ -22,7 +22,8 @@ public sealed record OverviewLineDto(
bool ProbeConnected,
int EndpointChanges,
SummaryDto Summary,
TestSummaryDto? LastTest);
TestSummaryDto? LastTest,
string IpVersion = "ipv4");
public sealed record OverviewDto(string Range, DateTimeOffset From, DateTimeOffset To, string Step, string Source, bool Degraded, IReadOnlyList<OverviewLineDto> Lines);
@@ -43,7 +44,8 @@ public sealed record SeriesDto(
EdgePartialDto EdgePartial,
IReadOnlyList<GapDto> Gaps,
IReadOnlyList<EventDto> Events,
IReadOnlyDictionary<string, string> Versions);
IReadOnlyDictionary<string, string> Versions,
string IpVersion = "ipv4");
public sealed record CompareDto(
string Metric,
@@ -81,7 +83,9 @@ public sealed record LineDto(
bool ProbeConnected,
DateTimeOffset? DataSince,
IReadOnlyList<string> Kinds,
TestSummaryDto? LastTest);
TestSummaryDto? LastTest,
string IpVersion = "ipv4",
bool Ipv6Available = false);
public sealed record DimensionDto(string Id, string Label, double Weight, double? Value, double? Score);
@@ -109,7 +113,8 @@ public sealed record TestDto(
DateTimeOffset RequestedAt,
DateTimeOffset? StartedAt,
DateTimeOffset? FinishedAt,
TestResultDto? Result);
TestResultDto? Result,
string IpVersion = "ipv4");
// ── 目录(由代码注册表生成) ──────────────────────────────────────────
@@ -33,8 +33,14 @@ public static class LiveEndpoints
private static async Task<IResult> StreamAsync(
HttpContext http, AgentService agents, LineService lineService, TestService tests, QueryService query, LiveHub hub, SseLimiter limiter,
BackendOptions options, BackendCounters counters, TimeProvider time, IHostApplicationLifetime lifetime)
BackendOptions options, BackendCounters counters, TimeProvider time, IHostApplicationLifetime lifetime, string? ip_version)
{
string ipVersion = ip_version ?? IpVersions.V4;
if (!IpVersions.IsValid(ipVersion))
{
return Problems.From(http, ValidationError.Single("ip_version", "must be ipv4 or ipv6"));
}
StringValues raw = http.Request.Query["topics"];
Result<IReadOnlyList<LiveTopic>> parsed = LiveTopic.ParseAll(raw.Where(v => v is not null)!, options.MaxSseTopics);
if (!parsed.TryGet(out IReadOnlyList<LiveTopic>? topics, out FleetError? error))
@@ -78,7 +84,7 @@ public static class LiveEndpoints
http.Response.Headers["X-Accel-Buffering"] = "no";
await http.Response.Body.FlushAsync(http.RequestAborted);
var session = new LiveSession(http, agents, lineService, tests, query, hub, options, time, topics, lineKeys);
var session = new LiveSession(http, agents, lineService, tests, query, hub, options, time, topics, lineKeys, ipVersion);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(http.RequestAborted, lifetime.ApplicationStopping);
await session.RunAsync(linked.Token);
}
@@ -101,6 +107,7 @@ public static class LiveEndpoints
private readonly AgentService _agents;
private readonly LineService _lines;
private readonly IReadOnlyDictionary<string, LineKey> _lineKeys;
private readonly string _ipVersion;
private readonly TestService _tests;
private readonly QueryService _query;
private readonly LiveHub _hub;
@@ -117,12 +124,13 @@ public static class LiveEndpoints
private long _lastWriteMs;
private long _lastPeriodicMs;
public LiveSession(HttpContext http, AgentService agents, LineService lines, TestService tests, QueryService query, LiveHub hub, BackendOptions options, TimeProvider time, IReadOnlyList<LiveTopic> topics, IReadOnlyDictionary<string, LineKey> lineKeys)
public LiveSession(HttpContext http, AgentService agents, LineService lines, TestService tests, QueryService query, LiveHub hub, BackendOptions options, TimeProvider time, IReadOnlyList<LiveTopic> topics, IReadOnlyDictionary<string, LineKey> lineKeys, string ipVersion)
{
_http = http;
_agents = agents;
_lines = lines;
_lineKeys = lineKeys;
_ipVersion = ipVersion;
_tests = tests;
_query = query;
_hub = hub;
@@ -297,7 +305,7 @@ public static class LiveEndpoints
{
MetricSelection selection = MetricSelection.Resolve(null, topic.Preset, _options.MaxMetricsPerQuery).Value;
IReadOnlyList<LineView> visible = await _lines.ListAsync(a => a.Agent.Status == Fleet.Server.NodeStatus.Active && IsVisible(a), ct);
Result<OverviewResult> result = await _query.OverviewAsync(visible, new RangeRequest(topic.Range, null, null, null, null), selection, ct);
Result<OverviewResult> result = await _query.OverviewAsync([.. visible.Where(l => l.IpVersion == _ipVersion)], new RangeRequest(topic.Range, null, null, null, null), selection, ct);
if (result.TryGet(out OverviewResult? overview, out _))
{
var dto = new OverviewDto(
@@ -43,9 +43,15 @@ public static class QueryEndpoints
private static Task<IResult> OverviewAsync(
HttpContext http, LineService lineService, QueryService query, BackendOptions options, TimeProvider time,
string? range, DateTimeOffset? from, DateTimeOffset? to, string? preset, string? metrics, string? sort, string? order) =>
string? range, DateTimeOffset? from, DateTimeOffset? to, string? preset, string? metrics, string? sort, string? order, string? ip_version) =>
QueryHelpers.GuardedAsync(http, async ct =>
{
string ipVersion = ip_version ?? IpVersions.V4;
if (!IpVersions.IsValid(ipVersion))
{
return Problems.From(http, ValidationError.Single("ip_version", "must be ipv4 or ipv6"));
}
Result<MetricSelection> selection = MetricSelection.Resolve(metrics, preset ?? (metrics is null ? DefaultOverviewPreset : null), options.MaxMetricsPerQuery);
if (!selection.TryGet(out MetricSelection? sel, out FleetError? error))
{
@@ -53,7 +59,7 @@ public static class QueryEndpoints
}
IReadOnlyList<LineView> visible = await lineService.ListAsync(a => a.Agent.Status == Fleet.Server.NodeStatus.Active && (a.Agent.PublicVisible || PublicGuard.CanSeeHidden(http)), ct);
Result<OverviewResult> result = await query.OverviewAsync(visible, new RangeRequest(range, from, to, null, null), sel, ct);
Result<OverviewResult> result = await query.OverviewAsync([.. visible.Where(l => l.IpVersion == ipVersion)], new RangeRequest(range, from, to, null, null), sel, ct);
if (!result.TryGet(out OverviewResult? overview, out error))
{
return Problems.From(http, error);
@@ -81,12 +87,12 @@ public static class QueryEndpoints
}
AgentView view = line.Agent;
IReadOnlyList<TestRecord> recent = await tests.ListAsync(view.Agent.Id, new ProbeId(line.Probe.Id), 1, ct);
IReadOnlyList<TestRecord> recent = await tests.ListAsync(view.Agent.Id, new ProbeId(line.Probe.Id), 1, line.IpVersion, ct);
TestRecord? last = recent.Count > 0 ? recent[0] : null;
return TypedResults.Json(
new LineDto(
line.Key.Id, view.Agent.Id.Value, line.Probe.Id, line.Probe.Name, view.Agent.Name, view.Agent.Labels, view.Online, line.Connected,
Mappers.Time(await query.EarliestAsync(line.Key, ct)), view.Agent.Kinds, Mappers.Summary(last)),
Mappers.Time(await query.EarliestAsync(line.Key, ct)), view.Agent.Kinds, Mappers.Summary(last), line.IpVersion, view.Agent.Ipv6Host.Length > 0),
ApiJsonContext.Default.LineDto);
}