using System.Text.Json; using Fleet.Core; using Fleet.Rpc; using Fleet.Server; using Monitor.Protocol; namespace Monitor.Backend.Application; /// Monitor 在 Fleet 之上的接入点:注册之后补全公开地址与端口。 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); } } } /// 节点上线、下线、hello、心跳:更新运行时状态,补全 Agent 信息,并推送给 SSE。 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(); 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;