feat: use Alpine AOT images, static SQLite and Podman host metrics [CI]
This commit is contained in:
1 parent
6e945657c1
commit
103ef27ed3
37 files changed
+474
-64
No files matched your search
@@ -23,8 +23,9 @@ Monitor is a self-hosted network path quality monitor. Backend coordinates Agent
|
||||
|
||||
## 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 tests: `cd backend && dotnet test --solution Monitor.slnx -c Release`
|
||||
- Frontend dependencies/build: `npm ci --prefix web && npm run build --prefix web`
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<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>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
|
||||
<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.Extensions.ApiDescription.Server" Version="10.0.12" />
|
||||
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.0.0" />
|
||||
@@ -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>
|
||||
@@ -23,18 +23,19 @@ Probe <──测量会话──> Agent ──长连接与数据上报──> Bac
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
- .NET SDK 10;项目使用 `backend/global.json` 指定 SDK 版本和滚动策略。
|
||||
- .NET SDK 10;项目使用 `global.json` 指定 SDK 版本和滚动策略。
|
||||
- Node.js 与 npm,用于构建 Web 前端。
|
||||
|
||||
### 构建
|
||||
|
||||
从仓库根目录构建后端:
|
||||
从仓库根目录构建全部 .NET 项目(Backend、Agent、Probe、Fleet、测试和 CI 工具):
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
dotnet build Monitor.slnx -c Release
|
||||
```
|
||||
|
||||
NuGet 包版本统一在根目录 `Directory.Packages.props` 管理,项目中的 `PackageReference` 不写版本号。AotAnywhere 的 MSBuild SDK 版本在根目录 `global.json` 的 `msbuild-sdks` 中管理。`backend/Monitor.slnx` 保留用于只构建后端目录内的项目。
|
||||
|
||||
安装前端依赖并构建展示端和管理端(在仓库根目录运行):
|
||||
|
||||
```sh
|
||||
@@ -92,7 +93,6 @@ npm run dev --prefix web
|
||||
## 测试
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
dotnet test --solution Monitor.slnx
|
||||
```
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<EventSourceSupport>false</EventSourceSupport>
|
||||
<DebuggerSupport>false</DebuggerSupport>
|
||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
||||
<Http3Support>false</Http3Support>
|
||||
<MetricsSupport>false</MetricsSupport>
|
||||
<UseSizeOptimizedLinq>true</UseSizeOptimizedLinq>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -6,23 +6,30 @@ namespace Monitor.Agent;
|
||||
/// <summary>
|
||||
/// 主机资源占用的只读快照:CPU 占用、内存、根分区、网卡累计流量与瞬时网速、系统运行时长。
|
||||
/// 只有汇总的数字:<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>
|
||||
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 Func<string, string?> _read;
|
||||
private readonly Func<(long Total, long Used)?> _disk;
|
||||
private readonly string _procRoot;
|
||||
private readonly string _sysRoot;
|
||||
private (long Total, long Idle)? _previousCpu;
|
||||
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;
|
||||
_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()
|
||||
@@ -30,7 +37,7 @@ public sealed class HostSampler
|
||||
long now = _time.GetTimestamp();
|
||||
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)
|
||||
{
|
||||
@@ -41,7 +48,7 @@ public sealed class HostSampler
|
||||
_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.MemUsed = memory.Used;
|
||||
@@ -53,10 +60,11 @@ public sealed class HostSampler
|
||||
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.NetTxTotal = net.Tx;
|
||||
snapshot.NetLinkMbps = ReadLinkSpeed(dev);
|
||||
if (_previousNet is { } before && net.Rx >= before.Rx && net.Tx >= before.Tx)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
string root = OperatingSystem.IsWindows() ? Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\" : "/";
|
||||
if (!Directory.Exists(root))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var drive = new DriveInfo(root);
|
||||
return drive.IsReady ? (drive.TotalSize, drive.TotalSize - drive.AvailableFreeSpace) : null;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
<AssemblyName>monitor-agent</AssemblyName>
|
||||
<RootNamespace>Monitor.Agent</RootNamespace>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<PublishAot>true</PublishAot>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>full</TrimMode>
|
||||
<StripSymbols>true</StripSymbols>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Monitor.Backend.Tests" />
|
||||
|
||||
@@ -7,6 +7,25 @@
|
||||
<AssemblyName>fleet-enroll</AssemblyName>
|
||||
<RootNamespace>FleetEnroll</RootNamespace>
|
||||
<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>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Fleet.Enrollment.Client\Fleet.Enrollment.Client.csproj" />
|
||||
|
||||
@@ -5172,6 +5172,7 @@
|
||||
"net_tx_total",
|
||||
"net_rx_bps",
|
||||
"net_tx_bps",
|
||||
"net_link_mbps",
|
||||
"uptime_seconds",
|
||||
"at"
|
||||
],
|
||||
@@ -5258,6 +5259,15 @@
|
||||
],
|
||||
"format": "int64"
|
||||
},
|
||||
"net_link_mbps": {
|
||||
"pattern": "^-?(?:0|[1-9]\\d*)$",
|
||||
"type": [
|
||||
"null",
|
||||
"integer",
|
||||
"string"
|
||||
],
|
||||
"format": "int64"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"pattern": "^-?(?:0|[1-9]\\d*)$",
|
||||
"type": [
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<EventSourceSupport>false</EventSourceSupport>
|
||||
<DebuggerSupport>false</DebuggerSupport>
|
||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
||||
<Http3Support>false</Http3Support>
|
||||
<MetricsSupport>false</MetricsSupport>
|
||||
<UseSizeOptimizedLinq>true</UseSizeOptimizedLinq>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
<AssemblyName>monitor-probe</AssemblyName>
|
||||
<RootNamespace>Monitor.Probe</RootNamespace>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<PublishAot>true</PublishAot>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>full</TrimMode>
|
||||
<StripSymbols>true</StripSymbols>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Monitor.Backend.Tests" />
|
||||
|
||||
@@ -339,6 +339,10 @@ public sealed class HostSnapshot
|
||||
[JsonPropertyName("net_tx_bps")]
|
||||
public long? NetTxBps { get; set; }
|
||||
|
||||
/// <summary>已统计接口的内核报告链路速率之和(Mbit/s);驱动不支持或无法读取时为空,不代表互联网带宽。</summary>
|
||||
[JsonPropertyName("net_link_mbps")]
|
||||
public long? NetLinkMbps { get; set; }
|
||||
|
||||
[JsonPropertyName("uptime_s")]
|
||||
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));
|
||||
|
||||
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));
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ public sealed record HostDto(
|
||||
long? NetTxTotal,
|
||||
long? NetRxBps,
|
||||
long? NetTxBps,
|
||||
long? NetLinkMbps,
|
||||
long? UptimeSeconds,
|
||||
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>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>monitor-backend</AssemblyName>
|
||||
<RootNamespace>Monitor.Backend</RootNamespace>
|
||||
<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>
|
||||
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
|
||||
<OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
|
||||
@@ -12,8 +26,13 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<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.Gateway\Monitor.Backend.Gateway.csproj" />
|
||||
<ProjectReference Include="..\Monitor.Backend.Storage.Sqlite\Monitor.Backend.Storage.Sqlite.csproj" />
|
||||
</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>
|
||||
@@ -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>"$(CppCompilerAndLinker)"</_SqliteCompiler>
|
||||
<_SqliteCompiler Condition="'$(CppCompilerAndLinker)' == ''">cc</_SqliteCompiler>
|
||||
<_SqliteCompiler Condition="'$(TargetTriple)' != ''">$(_SqliteCompiler) --target=$(TargetTriple)</_SqliteCompiler>
|
||||
<_SqliteCompiler Condition="'$(SysRoot)' != ''">$(_SqliteCompiler) --sysroot="$(SysRoot)"</_SqliteCompiler>
|
||||
<_SqliteCompiler Condition="'$(UseAotAnywhere)' == 'true'">"$(_AotAnywhereZigExe)" 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 "$(_SqliteSource)" -o "$(_SqliteObject)" -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 "$(_SqliteSource)" /Fo"$(_SqliteObject)" /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=""$(_SqliteObject)"" />
|
||||
</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>();
|
||||
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 签名认证 → 期望状态
|
||||
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>();
|
||||
using var anonymous = new HttpClient { BaseAddress = baseAddress };
|
||||
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);
|
||||
TaskSet tasks = await agent.WaitForTaskSetAsync(s => s.Tasks.Count >= 2);
|
||||
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";
|
||||
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(600, hot["summary"]!["samples"]!["udp_echo"]!.GetValue<long>());
|
||||
Assert.Equal(10.0, hot["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 6);
|
||||
@@ -62,7 +71,7 @@ public sealed class AotSmokeTests
|
||||
JsonNode persisted;
|
||||
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)
|
||||
{
|
||||
break;
|
||||
@@ -80,16 +89,16 @@ public sealed class AotSmokeTests
|
||||
Assert.True(RawChunkCodec.Decode(bytes).IsSuccess);
|
||||
|
||||
// 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");
|
||||
|
||||
// 按需测试:Probe 事件 → 下发 → 状态
|
||||
await agent.SendEventAsync(AgentEventKinds.ProbeConnected);
|
||||
await agent.SendEventAsync(AgentEventKinds.ProbeConnected, probeId);
|
||||
JsonNode started = null!;
|
||||
long begun = TimeProvider.System.GetTimestamp();
|
||||
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)
|
||||
{
|
||||
started = JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class HostSamplerTests
|
||||
Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1));
|
||||
Assert.Equal(3600L, HostSampler.ParseUptime("3600.55 7000.1\n"));
|
||||
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]
|
||||
@@ -29,7 +30,7 @@ public sealed class HostSamplerTests
|
||||
var time = new Microsoft.Extensions.Time.Testing.FakeTimeProvider();
|
||||
string stat = Stat1;
|
||||
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();
|
||||
Assert.Null(first.CpuPercent); // 第一次没有基线
|
||||
Assert.Null(first.NetRxBps);
|
||||
@@ -45,6 +46,54 @@ public sealed class HostSamplerTests
|
||||
Assert.Equal(2000, second.NetTxBps);
|
||||
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")]
|
||||
@@ -61,6 +110,7 @@ public sealed class HostStatusTests
|
||||
{
|
||||
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,
|
||||
NetLinkMbps = 1000,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
@@ -81,6 +131,7 @@ public sealed class HostStatusTests
|
||||
JsonNode item = list["items"]!.AsArray().Single(i => i!["agent_id"]!.GetValue<string>() == id)!;
|
||||
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>());
|
||||
|
||||
// 展示页能拿到的只有汇总数字:整份响应里没有型号、主机名、地址之类的字段
|
||||
string text = list.ToJsonString();
|
||||
|
||||
+8
-6
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
arch="${1:?usage: release-linux.sh <amd64|arm64>}"
|
||||
@@ -28,7 +28,8 @@ if [[ "$image_publish" == true ]]; then
|
||||
}
|
||||
fi
|
||||
case "$arch" in
|
||||
amd64|arm64) ;;
|
||||
amd64) rid=linux-musl-x64 ;;
|
||||
arm64) rid=linux-musl-arm64 ;;
|
||||
*) echo "unsupported architecture: $arch" >&2; exit 2 ;;
|
||||
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)}"
|
||||
|
||||
(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:UseAppHost=false -p:OpenApiGenerateDocuments=false \
|
||||
-p:PublishAot=true -p:UseAotAnywhere=true -p:SqliteStaticLink=true -p:OpenApiGenerateDocuments=false \
|
||||
--artifacts-path "$build_root" -o "$output")
|
||||
find "$output" -type f -name '*.pdb' -delete
|
||||
cp -a "$output/." "$container_context/app/"
|
||||
[[ -x "$output/monitor-backend" ]] || { echo "NativeAOT Backend executable is missing" >&2; exit 1; }
|
||||
[[ "$(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"
|
||||
|
||||
# Woodpecker's local backend isolates HOME per workflow. Keep Buildah's image
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
tag="${CI_COMMIT_TAG:-}"
|
||||
@@ -113,7 +113,7 @@ publish_index() {
|
||||
}
|
||||
|
||||
for component in agent probe; do
|
||||
build_arch_image "$component" amd64 amd64
|
||||
build_arch_image "$component" arm64 arm64
|
||||
build_arch_image "$component" amd64 musl-amd64
|
||||
build_arch_image "$component" arm64 musl-arm64
|
||||
publish_index "$component"
|
||||
done
|
||||
@@ -18,6 +18,9 @@ GLOBAL_BACKEND_FILES = {
|
||||
"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 = {
|
||||
"agent": ROOT / "packaging/container/Agent.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)
|
||||
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.mkdir()
|
||||
(config / ".keep").write_text("", encoding="utf-8")
|
||||
|
||||
+35
-8
@@ -24,7 +24,7 @@ macOS 的 Apple Silicon 产物带临时 ad-hoc 签名,Intel 产物未签名。
|
||||
|
||||
## 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`),然后启动:
|
||||
|
||||
@@ -34,11 +34,13 @@ docker compose up -d
|
||||
|
||||
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)。
|
||||
|
||||
## 节点二进制发布
|
||||
|
||||
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 工作流
|
||||
|
||||
@@ -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>` |
|
||||
| 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 配置相同的网络、卷和能力。
|
||||
|
||||
实测体积(`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 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -212,7 +218,7 @@ 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 --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
|
||||
|
||||
[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`)。
|
||||
- **首次注册 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)的认证文件,更新器用它拉取,容器本身用不到。
|
||||
- **挂载宿主机的 `/etc/os-release`(只读)**:节点读系统名展示在面板上,不挂的话读到的是镜像里的 Ubuntu。
|
||||
- **挂载宿主机的 `/etc/os-release`(只读)**:节点读系统名展示在面板上,不挂的话读到的是镜像里的 Alpine。
|
||||
- **ICMP 目标任务**要在 `podman run` 里加 `--cap-add NET_RAW`。
|
||||
- **经隧道注册之后检查 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)
|
||||
|
||||
|
||||
@@ -19,11 +19,13 @@ monitor/
|
||||
├─ packaging/ # install.sh、systemd 单元、OpenWrt 文件
|
||||
├─ 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(构建期强制)
|
||||
├─ BannedSymbols.Node.txt # 仅用于常驻节点进程:纯采集边界
|
||||
│
|
||||
└─ backend/ # 所有 C# 代码(构建产物在 backend/artifacts/)
|
||||
├─ ci/Monitor.Tar/ # CI 归档工具,也加入根解决方案
|
||||
└─ backend/ # 服务和节点 C# 代码(构建产物在 backend/artifacts/)
|
||||
├─ Directory.Build.props Monitor.slnx # 后端编译设置和后端目录解决方案
|
||||
├─ fleet/ # 通用节点框架(不认识 Monitor,以后可以拆成独立仓库)
|
||||
│ ├─ src/ Fleet.Core Fleet.Identity Fleet.Rpc Fleet.Link Fleet.Hosting Fleet.Update
|
||||
│ │ Fleet.Enrollment.Client Fleet.Server.{Enrollment,Link,Releases,Sqlite}
|
||||
|
||||
File renamed without changes.
@@ -7,7 +7,8 @@
|
||||
# --server "$MONITOR_SERVER" --join "$MONITOR_JOIN_TOKEN" --name "$MONITOR_AGENT_NAME" --identity-dir /etc/monitor/agent-id
|
||||
# 再在 .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}}
|
||||
# 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。
|
||||
# 这种方式没有宿主机更新器:管理端的「更新」按钮对它不起作用;要自动更新请用 podman + systemd(docs/deployment.md「容器节点的运行」)。.data 不动,身份和历史都保留。
|
||||
@@ -21,11 +22,20 @@ services:
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./.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
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=64m,mode=1777
|
||||
security_opt:
|
||||
- label=disable
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
# 第一次使用:
|
||||
# mkdir -p .data && sudo chown 1654:1654 .data # 容器以 UID/GID 1654 运行,需要读写这个目录
|
||||
# 在管理端创建 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。
|
||||
# 这种方式没有宿主机更新器:管理端的「更新」按钮对它不起作用;要自动更新请用 podman + systemd(docs/deployment.md「容器节点的运行」)。.data 不动。
|
||||
@@ -16,11 +17,12 @@ services:
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./.data:/etc/monitor
|
||||
- /etc/os-release:/etc/os-release:ro # 让节点读到宿主机的系统名(镜像里是 Ubuntu)
|
||||
- /etc/os-release:/etc/os-release:ro
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=64m,mode=1777
|
||||
security_opt:
|
||||
- label=disable
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
@@ -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
|
||||
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
|
||||
COPY restore/backend/ ./
|
||||
# restore 也要带 PublishAot:不然不会还原 ILCompiler,之后 --no-restore 的发布就悄悄变成「裁剪过的框架依赖程序」
|
||||
RUN dotnet restore agent/src/Monitor.Agent -r linux-x64 -p:PublishAot=true --artifacts-path /artifacts \
|
||||
&& dotnet restore fleet/tools/fleet-enroll -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-musl-x64 -p:PublishAot=true --artifacts-path /artifacts
|
||||
|
||||
COPY source/backend/ ./
|
||||
# 构建信息:CI 通过 --build-arg 传入;不传就是空(放在 restore 之后,每次构建都变的时间不会让依赖还原的缓存失效)
|
||||
@@ -12,11 +12,11 @@ ARG MONITOR_TAG=
|
||||
ARG MONITOR_COMMIT=
|
||||
ARG 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 \
|
||||
-p:UseAppHost=true -p:StripSymbols=true \
|
||||
-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 \
|
||||
-p:UseAppHost=true -p:StripSymbols=true \
|
||||
-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 \
|
||||
&& 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
|
||||
LABEL org.opencontainers.image.title="Monitor Agent" \
|
||||
|
||||
@@ -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
|
||||
LABEL org.opencontainers.image.title="Monitor Backend" \
|
||||
@@ -7,7 +7,7 @@ LABEL org.opencontainers.image.title="Monitor Backend" \
|
||||
org.opencontainers.image.version="$APP_VERSION"
|
||||
|
||||
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
|
||||
|
||||
ENV ASPNETCORE_HTTP_PORTS=8080 \
|
||||
@@ -17,4 +17,4 @@ ENV ASPNETCORE_HTTP_PORTS=8080 \
|
||||
USER 1654:1654
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "/app/monitor-backend.dll"]
|
||||
ENTRYPOINT ["/app/monitor-backend"]
|
||||
@@ -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
|
||||
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
|
||||
COPY restore/backend/ ./
|
||||
# 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/ ./
|
||||
# 构建信息:CI 通过 --build-arg 传入;不传就是空(放在 restore 之后,每次构建都变的时间不会让依赖还原的缓存失效)
|
||||
@@ -11,7 +11,7 @@ ARG MONITOR_TAG=
|
||||
ARG MONITOR_COMMIT=
|
||||
ARG 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 \
|
||||
-p:UseAppHost=true -p:StripSymbols=true \
|
||||
-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 \
|
||||
&& 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
|
||||
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
@@ -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)
|
||||
|
||||
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
|
||||
linux-musl-x64)
|
||||
@@ -39,8 +43,8 @@ case "$rid" in
|
||||
exit 3
|
||||
;;
|
||||
*)
|
||||
cd "$root/backend" # global.json 在 backend/ 下
|
||||
dotnet publish "${csproj#backend/}" -c Release -r "$rid" -p:PublishAot=true -p:DebugType=none -p:DebugSymbols=false -o "$root/$out" -nologo -v q
|
||||
cd "$root/backend" # 继承仓库根目录的 global.json
|
||||
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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Host {
|
||||
net_tx_total: number | null
|
||||
net_rx_bps: number | null
|
||||
net_tx_bps: number | null
|
||||
net_link_mbps: number | null
|
||||
uptime_seconds: number | null
|
||||
at: string
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
</span>
|
||||
</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">
|
||||
<span className="k">运行时间</span>
|
||||
<span className="num">{fmtUptime(h.uptime_seconds)}</span>
|
||||
|
||||
@@ -1214,6 +1214,8 @@ export interface components {
|
||||
/** Format: int64 */
|
||||
net_tx_bps: null | number | string;
|
||||
/** Format: int64 */
|
||||
net_link_mbps: null | number | string;
|
||||
/** Format: int64 */
|
||||
uptime_seconds: null | number | string;
|
||||
/** Format: date-time */
|
||||
at: unknown;
|
||||
|
||||
Reference in new issue
Block a user