Files
MoShiro/backend/src/Monitor.Backend.Application/Agents/MonitorFleetHooks.cs
T
2026-10-04 21:46:39 +08:00

156 lines
5.9 KiB
C#

using System.Text.Json;
using Fleet.Core;
using Fleet.Rpc;
using Fleet.Server;
using Monitor.Protocol;
namespace Monitor.Backend.Application;
/// <summary>Monitor 在 Fleet 之上的接入点:注册之后补全公开地址与端口。</summary>
public sealed class MonitorEnrollmentHook : IEnrollmentHook
{
private readonly IAgentStore _agents;
private readonly AgentService _service;
public MonitorEnrollmentHook(IAgentStore agents, AgentService service)
{
_agents = agents;
_service = service;
}
public async ValueTask OnEnrolledAsync(NodeRecord node, EnrollmentContext context, CancellationToken ct)
{
AgentEnrollMeta meta = new();
if (context.ProductMeta is { } raw)
{
try
{
meta = raw.Deserialize(MonitorJsonContext.Default.AgentEnrollMeta) ?? meta;
}
catch (JsonException)
{
// 元数据格式不对就用默认值,节点之后可以通过 hello 补全
}
}
// public_host 未提供时,取注册请求的来源 IP
string host = !string.IsNullOrWhiteSpace(meta.PublicHost) ? meta.PublicHost : context.RemoteIp ?? "";
await _agents.ApplyEnrollmentAsync(node.Id, new AgentEnrollment(host, meta.ProbeTcpPort, meta.ProbeUdpPorts, meta.Kinds), ct);
if (node.Status == NodeStatus.Active)
{
await _service.AfterAgentSetChangedAsync(ct);
}
}
}
/// <summary>节点上线、下线、hello、心跳:更新运行时状态,补全 Agent 信息,并推送给 SSE。</summary>
public sealed class MonitorNodeEventSink : INodeEventSink
{
private readonly IAgentStore _agents;
private readonly AgentRuntime _runtime;
private readonly IEventStore _events;
private readonly LiveHub _live;
private readonly AgentService _service;
public MonitorNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service)
{
_agents = agents;
_runtime = runtime;
_events = events;
_live = live;
_service = service;
}
public async ValueTask OnEventAsync(NodeEvent nodeEvent, CancellationToken ct)
{
NodeId id = nodeEvent.Node.Id;
switch (nodeEvent.Kind)
{
case NodeEventKind.Online:
_live.Publish(new NodeSignal("agent", id.Value, true, nodeEvent.AtMs));
await _events.AddAsync(id, "agent_online", nodeEvent.AtMs, null, ct);
break;
case NodeEventKind.Offline:
_runtime.MarkOffline(id);
_live.Publish(new NodeSignal("agent", id.Value, false, nodeEvent.AtMs));
await _events.AddAsync(id, "agent_offline", nodeEvent.AtMs, null, ct);
break;
case NodeEventKind.Hello when nodeEvent.Hello is { } hello:
await ApplyHelloAsync(id, hello, ct);
break;
case NodeEventKind.Heartbeat when nodeEvent.Extra is { } extra:
try
{
AgentRuntimeState state = _runtime.Get(id);
AgentHeartbeatExtra? beat = extra.Deserialize(MonitorJsonContext.Default.AgentHeartbeatExtra);
state.LastHeartbeat = beat;
state.LastHeartbeatAtMs = nodeEvent.AtMs;
if (beat is not null)
{
state.ReplaceProbes(beat.Probes.Select(p => new ConnectedProbeInfo(p.Id.Value, p.Version, p.Remote, p.SinceUtcMs)));
}
}
catch (JsonException)
{
// 忽略格式不正确的附加字段
}
break;
}
}
private async ValueTask ApplyHelloAsync(NodeId id, HelloParams hello, CancellationToken ct)
{
var kinds = new List<string>();
foreach (string capability in hello.Capabilities)
{
if (capability.StartsWith(MonitorCapabilities.KindsPrefix, StringComparison.Ordinal))
{
kinds.AddRange(capability[MonitorCapabilities.KindsPrefix.Length..].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
}
}
AgentHelloMeta meta = new();
if (hello.Meta is { } raw)
{
try
{
meta = raw.Deserialize(MonitorJsonContext.Default.AgentHelloMeta) ?? meta;
}
catch (JsonException)
{
// 忽略
}
}
byte[]? spki = null;
if (meta.TlsSpki is not null)
{
try
{
spki = Convert.FromBase64String(meta.TlsSpki);
}
catch (FormatException)
{
// 忽略
}
}
AgentRecord? before = await _agents.GetAsync(id, ct);
string? limitsJson = meta.LocalLimits is null ? null : JsonSerializer.Serialize(meta.LocalLimits, LocalLimitsJson.Default.LocalLimits);
await _agents.ApplyHelloAsync(id, new AgentHello(kinds, hello.NodeVersion, limitsJson, spki is { Length: 32 } ? spki : null, meta.ProbeTcpPort, meta.ProbeUdpPorts), ct);
// 端口或证书指纹变了,Probe 需要新的列表
AgentRecord? after = await _agents.GetAsync(id, ct);
if (before is not null && after is not null
&& (!before.TlsSpki.AsSpan().SequenceEqual(after.TlsSpki) || before.ProbeTcpPort != after.ProbeTcpPort || !before.ProbeUdpPorts.SequenceEqual(after.ProbeUdpPorts))
&& after.Status == NodeStatus.Active)
{
await _service.AfterAgentSetChangedAsync(ct);
}
}
}
[System.Text.Json.Serialization.JsonSerializable(typeof(LocalLimits))]
internal sealed partial class LocalLimitsJson : System.Text.Json.Serialization.JsonSerializerContext;