fleet: send log counters in heartbeats and pull the text on demand

Heartbeats carry per-level counts, the most frequent log types (a stable hash of the message) and the newest id, no text. The node keeps its last 2048 lines; moshiro.logs.fetch lets the backend pull them. The log page pulls when it is open, and the backend pulls warnings and errors once a minute after a node reports an error.
This commit is contained in:
justme committed 2026-10-22 05:28:29 +08:00
1 parent 4c31a4a79e
commit fb3a9c260f
17 files changed
+482 -110

No files matched your search

@@ -19,6 +19,7 @@ public static class MoShiroMethodTable
MethodSpec.Request(MoShiroMethods.TestRun, RpcSide.Server),
MethodSpec.Request(MoShiroMethods.TestCancel, RpcSide.Server),
MethodSpec.Request(MoShiroMethods.DialerDisconnect, RpcSide.Server),
MethodSpec.Request(MoShiroMethods.LogsFetch, RpcSide.Server),
MethodSpec.Notification(MoShiroMethods.LinkRetry, RpcSide.Server),
MethodSpec.Notification(MoShiroMethods.UpdateOffer, RpcSide.Server),
MethodSpec.Notification(MoShiroMethods.UpdateApply, RpcSide.Server),
@@ -17,6 +17,7 @@ public static class MoShiroMethods
public const string UpdateOffer = "moshiro.update.offer";
public const string UpdateApply = "moshiro.update.apply";
public const string Schedule = "moshiro.schedule";
public const string LogsFetch = "moshiro.logs.fetch";
// 拨号方 ↔ 监听方(点对点会话)
public const string Clock = "moshiro.clock";
@@ -338,11 +339,84 @@ public sealed class NodeHeartbeatExtra
[JsonPropertyName("update")]
public UpdateStatus? Update { get; set; }
[JsonPropertyName("runtime_logs")]
public List<NodeLogRecord>? RuntimeLogs { get; set; }
/// <summary>日志只报计数(不带文本):自上次心跳以来各级别的条数、最多几个出现最多的类型,以及最新一条的编号;文本由 Backend 用 <c>moshiro.logs.fetch</c> 按需拉取。</summary>
[JsonPropertyName("log_summary")]
public LogSummaryDto? LogSummary { get; set; }
}
[JsonPropertyName("dropped_runtime_logs")]
public long? DroppedRuntimeLogs { get; set; }
/// <summary>某一类日志(消息文本的稳定哈希)在一个级别上出现了多少次。</summary>
public sealed class LogTypeCountDto
{
[JsonPropertyName("type")]
public uint Type { get; set; }
[JsonPropertyName("level")]
public string Level { get; set; } = "info";
[JsonPropertyName("count")]
public long Count { get; set; }
}
public sealed class LogSummaryDto
{
[JsonPropertyName("last_id")]
public long LastId { get; set; }
[JsonPropertyName("info")]
public long Info { get; set; }
[JsonPropertyName("warn")]
public long Warn { get; set; }
[JsonPropertyName("error")]
public long Error { get; set; }
[JsonPropertyName("types")]
public List<LogTypeCountDto> Types { get; set; } = [];
}
/// <summary>Backend → 节点:拉取编号大于 <c>after_id</c> 的日志(最旧的在前),级别不低于 <c>min_level</c>。</summary>
public sealed class LogsFetchParams
{
[JsonPropertyName("after_id")]
public long AfterId { get; set; }
[JsonPropertyName("limit")]
public int Limit { get; set; } = 100;
[JsonPropertyName("min_level")]
public string? MinLevel { get; set; }
}
public sealed class LogEntryDto
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("type")]
public uint Type { get; set; }
[JsonPropertyName("at_utc_ms")]
public long AtUtcMs { get; set; }
[JsonPropertyName("level")]
public string Level { get; set; } = "info";
[JsonPropertyName("message")]
public string Message { get; set; } = "";
[JsonPropertyName("fields")]
public Dictionary<string, string> Fields { get; set; } = [];
}
public sealed class LogsFetchResult
{
[JsonPropertyName("items")]
public List<LogEntryDto> Items { get; set; } = [];
/// <summary>节点上最新一条的编号:比 <c>after_id</c> 小说明节点重启过,编号从头开始。</summary>
[JsonPropertyName("last_id")]
public long LastId { get; set; }
}
/// <summary>
@@ -749,6 +823,9 @@ public static class FragmentKeys
[JsonSerializable(typeof(DialTargets))]
[JsonSerializable(typeof(UpdateOfferParams))]
[JsonSerializable(typeof(ScheduleParams))]
[JsonSerializable(typeof(LogsFetchParams))]
[JsonSerializable(typeof(LogsFetchResult))]
[JsonSerializable(typeof(LogSummaryDto))]
[JsonSerializable(typeof(UpdateStatus))]
[JsonSerializable(typeof(NodeHeartbeatExtra))]
[JsonSerializable(typeof(NodeHelloMeta))]