feat: use Alpine AOT images, static SQLite and Podman host metrics [CI]

This commit is contained in:
geruipu committed 2026-10-07 01:03:31 +08:00
1 parent 6e945657c1
commit 103ef27ed3
37 files changed
+474 -64

No files matched your search

+2 -1
View File
@@ -23,8 +23,9 @@ Monitor is a self-hosted network path quality monitor. Backend coordinates Agent
## Local development and builds ## Local development and builds
Requirements: .NET SDK 10 and Node.js/npm. The SDK selection is defined by `backend/global.json`. Requirements: .NET SDK 10 and Node.js/npm. The SDK selection is defined by the root `global.json`; NuGet package versions are centralized in the root `Directory.Packages.props`.
- All .NET projects: `dotnet build Monitor.slnx -c Release` from the repository root.
- Backend build: `cd backend && dotnet build Monitor.slnx -c Release` - Backend build: `cd backend && dotnet build Monitor.slnx -c Release`
- Backend tests: `cd backend && dotnet test --solution Monitor.slnx -c Release` - Backend tests: `cd backend && dotnet test --solution Monitor.slnx -c Release`
- Frontend dependencies/build: `npm ci --prefix web && npm run build --prefix web` - Frontend dependencies/build: `npm ci --prefix web && npm run build --prefix web`
@@ -1,10 +1,15 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<!-- Native SQLite source is pinned separately from the JIT/test binary package. -->
<MonitorSqliteSourceVersion>3530400</MonitorSqliteSourceVersion>
<MonitorSqliteSourceYear>2026</MonitorSqliteSourceYear>
<MonitorSqliteSourceSha256>1e71ddf93849c6a6ecf58b827c0692073d2dd7ee40196158068f7b29f422e87d</MonitorSqliteSourceSha256>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.12" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.12" />
<PackageVersion Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.53.3" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageVersion Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.12" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.0.0" /> <PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.0.0" />
+52
View File
@@ -0,0 +1,52 @@
<Solution>
<Folder Name="/backend/fleet/src/">
<Project Path="backend/fleet/src/Fleet.Core/Fleet.Core.csproj" />
<Project Path="backend/fleet/src/Fleet.Enrollment.Client/Fleet.Enrollment.Client.csproj" />
<Project Path="backend/fleet/src/Fleet.Hosting/Fleet.Hosting.csproj" />
<Project Path="backend/fleet/src/Fleet.Identity/Fleet.Identity.csproj" />
<Project Path="backend/fleet/src/Fleet.Link/Fleet.Link.csproj" />
<Project Path="backend/fleet/src/Fleet.Rpc/Fleet.Rpc.csproj" />
<Project Path="backend/fleet/src/Fleet.Server.Abstractions/Fleet.Server.Abstractions.csproj" />
<Project Path="backend/fleet/src/Fleet.Server.Enrollment/Fleet.Server.Enrollment.csproj" />
<Project Path="backend/fleet/src/Fleet.Server.Link/Fleet.Server.Link.csproj" />
</Folder>
<Folder Name="/backend/fleet/tests/">
<Project Path="backend/fleet/tests/Fleet.Tests/Fleet.Tests.csproj" />
</Folder>
<Folder Name="/backend/fleet/tools/">
<Project Path="backend/fleet/tools/fleet-enroll/fleet-enroll.csproj" />
</Folder>
<Folder Name="/backend/shared/">
<Project Path="backend/shared/Monitor.Measurement/Monitor.Measurement.csproj" />
<Project Path="backend/shared/Monitor.Protocol/Monitor.Protocol.csproj" />
</Folder>
<Folder Name="/backend/shared/tests/">
<Project Path="backend/shared/tests/Monitor.Measurement.Tests/Monitor.Measurement.Tests.csproj" />
<Project Path="backend/shared/tests/Monitor.Protocol.Tests/Monitor.Protocol.Tests.csproj" />
</Folder>
<Folder Name="/backend/agent/src/">
<Project Path="backend/agent/src/Monitor.Agent/Monitor.Agent.csproj" />
</Folder>
<Folder Name="/backend/probe/src/">
<Project Path="backend/probe/src/Monitor.Probe/Monitor.Probe.csproj" />
</Folder>
<Folder Name="/backend/probe/tests/">
<Project Path="backend/probe/tests/Monitor.Probe.Tests/Monitor.Probe.Tests.csproj" />
</Folder>
<Folder Name="/backend/src/">
<Project Path="backend/src/Monitor.Backend.Analysis/Monitor.Backend.Analysis.csproj" />
<Project Path="backend/src/Monitor.Backend.Api/Monitor.Backend.Api.csproj" />
<Project Path="backend/src/Monitor.Backend.Application/Monitor.Backend.Application.csproj" />
<Project Path="backend/src/Monitor.Backend.Gateway/Monitor.Backend.Gateway.csproj" />
<Project Path="backend/src/Monitor.Backend.Storage.Sqlite/Monitor.Backend.Storage.Sqlite.csproj" />
<Project Path="backend/src/Monitor.Backend/Monitor.Backend.csproj" />
</Folder>
<Folder Name="/backend/tests/">
<Project Path="backend/tests/Monitor.Backend.Analysis.Tests/Monitor.Backend.Analysis.Tests.csproj" />
<Project Path="backend/tests/Monitor.Backend.Tests/Monitor.Backend.Tests.csproj" />
<Project Path="backend/tests/Monitor.TestSupport/Monitor.TestSupport.csproj" />
</Folder>
<Folder Name="/ci/">
<Project Path="ci/Monitor.Tar/Monitor.Tar.csproj" />
</Folder>
</Solution>
+4 -4
View File
@@ -23,18 +23,19 @@ Probe <──测量会话──> Agent ──长连接与数据上报──> Bac
## 快速开始 ## 快速开始
### 环境要求 ### 环境要求
- .NET SDK 10;项目使用 `backend/global.json` 指定 SDK 版本和滚动策略。 - .NET SDK 10;项目使用 `global.json` 指定 SDK 版本和滚动策略。
- Node.js 与 npm,用于构建 Web 前端。 - Node.js 与 npm,用于构建 Web 前端。
### 构建 ### 构建
从仓库根目录构建后端: 从仓库根目录构建全部 .NET 项目(Backend、Agent、Probe、Fleet、测试和 CI 工具):
```sh ```sh
cd backend
dotnet build Monitor.slnx -c Release dotnet build Monitor.slnx -c Release
``` ```
NuGet 包版本统一在根目录 `Directory.Packages.props` 管理,项目中的 `PackageReference` 不写版本号。AotAnywhere 的 MSBuild SDK 版本在根目录 `global.json` 的 `msbuild-sdks` 中管理。`backend/Monitor.slnx` 保留用于只构建后端目录内的项目。
安装前端依赖并构建展示端和管理端(在仓库根目录运行): 安装前端依赖并构建展示端和管理端(在仓库根目录运行):
```sh ```sh
@@ -92,7 +93,6 @@ npm run dev --prefix web
## 测试 ## 测试
```sh ```sh
cd backend
dotnet test --solution Monitor.slnx dotnet test --solution Monitor.slnx
``` ```
+4
View File
@@ -8,6 +8,10 @@
<EventSourceSupport>false</EventSourceSupport> <EventSourceSupport>false</EventSourceSupport>
<DebuggerSupport>false</DebuggerSupport> <DebuggerSupport>false</DebuggerSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport> <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<Http3Support>false</Http3Support>
<MetricsSupport>false</MetricsSupport>
<UseSizeOptimizedLinq>true</UseSizeOptimizedLinq>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
<ServerGarbageCollection>false</ServerGarbageCollection> <ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection> <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
</PropertyGroup> </PropertyGroup>
@@ -6,23 +6,30 @@ namespace Monitor.Agent;
/// <summary> /// <summary>
/// 主机资源占用的只读快照:CPU 占用、内存、根分区、网卡累计流量与瞬时网速、系统运行时长。 /// 主机资源占用的只读快照:CPU 占用、内存、根分区、网卡累计流量与瞬时网速、系统运行时长。
/// 只有汇总的数字:<b>不采集 CPU 型号、主机名、网卡名和任何 IP 地址</b>,所以面板和展示页想泄漏也泄漏不出去。 /// 只有汇总的数字:<b>不采集 CPU 型号、主机名、网卡名和任何 IP 地址</b>,所以面板和展示页想泄漏也泄漏不出去。
/// Linux 读 <c>/proc</c>;读不到的项是 null(例如 Windows 上的 CPU)。不执行任何命令,不写文件。 /// Linux 读 <c>/proc</c> 和 <c>/sys</c>;容器通过 HOST_PROC、HOST_SYS、HOST_ROOT 指定宿主路径。
/// 读不到的项是 null(例如 Windows 上的 CPU)。不执行任何命令,不写文件。
/// </summary> /// </summary>
public sealed class HostSampler public sealed class HostSampler
{ {
private static readonly string[] s_virtualPrefixes = ["lo", "veth", "docker", "br-", "virbr", "cni", "flannel", "cali", "tun", "tap"]; private static readonly string[] s_virtualPrefixes = ["lo", "veth", "docker", "podman", "br-", "virbr", "cni", "flannel", "cali", "tun", "tap"];
private readonly TimeProvider _time; private readonly TimeProvider _time;
private readonly Func<string, string?> _read; private readonly Func<string, string?> _read;
private readonly Func<(long Total, long Used)?> _disk; private readonly Func<(long Total, long Used)?> _disk;
private readonly string _procRoot;
private readonly string _sysRoot;
private (long Total, long Idle)? _previousCpu; private (long Total, long Idle)? _previousCpu;
private (long At, long Rx, long Tx)? _previousNet; private (long At, long Rx, long Tx)? _previousNet;
public HostSampler(TimeProvider time, Func<string, string?>? read = null, Func<(long Total, long Used)?>? disk = null) public HostSampler(TimeProvider time, Func<string, string?>? read = null, Func<(long Total, long Used)?>? disk = null,
string? procRoot = null, string? sysRoot = null, string? diskRoot = null)
{ {
_time = time; _time = time;
_read = read ?? ReadProc; _read = read ?? ReadProc;
_disk = disk ?? ReadRootDisk; _procRoot = Root(procRoot, "HOST_PROC", "/proc");
_sysRoot = Root(sysRoot, "HOST_SYS", "/sys");
string root = Root(diskRoot, "HOST_ROOT", OperatingSystem.IsWindows() ? Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\" : "/");
_disk = disk ?? (() => ReadRootDisk(root));
} }
public HostSnapshot Sample() public HostSnapshot Sample()
@@ -30,7 +37,7 @@ public sealed class HostSampler
long now = _time.GetTimestamp(); long now = _time.GetTimestamp();
var snapshot = new HostSnapshot { AtUtcMs = _time.GetUtcNow().ToUnixTimeMilliseconds() }; var snapshot = new HostSnapshot { AtUtcMs = _time.GetUtcNow().ToUnixTimeMilliseconds() };
if (_read("/proc/stat") is { } stat && ParseCpu(stat) is { } cpu) if (_read(Path.Combine(_procRoot, "stat")) is { } stat && ParseCpu(stat) is { } cpu)
{ {
if (_previousCpu is { } before && cpu.Total > before.Total) if (_previousCpu is { } before && cpu.Total > before.Total)
{ {
@@ -41,7 +48,7 @@ public sealed class HostSampler
_previousCpu = cpu; _previousCpu = cpu;
} }
if (_read("/proc/meminfo") is { } meminfo && ParseMemory(meminfo) is { } memory) if (_read(Path.Combine(_procRoot, "meminfo")) is { } meminfo && ParseMemory(meminfo) is { } memory)
{ {
snapshot.MemTotal = memory.Total; snapshot.MemTotal = memory.Total;
snapshot.MemUsed = memory.Used; snapshot.MemUsed = memory.Used;
@@ -53,10 +60,11 @@ public sealed class HostSampler
snapshot.DiskUsed = disk.Used; snapshot.DiskUsed = disk.Used;
} }
if (_read("/proc/net/dev") is { } dev && ParseNetwork(dev) is { } net) if (_read(Path.Combine(_procRoot, "net/dev")) is { } dev && ParseNetwork(dev) is { } net)
{ {
snapshot.NetRxTotal = net.Rx; snapshot.NetRxTotal = net.Rx;
snapshot.NetTxTotal = net.Tx; snapshot.NetTxTotal = net.Tx;
snapshot.NetLinkMbps = ReadLinkSpeed(dev);
if (_previousNet is { } before && net.Rx >= before.Rx && net.Tx >= before.Tx) if (_previousNet is { } before && net.Rx >= before.Rx && net.Tx >= before.Tx)
{ {
double seconds = Math.Max(0.001, _time.GetElapsedTime(before.At, now).TotalSeconds); double seconds = Math.Max(0.001, _time.GetElapsedTime(before.At, now).TotalSeconds);
@@ -67,7 +75,11 @@ public sealed class HostSampler
_previousNet = (now, net.Rx, net.Tx); _previousNet = (now, net.Rx, net.Tx);
} }
snapshot.UptimeSeconds = _read("/proc/uptime") is { } uptime && ParseUptime(uptime) is { } up ? up : Environment.TickCount64 / 1000; snapshot.UptimeSeconds = _read(Path.Combine(_procRoot, "uptime")) is { } uptime && ParseUptime(uptime) is { } up ? up : null;
if (!OperatingSystem.IsLinux() && snapshot.UptimeSeconds is null)
{
snapshot.UptimeSeconds = Environment.TickCount64 / 1000;
}
return snapshot; return snapshot;
} }
@@ -165,11 +177,54 @@ public sealed class HostSampler
} }
} }
private static (long Total, long Used)? ReadRootDisk() private static string Root(string? value, string variable, string fallback)
{
value ??= Environment.GetEnvironmentVariable(variable);
return string.IsNullOrWhiteSpace(value) ? fallback : value;
}
private long? ReadLinkSpeed(string dev)
{
long total = 0;
bool any = false;
foreach (string line in dev.Split('\n'))
{
int colon = line.IndexOf(':', StringComparison.Ordinal);
if (colon < 0)
{
continue;
}
string name = line[..colon].Trim();
if (s_virtualPrefixes.Any(p => name.StartsWith(p, StringComparison.Ordinal)))
{
continue;
}
string? speed = _read(Path.Combine(_sysRoot, "class/net", name, "speed"));
if (long.TryParse(speed?.Trim(), CultureInfo.InvariantCulture, out long mbps) && mbps > 0)
{
total += mbps;
any = true;
}
else
{
return null;
}
}
return any ? total : null;
}
private static (long Total, long Used)? ReadRootDisk(string root)
{ {
try try
{ {
string root = OperatingSystem.IsWindows() ? Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\" : "/"; if (!Directory.Exists(root))
{
return null;
}
var drive = new DriveInfo(root); var drive = new DriveInfo(root);
return drive.IsReady ? (drive.TotalSize, drive.TotalSize - drive.AvailableFreeSpace) : null; return drive.IsReady ? (drive.TotalSize, drive.TotalSize - drive.AvailableFreeSpace) : null;
} }
@@ -7,6 +7,12 @@
<AssemblyName>monitor-agent</AssemblyName> <AssemblyName>monitor-agent</AssemblyName>
<RootNamespace>Monitor.Agent</RootNamespace> <RootNamespace>Monitor.Agent</RootNamespace>
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<StripSymbols>true</StripSymbols>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="Monitor.Backend.Tests" /> <InternalsVisibleTo Include="Monitor.Backend.Tests" />
@@ -7,6 +7,25 @@
<AssemblyName>fleet-enroll</AssemblyName> <AssemblyName>fleet-enroll</AssemblyName>
<RootNamespace>FleetEnroll</RootNamespace> <RootNamespace>FleetEnroll</RootNamespace>
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<StripSymbols>true</StripSymbols>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
<OptimizationPreference>Size</OptimizationPreference>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<StackTraceSupport>false</StackTraceSupport>
<EventSourceSupport>false</EventSourceSupport>
<DebuggerSupport>false</DebuggerSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<Http3Support>false</Http3Support>
<MetricsSupport>false</MetricsSupport>
<UseSizeOptimizedLinq>true</UseSizeOptimizedLinq>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Fleet.Enrollment.Client\Fleet.Enrollment.Client.csproj" /> <ProjectReference Include="..\..\src\Fleet.Enrollment.Client\Fleet.Enrollment.Client.csproj" />
+10
View File
@@ -5172,6 +5172,7 @@
"net_tx_total", "net_tx_total",
"net_rx_bps", "net_rx_bps",
"net_tx_bps", "net_tx_bps",
"net_link_mbps",
"uptime_seconds", "uptime_seconds",
"at" "at"
], ],
@@ -5258,6 +5259,15 @@
], ],
"format": "int64" "format": "int64"
}, },
"net_link_mbps": {
"pattern": "^-?(?:0|[1-9]\\d*)$",
"type": [
"null",
"integer",
"string"
],
"format": "int64"
},
"uptime_seconds": { "uptime_seconds": {
"pattern": "^-?(?:0|[1-9]\\d*)$", "pattern": "^-?(?:0|[1-9]\\d*)$",
"type": [ "type": [
+4
View File
@@ -8,6 +8,10 @@
<EventSourceSupport>false</EventSourceSupport> <EventSourceSupport>false</EventSourceSupport>
<DebuggerSupport>false</DebuggerSupport> <DebuggerSupport>false</DebuggerSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport> <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<Http3Support>false</Http3Support>
<MetricsSupport>false</MetricsSupport>
<UseSizeOptimizedLinq>true</UseSizeOptimizedLinq>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
<ServerGarbageCollection>false</ServerGarbageCollection> <ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection> <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
</PropertyGroup> </PropertyGroup>
@@ -7,6 +7,12 @@
<AssemblyName>monitor-probe</AssemblyName> <AssemblyName>monitor-probe</AssemblyName>
<RootNamespace>Monitor.Probe</RootNamespace> <RootNamespace>Monitor.Probe</RootNamespace>
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<StripSymbols>true</StripSymbols>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="Monitor.Backend.Tests" /> <InternalsVisibleTo Include="Monitor.Backend.Tests" />
@@ -339,6 +339,10 @@ public sealed class HostSnapshot
[JsonPropertyName("net_tx_bps")] [JsonPropertyName("net_tx_bps")]
public long? NetTxBps { get; set; } public long? NetTxBps { get; set; }
/// <summary>已统计接口的内核报告链路速率之和(Mbit/s);驱动不支持或无法读取时为空,不代表互联网带宽。</summary>
[JsonPropertyName("net_link_mbps")]
public long? NetLinkMbps { get; set; }
[JsonPropertyName("uptime_s")] [JsonPropertyName("uptime_s")]
public long? UptimeSeconds { get; set; } public long? UptimeSeconds { get; set; }
@@ -34,7 +34,7 @@ public static class Mappers
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));
public static HostDto ToDto(HostSnapshot h) => 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.UptimeSeconds, Time(h.AtUtcMs)); 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));
@@ -163,6 +163,7 @@ public sealed record HostDto(
long? NetTxTotal, long? NetTxTotal,
long? NetRxBps, long? NetRxBps,
long? NetTxBps, long? NetTxBps,
long? NetLinkMbps,
long? UptimeSeconds, long? UptimeSeconds,
DateTimeOffset At); DateTimeOffset At);
@@ -1,9 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<Import Project="Sdk.props" Sdk="StuDev.AotAnywhere" Condition="'$(UseAotAnywhere)' == 'true'" />
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<AssemblyName>monitor-backend</AssemblyName> <AssemblyName>monitor-backend</AssemblyName>
<RootNamespace>Monitor.Backend</RootNamespace> <RootNamespace>Monitor.Backend</RootNamespace>
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
<OptimizationPreference>Size</OptimizationPreference>
<StackTraceSupport>false</StackTraceSupport>
<EventSourceSupport>false</EventSourceSupport>
<DebuggerSupport>false</DebuggerSupport>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
<StripSymbols>true</StripSymbols>
<!-- This service exposes APIs only; there are no static web assets to publish. -->
<StaticWebAssetsEnabled>false</StaticWebAssetsEnabled>
<SqliteStaticLink Condition="'$(SqliteStaticLink)' == '' and '$(PublishAot)' == 'true' and '$(RuntimeIdentifier)' != ''">true</SqliteStaticLink>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator> <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
<OpenApiGenerateDocuments>true</OpenApiGenerateDocuments> <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
@@ -12,8 +26,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" PrivateAssets="all" /> <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" PrivateAssets="all" />
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Condition="'$(SqliteStaticLink)' != 'true'" />
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" ExcludeAssets="native" Condition="'$(SqliteStaticLink)' == 'true'" />
<ProjectReference Include="..\Monitor.Backend.Api\Monitor.Backend.Api.csproj" /> <ProjectReference Include="..\Monitor.Backend.Api\Monitor.Backend.Api.csproj" />
<ProjectReference Include="..\Monitor.Backend.Gateway\Monitor.Backend.Gateway.csproj" /> <ProjectReference Include="..\Monitor.Backend.Gateway\Monitor.Backend.Gateway.csproj" />
<ProjectReference Include="..\Monitor.Backend.Storage.Sqlite\Monitor.Backend.Storage.Sqlite.csproj" /> <ProjectReference Include="..\Monitor.Backend.Storage.Sqlite\Monitor.Backend.Storage.Sqlite.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="StaticSqlite.targets" />
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<Import Project="Sdk.targets" Sdk="StuDev.AotAnywhere" Condition="'$(UseAotAnywhere)' == 'true'" />
</Project> </Project>
@@ -0,0 +1,71 @@
<Project>
<PropertyGroup>
<_SqliteToolchainTargets>SetupOSSpecificProps</_SqliteToolchainTargets>
<_SqliteToolchainTargets Condition="'$(UseAotAnywhere)' == 'true'">SetupOSSpecificProps;OverwriteTargetTriple;SetPathToZig</_SqliteToolchainTargets>
</PropertyGroup>
<ItemGroup Condition="'$(SqliteStaticLink)' == 'true'">
<DirectPInvoke Include="e_sqlite3" />
</ItemGroup>
<Target Name="PrepareStaticSqliteSource" Condition="'$(SqliteStaticLink)' == 'true'">
<Error Condition="'$(RuntimeIdentifier)' != 'linux-x64' and '$(RuntimeIdentifier)' != 'linux-arm64' and
'$(RuntimeIdentifier)' != 'linux-musl-x64' and '$(RuntimeIdentifier)' != 'linux-musl-arm64' and
'$(RuntimeIdentifier)' != 'osx-x64' and '$(RuntimeIdentifier)' != 'osx-arm64' and
'$(RuntimeIdentifier)' != 'win-x64' and '$(RuntimeIdentifier)' != 'win-arm64'"
Text="Static SQLite is configured for the eight supported x64/arm64 Linux, musl, macOS and Windows targets. Unsupported RID: $(RuntimeIdentifier)." />
<PropertyGroup>
<_SqliteCache>$([System.IO.Path]::GetFullPath('$(BaseIntermediateOutputPath)sqlite/$(MonitorSqliteSourceVersion)/'))</_SqliteCache>
<_SqliteArchive>$(_SqliteCache)sqlite-amalgamation-$(MonitorSqliteSourceVersion).zip</_SqliteArchive>
<_SqliteSource>$(_SqliteCache)sqlite-amalgamation-$(MonitorSqliteSourceVersion)/sqlite3.c</_SqliteSource>
<_SqliteObjectDirectory>$(_SqliteCache)$(RuntimeIdentifier)/$(UseAotAnywhere)/</_SqliteObjectDirectory>
<_SqliteObject>$(_SqliteObjectDirectory)e_sqlite3.o</_SqliteObject>
<_SqliteObject Condition="$(RuntimeIdentifier.StartsWith('win'))">$(_SqliteObjectDirectory)e_sqlite3.obj</_SqliteObject>
</PropertyGroup>
<MakeDir Directories="$(_SqliteObjectDirectory)" />
<DownloadFile SourceUrl="https://www.sqlite.org/$(MonitorSqliteSourceYear)/sqlite-amalgamation-$(MonitorSqliteSourceVersion).zip"
DestinationFolder="$(_SqliteCache)" Condition="!Exists('$(_SqliteArchive)')" />
<GetFileHash Files="$(_SqliteArchive)" Algorithm="SHA256">
<Output TaskParameter="Hash" PropertyName="_SqliteArchiveHash" />
</GetFileHash>
<Error Condition="'$(_SqliteArchiveHash.ToLowerInvariant())' != '$(MonitorSqliteSourceSha256)'"
Text="SQLite source SHA256 mismatch: $(_SqliteArchiveHash)." />
<Unzip SourceFiles="$(_SqliteArchive)" DestinationFolder="$(_SqliteCache)" Condition="!Exists('$(_SqliteSource)')" />
</Target>
<Target Name="CompileStaticSqliteObject" Condition="'$(SqliteStaticLink)' == 'true'"
DependsOnTargets="PrepareStaticSqliteSource;$(_SqliteToolchainTargets)"
Inputs="$(_SqliteSource);$(MSBuildThisFileFullPath)" Outputs="$(_SqliteObject)">
<PropertyGroup>
<_SqliteUseMsvc>false</_SqliteUseMsvc>
<_SqliteUseMsvc Condition="$(RuntimeIdentifier.StartsWith('win')) and '$(_AotAnywhereWindowsCross)' != 'true'">true</_SqliteUseMsvc>
<_SqliteCompiler>&quot;$(CppCompilerAndLinker)&quot;</_SqliteCompiler>
<_SqliteCompiler Condition="'$(CppCompilerAndLinker)' == ''">cc</_SqliteCompiler>
<_SqliteCompiler Condition="'$(TargetTriple)' != ''">$(_SqliteCompiler) --target=$(TargetTriple)</_SqliteCompiler>
<_SqliteCompiler Condition="'$(SysRoot)' != ''">$(_SqliteCompiler) --sysroot=&quot;$(SysRoot)&quot;</_SqliteCompiler>
<_SqliteCompiler Condition="'$(UseAotAnywhere)' == 'true'">&quot;$(_AotAnywhereZigExe)&quot; cc -target $(TargetTriple)</_SqliteCompiler>
<_SqliteCompiler Condition="'$(_SqliteUseMsvc)' == 'true'">cl</_SqliteCompiler>
</PropertyGroup>
<Message Importance="high" Text="Compiling SQLite $(MonitorSqliteSourceVersion) into the $(RuntimeIdentifier) NativeAOT executable." />
<Exec Condition="'$(_SqliteUseMsvc)' != 'true'"
Command="$(_SqliteCompiler) -Os -fPIC -c &quot;$(_SqliteSource)&quot; -o &quot;$(_SqliteObject)&quot; -DSQLITE_API= -DSQLITE_THREADSAFE=1 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_SNAPSHOT -DSQLITE_ENABLE_UNLOCK_NOTIFY" />
<Exec Condition="'$(_SqliteUseMsvc)' == 'true'"
Command="cl /nologo /O1 /MT /c &quot;$(_SqliteSource)&quot; /Fo&quot;$(_SqliteObject)&quot; /DSQLITE_API= /DSQLITE_THREADSAFE=1 /DSQLITE_DEFAULT_MEMSTATUS=0 /DSQLITE_ENABLE_COLUMN_METADATA /DSQLITE_ENABLE_FTS4 /DSQLITE_ENABLE_FTS5 /DSQLITE_ENABLE_MATH_FUNCTIONS /DSQLITE_ENABLE_RTREE /DSQLITE_ENABLE_SNAPSHOT /DSQLITE_ENABLE_UNLOCK_NOTIFY" />
</Target>
<!-- AotAnywhere runs before LinkNative; register the object before either linker starts. -->
<Target Name="LinkStaticSqlite" Condition="'$(SqliteStaticLink)' == 'true'"
BeforeTargets="AotAnywhereDirectLinkNative;AotAnywhereDirectLinkMacNative;AotAnywhereDirectLinkWindows;LinkNative"
DependsOnTargets="CompileStaticSqliteObject">
<ItemGroup>
<NativeLibrary Include="$(_SqliteObject)" />
<LinkerArg Include="&quot;$(_SqliteObject)&quot;" />
</ItemGroup>
</Target>
<Target Name="RemoveStaticSqliteRuntimeFiles" Condition="'$(SqliteStaticLink)' == 'true'" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
Condition="'%(ResolvedFileToPublish.Filename)' == 'libe_sqlite3' or '%(ResolvedFileToPublish.Filename)' == 'e_sqlite3'" />
</ItemGroup>
</Target>
</Project>
@@ -35,11 +35,20 @@ public sealed class AotSmokeTests
string token = JsonNode.Parse(await tokenResponse.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>(); string token = JsonNode.Parse(await tokenResponse.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
using HttpResponseMessage settingsResponse = await http.PatchAsync("/api/admin/settings", BackendFixture.Json(new JsonObject { ["site.public"] = true }, "application/merge-patch+json"));
settingsResponse.EnsureSuccessStatusCode();
using HttpResponseMessage probeResponse = await http.PostAsync("/api/admin/probes", BackendFixture.Json(new JsonObject { ["name"] = "aot-probe-" + Guid.NewGuid().ToString("N")[..6] }));
probeResponse.EnsureSuccessStatusCode();
string probeId = JsonNode.Parse(await probeResponse.Content.ReadAsStringAsync())!["probe_id"]!.GetValue<string>();
// Agent:加入令牌 → 注册(ECDSA 持有证明)→ WebSocket 签名认证 → 期望状态 // Agent:加入令牌 → 注册(ECDSA 持有证明)→ WebSocket 签名认证 → 期望状态
using HttpResponseMessage joinResponse = await http.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["max_uses"] = 1 })); using HttpResponseMessage joinResponse = await http.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["max_uses"] = 1 }));
joinResponse.EnsureSuccessStatusCode();
string joinToken = JsonNode.Parse(await joinResponse.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>(); string joinToken = JsonNode.Parse(await joinResponse.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>();
using var anonymous = new HttpClient { BaseAddress = baseAddress }; using var anonymous = new HttpClient { BaseAddress = baseAddress };
await using AgentSimulator agent = await AgentSimulator.EnrollAsync(anonymous, joinToken, "aot-" + Guid.NewGuid().ToString("N")[..6]); await using AgentSimulator agent = await AgentSimulator.EnrollAsync(anonymous, joinToken, "aot-" + Guid.NewGuid().ToString("N")[..6]);
agent.ProbeForChunks = new ProbeId(probeId);
string lineId = $"{agent.NodeId.Value}~{probeId}";
await agent.ConnectAsync(baseAddress); await agent.ConnectAsync(baseAddress);
TaskSet tasks = await agent.WaitForTaskSetAsync(s => s.Tasks.Count >= 2); TaskSet tasks = await agent.WaitForTaskSetAsync(s => s.Tasks.Count >= 2);
Assert.Contains(tasks.Tasks, t => t.Kind == "udp_echo"); Assert.Contains(tasks.Tasks, t => t.Kind == "udp_echo");
@@ -54,7 +63,7 @@ public sealed class AotSmokeTests
// 查询:热层的精确汇总,随后是持久层的分钟窗口 // 查询:热层的精确汇总,随后是持久层的分钟窗口
string range = $"from={Iso(baseMs)}&to={Iso(baseMs + minute)}&metrics=loss.total,rtt.p95"; string range = $"from={Iso(baseMs)}&to={Iso(baseMs + minute)}&metrics=loss.total,rtt.p95";
JsonNode hot = await GetAsync(http, $"/api/public/lines/{agent.NodeId.Value}/series?{range}&step=1s"); JsonNode hot = await GetAsync(http, $"/api/public/lines/{lineId}/series?{range}&step=1s");
Assert.Equal("hot", hot["source"]!.GetValue<string>()); Assert.Equal("hot", hot["source"]!.GetValue<string>());
Assert.Equal(600, hot["summary"]!["samples"]!["udp_echo"]!.GetValue<long>()); Assert.Equal(600, hot["summary"]!["samples"]!["udp_echo"]!.GetValue<long>());
Assert.Equal(10.0, hot["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 6); Assert.Equal(10.0, hot["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 6);
@@ -62,7 +71,7 @@ public sealed class AotSmokeTests
JsonNode persisted; JsonNode persisted;
do do
{ {
persisted = await GetAsync(http, $"/api/public/lines/{agent.NodeId.Value}/series?{range}&step=1m"); persisted = await GetAsync(http, $"/api/public/lines/{lineId}/series?{range}&step=1m");
if (persisted["summary"]!["samples"]!["udp_echo"]?.GetValue<long>() == 600) if (persisted["summary"]!["samples"]!["udp_echo"]?.GetValue<long>() == 600)
{ {
break; break;
@@ -80,16 +89,16 @@ public sealed class AotSmokeTests
Assert.True(RawChunkCodec.Decode(bytes).IsSuccess); Assert.True(RawChunkCodec.Decode(bytes).IsSuccess);
// SSE:快照 + 增量 // SSE:快照 + 增量
await using SseClient sse = await SseClient.ConnectAsync(http, $"/api/public/live?topics=nodes,series:{agent.NodeId.Value}:1s:udp"); await using SseClient sse = await SseClient.ConnectAsync(http, $"/api/public/live?topics=nodes,series:{lineId}:1s:udp");
await sse.WaitForAsync(e => e.Name == "series"); await sse.WaitForAsync(e => e.Name == "series");
// 按需测试:Probe 事件 → 下发 → 状态 // 按需测试:Probe 事件 → 下发 → 状态
await agent.SendEventAsync(AgentEventKinds.ProbeConnected); await agent.SendEventAsync(AgentEventKinds.ProbeConnected, probeId);
JsonNode started = null!; JsonNode started = null!;
long begun = TimeProvider.System.GetTimestamp(); long begun = TimeProvider.System.GetTimestamp();
while (TimeProvider.System.GetElapsedTime(begun) < TimeSpan.FromSeconds(10)) while (TimeProvider.System.GetElapsedTime(begun) < TimeSpan.FromSeconds(10))
{ {
using HttpResponseMessage response = await http.PostAsync($"/api/admin/lines/{agent.NodeId.Value}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "reality_session" })); using HttpResponseMessage response = await http.PostAsync($"/api/admin/lines/{lineId}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "reality_session" }));
if (response.StatusCode == System.Net.HttpStatusCode.Accepted) if (response.StatusCode == System.Net.HttpStatusCode.Accepted)
{ {
started = JsonNode.Parse(await response.Content.ReadAsStringAsync())!; started = JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
@@ -21,6 +21,7 @@ public sealed class HostSamplerTests
Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1)); Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1));
Assert.Equal(3600L, HostSampler.ParseUptime("3600.55 7000.1\n")); Assert.Equal(3600L, HostSampler.ParseUptime("3600.55 7000.1\n"));
Assert.Null(HostSampler.ParseNetwork("nothing useful")); Assert.Null(HostSampler.ParseNetwork("nothing useful"));
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] [Fact]
@@ -29,7 +30,7 @@ public sealed class HostSamplerTests
var time = new Microsoft.Extensions.Time.Testing.FakeTimeProvider(); var time = new Microsoft.Extensions.Time.Testing.FakeTimeProvider();
string stat = Stat1; string stat = Stat1;
string dev = Dev1; string dev = Dev1;
var sampler = new HostSampler(time, path => path switch { "/proc/stat" => stat, "/proc/net/dev" => dev, "/proc/uptime" => "100.0 1.0", _ => null }, () => (1000, 250)); var sampler = new HostSampler(time, path => path switch { "/proc/stat" => stat, "/proc/net/dev" => dev, "/proc/uptime" => "100.0 1.0", _ => null }, () => (1000, 250), procRoot: "/proc", sysRoot: "/sys");
HostSnapshot first = sampler.Sample(); HostSnapshot first = sampler.Sample();
Assert.Null(first.CpuPercent); // 第一次没有基线 Assert.Null(first.CpuPercent); // 第一次没有基线
Assert.Null(first.NetRxBps); Assert.Null(first.NetRxBps);
@@ -45,6 +46,54 @@ public sealed class HostSamplerTests
Assert.Equal(2000, second.NetTxBps); Assert.Equal(2000, second.NetTxBps);
Assert.Equal(3000, second.NetRxTotal); Assert.Equal(3000, second.NetRxTotal);
} }
[Fact]
public void Sample_UsesMountedHostProcAndSys_InsteadOfContainerPaths()
{
var reads = new List<string>();
var sampler = new HostSampler(TimeProvider.System, path =>
{
reads.Add(path);
return path switch
{
"/host/proc/stat" => Stat1,
"/host/proc/meminfo" => "MemTotal: 8192 kB\nMemAvailable: 6144 kB\n",
"/host/proc/net/dev" => Dev1,
"/host/proc/uptime" => "3600.0 7000.0",
"/host/sys/class/net/eth0/speed" => "10000\n",
_ => throw new InvalidOperationException("unexpected container path: " + path),
};
}, () => (1000, 250), procRoot: "/host/proc", sysRoot: "/host/sys");
HostSnapshot snapshot = sampler.Sample();
Assert.Equal(8L * 1024 * 1024, snapshot.MemTotal);
Assert.Equal(1000, snapshot.NetRxTotal);
Assert.Equal(10000, snapshot.NetLinkMbps);
Assert.Equal(3600, snapshot.UptimeSeconds);
Assert.DoesNotContain(reads, path => path.StartsWith("/proc/", StringComparison.Ordinal));
Assert.DoesNotContain(reads, path => path.Contains("docker0", StringComparison.Ordinal) || path.Contains("/lo/", StringComparison.Ordinal));
}
[Theory]
[InlineData(null)]
[InlineData("-1")]
[InlineData("0")]
[InlineData("not supported")]
public void Sample_DoesNotInventLinkSpeedOrContainerDisk_WhenHostDataIsUnavailable(string? speed)
{
var sampler = new HostSampler(TimeProvider.System,
path => path.EndsWith("net/dev", StringComparison.Ordinal) ? Dev1 : path.EndsWith("/speed", StringComparison.Ordinal) ? speed : null,
procRoot: "/host/proc", sysRoot: "/host/sys", diskRoot: Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
HostSnapshot snapshot = sampler.Sample();
Assert.Null(snapshot.NetLinkMbps);
Assert.Null(snapshot.DiskTotal);
Assert.Null(snapshot.DiskUsed);
if (OperatingSystem.IsLinux())
{
Assert.Null(snapshot.UptimeSeconds);
}
}
} }
[Collection("backend")] [Collection("backend")]
@@ -61,6 +110,7 @@ public sealed class HostStatusTests
{ {
CpuPercent = cpu, MemUsed = 2L << 30, MemTotal = 8L << 30, DiskUsed = 10L << 30, DiskTotal = 40L << 30, CpuPercent = cpu, MemUsed = 2L << 30, MemTotal = 8L << 30, DiskUsed = 10L << 30, DiskTotal = 40L << 30,
NetRxTotal = 600L << 30, NetTxTotal = 180L << 30, NetRxBps = 2500, NetTxBps = 4800, UptimeSeconds = 3_000_000, NetRxTotal = 600L << 30, NetTxTotal = 180L << 30, NetRxBps = 2500, NetTxBps = 4800, UptimeSeconds = 3_000_000,
NetLinkMbps = 1000,
}; };
[Fact] [Fact]
@@ -81,6 +131,7 @@ public sealed class HostStatusTests
JsonNode item = list["items"]!.AsArray().Single(i => i!["agent_id"]!.GetValue<string>() == id)!; JsonNode item = list["items"]!.AsArray().Single(i => i!["agent_id"]!.GetValue<string>() == id)!;
Assert.False(item["stale"]!.GetValue<bool>()); Assert.False(item["stale"]!.GetValue<bool>());
Assert.Equal(40L << 30, item["host"]!["disk_total"]!.GetValue<long>()); Assert.Equal(40L << 30, item["host"]!["disk_total"]!.GetValue<long>());
Assert.Equal(1000, item["host"]!["net_link_mbps"]!.GetValue<long>());
// 展示页能拿到的只有汇总数字:整份响应里没有型号、主机名、地址之类的字段 // 展示页能拿到的只有汇总数字:整份响应里没有型号、主机名、地址之类的字段
string text = list.ToJsonString(); string text = list.ToJsonString();
+8 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build a framework-dependent Backend OCI image for the requested arch; publishing is optional. # Build a musl NativeAOT Backend OCI image with statically linked SQLite; publishing is optional.
set -euo pipefail set -euo pipefail
arch="${1:?usage: release-linux.sh <amd64|arm64>}" arch="${1:?usage: release-linux.sh <amd64|arm64>}"
@@ -28,7 +28,8 @@ if [[ "$image_publish" == true ]]; then
} }
fi fi
case "$arch" in case "$arch" in
amd64|arm64) ;; amd64) rid=linux-musl-x64 ;;
arm64) rid=linux-musl-arm64 ;;
*) echo "unsupported architecture: $arch" >&2; exit 2 ;; *) echo "unsupported architecture: $arch" >&2; exit 2 ;;
esac esac
@@ -51,12 +52,13 @@ export MONITOR_COMMIT="${MONITOR_COMMIT:-${CI_COMMIT_SHA:0:10}}"
export MONITOR_BUILT_AT="${MONITOR_BUILT_AT:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" export MONITOR_BUILT_AT="${MONITOR_BUILT_AT:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
(cd "$root/backend" && dotnet publish src/Monitor.Backend \ (cd "$root/backend" && dotnet publish src/Monitor.Backend \
-m:3 -c Release --no-self-contained \ -m:3 -c Release -r "$rid" --self-contained true \
-p:DebugType=none -p:DebugSymbols=false \ -p:DebugType=none -p:DebugSymbols=false \
-p:UseAppHost=false -p:OpenApiGenerateDocuments=false \ -p:PublishAot=true -p:UseAotAnywhere=true -p:SqliteStaticLink=true -p:OpenApiGenerateDocuments=false \
--artifacts-path "$build_root" -o "$output") --artifacts-path "$build_root" -o "$output")
find "$output" -type f -name '*.pdb' -delete [[ -x "$output/monitor-backend" ]] || { echo "NativeAOT Backend executable is missing" >&2; exit 1; }
cp -a "$output/." "$container_context/app/" [[ "$(wc -c < "$output/monitor-backend")" -gt 2000000 ]] || { echo "Backend output is an apphost, not a NativeAOT executable" >&2; exit 1; }
install -m 0755 "$output/monitor-backend" "$container_context/app/monitor-backend"
: > "$container_context/data/.keep" : > "$container_context/data/.keep"
# Woodpecker's local backend isolates HOME per workflow. Keep Buildah's image # Woodpecker's local backend isolates HOME per workflow. Keep Buildah's image
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Publish glibc OCI images for the Linux Agent and Probe on amd64 and arm64. # Publish musl OCI images for the Linux Agent and Probe on amd64 and arm64.
set -euo pipefail set -euo pipefail
tag="${CI_COMMIT_TAG:-}" tag="${CI_COMMIT_TAG:-}"
@@ -113,7 +113,7 @@ publish_index() {
} }
for component in agent probe; do for component in agent probe; do
build_arch_image "$component" amd64 amd64 build_arch_image "$component" amd64 musl-amd64
build_arch_image "$component" arm64 arm64 build_arch_image "$component" arm64 musl-arm64
publish_index "$component" publish_index "$component"
done done
+7
View File
@@ -18,6 +18,9 @@ GLOBAL_BACKEND_FILES = {
"NuGet.Config", "NuGet.Config",
"NuGet.config", "NuGet.config",
} }
# The container stages backend as a standalone tree. Copy the repository-wide
# SDK and package versions into it so restore/publish use the same configuration.
ROOT_BUILD_FILES = {"Directory.Packages.props", "global.json"}
CONTAINERFILES = { CONTAINERFILES = {
"agent": ROOT / "packaging/container/Agent.NativeBuild.Containerfile", "agent": ROOT / "packaging/container/Agent.NativeBuild.Containerfile",
"probe": ROOT / "packaging/container/Probe.NativeBuild.Containerfile", "probe": ROOT / "packaging/container/Probe.NativeBuild.Containerfile",
@@ -49,6 +52,10 @@ def stage(component: str, destination: Path) -> None:
target.parent.mkdir(parents=True, exist_ok=True) target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target) shutil.copy2(path, target)
for name in ROOT_BUILD_FILES:
for directory in (restore, source):
shutil.copy2(ROOT / name, directory / name)
config = destination / "config" config = destination / "config"
config.mkdir() config.mkdir()
(config / ".keep").write_text("", encoding="utf-8") (config / ".keep").write_text("", encoding="utf-8")
+35 -8
View File
@@ -24,7 +24,7 @@ macOS 的 Apple Silicon 产物带临时 ad-hoc 签名,Intel 产物未签名。
## Backend 容器 ## Backend 容器
Backend 使用 OCI 标准镜像,通过 `registry.oeo.one/monitor/monitor-backend:<version>` 分发,支持 `linux/amd64` 和 `linux/arm64`。容器基于 .NET 10 ASP.NET noble-chiseled-extra 运行时镜像,以非 root 用户运行,在 `8080/tcp` 提供 HTTP 服务;数据库和原始数据统一写入 `/data`。`extra` 变体带 IANA 时区数据,供按时区查询使用。将 `/data` 挂载为持久卷,并在反向代理处配置 HTTPS。 Backend 使用 OCI 标准镜像,通过 `registry.oeo.one/monitor/monitor-backend:<version>` 分发,支持 `linux/amd64` 和 `linux/arm64`。容器使用 musl NativeAOT 单文件程序,基底为 `mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine`,以非 root 用户运行,在 `8080/tcp` 提供 HTTP 服务;数据库和原始数据统一写入 `/data`。程序启用 InvariantGlobalization,无需 .NET 运行时或 ICU;时间查询使用 UTC。SQLite 从固定版本的官方源码编译并静态链接,不再随镜像携带 `libe_sqlite3.so`。将 `/data` 挂载为持久卷,并在反向代理处配置 HTTPS。
在部署目录保存此仓库的 `compose.yaml`,先通过部署平台的环境变量或密钥管理设置 `MONITOR_ADMIN_PASSWORD`(可选设置 `MONITOR_ADMIN_USER`、`MONITOR_VERSION`),然后启动: 在部署目录保存此仓库的 `compose.yaml`,先通过部署平台的环境变量或密钥管理设置 `MONITOR_ADMIN_PASSWORD`(可选设置 `MONITOR_ADMIN_USER`、`MONITOR_VERSION`),然后启动:
@@ -34,11 +34,13 @@ docker compose up -d
Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 HTTPS 反向代理转发;数据保存在 Docker 命名卷 `monitor-data`。首次启动成功创建管理员后,改密应在管理端完成。 Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 HTTPS 反向代理转发;数据保存在 Docker 命名卷 `monitor-data`。首次启动成功创建管理员后,改密应在管理端完成。
Backend 的 NativeAOT 发布默认启用 `SqliteStaticLink`,支持 Linux glibc/musl、macOS、Windows 的 x64/arm64 RID;跨平台构建使用 `-p:UseAotAnywhere=true`。SQLite 源码版本、下载年份和 SHA-256 统一配置在根目录 `Directory.Packages.props`,构建会校验下载文件并按 RID 缓存原生对象。普通 JIT 开发构建仍使用 NuGet 原生库。发布 Backend 时设置 `-p:OpenApiGenerateDocuments=false`,API 文档在普通开发构建中生成。
健康检查端点为 `GET /healthz`。容器定义见 [`../packaging/container/Backend.Containerfile`](../packaging/container/Backend.Containerfile)。 健康检查端点为 `GET /healthz`。容器定义见 [`../packaging/container/Backend.Containerfile`](../packaging/container/Backend.Containerfile)。
## 节点二进制发布 ## 节点二进制发布
Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上,由 Windows runner 使用 AotAnywhere 并行构建 Agent/Probe 的 Linux glibc、Linux musl、macOS 和 Windows 产物。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。Linux glibc 的 Agent 与 Probe 二进制还会打包为 OCI 镜像,支持 `linux/amd64` 和 `linux/arm64`;musl、macOS、Windows 仍通过对应平台归档分发。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [`../backend/agent/README.md`](../backend/agent/README.md) 与 [`../backend/probe/README.md`](../backend/probe/README.md)。 Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上,由 Windows runner 使用 AotAnywhere 并行构建 Agent/Probe 的 Linux glibc、Linux musl、macOS 和 Windows 产物。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。Linux musl 的 Agent 与 Probe 二进制还会打包为 OCI 镜像,基底为 `mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine`,支持 `linux/amd64` 和 `linux/arm64`;所有平台仍提供对应归档。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [`../backend/agent/README.md`](../backend/agent/README.md) 与 [`../backend/probe/README.md`](../backend/probe/README.md)。
## CI 工作流 ## CI 工作流
@@ -56,7 +58,7 @@ Pull request 和 `dev` push 会运行 Backend tests;通过后,独立的 `web
| Agent | `registry.oeo.one/monitor-dev/monitor-agent:latest` | `registry.oeo.one/monitor-dev/monitor-agent:dev-<12位SHA>` | | Agent | `registry.oeo.one/monitor-dev/monitor-agent:latest` | `registry.oeo.one/monitor-dev/monitor-agent:dev-<12位SHA>` |
| Probe | `registry.oeo.one/monitor-dev/monitor-probe:latest` | `registry.oeo.one/monitor-dev/monitor-probe:dev-<12位SHA>` | | Probe | `registry.oeo.one/monitor-dev/monitor-probe:latest` | `registry.oeo.one/monitor-dev/monitor-probe:dev-<12位SHA>` |
Dev 镜像目前仅支持 amd64。它们不创建 Gitea Release,也不修改正式版 `monitor/` 仓库中的标签。普通 main push 不会构建镜像。 Dev 镜像目前仅支持 amd64;Agent/Probe 在 `sdk:10.0-alpine-aot` 中构建 `linux-musl-x64` 产物,运行基底为 `runtime-deps:10.0-alpine`。它们不创建 Gitea Release,也不修改正式版 `monitor/` 仓库中的标签。普通 main push 不会构建镜像。
### 正式版发布 ### 正式版发布
@@ -76,7 +78,11 @@ docker run -d --name monitor-probe --restart unless-stopped --network host \
宿主机配置目录应允许 UID/GID 1654 读写;Probe 需要写权限来更新 `agents.json`。也可通过 Docker Compose 配置相同的网络、卷和能力。 宿主机配置目录应允许 UID/GID 1654 读写;Probe 需要写权限来更新 `agents.json`。也可通过 Docker Compose 配置相同的网络、卷和能力。
实测体积(`StackTraceSupport=false`、`IlcFoldIdenticalMethodBodies=true`、`OptimizationPreference=Size`): Agent 和 Probe 的默认发布配置:NativeAOT、完整裁剪、`OptimizationPreference=Size`、符号剥离、相同方法体折叠、InvariantGlobalization;关闭 Server/Concurrent GC、异常栈、EventSource、调试、Hot Reload、HTTP tracing、HTTP/3 和 .NET Metrics。JSON 全部使用源码生成上下文,禁用反射序列化回退;LINQ 使用体积优化实现。HTTP/3 裁剪不影响现有 HTTP/WebSocket/TLS 连接,也不影响程序自身上报的监控指标。
`fleet-enroll` 使用同一套体积优化开关。Fleet 共享库已声明 AOT 兼容,由 Agent、Probe、Backend 或注册工具的最终发布一起裁剪;GC 和运行时功能开关配置在各可执行项目中。
历史实测体积(后续以各 RID 的 CI 产物为准):
| 程序 | linux-musl-x64 | linux-x64 | win-x64 | | 程序 | linux-musl-x64 | linux-x64 | win-x64 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@@ -212,7 +218,7 @@ Restart=always
RestartSec=5 RestartSec=5
TimeoutStopSec=30 TimeoutStopSec=30
ExecStartPre=-/usr/bin/podman rm -f monitor-agent ExecStartPre=-/usr/bin/podman rm -f monitor-agent
ExecStart=/usr/bin/podman run --rm --name monitor-agent --network host --read-only --security-opt no-new-privileges --cap-drop all -v /etc/os-release:/etc/os-release:ro -v /opt/monitor/etc:/etc/monitor:Z ${IMAGE} ExecStart=/usr/bin/podman run --rm --name monitor-agent --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro -e HOST_PROC=/host/proc -e HOST_SYS=/host/sys -e HOST_ROOT=/host/root -v /opt/monitor/etc:/etc/monitor ${IMAGE}
ExecStop=/usr/bin/podman stop -t 20 monitor-agent ExecStop=/usr/bin/podman stop -t 20 monitor-agent
[Install] [Install]
@@ -220,12 +226,33 @@ WantedBy=multi-user.target
``` ```
- **数据目录**(上面挂到容器 `/etc/monitor` 的 `/opt/monitor/etc`)归容器用户(UID/GID 1654),里面放 `agent.json`、身份目录(`fleet-enroll` 注册之后的 `agent-id/`),Probe 放 `probe.json` 和 `agents.json`。`agent.json` 里的 `fleet.identity_dir` 写容器里的路径(例如 `/etc/monitor/agent-id`)。 - **数据目录**(上面挂到容器 `/etc/monitor` 的 `/opt/monitor/etc`)归容器用户(UID/GID 1654),里面放 `agent.json`、身份目录(`fleet-enroll` 注册之后的 `agent-id/`),Probe 放 `probe.json` 和 `agents.json`。`agent.json` 里的 `fleet.identity_dir` 写容器里的路径(例如 `/etc/monitor/agent-id`)。
- **首次注册 Agent**:`podman run --rm --network host -v <数据目录>:/etc/monitor --entrypoint /usr/local/bin/fleet-enroll <镜像> --server https://<面板> --join <加入令牌> --name <名字> --identity-dir /etc/monitor/agent-id`,再写 `agent.json`。Probe:在管理端创建(或轮换密钥)之后下载 `probe.json`,放进数据目录。 - **首次注册 Agent**:`sudo podman run --rm --network host --security-opt label=disable -v <数据目录>:/etc/monitor --entrypoint /usr/local/bin/fleet-enroll <镜像> --server https://<面板> --join <加入令牌> --name <名字> --identity-dir /etc/monitor/agent-id`,再写 `agent.json`。Probe:在管理端创建(或轮换密钥)之后下载 `probe.json`,放进数据目录。
- **拉取私有镜像**需要 Registry 的登录信息:用 `podman login --authfile /etc/monitor-update/registry-auth.json …` 生成一份只有 root 能读(600)的认证文件,更新器用它拉取,容器本身用不到。 - **拉取私有镜像**需要 Registry 的登录信息:用 `podman login --authfile /etc/monitor-update/registry-auth.json …` 生成一份只有 root 能读(600)的认证文件,更新器用它拉取,容器本身用不到。
- **挂载宿主机的 `/etc/os-release`(只读)**:节点读系统名展示在面板上,不挂的话读到的是镜像里的 Ubuntu。 - **挂载宿主机的 `/etc/os-release`(只读)**:节点读系统名展示在面板上,不挂的话读到的是镜像里的 Alpine。
- **ICMP 目标任务**要在 `podman run` 里加 `--cap-add NET_RAW`。 - **ICMP 目标任务**要在 `podman run` 里加 `--cap-add NET_RAW`。
- **经隧道注册之后检查 Agent 的 `public_host`**:面板看到的来源地址是隧道的出口(通常是内网地址),要在管理端改成这台机器自己的公网地址(`PATCH /api/admin/agents/{id}`),Probe 才连得上。 - **经隧道注册之后检查 Agent 的 `public_host`**:面板看到的来源地址是隧道的出口(通常是内网地址),要在管理端改成这台机器自己的公网地址(`PATCH /api/admin/agents/{id}`),Probe 才连得上。
- `packaging/compose/` 里的 compose 文件是不带宿主机更新器的简单方式:更新靠手动改版本再 `docker compose pull && up -d`,面板的更新按钮对它不起作用。 - `packaging/compose/` 里的 compose 文件可用 `sudo podman compose up -d` 或 `docker compose up -d`;Podman Compose 需要安装 Compose provider。更新靠手动改版本再 `sudo podman compose pull && sudo podman compose up -d`,面板的更新按钮对它不起作用。
### Podman 的宿主指标与权限
这里使用 Linux 上的 **rootful Podman**(`sudo podman`),容器内仍以 UID/GID 1654 运行。配置目录需由 1654 用户读写;`label=disable` 不会改变 Unix 文件权限。Rootless Podman 的 UID 映射不同,不能直接沿用 `chown 1654:1654` 的准备步骤。
Agent 的 Compose 模板已经包含以下只读挂载和环境变量,systemd 示例也采用相同配置:
| 宿主来源 | 容器路径 | 配置 | 指标 |
| --- | --- | --- | --- |
| `/proc` | `/host/proc` | `HOST_PROC=/host/proc` | CPU、内存、累计网络流量、瞬时收发速率、运行时长 |
| `/sys` | `/host/sys` | `HOST_SYS=/host/sys` | 网卡驱动报告的链路速率 |
| `/` | `/host/root` | `HOST_ROOT=/host/root` | 宿主根文件系统容量和已用空间 |
| `/etc/os-release` | `/etc/os-release` | 无 | 宿主发行版 |
系统目录不加 `:z` 或 `:Z`,容器统一设置 `--security-opt label=disable`。读取失败的指标返回空值;配置了 `HOST_ROOT` 但没有挂载时不会改读容器 overlay 根分区。未设置这些环境变量时,原生进程继续读取本机默认路径。
磁盘指标是根文件系统容量,不是所有硬盘合计,也不采集磁盘 I/O。链路速率是参与流量统计的接口速率合计,单位 Mbit/s,不是互联网测速结果;任一接口不支持读取速率时显示空值。接口仍按名称排除回环、容器、网桥和部分隧道,复杂 bonding/VLAN 配置可能存在重复计数。主机名、硬件型号、MAC/IP 不作为资源指标上报。
Probe 保持现有功能,只上报系统、架构与节点状态,不上报 CPU、内存、磁盘或网卡指标。因此 Probe 的 Compose 模板只需要配置目录和 `/etc/os-release` 挂载;从 Agent systemd 示例改为 Probe 时,去掉 `/proc`、`/sys`、`/` 挂载及 `HOST_*` 环境变量。
可直接安装的 unit 模板见 `packaging/systemd/monitor-agent-container.service` 和 `packaging/systemd/monitor-probe-container.service`;它们与上述示例使用相同策略,分别读取 `/etc/monitor-update/agent.image` 与 `probe.image`。在 Linux 虚拟机上运行容器时,采集对象是该 Linux 虚拟机。
## 容器节点的更新(Agent、Probe) ## 容器节点的更新(Agent、Probe)
+4 -2
View File
@@ -19,11 +19,13 @@ monitor/
├─ packaging/ # install.sh、systemd 单元、OpenWrt 文件 ├─ packaging/ # install.sh、systemd 单元、OpenWrt 文件
├─ web/ # 前端:Vite + React + TypeScript(自写),只依赖 backend/openapi/v1.json ├─ web/ # 前端:Vite + React + TypeScript(自写),只依赖 backend/openapi/v1.json
│ │
├─ global.json Directory.Build.props Directory.Packages.props Monitor.slnx ├─ global.json Directory.Packages.props Monitor.slnx # SDK、NuGet 版本和全部 .NET 项目入口
├─ .editorconfig BannedSymbols.txt # 风格与禁用 API(构建期强制) ├─ .editorconfig BannedSymbols.txt # 风格与禁用 API(构建期强制)
├─ BannedSymbols.Node.txt # 仅用于常驻节点进程:纯采集边界 ├─ BannedSymbols.Node.txt # 仅用于常驻节点进程:纯采集边界
│ │
└─ backend/ # 所有 C# 代码(构建产物在 backend/artifacts/) ├─ ci/Monitor.Tar/ # CI 归档工具,也加入根解决方案
└─ backend/ # 服务和节点 C# 代码(构建产物在 backend/artifacts/)
├─ Directory.Build.props Monitor.slnx # 后端编译设置和后端目录解决方案
├─ fleet/ # 通用节点框架(不认识 Monitor,以后可以拆成独立仓库) ├─ fleet/ # 通用节点框架(不认识 Monitor,以后可以拆成独立仓库)
│ ├─ src/ Fleet.Core Fleet.Identity Fleet.Rpc Fleet.Link Fleet.Hosting Fleet.Update │ ├─ src/ Fleet.Core Fleet.Identity Fleet.Rpc Fleet.Link Fleet.Hosting Fleet.Update
│ │ Fleet.Enrollment.Client Fleet.Server.{Enrollment,Link,Releases,Sqlite} │ │ Fleet.Enrollment.Client Fleet.Server.{Enrollment,Link,Releases,Sqlite}
View File
File renamed without changes.
+12 -2
View File
@@ -7,7 +7,8 @@
# --server "$MONITOR_SERVER" --join "$MONITOR_JOIN_TOKEN" --name "$MONITOR_AGENT_NAME" --identity-dir /etc/monitor/agent-id # --server "$MONITOR_SERVER" --join "$MONITOR_JOIN_TOKEN" --name "$MONITOR_AGENT_NAME" --identity-dir /etc/monitor/agent-id
# 再在 .data/agent.json 里写(fleet.identity_dir 要和上面一致): # 再在 .data/agent.json 里写(fleet.identity_dir 要和上面一致):
# {"fleet":{"server":"https://<backend>","identity_dir":"/etc/monitor/agent-id"},"listen":{"bind":"0.0.0.0"},"limits":{"max_total_mbps":100,"max_total_pps":10000}} # {"fleet":{"server":"https://<backend>","identity_dir":"/etc/monitor/agent-id"},"listen":{"bind":"0.0.0.0"},"limits":{"max_total_mbps":100,"max_total_pps":10000}}
# docker compose up -d # sudo chown -R 1654:1654 .data
# sudo podman compose up -d # Linux rootful Podman;也可用 docker compose
# #
# 更新:改 MONITOR_VERSION(或 MONITOR_IMAGE),再 docker compose pull && docker compose up -d。 # 更新:改 MONITOR_VERSION(或 MONITOR_IMAGE),再 docker compose pull && docker compose up -d。
# 这种方式没有宿主机更新器:管理端的「更新」按钮对它不起作用;要自动更新请用 podman + systemd(docs/deployment.md「容器节点的运行」)。.data 不动,身份和历史都保留。 # 这种方式没有宿主机更新器:管理端的「更新」按钮对它不起作用;要自动更新请用 podman + systemd(docs/deployment.md「容器节点的运行」)。.data 不动,身份和历史都保留。
@@ -21,11 +22,20 @@ services:
network_mode: host network_mode: host
volumes: volumes:
- ./.data:/etc/monitor - ./.data:/etc/monitor
- /etc/os-release:/etc/os-release:ro # 让节点读到宿主机的系统名(镜像里是 Ubuntu) - /etc/os-release:/etc/os-release:ro
# 系统目录只读,不加 :z/:Z;SELinux 隔离在容器级别禁用。
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/host/root:ro
environment:
HOST_PROC: /host/proc
HOST_SYS: /host/sys
HOST_ROOT: /host/root
read_only: true read_only: true
tmpfs: tmpfs:
- /tmp:rw,noexec,nosuid,size=64m,mode=1777 - /tmp:rw,noexec,nosuid,size=64m,mode=1777
security_opt: security_opt:
- label=disable
- no-new-privileges:true - no-new-privileges:true
cap_drop: cap_drop:
- ALL - ALL
+4 -2
View File
@@ -3,7 +3,8 @@
# 第一次使用: # 第一次使用:
# mkdir -p .data && sudo chown 1654:1654 .data # 容器以 UID/GID 1654 运行,需要读写这个目录 # mkdir -p .data && sudo chown 1654:1654 .data # 容器以 UID/GID 1654 运行,需要读写这个目录
# 在管理端创建 Probe,下载一次性身份配置,保存为 .data/probe.json(含私钥,权限 600,不要提交到版本控制) # 在管理端创建 Probe,下载一次性身份配置,保存为 .data/probe.json(含私钥,权限 600,不要提交到版本控制)
# docker compose up -d # sudo chown -R 1654:1654 .data # probe.json 的 600 权限要求文件也归容器用户所有
# sudo podman compose up -d # Linux rootful Podman;也可用 docker compose
# #
# 更新:改 MONITOR_VERSION(或 MONITOR_IMAGE),再 docker compose pull && docker compose up -d。 # 更新:改 MONITOR_VERSION(或 MONITOR_IMAGE),再 docker compose pull && docker compose up -d。
# 这种方式没有宿主机更新器:管理端的「更新」按钮对它不起作用;要自动更新请用 podman + systemd(docs/deployment.md「容器节点的运行」)。.data 不动。 # 这种方式没有宿主机更新器:管理端的「更新」按钮对它不起作用;要自动更新请用 podman + systemd(docs/deployment.md「容器节点的运行」)。.data 不动。
@@ -16,11 +17,12 @@ services:
network_mode: host network_mode: host
volumes: volumes:
- ./.data:/etc/monitor - ./.data:/etc/monitor
- /etc/os-release:/etc/os-release:ro # 让节点读到宿主机的系统名(镜像里是 Ubuntu) - /etc/os-release:/etc/os-release:ro
read_only: true read_only: true
tmpfs: tmpfs:
- /tmp:rw,noexec,nosuid,size=64m,mode=1777 - /tmp:rw,noexec,nosuid,size=64m,mode=1777
security_opt: security_opt:
- label=disable
- no-new-privileges:true - no-new-privileges:true
cap_drop: cap_drop:
- ALL - ALL
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
ARG APP_VERSION=unknown ARG APP_VERSION=unknown
LABEL org.opencontainers.image.title="Monitor Agent" \ LABEL org.opencontainers.image.title="Monitor Agent" \
@@ -1,10 +1,10 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble-aot AS build FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine-aot AS build
WORKDIR /src/backend WORKDIR /src/backend
COPY restore/backend/ ./ COPY restore/backend/ ./
# restore 也要带 PublishAot:不然不会还原 ILCompiler,之后 --no-restore 的发布就悄悄变成「裁剪过的框架依赖程序」 # restore 也要带 PublishAot:不然不会还原 ILCompiler,之后 --no-restore 的发布就悄悄变成「裁剪过的框架依赖程序」
RUN dotnet restore agent/src/Monitor.Agent -r linux-x64 -p:PublishAot=true --artifacts-path /artifacts \ RUN dotnet restore agent/src/Monitor.Agent -r linux-musl-x64 -p:PublishAot=true --artifacts-path /artifacts \
&& dotnet restore fleet/tools/fleet-enroll -r linux-x64 -p:PublishAot=true --artifacts-path /artifacts && dotnet restore fleet/tools/fleet-enroll -r linux-musl-x64 -p:PublishAot=true --artifacts-path /artifacts
COPY source/backend/ ./ COPY source/backend/ ./
# 构建信息:CI 通过 --build-arg 传入;不传就是空(放在 restore 之后,每次构建都变的时间不会让依赖还原的缓存失效) # 构建信息:CI 通过 --build-arg 传入;不传就是空(放在 restore 之后,每次构建都变的时间不会让依赖还原的缓存失效)
@@ -12,11 +12,11 @@ ARG MONITOR_TAG=
ARG MONITOR_COMMIT= ARG MONITOR_COMMIT=
ARG MONITOR_BUILT_AT= ARG MONITOR_BUILT_AT=
ENV MONITOR_TAG=$MONITOR_TAG MONITOR_COMMIT=$MONITOR_COMMIT MONITOR_BUILT_AT=$MONITOR_BUILT_AT ENV MONITOR_TAG=$MONITOR_TAG MONITOR_COMMIT=$MONITOR_COMMIT MONITOR_BUILT_AT=$MONITOR_BUILT_AT
RUN dotnet publish agent/src/Monitor.Agent -m:3 -c Release -r linux-x64 \ RUN dotnet publish agent/src/Monitor.Agent -m:3 -c Release -r linux-musl-x64 \
--self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=false \ --self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=false \
-p:UseAppHost=true -p:StripSymbols=true \ -p:UseAppHost=true -p:StripSymbols=true \
-p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/agent \ -p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/agent \
&& dotnet publish fleet/tools/fleet-enroll -m:3 -c Release -r linux-x64 \ && dotnet publish fleet/tools/fleet-enroll -m:3 -c Release -r linux-musl-x64 \
--self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=false \ --self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=false \
-p:UseAppHost=true -p:StripSymbols=true \ -p:UseAppHost=true -p:StripSymbols=true \
-p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/enroll -p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/enroll
@@ -24,7 +24,7 @@ RUN dotnet publish agent/src/Monitor.Agent -m:3 -c Release -r linux-x64 \
RUN test -x /out/agent/monitor-agent && test -x /out/enroll/fleet-enroll \ RUN test -x /out/agent/monitor-agent && test -x /out/enroll/fleet-enroll \
&& test "$(stat -c%s /out/agent/monitor-agent)" -gt 2000000 && test "$(stat -c%s /out/enroll/fleet-enroll)" -gt 2000000 && test "$(stat -c%s /out/agent/monitor-agent)" -gt 2000000 && test "$(stat -c%s /out/enroll/fleet-enroll)" -gt 2000000
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
ARG APP_VERSION=main ARG APP_VERSION=main
LABEL org.opencontainers.image.title="Monitor Agent" \ LABEL org.opencontainers.image.title="Monitor Agent" \
+3 -3
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
ARG APP_VERSION=unknown ARG APP_VERSION=unknown
LABEL org.opencontainers.image.title="Monitor Backend" \ LABEL org.opencontainers.image.title="Monitor Backend" \
@@ -7,7 +7,7 @@ LABEL org.opencontainers.image.title="Monitor Backend" \
org.opencontainers.image.version="$APP_VERSION" org.opencontainers.image.version="$APP_VERSION"
WORKDIR /app WORKDIR /app
COPY --chown=1654:1654 app/ /app/ COPY --chown=1654:1654 app/monitor-backend /app/monitor-backend
COPY --chown=1654:1654 data/.keep /data/.keep COPY --chown=1654:1654 data/.keep /data/.keep
ENV ASPNETCORE_HTTP_PORTS=8080 \ ENV ASPNETCORE_HTTP_PORTS=8080 \
@@ -17,4 +17,4 @@ ENV ASPNETCORE_HTTP_PORTS=8080 \
USER 1654:1654 USER 1654:1654
VOLUME ["/data"] VOLUME ["/data"]
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["dotnet", "/app/monitor-backend.dll"] ENTRYPOINT ["/app/monitor-backend"]
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
ARG APP_VERSION=unknown ARG APP_VERSION=unknown
LABEL org.opencontainers.image.title="Monitor Probe" \ LABEL org.opencontainers.image.title="Monitor Probe" \
@@ -1,9 +1,9 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble-aot AS build FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine-aot AS build
WORKDIR /src/backend WORKDIR /src/backend
COPY restore/backend/ ./ COPY restore/backend/ ./
# restore 也要带 PublishAot:不然不会还原 ILCompiler,之后 --no-restore 的发布就悄悄变成「裁剪过的框架依赖程序」 # restore 也要带 PublishAot:不然不会还原 ILCompiler,之后 --no-restore 的发布就悄悄变成「裁剪过的框架依赖程序」
RUN dotnet restore probe/src/Monitor.Probe -r linux-x64 -p:PublishAot=true --artifacts-path /artifacts RUN dotnet restore probe/src/Monitor.Probe -r linux-musl-x64 -p:PublishAot=true --artifacts-path /artifacts
COPY source/backend/ ./ COPY source/backend/ ./
# 构建信息:CI 通过 --build-arg 传入;不传就是空(放在 restore 之后,每次构建都变的时间不会让依赖还原的缓存失效) # 构建信息:CI 通过 --build-arg 传入;不传就是空(放在 restore 之后,每次构建都变的时间不会让依赖还原的缓存失效)
@@ -11,7 +11,7 @@ ARG MONITOR_TAG=
ARG MONITOR_COMMIT= ARG MONITOR_COMMIT=
ARG MONITOR_BUILT_AT= ARG MONITOR_BUILT_AT=
ENV MONITOR_TAG=$MONITOR_TAG MONITOR_COMMIT=$MONITOR_COMMIT MONITOR_BUILT_AT=$MONITOR_BUILT_AT ENV MONITOR_TAG=$MONITOR_TAG MONITOR_COMMIT=$MONITOR_COMMIT MONITOR_BUILT_AT=$MONITOR_BUILT_AT
RUN dotnet publish probe/src/Monitor.Probe -m:3 -c Release -r linux-x64 \ RUN dotnet publish probe/src/Monitor.Probe -m:3 -c Release -r linux-musl-x64 \
--self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=false \ --self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=false \
-p:UseAppHost=true -p:StripSymbols=true \ -p:UseAppHost=true -p:StripSymbols=true \
-p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/probe -p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/probe
@@ -19,7 +19,7 @@ RUN dotnet publish probe/src/Monitor.Probe -m:3 -c Release -r linux-x64 \
RUN test -x /out/probe/monitor-probe \ RUN test -x /out/probe/monitor-probe \
&& test "$(stat -c%s /out/probe/monitor-probe)" -gt 2000000 && test "$(stat -c%s /out/probe/monitor-probe)" -gt 2000000
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
ARG APP_VERSION=main ARG APP_VERSION=main
LABEL org.opencontainers.image.title="Monitor Probe" \ LABEL org.opencontainers.image.title="Monitor Probe" \
@@ -0,0 +1,16 @@
[Unit]
Description=Monitor Agent (Podman container)
After=network-online.target
Wants=network-online.target
[Service]
EnvironmentFile=/etc/monitor-update/agent.image
Restart=always
RestartSec=5
TimeoutStopSec=30
ExecStartPre=-/usr/bin/podman rm -f monitor-agent
ExecStart=/usr/bin/podman run --rm --name monitor-agent --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro -e HOST_PROC=/host/proc -e HOST_SYS=/host/sys -e HOST_ROOT=/host/root -v /opt/monitor/etc:/etc/monitor ${IMAGE}
ExecStop=/usr/bin/podman stop -t 20 monitor-agent
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,16 @@
[Unit]
Description=Monitor Probe (Podman container)
After=network-online.target
Wants=network-online.target
[Service]
EnvironmentFile=/etc/monitor-update/probe.image
Restart=always
RestartSec=5
TimeoutStopSec=30
ExecStartPre=-/usr/bin/podman rm -f monitor-probe
ExecStart=/usr/bin/podman run --rm --name monitor-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v /opt/monitor-probe/etc:/etc/monitor ${IMAGE}
ExecStop=/usr/bin/podman stop -t 20 monitor-probe
[Install]
WantedBy=multi-user.target
+7 -3
View File
@@ -23,7 +23,11 @@ export MONITOR_BUILT_AT="${MONITOR_BUILT_AT:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
build_env=(-e MONITOR_TAG -e MONITOR_COMMIT -e MONITOR_BUILT_AT) build_env=(-e MONITOR_TAG -e MONITOR_COMMIT -e MONITOR_BUILT_AT)
out="backend/artifacts/publish/$project/$rid" out="backend/artifacts/publish/$project/$rid"
args=(publish "$csproj" -c Release -r "$rid" -p:PublishAot=true -p:DebugType=none -p:DebugSymbols=false -o "/src/$out" --artifacts-path /artifacts -nologo -v q) publish_options=()
if [[ "$project" == backend ]]; then
publish_options+=(-p:OpenApiGenerateDocuments=false)
fi
args=(publish "$csproj" -c Release -r "$rid" -p:PublishAot=true -p:DebugType=none -p:DebugSymbols=false "${publish_options[@]}" -o "/src/$out" --artifacts-path /artifacts -nologo -v q)
case "$rid" in case "$rid" in
linux-musl-x64) linux-musl-x64)
@@ -39,8 +43,8 @@ case "$rid" in
exit 3 exit 3
;; ;;
*) *)
cd "$root/backend" # global.json 在 backend/ 下 cd "$root/backend" # 继承仓库根目录的 global.json
dotnet publish "${csproj#backend/}" -c Release -r "$rid" -p:PublishAot=true -p:DebugType=none -p:DebugSymbols=false -o "$root/$out" -nologo -v q dotnet publish "${csproj#backend/}" -c Release -r "$rid" -p:PublishAot=true -p:DebugType=none -p:DebugSymbols=false "${publish_options[@]}" -o "$root/$out" -nologo -v q
;; ;;
esac esac
+1
View File
@@ -14,6 +14,7 @@ export interface Host {
net_tx_total: number | null net_tx_total: number | null
net_rx_bps: number | null net_rx_bps: number | null
net_tx_bps: number | null net_tx_bps: number | null
net_link_mbps: number | null
uptime_seconds: number | null uptime_seconds: number | null
at: string at: string
} }
+4
View File
@@ -128,6 +128,10 @@ function Card({ agent, host, range, infos }: { agent: AgentCard; host: HostItem
↑ {fmtSpeed(agent.online ? h.net_tx_bps : null)} ↓ {fmtSpeed(agent.online ? h.net_rx_bps : null)} ↑ {fmtSpeed(agent.online ? h.net_tx_bps : null)} ↓ {fmtSpeed(agent.online ? h.net_rx_bps : null)}
</span> </span>
</div> </div>
<div className="stat">
<span className="k">链路速率</span>
<span className="num">{h.net_link_mbps ? `${h.net_link_mbps} Mbit/s` : '—'}</span>
</div>
<div className="stat"> <div className="stat">
<span className="k">运行时间</span> <span className="k">运行时间</span>
<span className="num">{fmtUptime(h.uptime_seconds)}</span> <span className="num">{fmtUptime(h.uptime_seconds)}</span>
+2
View File
@@ -1214,6 +1214,8 @@ export interface components {
/** Format: int64 */ /** Format: int64 */
net_tx_bps: null | number | string; net_tx_bps: null | number | string;
/** Format: int64 */ /** Format: int64 */
net_link_mbps: null | number | string;
/** Format: int64 */
uptime_seconds: null | number | string; uptime_seconds: null | number | string;
/** Format: date-time */ /** Format: date-time */
at: unknown; at: unknown;