refactor!: rename the project from Monitor to MoShiro [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
This commit is contained in:
justme committed 2026-10-07 02:03:25 +08:00
1 parent 27ac7fb787
commit 2e146d4da7
309 files changed
+1565 -1572

No files matched your search

@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
public enum AccKind : byte
{
@@ -2,7 +2,7 @@ using System.Buffers.Binary;
using System.Text;
using Fleet.Core;
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>一个时间桶里所有指标的累加器:<c>指标 id → 累加器</c>。</summary>
public sealed class MetricSet
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>
/// 固定对数桶直方图:100 µs ~ 30 s,每 2 倍 8 个桶(相对误差 ≤ 4.5%)。
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>
/// 估计两端的时钟偏移和线性漂移:单向时延 x(t) = 偏移 + 漂移·t + 真实的时延变化。
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>变点检测(均值漂移):找到使前后两段均值差最显著的位置,用来判断 QoS 从什么时候开始限速。</summary>
public static class ChangePoint
@@ -1,7 +1,7 @@
using System.Text.Json;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>算子把观测值交给这里;窗口评估器负责按时间分桶。</summary>
public interface IObservationSink
@@ -1,8 +1,8 @@
using System.Security.Cryptography;
using System.Text;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
public enum Unit
{
@@ -1,7 +1,7 @@
using System.Text.Json;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
public sealed class TestStepResult
{
@@ -1,6 +1,6 @@
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>
/// 把原始块按时间分桶算成累加器。流式路径(1s 桶)和持久路径(分钟窗口)调用同一份代码,
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>线路基础指标(udp_echo / icmp_echo 的回显流)。新增指标 = 加一行声明。</summary>
public static class LinkMetrics
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
public sealed record PresetDef(string Id, string Label, IReadOnlyList<string> MetricIds);
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>连接建立、长连接、UDP 压测(udp_train)的指标。</summary>
public static class SessionMetrics
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>TCP 指标:回显的 RTT 与卡顿、内核采样的重传、批量传输的吞吐。</summary>
public static class TcpMetrics
@@ -3,6 +3,6 @@
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\shared\Monitor.Protocol\Monitor.Protocol.csproj" />
<ProjectReference Include="..\..\shared\MoShiro.Protocol\MoShiro.Protocol.csproj" />
</ItemGroup>
</Project>
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>线路基础质量:RTT、丢包、抖动、连续丢包。</summary>
public sealed class BaseModel : ServiceModelBase
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>模型的输入:一个时间段内的导出值(例如 <c>rtt.p95</c>)。没有数据的指标不存在。</summary>
public sealed class MetricView
@@ -1,6 +1,6 @@
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
public enum Dir
{
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>算子工厂:<c>Ops.Rtt</c>、<c>Ops.Loss(Dir.Up)</c> 等,供 <c>MetricDef</c> 声明使用。</summary>
public static class Ops
@@ -1,6 +1,6 @@
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Analysis;
namespace MoShiro.Backend.Analysis;
/// <summary>tcp_info 流的辅助:<c>side</c> 为 0 表示 Probe 一侧记录(Probe 时钟),否则为 Agent 一侧。</summary>
internal static class TcpInfoTime
@@ -2,10 +2,10 @@ using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>Agent 管理:列表、修改、审批、启停、接管、删除;加入令牌;Probe 身份。</summary>
public static class AgentEndpoints
@@ -77,7 +77,7 @@ public static class AgentEndpoints
AgentView? view = await agents.GetAsync(new NodeId(id), ct);
if (view is null)
{
return Problems.Create(http, MonitorErrors.AgentNotFound, "agent not found");
return Problems.Create(http, MoShiroErrors.AgentNotFound, "agent not found");
}
Concurrency.SetETag(http, view.Agent.RowVersion);
@@ -166,7 +166,7 @@ public static class AgentEndpoints
private static async Task<IResult> GetProbeAsync(HttpContext http, ProbeService probes, string id, CancellationToken ct) =>
await probes.GetAsync(id, ct) is { } view
? TypedResults.Json(Mappers.ToDto(view), ApiJsonContext.Default.ProbeDto)
: Problems.Create(http, MonitorErrors.ProbeNotFound, "probe not found");
: Problems.Create(http, MoShiroErrors.ProbeNotFound, "probe not found");
private static async Task<IResult> PatchProbeAsync(HttpContext http, ProbeService probes, PatchProbeRequest request, string id, CancellationToken ct)
{
@@ -211,7 +211,7 @@ public static class AgentEndpoints
return Problems.From(http, error);
}
return dropped ? TypedResults.NoContent() : Problems.Create(http, MonitorErrors.ProbeOffline, "this probe has no session with this agent");
return dropped ? TypedResults.NoContent() : Problems.Create(http, MoShiroErrors.ProbeOffline, "this probe has no session with this agent");
}
/// <summary>Probe 要连的面板地址:配置的公开地址,没有就用这次请求的地址。</summary>
@@ -2,10 +2,10 @@ using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>登录、退出、当前身份。登录接口限流,失败一律返回 401,不区分“用户不存在”和“密码错误”。</summary>
public static class AuthEndpoints
@@ -35,7 +35,7 @@ public static class AuthEndpoints
string source = http.Connection.RemoteIpAddress?.ToString() ?? "unknown";
if (!limiter.TryAcquire(source, out int retryAfter))
{
return Problems.RetryAfter(http, new FleetError(MonitorErrors.TooManyRequests, "too many sign-in attempts"), retryAfter);
return Problems.RetryAfter(http, new FleetError(MoShiroErrors.TooManyRequests, "too many sign-in attempts"), retryAfter);
}
Result<LoginResult> result = await auth.LoginAsync(request.Username ?? "", request.Password ?? "", http.RequestAborted);
@@ -2,10 +2,10 @@ using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>构建信息与更新:后端和每个节点的版本、tag、构建时间,CI 签名的发布清单,以及「更新这个节点」的请求。更新本身由节点宿主机上的更新器验签并执行。</summary>
public static class BuildEndpoints
@@ -54,7 +54,7 @@ public static class BuildEndpoints
}
private static async Task<IResult> RequestUpdateAsync(HttpContext http, ReleaseService releases, string id, CancellationToken ct) =>
await releases.RequestUpdateAsync(new NodeId(id), ct) ? TypedResults.NoContent() : Problems.Create(http, MonitorErrors.AgentOffline, "the node is not connected");
await releases.RequestUpdateAsync(new NodeId(id), ct) ? TypedResults.NoContent() : Problems.Create(http, MoShiroErrors.AgentOffline, "the node is not connected");
private static ReleaseDto ToDto(ReleaseOffer offer) => new(offer.Component, offer.Seq, offer.Tag, offer.Version, offer.Digest);
@@ -3,10 +3,10 @@ using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>原始数据检索与下载、事件、指标重算进度。</summary>
public static class DataEndpoints
@@ -18,8 +18,8 @@ public static class DataEndpoints
group.MapGet("/raw/chunks", ListChunksAsync)
.Api("raw_chunks_list", "Raw chunk metadata", Tag).Returns<PageDto<RawChunkMetaDto>>().ProducesProblems(400, 401, 403);
group.MapGet("/raw/chunks/{chunk_id}", DownloadChunkAsync)
.Api("raw_chunks_get", "Download a raw chunk (application/vnd.monitor.rawchunk)", Tag)
.Produces(StatusCodes.Status200OK, contentType: "application/vnd.monitor.rawchunk").ProducesProblems(401, 403, 404, 410);
.Api("raw_chunks_get", "Download a raw chunk (application/vnd.moshiro.rawchunk)", Tag)
.Produces(StatusCodes.Status200OK, contentType: "application/vnd.moshiro.rawchunk").ProducesProblems(401, 403, 404, 410);
group.MapGet("/events", ListEventsAsync)
.Api("events_list", "Agent events", Tag).Returns<PageDto<EventDto>>().ProducesProblems(400, 401, 403);
group.MapGet("/analysis/recompute", RecomputeAsync)
@@ -57,7 +57,7 @@ public static class DataEndpoints
{
if (!Guid.TryParse(chunk_id, out Guid id))
{
return Problems.Create(http, MonitorErrors.ChunkNotFound, "chunk not found");
return Problems.Create(http, MoShiroErrors.ChunkNotFound, "chunk not found");
}
Result<(RawChunkMeta Meta, byte[] Bytes)> result = await raw.GetAsync(new ChunkId(id), http.RequestAborted);
@@ -67,7 +67,7 @@ public static class DataEndpoints
}
http.Response.Headers.CacheControl = "private, max-age=3600";
return TypedResults.File(found.Bytes, "application/vnd.monitor.rawchunk", chunk_id + ".mnrc");
return TypedResults.File(found.Bytes, "application/vnd.moshiro.rawchunk", chunk_id + ".msrc");
}
private static async Task<IResult> ListEventsAsync(
@@ -2,10 +2,10 @@ using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>线路开关:哪些(Probe,Agent)要连。自动连接的总开关是设置 <c>fleet.auto_link</c>;这里看每一对的结果,并且手动强制开、关或取消手动设置。</summary>
public static class LinkEndpoints
@@ -40,12 +40,12 @@ public static class LinkEndpoints
{
if (await probes.GetAsync(probe_id, ct) is not { Revoked: false } probe)
{
return Problems.Create(http, MonitorErrors.ProbeNotFound, "probe not found");
return Problems.Create(http, MoShiroErrors.ProbeNotFound, "probe not found");
}
if (await agents.GetAsync(new NodeId(agent_id), ct) is not { } agent)
{
return Problems.Create(http, MonitorErrors.AgentNotFound, "agent not found");
return Problems.Create(http, MoShiroErrors.AgentNotFound, "agent not found");
}
await links.SetAsync(probe_id, agent_id, request.Enabled, ct);
@@ -3,10 +3,10 @@ using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>API 令牌、站点设置与保留期、用户。令牌明文只在创建的响应里出现一次。</summary>
public static class SystemEndpoints
@@ -2,10 +2,10 @@ using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>任务(线路模板、单个 Agent 的覆盖、目标任务)与按需测试。参数按 Kind 的 ParamSpec 校验,并检查带宽预算。</summary>
public static class TaskEndpoints
@@ -76,7 +76,7 @@ public static class TaskEndpoints
TaskRecord? task = await tasks.GetAsync(new TaskId(id), ct);
if (task is null)
{
return Problems.Create(http, MonitorErrors.TaskNotFound, "task not found");
return Problems.Create(http, MoShiroErrors.TaskNotFound, "task not found");
}
Concurrency.SetETag(http, task.RowVersion);
@@ -5,17 +5,17 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Monitor.Backend.Application;
using MoShiro.Backend.Application;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>
/// Api 层的入口:服务注册、中间件、端点映射。新增资源时在 <see cref="MapMonitorApi"/> 里加一行。
/// Api 层的入口:服务注册、中间件、端点映射。新增资源时在 <see cref="MapMoShiroApi"/> 里加一行。
/// Api 只调用 Application;错误码到 HTTP 状态的映射只在这一层做一次(<see cref="ErrorCodeHttpMap"/>)。
/// </summary>
public static class ApiModule
{
public static IServiceCollection AddMonitorApi(this IServiceCollection services)
public static IServiceCollection AddMoShiroApi(this IServiceCollection services)
{
services.AddSingleton<SseLimiter>();
services.Configure<JsonOptions>(options =>
@@ -48,7 +48,7 @@ public static class ApiModule
/// 跨域(可选):只对 <c>/api/public</c> 和 <c>/api/admin</c> 生效,只放行名单里的来源,且不带凭据(Cookie 不会跨域),
/// 所以跨域的前端只能用 Bearer 令牌。需要放在认证和路由之前。
/// </summary>
public static IApplicationBuilder UseMonitorCors(this IApplicationBuilder app, string? origins)
public static IApplicationBuilder UseMoShiroCors(this IApplicationBuilder app, string? origins)
{
string[] allowed = (origins ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (allowed.Length == 0)
@@ -83,7 +83,7 @@ public static class ApiModule
}
/// <summary>认证中间件:解析 Bearer 令牌或会话 Cookie。需要放在路由之后、端点之前。</summary>
public static IApplicationBuilder UseMonitorApi(this IApplicationBuilder app)
public static IApplicationBuilder UseMoShiroApi(this IApplicationBuilder app)
{
app.UseExceptionHandler();
app.UseStatusCodePages();
@@ -91,7 +91,7 @@ public static class ApiModule
return app;
}
public static IEndpointRouteBuilder MapMonitorApi(this IEndpointRouteBuilder app)
public static IEndpointRouteBuilder MapMoShiroApi(this IEndpointRouteBuilder app)
{
RouteGroupBuilder publicGroup = app.MapGroup("/api/public").AddEndpointFilter(PublicGuard.InvokeAsync);
QueryEndpoints.Map(publicGroup);
@@ -1,7 +1,7 @@
using System.Globalization;
using Microsoft.AspNetCore.Http;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>并发控制:配置类资源返回 ETag(行版本),PATCH/DELETE 支持 If-Match,不匹配时返回 412。</summary>
internal static class Concurrency
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
public static class EndpointConventions
{
@@ -1,13 +1,13 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
public static class HttpContextPrincipalExtensions
{
private const string Key = "monitor.principal";
private const string Key = "moshiro.principal";
public static Principal GetPrincipal(this HttpContext http) =>
http.Items.TryGetValue(Key, out object? value) && value is Principal principal ? principal : Principal.Anonymous;
@@ -20,7 +20,7 @@ public static class HttpContextPrincipalExtensions
/// </summary>
public static class PrincipalResolver
{
public const string SessionCookie = "monitor_session";
public const string SessionCookie = "moshiro_session";
public const string CsrfHeader = "X-CSRF-Token";
public static string? BearerOf(HttpContext http)
@@ -63,8 +63,8 @@ public static class AdminGuard
if (!principal.IsAdmin)
{
return principal.Scope != ApiScope.None
? Problems.Create(http, MonitorErrors.Forbidden, "the token scope does not allow this operation")
: Problems.Create(http, MonitorErrors.Unauthorized, "authentication is required");
? Problems.Create(http, MoShiroErrors.Forbidden, "the token scope does not allow this operation")
: Problems.Create(http, MoShiroErrors.Unauthorized, "authentication is required");
}
if (principal.Kind == PrincipalKind.Session && !HttpMethods.IsGet(http.Request.Method) && !HttpMethods.IsHead(http.Request.Method) && !HttpMethods.IsOptions(http.Request.Method))
@@ -72,7 +72,7 @@ public static class AdminGuard
string? csrf = http.Request.Headers[PrincipalResolver.CsrfHeader];
if (csrf is null || principal.CsrfToken is null || !AuthService.FixedTimeEquals(csrf, principal.CsrfToken))
{
return Problems.Create(http, MonitorErrors.CsrfFailed, "a valid X-CSRF-Token header is required");
return Problems.Create(http, MoShiroErrors.CsrfFailed, "a valid X-CSRF-Token header is required");
}
}
@@ -95,7 +95,7 @@ public static class PublicGuard
HttpContext http = context.HttpContext;
if (!http.GetPrincipal().CanRead && !await http.RequestServices.GetRequiredService<SettingsService>().IsSitePublicAsync(http.RequestAborted))
{
return Problems.Create(http, MonitorErrors.SitePrivate, "this site is private; sign in or use a read-only token");
return Problems.Create(http, MoShiroErrors.SitePrivate, "this site is private; sign in or use a read-only token");
}
return await next(context);
@@ -1,10 +1,10 @@
using System.Text.Json;
using Fleet.Server;
using Monitor.Backend.Analysis;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>应用层模型 → REST DTO。public 层的 DTO 只暴露可以公开的字段。</summary>
public static class Mappers
@@ -1,10 +1,10 @@
using System.Diagnostics;
using Fleet.Core;
using Microsoft.AspNetCore.Http;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>错误码到 HTTP 状态的映射:只在 Api 层做一次。</summary>
public static class ErrorCodeHttpMap
@@ -44,19 +44,19 @@ public static class ErrorCodeHttpMap
}
}
Add(400, MonitorErrors.ValidationFailed, MonitorErrors.BudgetExceeded, FleetErrors.InvalidParams);
Add(401, MonitorErrors.Unauthorized, MonitorErrors.CsrfFailed, FleetErrors.Unauthorized);
Add(403, MonitorErrors.Forbidden, MonitorErrors.SitePrivate, FleetErrors.Forbidden, FleetErrors.ProofInvalid);
Add(400, MoShiroErrors.ValidationFailed, MoShiroErrors.BudgetExceeded, FleetErrors.InvalidParams);
Add(401, MoShiroErrors.Unauthorized, MoShiroErrors.CsrfFailed, FleetErrors.Unauthorized);
Add(403, MoShiroErrors.Forbidden, MoShiroErrors.SitePrivate, FleetErrors.Forbidden, FleetErrors.ProofInvalid);
Add(
404, MonitorErrors.NotFound, MonitorErrors.AgentNotFound, MonitorErrors.ProbeNotFound, MonitorErrors.TaskNotFound, MonitorErrors.TestNotFound, MonitorErrors.ChunkNotFound,
MonitorErrors.TokenNotFound, MonitorErrors.UnknownKind, MonitorErrors.UnknownPlan, FleetErrors.NotFound);
Add(409, MonitorErrors.NameConflict, MonitorErrors.ProbeRevoked, MonitorErrors.ProbeOffline, MonitorErrors.TestBusy, MonitorErrors.AgentNotActive, MonitorErrors.AgentOffline, FleetErrors.NameConflict, FleetErrors.NodeOffline);
Add(410, MonitorErrors.RawExpired, MonitorErrors.JoinTokenExpired, FleetErrors.JoinTokenExpired);
Add(412, MonitorErrors.PreconditionFailed);
Add(413, MonitorErrors.PayloadTooLarge);
Add(422, MonitorErrors.ProbeRequired, MonitorErrors.RangeTooLarge, MonitorErrors.InvalidStep, MonitorErrors.InvalidRange, MonitorErrors.UnknownMetric, MonitorErrors.UnknownPreset, MonitorErrors.TooManyMetrics, FleetErrors.Unsupported);
Add(429, MonitorErrors.TooManyRequests, MonitorErrors.TooManyStreams);
Add(503, MonitorErrors.StorageUnavailable, FleetErrors.Timeout);
404, MoShiroErrors.NotFound, MoShiroErrors.AgentNotFound, MoShiroErrors.ProbeNotFound, MoShiroErrors.TaskNotFound, MoShiroErrors.TestNotFound, MoShiroErrors.ChunkNotFound,
MoShiroErrors.TokenNotFound, MoShiroErrors.UnknownKind, MoShiroErrors.UnknownPlan, FleetErrors.NotFound);
Add(409, MoShiroErrors.NameConflict, MoShiroErrors.ProbeRevoked, MoShiroErrors.ProbeOffline, MoShiroErrors.TestBusy, MoShiroErrors.AgentNotActive, MoShiroErrors.AgentOffline, FleetErrors.NameConflict, FleetErrors.NodeOffline);
Add(410, MoShiroErrors.RawExpired, MoShiroErrors.JoinTokenExpired, FleetErrors.JoinTokenExpired);
Add(412, MoShiroErrors.PreconditionFailed);
Add(413, MoShiroErrors.PayloadTooLarge);
Add(422, MoShiroErrors.ProbeRequired, MoShiroErrors.RangeTooLarge, MoShiroErrors.InvalidStep, MoShiroErrors.InvalidRange, MoShiroErrors.UnknownMetric, MoShiroErrors.UnknownPreset, MoShiroErrors.TooManyMetrics, FleetErrors.Unsupported);
Add(429, MoShiroErrors.TooManyRequests, MoShiroErrors.TooManyStreams);
Add(503, MoShiroErrors.StorageUnavailable, FleetErrors.Timeout);
map[NotImplemented.Value] = 501;
return map;
}
@@ -3,10 +3,10 @@ using System.Text;
using Fleet.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
internal static class QueryHelpers
{
@@ -22,7 +22,7 @@ internal static class QueryHelpers
}
catch (OperationCanceledException) when (!http.RequestAborted.IsCancellationRequested)
{
return Problems.Create(http, MonitorErrors.StorageUnavailable, null);
return Problems.Create(http, MoShiroErrors.StorageUnavailable, null);
}
}
@@ -81,7 +81,7 @@ internal static class QueryHelpers
return line.Agent.Agent.Status == Fleet.Server.NodeStatus.Active && (line.Agent.Agent.PublicVisible || PublicGuard.CanSeeHidden(http)) && line.Probe.RevokedAtMs is null
? found
: Result<LineView>.Fail(MonitorErrors.AgentNotFound, "line not found");
: Result<LineView>.Fail(MoShiroErrors.AgentNotFound, "line not found");
}
/// <summary>找到可以展示的线路:已激活,并且(对外可见,或者访问者已认证)。否则 404(不暴露被隐藏的线路存在与否)。</summary>
@@ -90,6 +90,6 @@ internal static class QueryHelpers
AgentView? view = await agents.GetAsync(new NodeId(id), ct);
return view is { Agent.Status: Fleet.Server.NodeStatus.Active } && (view.Agent.PublicVisible || PublicGuard.CanSeeHidden(http))
? Result<AgentView>.Ok(view)
: Result<AgentView>.Fail(MonitorErrors.AgentNotFound, "agent not found");
: Result<AgentView>.Fail(MoShiroErrors.AgentNotFound, "agent not found");
}
}
@@ -1,6 +1,6 @@
using System.Text.Json;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary><c>return_session</c>:跨域的前端用——不设 Cookie,把会话令牌放在响应里,之后用 <c>Authorization: Bearer</c> 带上。</summary>
public sealed record LoginRequest(string Username, string Password, bool? ReturnSession);
@@ -1,7 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>
/// REST 的 JSON 源生成上下文(snake_case,字段始终存在、未知值写 null,枚举与时间的格式见数据表示规则)。
@@ -80,6 +80,6 @@ namespace Monitor.Backend.Api;
[JsonSerializable(typeof(CreateUserRequest))]
[JsonSerializable(typeof(ChangePasswordRequest))]
[JsonSerializable(typeof(RecomputeDto))]
[JsonSerializable(typeof(Monitor.Protocol.LocalLimits))]
[JsonSerializable(typeof(MoShiro.Protocol.LocalLimits))]
[JsonSerializable(typeof(List<Fleet.Rpc.RejectedItem>))]
public sealed partial class ApiJsonContext : JsonSerializerContext;
@@ -2,7 +2,7 @@ using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>时间点统一使用 RFC 3339 UTC,以 Z 结尾,精确到毫秒(唯一例外:列式时序里的时间轴 <c>t</c> 是 Unix 毫秒)。</summary>
public sealed class Rfc3339Converter : JsonConverter<DateTimeOffset>
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>汇总:整个时间段由累加器精确合并得出的值(不是各点的平均)。</summary>
public sealed record SummaryDto(IReadOnlyDictionary<string, double?> Metrics, IReadOnlyDictionary<string, long> Samples, double Coverage);
@@ -6,6 +6,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<ProjectReference Include="..\Monitor.Backend.Application\Monitor.Backend.Application.csproj" />
<ProjectReference Include="..\MoShiro.Backend.Application\MoShiro.Backend.Application.csproj" />
</ItemGroup>
</Project>
@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>运维端点:<c>/healthz</c>、<c>/metrics</c>(admin 令牌)、安装脚本。</summary>
public static class OpsEndpoints
@@ -40,8 +40,8 @@ public static class OpsEndpoints
if (!principal.IsAdmin)
{
return principal.CanRead
? Problems.Create(http, MonitorErrors.Forbidden, "an admin token is required")
: Problems.Create(http, MonitorErrors.Unauthorized, "an admin token is required");
? Problems.Create(http, MoShiroErrors.Forbidden, "an admin token is required")
: Problems.Create(http, MoShiroErrors.Unauthorized, "an admin token is required");
}
return TypedResults.Text(await ops.RenderPrometheusAsync(http.RequestAborted), "text/plain; version=0.0.4");
@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>目录:由代码里的注册表生成。新增 Kind、方案、指标、分组、模型后,目录自动更新,前端不需要改。</summary>
public static class CatalogEndpoints
@@ -27,12 +27,12 @@ public static class CatalogEndpoints
}
private static IResult Kinds(HttpContext http) =>
QueryHelpers.ApplyCatalogCaching(http, string.Join(',', Monitor.Protocol.Kinds.All.Select(k => k.Id)))
?? TypedResults.Json(new PageDto<KindDto>([.. Monitor.Protocol.Kinds.All.Select(Mappers.ToDto)], null), ApiJsonContext.Default.PageDtoKindDto);
QueryHelpers.ApplyCatalogCaching(http, string.Join(',', MoShiro.Protocol.Kinds.All.Select(k => k.Id)))
?? TypedResults.Json(new PageDto<KindDto>([.. MoShiro.Protocol.Kinds.All.Select(Mappers.ToDto)], null), ApiJsonContext.Default.PageDtoKindDto);
private static IResult Plans(HttpContext http) =>
QueryHelpers.ApplyCatalogCaching(http, string.Join(',', Monitor.Backend.Application.Plans.All.Select(p => p.Id)))
?? TypedResults.Json(new PageDto<PlanDto>([.. Monitor.Backend.Application.Plans.All.Select(Mappers.ToDto)], null), ApiJsonContext.Default.PageDtoPlanDto);
QueryHelpers.ApplyCatalogCaching(http, string.Join(',', MoShiro.Backend.Application.Plans.All.Select(p => p.Id)))
?? TypedResults.Json(new PageDto<PlanDto>([.. MoShiro.Backend.Application.Plans.All.Select(Mappers.ToDto)], null), ApiJsonContext.Default.PageDtoPlanDto);
private static IResult Metrics(HttpContext http) =>
QueryHelpers.ApplyCatalogCaching(http, string.Join(',', MetricRegistry.All.Select(m => m.Fingerprint)))
@@ -2,10 +2,10 @@ using Fleet.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>Agent 的主机资源占用:最新值(实时的部分走 SSE 的 <c>hosts</c> 主题)和历史曲线。只有汇总的数字,没有型号和 IP。</summary>
public static class HostEndpoints
@@ -44,12 +44,12 @@ public static class HostEndpoints
AgentView? view = await agents.GetAsync(new NodeId(agent_id), ct);
if (view is not { Agent.Status: Fleet.Server.NodeStatus.Active } || !(view.Agent.PublicVisible || PublicGuard.CanSeeHidden(http)))
{
return Problems.Create(http, MonitorErrors.AgentNotFound, "agent not found");
return Problems.Create(http, MoShiroErrors.AgentNotFound, "agent not found");
}
if (!RangeResolver.TryParseSpan(range ?? "1h", out long span) || span < Steps.Hour || span > 30 * Steps.Day)
{
return Problems.From(http, new FleetError(MonitorErrors.InvalidRange, "range must look like 1h, 6h, 24h or 7d (between 1 hour and 30 days)"));
return Problems.From(http, new FleetError(MoShiroErrors.InvalidRange, "range must look like 1h, 6h, 24h or 7d (between 1 hour and 30 days)"));
}
long now = time.GetUtcNow().ToUnixTimeMilliseconds();
@@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Primitives;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>
/// 实时推送(SSE):先推送当前快照,再推送增量(不补发历史)。SSE 只负责“更快地获得变化”,
@@ -67,7 +67,7 @@ public static class LiveEndpoints
string client = http.Connection.RemoteIpAddress?.ToString() ?? "unknown";
if (!limiter.TryAcquire(client))
{
return Problems.Create(http, MonitorErrors.TooManyStreams, $"at most {options.MaxSsePerClient} live connections per client");
return Problems.Create(http, MoShiroErrors.TooManyStreams, $"at most {options.MaxSsePerClient} live connections per client");
}
counters.SseOpened();
@@ -1,9 +1,9 @@
using System.Collections.Concurrent;
using Fleet.Core;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
internal enum LiveTopicKind
{
@@ -46,7 +46,7 @@ internal sealed record LiveTopic(LiveTopicKind Kind, string Raw, string? Range,
if (tokens.Count > maxTopics)
{
return Result<IReadOnlyList<LiveTopic>>.Fail(MonitorErrors.TooManyStreams, $"at most {maxTopics} topics per connection");
return Result<IReadOnlyList<LiveTopic>>.Fail(MoShiroErrors.TooManyStreams, $"at most {maxTopics} topics per connection");
}
var topics = new List<LiveTopic>();
@@ -83,7 +83,7 @@ internal sealed record LiveTopic(LiveTopicKind Kind, string Raw, string? Range,
case "series" when parts.Length == 4:
if (!Steps.TryParse(parts[2], out long step) || step > Steps.Minute)
{
return Result<LiveTopic>.Fail(MonitorErrors.InvalidStep, $"{token}: series topics support steps up to 1m; refetch coarser steps over REST");
return Result<LiveTopic>.Fail(MoShiroErrors.InvalidStep, $"{token}: series topics support steps up to 1m; refetch coarser steps over REST");
}
return Result<LiveTopic>.Ok(new LiveTopic(LiveTopicKind.Series, token, null, step, parts[3], parts[1], null));
@@ -1,9 +1,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Application;
using MoShiro.Backend.Application;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>
/// 节点的展示信息:系统、架构、国家、运营商。<b>只有这几项,没有任何 IP 地址、城市、ASN、经纬度</b>——
@@ -25,7 +25,7 @@ public static class NodeEndpoints
/// <summary>这个后端自己的构建信息;没有(本地构建)的字段是 <c>null</c>。</summary>
internal static BuildDto Backend(bool withCommit) =>
new(Monitor.Protocol.BuildInfo.Version, Empty(Monitor.Protocol.BuildInfo.Tag), withCommit ? Empty(Monitor.Protocol.BuildInfo.Commit) : null, Empty(Monitor.Protocol.BuildInfo.BuiltAt));
new(MoShiro.Protocol.BuildInfo.Version, Empty(MoShiro.Protocol.BuildInfo.Tag), withCommit ? Empty(MoShiro.Protocol.BuildInfo.Commit) : null, Empty(MoShiro.Protocol.BuildInfo.BuiltAt));
private static string? Empty(string value) => value.Length == 0 ? null : value;
@@ -3,11 +3,11 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Monitor.Backend.Analysis;
using Monitor.Backend.Application;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace Monitor.Backend.Api;
namespace MoShiro.Backend.Api;
/// <summary>展示数据的查询端点:参数解析、校验与缓存头,业务都在 Application.Query。</summary>
public static class QueryEndpoints
@@ -211,13 +211,13 @@ public static class QueryEndpoints
TestDetail? detail = await tests.GetAsync(new TestId(test_id), ct);
if (detail is null)
{
return Problems.Create(http, MonitorErrors.TestNotFound, "test not found");
return Problems.Create(http, MoShiroErrors.TestNotFound, "test not found");
}
Result<AgentView> visible = await QueryHelpers.FindVisibleAgentAsync(http, agents, detail.Test.AgentId.Value, ct);
if (!visible.IsSuccess)
{
return Problems.Create(http, MonitorErrors.TestNotFound, "test not found");
return Problems.Create(http, MoShiroErrors.TestNotFound, "test not found");
}
return TypedResults.Json(Mappers.ToDto(detail), ApiJsonContext.Default.TestDto);
@@ -1,6 +1,6 @@
using Fleet.Core;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed class EventService
{
@@ -1,6 +1,6 @@
using Fleet.Core;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record CreateJoinTokenRequest(string? Note, IReadOnlyList<string>? Labels, bool AutoApprove, int MaxUses, int ExpiresInHours);
@@ -1,7 +1,7 @@
using System.Globalization;
using System.Text;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>运维:健康检查和 Prometheus 指标(在线数、块入库速率、分析队列长度、重算进度、磁盘占用、SSE 连接数)。</summary>
public sealed class OpsService
@@ -40,17 +40,17 @@ public sealed class OpsService
}
RecomputeProgress progress = _recompute.Progress;
Metric("monitor_agents_online", "Agents with an open control connection.", _gateway.OnlineAgents.Count);
Metric("monitor_chunks_ingested_total", "Raw chunks stored.", _counters.ChunksIngested, "counter");
Metric("monitor_chunks_duplicate_total", "Raw chunks received more than once.", _counters.ChunksDuplicate, "counter");
Metric("monitor_chunks_discarded_total", "Raw chunks rejected as invalid.", _counters.ChunksDiscarded, "counter");
Metric("monitor_ingest_bytes_total", "Bytes of raw chunks stored.", _counters.BytesIngested, "counter");
Metric("monitor_windows_analyzed_total", "Minute windows computed.", _counters.WindowsAnalyzed, "counter");
Metric("monitor_analysis_queue_length", "Minute windows waiting to be computed.", await _metrics.QueueLengthAsync(ct));
Metric("monitor_recompute_windows_done", "Windows recomputed by the current backfill.", progress.WindowsDone);
Metric("monitor_recompute_windows_total", "Windows to recompute in the current backfill.", progress.WindowsTotal);
Metric("monitor_raw_bytes", "Disk used by raw segment files.", _raw.TotalBytes);
Metric("monitor_sse_connections", "Open server-sent event connections.", _counters.SseConnections);
Metric("moshiro_agents_online", "Agents with an open control connection.", _gateway.OnlineAgents.Count);
Metric("moshiro_chunks_ingested_total", "Raw chunks stored.", _counters.ChunksIngested, "counter");
Metric("moshiro_chunks_duplicate_total", "Raw chunks received more than once.", _counters.ChunksDuplicate, "counter");
Metric("moshiro_chunks_discarded_total", "Raw chunks rejected as invalid.", _counters.ChunksDiscarded, "counter");
Metric("moshiro_ingest_bytes_total", "Bytes of raw chunks stored.", _counters.BytesIngested, "counter");
Metric("moshiro_windows_analyzed_total", "Minute windows computed.", _counters.WindowsAnalyzed, "counter");
Metric("moshiro_analysis_queue_length", "Minute windows waiting to be computed.", await _metrics.QueueLengthAsync(ct));
Metric("moshiro_recompute_windows_done", "Windows recomputed by the current backfill.", progress.WindowsDone);
Metric("moshiro_recompute_windows_total", "Windows to recompute in the current backfill.", progress.WindowsTotal);
Metric("moshiro_raw_bytes", "Disk used by raw segment files.", _raw.TotalBytes);
Metric("moshiro_sse_connections", "Open server-sent event connections.", _counters.SseConnections);
return text.ToString();
}
}
@@ -1,7 +1,7 @@
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>原始数据的检索与下载(管理员)。块过了保留期返回 raw_expired,从来不存在返回 chunk_not_found。</summary>
public sealed class RawBrowserService
@@ -26,7 +26,7 @@ public sealed class RawBrowserService
{
byte[]? bytes = await _raw.ReadAsync(meta, ct);
return bytes is null
? Result<(RawChunkMeta, byte[])>.Fail(MonitorErrors.RawExpired, "the raw data has expired")
? Result<(RawChunkMeta, byte[])>.Fail(MoShiroErrors.RawExpired, "the raw data has expired")
: Result<(RawChunkMeta, byte[])>.Ok((meta, bytes));
}
@@ -34,8 +34,8 @@ public sealed class RawBrowserService
int days = await _settings.GetIntAsync(SettingsService.TestRawDays, ct);
long cutoff = _time.GetUtcNow().ToUnixTimeMilliseconds() - (days * 86_400_000L);
return ChunkTimestampMs(id.Value) is { } created && created < cutoff
? Result<(RawChunkMeta, byte[])>.Fail(MonitorErrors.RawExpired, "the raw data has expired")
: Result<(RawChunkMeta, byte[])>.Fail(MonitorErrors.ChunkNotFound, "chunk not found");
? Result<(RawChunkMeta, byte[])>.Fail(MoShiroErrors.RawExpired, "the raw data has expired")
: Result<(RawChunkMeta, byte[])>.Fail(MoShiroErrors.ChunkNotFound, "chunk not found");
}
/// <summary>UUIDv7 的前 48 位是毫秒时间戳。</summary>
@@ -2,7 +2,7 @@ using System.Globalization;
using System.Text.Json;
using Fleet.Core;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record SettingSpec(string Key, string Type, string Default, long Min, long Max, string Description);
@@ -1,9 +1,9 @@
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary><c>monitor.event</c>:Agent 报告的事件(Probe 连上/断开、端点变化、时钟跳变、缺口、CPU 饱和)。</summary>
/// <summary><c>moshiro.event</c>:Agent 报告的事件(Probe 连上/断开、端点变化、时钟跳变、缺口、CPU 饱和)。</summary>
public sealed class AgentEventService
{
private readonly AgentRuntime _runtime;
@@ -1,8 +1,8 @@
using System.Collections.Concurrent;
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>Agent 眼中的一个已连接 Probe(Agent 在事件和心跳里上报)。</summary>
public sealed record ConnectedProbeInfo(string Id, string? Version, string? Remote, long SinceMs);
@@ -1,9 +1,9 @@
using System.Text.RegularExpressions;
using Fleet.Core;
using Fleet.Server;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record AgentView(AgentRecord Agent, bool Online, IReadOnlyList<ConnectedProbeInfo> Probes, AgentHeartbeatExtra? Heartbeat, int EndpointChanges, long? HeartbeatAtMs = null, HostSnapshot? Host = null)
{
@@ -1,10 +1,10 @@
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// <c>monitor.host</c>:Agent 每隔几秒上报的主机资源占用快照。最新值放在内存里并实时推给订阅者;每个 Agent 每分钟存一个样本做历史。
/// <c>moshiro.host</c>:Agent 每隔几秒上报的主机资源占用快照。最新值放在内存里并实时推给订阅者;每个 Agent 每分钟存一个样本做历史。
/// 快照里没有 CPU 型号、主机名、网卡名和 IP(Agent 根本不采集它们)。
/// </summary>
public sealed class HostStatusService
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>某一时刻的「线路」开关:自动连接的总开关,加上每一对(Probe,Agent)的手动设置。</summary>
public sealed class LinkSnapshot
@@ -2,17 +2,17 @@ using System.Text.Json;
using Fleet.Core;
using Fleet.Rpc;
using Fleet.Server;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>Monitor 在 Fleet 之上的接入点:注册之后补全公开地址与端口。</summary>
public sealed class MonitorEnrollmentHook : IEnrollmentHook
/// <summary>MoShiro 在 Fleet 之上的接入点:注册之后补全公开地址与端口。</summary>
public sealed class MoShiroEnrollmentHook : IEnrollmentHook
{
private readonly IAgentStore _agents;
private readonly AgentService _service;
public MonitorEnrollmentHook(IAgentStore agents, AgentService service)
public MoShiroEnrollmentHook(IAgentStore agents, AgentService service)
{
_agents = agents;
_service = service;
@@ -25,7 +25,7 @@ public sealed class MonitorEnrollmentHook : IEnrollmentHook
{
try
{
meta = raw.Deserialize(MonitorJsonContext.Default.AgentEnrollMeta) ?? meta;
meta = raw.Deserialize(MoShiroJsonContext.Default.AgentEnrollMeta) ?? meta;
}
catch (JsonException)
{
@@ -44,7 +44,7 @@ public sealed class MonitorEnrollmentHook : IEnrollmentHook
}
/// <summary>节点上线、下线、hello、心跳:更新运行时状态,补全 Agent 信息,并推送给 SSE。</summary>
public sealed class MonitorNodeEventSink : INodeEventSink
public sealed class MoShiroNodeEventSink : INodeEventSink
{
private readonly IAgentStore _agents;
private readonly AgentRuntime _runtime;
@@ -54,7 +54,7 @@ public sealed class MonitorNodeEventSink : INodeEventSink
private readonly NodeInfoService _nodes;
private readonly ReleaseService _releases;
public MonitorNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases)
public MoShiroNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases)
{
_releases = releases;
_nodes = nodes;
@@ -93,7 +93,7 @@ public sealed class MonitorNodeEventSink : INodeEventSink
try
{
AgentRuntimeState state = _runtime.Get(id);
AgentHeartbeatExtra? beat = extra.Deserialize(MonitorJsonContext.Default.AgentHeartbeatExtra);
AgentHeartbeatExtra? beat = extra.Deserialize(MoShiroJsonContext.Default.AgentHeartbeatExtra);
state.LastHeartbeat = beat;
state.LastHeartbeatAtMs = nodeEvent.AtMs;
_releases.RecordStatus(id, beat?.Update);
@@ -128,7 +128,7 @@ public sealed class MonitorNodeEventSink : INodeEventSink
{
try
{
ProbeHelloMeta? meta = raw.Deserialize(MonitorJsonContext.Default.ProbeHelloMeta);
ProbeHelloMeta? meta = raw.Deserialize(MoShiroJsonContext.Default.ProbeHelloMeta);
await _nodes.SetPlatformAsync(id.Value, NodeKinds.Probe, meta?.Platform, ct);
}
catch (JsonException)
@@ -146,7 +146,7 @@ public sealed class MonitorNodeEventSink : INodeEventSink
case NodeEventKind.Heartbeat when nodeEvent.Extra is { } extra:
try
{
_releases.RecordStatus(id, extra.Deserialize(MonitorJsonContext.Default.ProbeHeartbeatExtra)?.Update);
_releases.RecordStatus(id, extra.Deserialize(MoShiroJsonContext.Default.ProbeHeartbeatExtra)?.Update);
}
catch (JsonException)
{
@@ -162,9 +162,9 @@ public sealed class MonitorNodeEventSink : INodeEventSink
var kinds = new List<string>();
foreach (string capability in hello.Capabilities)
{
if (capability.StartsWith(MonitorCapabilities.KindsPrefix, StringComparison.Ordinal))
if (capability.StartsWith(MoShiroCapabilities.KindsPrefix, StringComparison.Ordinal))
{
kinds.AddRange(capability[MonitorCapabilities.KindsPrefix.Length..].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
kinds.AddRange(capability[MoShiroCapabilities.KindsPrefix.Length..].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
}
}
@@ -173,7 +173,7 @@ public sealed class MonitorNodeEventSink : INodeEventSink
{
try
{
meta = raw.Deserialize(MonitorJsonContext.Default.AgentHelloMeta) ?? meta;
meta = raw.Deserialize(MoShiroJsonContext.Default.AgentHelloMeta) ?? meta;
}
catch (JsonException)
{
@@ -2,9 +2,9 @@ using System.Text.RegularExpressions;
using Fleet.Core;
using Fleet.Identity;
using Fleet.Server;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>本地硬上限:写在 Probe 本机的配置里,面板无法修改(在路由器上建议调低)。</summary>
public sealed record ProbeLimits(double MaxTestMbps, long MaxTotalPps);
@@ -56,7 +56,7 @@ public sealed partial class ProbeService
if ((await _probes.ListAsync(ct)).Any(p => string.Equals(p.Name, finalName, StringComparison.OrdinalIgnoreCase)))
{
return Result<ProbeIdentityFile>.Fail(MonitorErrors.NameConflict, $"a probe named {finalName} already exists");
return Result<ProbeIdentityFile>.Fail(MoShiroErrors.NameConflict, $"a probe named {finalName} already exists");
}
using NodeIdentity identity = NodeIdentity.Generate();
@@ -80,7 +80,7 @@ public sealed partial class ProbeService
{
if (await _probes.GetAsync(id, ct) is not { } probe)
{
return Result<ProbeView>.Fail(MonitorErrors.ProbeNotFound, "probe not found");
return Result<ProbeView>.Fail(MoShiroErrors.ProbeNotFound, "probe not found");
}
string trimmed = name.Trim();
@@ -91,7 +91,7 @@ public sealed partial class ProbeService
if ((await _probes.ListAsync(ct)).Any(p => p.Id != id && string.Equals(p.Name, trimmed, StringComparison.OrdinalIgnoreCase)))
{
return Result<ProbeView>.Fail(MonitorErrors.NameConflict, $"a probe named {trimmed} already exists");
return Result<ProbeView>.Fail(MoShiroErrors.NameConflict, $"a probe named {trimmed} already exists");
}
await _probes.SaveAsync(probe with { Name = trimmed }, ct);
@@ -103,12 +103,12 @@ public sealed partial class ProbeService
{
if (await _probes.GetAsync(id, ct) is not { } probe)
{
return Result<ProbeIdentityFile>.Fail(MonitorErrors.ProbeNotFound, "probe not found");
return Result<ProbeIdentityFile>.Fail(MoShiroErrors.ProbeNotFound, "probe not found");
}
if (probe.RevokedAtMs is not null)
{
return Result<ProbeIdentityFile>.Fail(MonitorErrors.ProbeRevoked, "a revoked probe cannot get a new key; create a new probe instead");
return Result<ProbeIdentityFile>.Fail(MoShiroErrors.ProbeRevoked, "a revoked probe cannot get a new key; create a new probe instead");
}
using NodeIdentity identity = NodeIdentity.Generate();
@@ -124,7 +124,7 @@ public sealed partial class ProbeService
{
if (await _probes.GetAsync(id, ct) is null)
{
return Result<ProbeView>.Fail(MonitorErrors.ProbeNotFound, "probe not found");
return Result<ProbeView>.Fail(MoShiroErrors.ProbeNotFound, "probe not found");
}
await _probes.RevokeAsync(id, _time.GetUtcNow().ToUnixTimeMilliseconds(), ct);
@@ -151,12 +151,12 @@ public sealed partial class ProbeService
{
if (await _probes.GetAsync(probeId, ct) is null)
{
return Result<bool>.Fail(MonitorErrors.ProbeNotFound, "probe not found");
return Result<bool>.Fail(MoShiroErrors.ProbeNotFound, "probe not found");
}
if (await _agents.GetAsync(agentId, ct) is null)
{
return Result<bool>.Fail(MonitorErrors.AgentNotFound, "agent not found");
return Result<bool>.Fail(MoShiroErrors.AgentNotFound, "agent not found");
}
return await _gateway.DisconnectProbeAsync(agentId, new ProbeId(probeId), ct);
@@ -1,9 +1,9 @@
using Fleet.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Monitor.Backend.Analysis;
using MoShiro.Backend.Analysis;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 持久路径:分钟窗口就绪(窗口结束 + 宽限,或迟到的块到达)之后从原始块计算 → 写入 metric_1m → 标记降采样脏桶;
@@ -1,9 +1,9 @@
using Fleet.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Monitor.Backend.Analysis;
using MoShiro.Backend.Analysis;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record RecomputeProgress(string State, IReadOnlyList<string> Metrics, long WindowsDone, long WindowsTotal, long? FromMs, long? ToMs);
@@ -1,8 +1,8 @@
using Fleet.Core;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 从原始块重新计算一个窗口(持久路径与回填共用):读取与窗口重叠的块 → 评估 → 取出窗口对应的累加器。
@@ -2,7 +2,7 @@ using Fleet.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public static class ApplicationServiceCollectionExtensions
{
@@ -10,7 +10,7 @@ public static class ApplicationServiceCollectionExtensions
/// 注册 Application 层的全部服务。需要事先注册端口的实现(存储、<see cref="IAgentGateway"/>)和 <see cref="TimeProvider"/>。
/// 后台任务按这里的顺序启动:<see cref="StartupTasks"/> 必须在最前面。
/// </summary>
public static IServiceCollection AddMonitorApplication(this IServiceCollection services, BackendOptions options)
public static IServiceCollection AddMoShiroApplication(this IServiceCollection services, BackendOptions options)
{
services.AddSingleton(options);
services.AddHostedService<StartupTasks>();
@@ -47,8 +47,8 @@ public static class ApplicationServiceCollectionExtensions
services.AddSingleton<RowLoader>();
services.AddSingleton<QueryService>();
services.AddSingleton<IEnrollmentHook, MonitorEnrollmentHook>();
services.AddSingleton<INodeEventSink, MonitorNodeEventSink>();
services.AddSingleton<IEnrollmentHook, MoShiroEnrollmentHook>();
services.AddSingleton<INodeEventSink, MoShiroNodeEventSink>();
services.AddSingleton<AnalysisScheduler>();
services.AddHostedService(provider => provider.GetRequiredService<AnalysisScheduler>());
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public enum PrincipalKind
{
@@ -2,9 +2,9 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>面板用户、Cookie 会话、API 令牌。密码用 PBKDF2-SHA256,令牌和会话只保存 SHA-256。</summary>
public sealed partial class AuthService
@@ -34,7 +34,7 @@ public sealed partial class AuthService
byte[] hash = Hash(password, salt, iterations);
if (user is null || !CryptographicOperations.FixedTimeEquals(hash, user.PasswordHash))
{
return Result<LoginResult>.Fail(MonitorErrors.Unauthorized, "invalid credentials");
return Result<LoginResult>.Fail(MoShiroErrors.Unauthorized, "invalid credentials");
}
byte[] token = RandomNumberGenerator.GetBytes(32);
@@ -111,7 +111,7 @@ public sealed partial class AuthService
var user = new UserRecord(Ulid.NewPrefixed("usr", _time), username, Hash(password, salt, _options.Pbkdf2Iterations), salt, _options.Pbkdf2Iterations, _time.GetUtcNow().ToUnixTimeMilliseconds());
return await _store.CreateUserAsync(user, ct)
? Result<UserRecord>.Ok(user)
: Result<UserRecord>.Fail(MonitorErrors.NameConflict, "username already exists");
: Result<UserRecord>.Fail(MoShiroErrors.NameConflict, "username already exists");
}
public async ValueTask<Result<bool>> ChangePasswordAsync(string userId, string newPassword, CancellationToken ct)
@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>登录限流:每个来源在滑动的一分钟内最多尝试若干次。</summary>
public sealed class LoginRateLimiter
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>进程内计数器,用于 <c>/metrics</c>(Prometheus 格式)。</summary>
public sealed class BackendCounters
@@ -1,9 +1,9 @@
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>Backend 的可调参数(保留期等也可以通过 settings 改,这里是默认值与硬上限)。</summary>
public sealed class BackendOptions
{
/// <summary>数据目录:<c>monitor.db</c> 与 <c>raw/</c>。</summary>
/// <summary>数据目录:<c>moshiro.db</c> 与 <c>raw/</c>。</summary>
public string DataDirectory { get; set; } = "data";
/// <summary>面板的对外地址(用于生成安装命令)。</summary>
@@ -1,9 +1,9 @@
using Fleet.Core;
using Microsoft.Extensions.Logging;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// <see cref="Stored"/> 和 <see cref="Duplicate"/> 都会让节点把消息从发件箱里移除。
@@ -1,9 +1,9 @@
using System.Collections.Concurrent;
using Fleet.Core;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 1 秒精度的内存热层:最近 1 小时的累加器。桶以 <see cref="AccumulatorCodec"/> 编码后的字节保存,
@@ -1,9 +1,9 @@
using Fleet.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>重启后热层为空:从原始块回填最近 1 小时(低优先级、逐块让出)。完成之前查询规划器不会选用热层。</summary>
public sealed partial class HotLayerBackfill : BackgroundService
@@ -1,9 +1,9 @@
using System.Collections.Concurrent;
using System.Threading.Channels;
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>发布到 SSE 订阅者的信号。SSE 只负责“更快地获得变化”,推送的内容都能由 REST 查询重建。</summary>
public abstract record LiveSignal;
@@ -4,7 +4,7 @@
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\Monitor.Backend.Analysis\Monitor.Backend.Analysis.csproj" />
<ProjectReference Include="..\MoShiro.Backend.Analysis\MoShiro.Backend.Analysis.csproj" />
<ProjectReference Include="..\..\fleet\src\Fleet.Server.Abstractions\Fleet.Server.Abstractions.csproj" />
<ProjectReference Include="..\..\fleet\src\Fleet.Identity\Fleet.Identity.csproj" />
</ItemGroup>
@@ -1,7 +1,7 @@
using Fleet.Core;
using Fleet.Server;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record AgentRecord(
NodeId Id,
@@ -45,7 +45,7 @@ public sealed record AgentHello(
int? ProbeTcpPort,
IReadOnlyList<int>? ProbeUdpPorts);
/// <summary>注册之后 Monitor 的补充:公开地址和端口。</summary>
/// <summary>注册之后 MoShiro 的补充:公开地址和端口。</summary>
public sealed record AgentEnrollment(string PublicHost, int ProbeTcpPort, IReadOnlyList<int> ProbeUdpPorts, IReadOnlyList<string> Kinds);
public sealed record JoinTokenInfo(
@@ -1,7 +1,7 @@
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>原始块的索引信息(块本身在段文件里)。</summary>
public sealed record RawChunkMeta(
@@ -1,8 +1,8 @@
using System.Text.Json;
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record TaskRecord(
TaskId Id,
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 把「ASN 和组织名」归成几个常见的国内运营商:mobile / telecom / unicom / broadnet;别的是 other(显示组织名)。
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 一个节点(Agent 或 Probe)用来展示的信息:系统和架构、国家、运营商,以及它自己上报的构建信息(版本、tag、提交、构建时间)。
@@ -1,9 +1,9 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 节点的展示信息:系统和架构(节点自报),国家和运营商(Backend 用节点的来源地址查一次)。
@@ -1,7 +1,7 @@
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// Application 通往 Agent 连接的端口(由 Gateway 实现):下发状态变化、发起按需测试。
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record UserRecord(string Id, string Username, byte[] PasswordHash, byte[] PasswordSalt, int Iterations, long CreatedAtMs);
@@ -1,8 +1,8 @@
using Fleet.Core;
using Fleet.Server;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public interface IAgentStore
{
@@ -137,7 +137,7 @@ public interface IEventStore
/// <summary>主机资源占用的历史:每个 Agent 每分钟一个样本。</summary>
public interface IHostStore
{
ValueTask SaveAsync(NodeId agent, long minuteMs, Monitor.Protocol.HostSnapshot snapshot, CancellationToken ct);
ValueTask SaveAsync(NodeId agent, long minuteMs, MoShiro.Protocol.HostSnapshot snapshot, CancellationToken ct);
ValueTask<IReadOnlyList<HostSample>> ReadAsync(NodeId agent, long fromMs, long toMs, CancellationToken ct);
@@ -1,6 +1,6 @@
using Monitor.Backend.Analysis;
using MoShiro.Backend.Analysis;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>把源行合并成查询步长的桶:计数相加、直方图逐桶相加,所以结果是精确的,不是各点的平均。</summary>
public sealed class BucketedRows
@@ -1,7 +1,7 @@
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>一条线路 = 一对(Agent,Probe)。存储里它的序列是 <c>line:&lt;probe_id&gt;</c>,对外的 id 是 <c>&lt;agent_id&gt;~&lt;probe_id&gt;</c>。</summary>
public readonly record struct LineKey(NodeId Agent, ProbeId Probe)
@@ -68,7 +68,7 @@ public sealed class LineService
{
if (!LineKey.TryParse(id, out LineKey key))
{
return Result<LineView>.Fail(MonitorErrors.AgentNotFound, "line not found");
return Result<LineView>.Fail(MoShiroErrors.AgentNotFound, "line not found");
}
agentPart = key.Agent.Value;
@@ -78,7 +78,7 @@ public sealed class LineService
AgentView? agent = await _agents.GetAsync(new NodeId(agentPart), ct);
if (agent is null)
{
return Result<LineView>.Fail(MonitorErrors.AgentNotFound, "line not found");
return Result<LineView>.Fail(MoShiroErrors.AgentNotFound, "line not found");
}
IReadOnlyList<ProbeRecord> probes = await _probes.ListAsync(ct);
@@ -88,7 +88,7 @@ public sealed class LineService
probe = probes.FirstOrDefault(p => p.Id == probePart);
if (probe is null)
{
return Result<LineView>.Fail(MonitorErrors.ProbeNotFound, "probe not found");
return Result<LineView>.Fail(MoShiroErrors.ProbeNotFound, "probe not found");
}
}
else
@@ -96,7 +96,7 @@ public sealed class LineService
ProbeRecord[] active = [.. probes.Where(p => p.RevokedAtMs is null)];
if (active.Length != 1)
{
return Result<LineView>.Fail(MonitorErrors.ProbeRequired, active.Length == 0 ? "there is no active probe" : "several probes exist: use <agent_id>~<probe_id> as the line id");
return Result<LineView>.Fail(MoShiroErrors.ProbeRequired, active.Length == 0 ? "there is no active probe" : "several probes exist: use <agent_id>~<probe_id> as the line id");
}
probe = active[0];
@@ -1,8 +1,8 @@
using Fleet.Core;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>请求的指标:导出 id(<c>rtt.p95</c>)与评分 id(<c>score.mc</c>)。</summary>
public sealed record MetricSelection(IReadOnlyList<string> ExportIds, IReadOnlyList<string> ScoreModelIds)
@@ -21,7 +21,7 @@ public sealed record MetricSelection(IReadOnlyList<string> ExportIds, IReadOnlyL
PresetDef? preset = MetricRegistry.FindPreset(presetId);
if (preset is null)
{
return Result<MetricSelection>.Fail(MonitorErrors.UnknownPreset, $"unknown preset {presetId}");
return Result<MetricSelection>.Fail(MoShiroErrors.UnknownPreset, $"unknown preset {presetId}");
}
ids.AddRange(preset.MetricIds);
@@ -36,7 +36,7 @@ public sealed record MetricSelection(IReadOnlyList<string> ExportIds, IReadOnlyL
ids = [.. ids.Distinct(StringComparer.Ordinal)];
if (ids.Count > maxMetrics)
{
return Result<MetricSelection>.Fail(MonitorErrors.TooManyMetrics, $"at most {maxMetrics} metrics per request");
return Result<MetricSelection>.Fail(MoShiroErrors.TooManyMetrics, $"at most {maxMetrics} metrics per request");
}
var exports = new List<string>();
@@ -48,7 +48,7 @@ public sealed record MetricSelection(IReadOnlyList<string> ExportIds, IReadOnlyL
string model = id["score.".Length..];
if (ModelRegistry.Find(model) is null)
{
return Result<MetricSelection>.Fail(MonitorErrors.UnknownMetric, $"unknown metric {id}");
return Result<MetricSelection>.Fail(MoShiroErrors.UnknownMetric, $"unknown metric {id}");
}
scores.Add(model);
@@ -59,7 +59,7 @@ public sealed record MetricSelection(IReadOnlyList<string> ExportIds, IReadOnlyL
}
else
{
return Result<MetricSelection>.Fail(MonitorErrors.UnknownMetric, $"unknown metric {id}");
return Result<MetricSelection>.Fail(MoShiroErrors.UnknownMetric, $"unknown metric {id}");
}
}
@@ -1,7 +1,7 @@
using Fleet.Core;
using Monitor.Backend.Analysis;
using MoShiro.Backend.Analysis;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record SummaryResult(
IReadOnlyDictionary<string, double?> Values,
@@ -1,9 +1,9 @@
using System.Text.Json;
using Fleet.Core;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 展示数据查询:任意时间段、自动步长、数据分层与现场合并、精确汇总、列式结果。
@@ -185,7 +185,7 @@ public sealed class QueryService
if (!MetricRegistry.TryFindExport(exportId, out _, out _))
{
return Result<CompareResult>.Fail(MonitorErrors.UnknownMetric, $"unknown metric {exportId}");
return Result<CompareResult>.Fail(MoShiroErrors.UnknownMetric, $"unknown metric {exportId}");
}
Result<(QueryRange Range, SourcePlan Plan)> planned = Plan(request, "1h");
@@ -229,7 +229,7 @@ public sealed class QueryService
MetricDef? def = MetricRegistry.Find(metricId);
if (def is null || def.Kind != AccKind.Histogram)
{
return Result<DistributionResult>.Fail(MonitorErrors.UnknownMetric, $"{metricId} is not a histogram metric");
return Result<DistributionResult>.Fail(MoShiroErrors.UnknownMetric, $"{metricId} is not a histogram metric");
}
Result<(QueryRange Range, SourcePlan Plan)> planned = Plan(request, "15m");
@@ -273,7 +273,7 @@ public sealed class QueryService
MetricDef? def = MetricRegistry.Find(metricId);
if (def is null || def.Kind != AccKind.Histogram)
{
return Result<HeatmapResult>.Fail(MonitorErrors.UnknownMetric, $"{metricId} is not a histogram metric");
return Result<HeatmapResult>.Fail(MoShiroErrors.UnknownMetric, $"{metricId} is not a histogram metric");
}
Result<(QueryRange Range, SourcePlan Plan)> planned = Plan(request, "6h");
@@ -1,9 +1,9 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <param name="Anchor"><c>now</c>(默认)或 <c>latest</c>:相对时间段(<c>range=</c>)以当前时间还是以这条线路最后一个样本为终点。后者让离线的线路显示它最后一段数据,而不是空图。</param>
public sealed record RangeRequest(string? Range, DateTimeOffset? From, DateTimeOffset? To, string? Step, string? TimeZone, string? Anchor = null);
@@ -63,7 +63,7 @@ public sealed partial class RangeResolver
{
if (!TryParseSpan(request.Range ?? defaultRange, out long span))
{
return Result<QueryRange>.Fail(MonitorErrors.InvalidRange, "range must look like 5m, 6h or 7d");
return Result<QueryRange>.Fail(MoShiroErrors.InvalidRange, "range must look like 5m, 6h or 7d");
}
to = nowMs;
@@ -73,12 +73,12 @@ public sealed partial class RangeResolver
if (from >= to)
{
return Result<QueryRange>.Fail(MonitorErrors.InvalidRange, "from must be earlier than to");
return Result<QueryRange>.Fail(MoShiroErrors.InvalidRange, "from must be earlier than to");
}
if (to - from > MaxSpanMs)
{
return Result<QueryRange>.Fail(MonitorErrors.RangeTooLarge, "the range is longer than 400 days");
return Result<QueryRange>.Fail(MoShiroErrors.RangeTooLarge, "the range is longer than 400 days");
}
long step;
@@ -88,14 +88,14 @@ public sealed partial class RangeResolver
}
else if (!Steps.TryParse(request.Step, out step))
{
return Result<QueryRange>.Fail(MonitorErrors.InvalidStep, "step must be one of: " + string.Join(", ", Steps.Common.Select(Steps.Format)));
return Result<QueryRange>.Fail(MoShiroErrors.InvalidStep, "step must be one of: " + string.Join(", ", Steps.Common.Select(Steps.Format)));
}
var range = new QueryRange(Steps.FloorTo(from, step), Steps.CeilTo(to, step), step, relative, from, to);
if (range.Points > _options.MaxPointsPerSeries)
{
long suggested = Steps.Common.FirstOrDefault(s => (Steps.CeilTo(to, s) - Steps.FloorTo(from, s)) / s <= _options.MaxPointsPerSeries, Steps.Day);
return Result<QueryRange>.Fail(MonitorErrors.RangeTooLarge, $"{range.Points} points exceed the limit of {_options.MaxPointsPerSeries}; use step={Steps.Format(suggested)} or a shorter range");
return Result<QueryRange>.Fail(MoShiroErrors.RangeTooLarge, $"{range.Points} points exceed the limit of {_options.MaxPointsPerSeries}; use step={Steps.Format(suggested)} or a shorter range");
}
return Result<QueryRange>.Ok(range);
@@ -1,8 +1,8 @@
using Fleet.Core;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public readonly record struct SourceRow(long Start, MetricSet Set, bool Partial);
@@ -92,7 +92,7 @@ public sealed class RowLoader
case DataSource.Raw:
if (!_gate.TryEnter())
{
return Result<IReadOnlyList<SourceRow>>.Fail(MonitorErrors.TooManyRequests, "too many queries are reading raw data; retry shortly or use a coarser step");
return Result<IReadOnlyList<SourceRow>>.Fail(MoShiroErrors.TooManyRequests, "too many queries are reading raw data; retry shortly or use a coarser step");
}
try
@@ -1,4 +1,4 @@
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public enum DataSource
{
@@ -1,6 +1,6 @@
using System.Globalization;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>常用步长:1s 2s 5s 10s 15s 30s 1m 2m 5m 10m 15m 30m 1h 2h 3h 6h 12h 1d。</summary>
public static class Steps
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>保留清理:原始段文件(持续任务与按需测试保留期不同)、各层指标、事件。保留期来自 settings,可以在线修改。</summary>
public sealed partial class RetentionService : BackgroundService
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Hosting;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 启动时最先执行:截断没有索引记录的段文件尾部、创建初始管理员、创建默认的线路任务模板。
@@ -1,7 +1,7 @@
using System.Text.Json;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record EffectiveTask(
TaskId Id,
@@ -2,9 +2,9 @@ using System.Security.Cryptography;
using System.Text.Json;
using Fleet.Core;
using Fleet.Server;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record DesiredTaskSet(long Version, TaskSet Document);
@@ -65,7 +65,7 @@ public sealed class TaskPlanner : IDisposable
document.Peers.Add(new PeerSpec { ProbeId = new ProbeId(probe.Id), PublicKey = Convert.ToBase64String(probe.PublicKey) });
}
string hash = Convert.ToHexStringLower(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(document, MonitorJsonContext.Default.TaskSet)));
string hash = Convert.ToHexStringLower(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(document, MoShiroJsonContext.Default.TaskSet)));
await _gate.WaitAsync(ct);
try
@@ -2,9 +2,9 @@ using System.Net;
using System.Text.Json;
using Fleet.Core;
using Fleet.Server;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record TaskInput(
string Scope,
@@ -97,7 +97,7 @@ public sealed class TaskService
TaskRecord? current = await _tasks.GetAsync(id, ct);
if (current is null)
{
return Result<TaskRecord>.Fail(MonitorErrors.TaskNotFound, "task not found");
return Result<TaskRecord>.Fail(MoShiroErrors.TaskNotFound, "task not found");
}
IReadOnlyDictionary<string, JsonElement> parameters = patch.Params ?? current.Params;
@@ -178,7 +178,7 @@ public sealed class TaskService
KindSpec? spec = Kinds.Find(kind);
if (spec is null)
{
return Result<ValidatedTask>.Fail(MonitorErrors.UnknownKind, $"unknown kind {kind}");
return Result<ValidatedTask>.Fail(MoShiroErrors.UnknownKind, $"unknown kind {kind}");
}
Scope wanted = scope == EffectiveTasks.LineScope ? Scope.Line : Scope.Target;
@@ -206,7 +206,7 @@ public sealed class TaskService
if (agentId is { } id && await _agents.GetAsync(id, ct) is null)
{
return Result<ValidatedTask>.Fail(MonitorErrors.AgentNotFound, "agent not found");
return Result<ValidatedTask>.Fail(MoShiroErrors.AgentNotFound, "agent not found");
}
if (scope == EffectiveTasks.TargetScope)
@@ -259,7 +259,7 @@ public sealed class TaskService
long bits = EffectiveTasks.ContinuousLineBudget(EffectiveTasks.Compute(after, agent with { Kinds = [] }, filterByAgentKinds: false));
if (bits > ProtocolLimits.MaxLineBudgetBitsPerSecond)
{
return Result<bool>.Fail(MonitorErrors.BudgetExceeded, $"continuous tasks for {agent.Name} would use {bits / 1000} kbps (limit {ProtocolLimits.MaxLineBudgetBitsPerSecond / 1000} kbps)");
return Result<bool>.Fail(MoShiroErrors.BudgetExceeded, $"continuous tasks for {agent.Name} would use {bits / 1000} kbps (limit {ProtocolLimits.MaxLineBudgetBitsPerSecond / 1000} kbps)");
}
}
@@ -270,7 +270,7 @@ public sealed class TaskService
long bits = EffectiveTasks.ContinuousLineBudget(EffectiveTasks.Compute(after, virtualAgent, filterByAgentKinds: false));
if (bits > ProtocolLimits.MaxLineBudgetBitsPerSecond)
{
return Result<bool>.Fail(MonitorErrors.BudgetExceeded, $"continuous line templates would use {bits / 1000} kbps (limit {ProtocolLimits.MaxLineBudgetBitsPerSecond / 1000} kbps)");
return Result<bool>.Fail(MoShiroErrors.BudgetExceeded, $"continuous line templates would use {bits / 1000} kbps (limit {ProtocolLimits.MaxLineBudgetBitsPerSecond / 1000} kbps)");
}
}
@@ -1,8 +1,8 @@
using System.Text.Json;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed record PhaseSpec(string Kind, IReadOnlyDictionary<string, JsonElement> Overrides, int DurationSeconds);
@@ -1,10 +1,10 @@
using Fleet.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>
/// 后台任务:给已成功结束的测试算结果(所有阶段的块到达之后,或结束一分钟后以现有的块为准),
@@ -1,8 +1,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Monitor.Backend.Analysis;
using MoShiro.Backend.Analysis;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
public sealed class StoredTestMetrics
{
@@ -1,10 +1,10 @@
using System.Text.Json;
using Fleet.Core;
using Fleet.Server;
using Monitor.Backend.Analysis;
using Monitor.Protocol;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>按需测试(once):校验与排队、下发给 Agent、状态更新、结果查询。</summary>
public sealed class TestService
@@ -33,18 +33,18 @@ public sealed class TestService
PlanSpec? plan = Plans.Find(request.Plan);
if (plan is null)
{
return Result<TestRecord>.Fail(MonitorErrors.UnknownPlan, $"unknown plan {request.Plan}");
return Result<TestRecord>.Fail(MoShiroErrors.UnknownPlan, $"unknown plan {request.Plan}");
}
AgentRecord? agent = await _agents.GetAsync(agentId, ct);
if (agent is null)
{
return Result<TestRecord>.Fail(MonitorErrors.AgentNotFound, "agent not found");
return Result<TestRecord>.Fail(MoShiroErrors.AgentNotFound, "agent not found");
}
if (agent.Status != NodeStatus.Active)
{
return Result<TestRecord>.Fail(MonitorErrors.AgentNotActive, "agent is not active");
return Result<TestRecord>.Fail(MoShiroErrors.AgentNotActive, "agent is not active");
}
// Agent 在 hello 里声明了它支持的采集方法:方案里有它不支持的就不下发(否则要等到阶段开始才失败)
@@ -62,17 +62,17 @@ public sealed class TestService
if (!_gateway.IsOnline(agentId))
{
return Result<TestRecord>.Fail(MonitorErrors.AgentOffline, "agent is offline");
return Result<TestRecord>.Fail(MoShiroErrors.AgentOffline, "agent is offline");
}
if (!(_runtime.TryGet(agentId, out AgentRuntimeState? state) && state.IsConnected(probeId.Value)))
{
return Result<TestRecord>.Fail(MonitorErrors.ProbeOffline, $"probe {probeId.Value} has no session with this agent");
return Result<TestRecord>.Fail(MoShiroErrors.ProbeOffline, $"probe {probeId.Value} has no session with this agent");
}
if (await _tests.HasActiveAsync(agentId, probeId, ct))
{
return Result<TestRecord>.Fail(MonitorErrors.TestBusy, "another test is already running on this line");
return Result<TestRecord>.Fail(MoShiroErrors.TestBusy, "another test is already running on this line");
}
var testId = new TestId(Ulid.NewPrefixed(TestId.Prefix, _time));
@@ -96,7 +96,7 @@ public sealed class TestService
TestRecord? test = await _tests.GetAsync(id, ct);
if (test is null)
{
return Result<TestRecord>.Fail(MonitorErrors.TestNotFound, "test not found");
return Result<TestRecord>.Fail(MoShiroErrors.TestNotFound, "test not found");
}
if (IsTerminal(test.Status))
@@ -115,7 +115,7 @@ public sealed class TestService
return Result<TestRecord>.Ok(await _tests.GetAsync(id, ct) ?? test);
}
/// <summary><c>monitor.test.status</c>:只接受该测试所属 Agent 的汇报。</summary>
/// <summary><c>moshiro.test.status</c>:只接受该测试所属 Agent 的汇报。</summary>
public async ValueTask OnStatusAsync(NodeId agent, TestStatusParams status, CancellationToken ct)
{
TestRecord? test = await _tests.GetAsync(status.TestId, ct);
@@ -244,7 +244,7 @@ public sealed class TestService
long bits = kind.BudgetBitsPerSecond(resolved.Values!);
if (limitMbps is > 0 && bits > limitMbps * 1_000_000)
{
return Result<List<TestPhase>>.Fail(MonitorErrors.BudgetExceeded, $"{phase.Kind} needs {bits / 1_000_000.0:0.#} Mbit/s but the agent allows {limitMbps} Mbit/s");
return Result<List<TestPhase>>.Fail(MoShiroErrors.BudgetExceeded, $"{phase.Kind} needs {bits / 1_000_000.0:0.#} Mbit/s but the agent allows {limitMbps} Mbit/s");
}
int duration = resolved.Values!.Values.TryGetValue("duration_s", out JsonElement d) && d.TryGetInt64(out long seconds) ? (int)seconds : Math.Max(1, (int)Math.Round(phase.DurationSeconds * phaseScale));
@@ -3,9 +3,9 @@ using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.RegularExpressions;
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>面板记下的、CI 签名的发布清单(只留每个组件最新的一份)。<c>Manifest</c> 是签名对象的原始文本,面板一个字节也不改。</summary>
public sealed record ReleaseOffer(string Component, long Seq, string Tag, string Version, string Digest, string Manifest, string Signature);
@@ -42,7 +42,7 @@ public sealed partial class ReleaseService
ReleaseOffer? current = await GetAsync(offer.Component, ct);
if (current is not null && offer.Seq <= current.Seq)
{
return Result<ReleaseOffer>.Fail(MonitorErrors.NameConflict, $"a release with seq {current.Seq} or newer is already registered");
return Result<ReleaseOffer>.Fail(MoShiroErrors.NameConflict, $"a release with seq {current.Seq} or newer is already registered");
}
await _settings.SetAsync(Key(offer.Component, "manifest"), offer.Manifest, ct);
@@ -1,11 +1,11 @@
using Fleet.Core;
using Monitor.Protocol;
using MoShiro.Protocol;
namespace Monitor.Backend.Application;
namespace MoShiro.Backend.Application;
/// <summary>校验失败,带逐字段的原因(键是 JSON 字段路径)。Api 层把它映射成 problem+json 的 <c>errors</c>。</summary>
public sealed record ValidationError(string Message, IReadOnlyDictionary<string, string> Fields)
: FleetError(MonitorErrors.ValidationFailed, Message)
: FleetError(MoShiroErrors.ValidationFailed, Message)
{
public static ValidationError Single(string field, string message) =>
new("Validation failed", new Dictionary<string, string> { [field] = message });
Loaded 100 of 309 files, more files were not shown because too many files have changed in this diff. Show more