feat: probe host stats, node cards with a separate lines panel grouped by carrier, Windows host sampling, manual carrier override [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
1 parent
bac1c27e72
commit
88b1a0a9bb
36 files changed
+750
-185
No files matched your search
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using MoShiro.Agent;
|
||||
using MoShiro.Measurement;
|
||||
using MoShiro.Protocol;
|
||||
using MoShiro.TestSupport;
|
||||
|
||||
@@ -24,6 +25,23 @@ public sealed class HostSamplerTests
|
||||
Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1 + "podman0: 8888 1 0 0 0 0 0 0 9999 1 0 0 0 0 0 0\n"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sample_OnWindows_ReadsCpuMemoryAndNetworkFromTheSystem()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sampler = new HostSampler(TimeProvider.System);
|
||||
sampler.Sample();
|
||||
await Task.Delay(300, TestContext.Current.CancellationToken);
|
||||
HostSnapshot snapshot = sampler.Sample();
|
||||
Assert.NotNull(snapshot.CpuPercent);
|
||||
Assert.InRange(snapshot.CpuPercent!.Value, 0, 100);
|
||||
Assert.True(snapshot.MemTotal > 0 && snapshot.MemUsed >= 0 && snapshot.MemUsed <= snapshot.MemTotal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sample_DerivesCpuPercentAndNetworkSpeedFromTheDifferenceBetweenTwoReads()
|
||||
{
|
||||
@@ -123,13 +141,15 @@ public sealed class HostStatusTests
|
||||
|
||||
await using SseClient sse = await SseClient.ConnectAsync(_backend.Anonymous, "/api/public/live?topics=hosts");
|
||||
await agent.SendHostAsync(Snapshot(18.5));
|
||||
SseEvent live = await sse.WaitForAsync(e => e.Name == "host" && e.Data!["agent_id"]!.GetValue<string>() == id);
|
||||
SseEvent live = await sse.WaitForAsync(e => e.Name == "host" && e.Data!["id"]!.GetValue<string>() == id);
|
||||
Assert.Equal("agent", live.Data!["kind"]!.GetValue<string>());
|
||||
Assert.Equal(18.5, live.Data!["host"]!["cpu_percent"]!.GetValue<double>());
|
||||
Assert.True(live.Data["online"]!.GetValue<bool>());
|
||||
|
||||
// REST:最新值,没有过期
|
||||
JsonNode list = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts");
|
||||
JsonNode item = list["items"]!.AsArray().Single(i => i!["agent_id"]!.GetValue<string>() == id)!;
|
||||
JsonNode item = list["items"]!.AsArray().Single(i => i!["id"]!.GetValue<string>() == id)!;
|
||||
Assert.Equal("agent", item["kind"]!.GetValue<string>());
|
||||
Assert.False(item["stale"]!.GetValue<bool>());
|
||||
Assert.Equal(40L << 30, item["host"]!["disk_total"]!.GetValue<long>());
|
||||
Assert.Equal(1000, item["host"]!["net_link_mbps"]!.GetValue<long>());
|
||||
@@ -150,7 +170,7 @@ public sealed class HostStatusTests
|
||||
JsonNode history = null!;
|
||||
await BackendFixture.EventuallyAsync(async () =>
|
||||
{
|
||||
history = await _backend.GetJsonAsync(_backend.Anonymous, $"/api/public/agents/{id}/host?range=1h");
|
||||
history = await _backend.GetJsonAsync(_backend.Anonymous, $"/api/public/hosts/{id}?range=1h");
|
||||
return history["values"]!["cpu"]!.AsArray().Any(v => v is not null);
|
||||
});
|
||||
Assert.Equal(25.0, history["values"]!["mem"]!.AsArray().Last(v => v is not null)!.GetValue<double>());
|
||||
@@ -159,8 +179,61 @@ public sealed class HostStatusTests
|
||||
// 隐藏的 Agent 对匿名访客不可见
|
||||
await _backend.Admin.PatchAsync($"/api/admin/agents/{id}", BackendFixture.Json(new JsonObject { ["public_visible"] = false }));
|
||||
JsonNode hidden = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts");
|
||||
Assert.DoesNotContain(hidden["items"]!.AsArray(), i => i!["agent_id"]!.GetValue<string>() == id);
|
||||
using HttpResponseMessage denied = await _backend.Anonymous.GetAsync($"/api/public/agents/{id}/host");
|
||||
Assert.DoesNotContain(hidden["items"]!.AsArray(), i => i!["id"]!.GetValue<string>() == id);
|
||||
using HttpResponseMessage denied = await _backend.Anonymous.GetAsync($"/api/public/hosts/{id}");
|
||||
Assert.Equal(System.Net.HttpStatusCode.NotFound, denied.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ARealProbeReportsItsHostStatus_LikeAnAgent_NeverCarriesModelsOrAddresses_AndDisappearsWhenRevoked()
|
||||
{
|
||||
await _backend.SiteMustBePublicAsync();
|
||||
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend, file => file.Reporting.HostIntervalSeconds = 1);
|
||||
|
||||
JsonNode? item = null;
|
||||
await BackendFixture.EventuallyAsync(
|
||||
async () =>
|
||||
{
|
||||
JsonNode list = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts");
|
||||
item = list["items"]!.AsArray().SingleOrDefault(i => i!["id"]!.GetValue<string>() == probe.ProbeId);
|
||||
return item?["host"] is not null;
|
||||
},
|
||||
TimeSpan.FromSeconds(30),
|
||||
"the probe's host status shows up");
|
||||
Assert.Equal("probe", item!["kind"]!.GetValue<string>());
|
||||
Assert.NotNull(item["host"]!["uptime_seconds"]); // 任何平台都读得到运行时长
|
||||
foreach (string forbidden in new[] { "model", "hostname", "\"ip\"", "address", "remote", "mac", "iface", "public_host" })
|
||||
{
|
||||
Assert.DoesNotContain(forbidden, item.ToJsonString(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// 历史也按节点 id 取
|
||||
using HttpResponseMessage history = await _backend.Anonymous.GetAsync($"/api/public/hosts/{probe.ProbeId}?range=1h");
|
||||
Assert.Equal(System.Net.HttpStatusCode.OK, history.StatusCode);
|
||||
|
||||
// 吊销之后展示页就看不到它了
|
||||
using HttpResponseMessage revoked = await _backend.Admin.PostAsync($"/api/admin/probes/{probe.ProbeId}/revoke", null);
|
||||
Assert.True(revoked.IsSuccessStatusCode);
|
||||
JsonNode after = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts");
|
||||
Assert.DoesNotContain(after["items"]!.AsArray(), i => i!["id"]!.GetValue<string>() == probe.ProbeId);
|
||||
using HttpResponseMessage gone = await _backend.Anonymous.GetAsync($"/api/public/hosts/{probe.ProbeId}");
|
||||
Assert.Equal(System.Net.HttpStatusCode.NotFound, gone.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AProbeThatTurnsReportingOff_SendsNothing()
|
||||
{
|
||||
await _backend.SiteMustBePublicAsync();
|
||||
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend, file =>
|
||||
{
|
||||
file.Reporting.HostStats = false;
|
||||
file.Reporting.HostIntervalSeconds = 1;
|
||||
});
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(4));
|
||||
|
||||
JsonNode list = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts");
|
||||
JsonNode item = list["items"]!.AsArray().Single(i => i!["id"]!.GetValue<string>() == probe.ProbeId)!;
|
||||
Assert.Null(item["host"]); // 节点还在,但一个字也没上报
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user