namespace XFEEDToolBox.Core.Services; public enum ActivitySeverity { Status,Information,Warning,Error } public sealed record ActivityEntry(DateTimeOffset Time,string Kind,string Message) { private static long nextId; public long Id{get;init;}=Interlocked.Increment(ref nextId); public DateTimeOffset LastSeen{get;init;}=Time; public int Occurrences{get;init;}=1; public ActivitySeverity Severity{get;init;}=ActivitySeverity.Information; public string Code{get;init;}=""; public bool Resolved{get;init;} public bool Superseded{get;init;} public DateTimeOffset? ClosedAt{get;init;} public string FirstMessage{get;init;}=Message; public string FirstDiagnostic{get;init;}=""; public string LastDiagnostic{get;init;}=""; public string TimeDisplay=>(ClosedAt??LastSeen).ToLocalTime().ToString("HH:mm:ss"); public string SeverityDisplay=>Resolved?"已恢复":Superseded?"已更新":Severity switch{ActivitySeverity.Status=>"状态",ActivitySeverity.Warning=>"警告",ActivitySeverity.Error=>"错误",_=>"信息"}; public string RepetitionDisplay=>Occurrences>1?$"出现 {Occurrences:N0} 次 · 首次 {Time.ToLocalTime():HH:mm:ss} · 最近 {LastSeen.ToLocalTime():HH:mm:ss}":""; public bool HasDiagnostic=>FirstDiagnostic.Length>0; public string DiagnosticDisplay=>$"标识:{Code}\n首次:{Time.ToLocalTime():yyyy-MM-dd HH:mm:ss}\n最近:{LastSeen.ToLocalTime():yyyy-MM-dd HH:mm:ss}\n次数:{Occurrences:N0}\n首次摘要:{FirstMessage}\n\n{FirstDiagnostic}"+ (ClosedAt is {} closed?$"\n本轮结束:{closed.ToLocalTime():yyyy-MM-dd HH:mm:ss}":"")+ (LastDiagnostic.Length>0&&LastDiagnostic!=FirstDiagnostic?$"\n\n最近诊断:\n{LastDiagnostic}":""); } /// One visible record per active failure, retaining its first/last observation and diagnostics. public sealed class ActivityLog(Func? utcNow=null,Action? persist=null) { private readonly object sync=new(); private readonly LinkedList rows=new(); private readonly Dictionary Node,string Signature,DateTimeOffset Persisted)> active=new(StringComparer.Ordinal); private DateTimeOffset Now=>utcNow?.Invoke()??DateTimeOffset.UtcNow; public IReadOnlyList Snapshot(int limit=200){lock(sync)return rows.Take(Math.Clamp(limit,0,300)).ToArray();} public void Append(string kind,string message) { lock(sync) { var entry=new ActivityEntry(Now,kind,message);Add(entry);Persist(entry); } } public void Report(string code,string kind,string message,ActivitySeverity severity,string diagnostic="",string? identity=null) { ArgumentException.ThrowIfNullOrWhiteSpace(code); lock(sync) { var now=Now;var signature=identity??message; if(active.TryGetValue(code,out var previous)&&previous.Signature==signature) { var escalated=severity>previous.Node.Value.Severity; var updated=previous.Node.Value with{LastSeen=now,Occurrences=previous.Node.Value.Occurrences+1,Kind=kind,Message=message,Severity=escalated?severity:previous.Node.Value.Severity,LastDiagnostic=diagnostic.Length>0?diagnostic:previous.Node.Value.LastDiagnostic}; previous.Node.Value=updated;rows.Remove(previous.Node);rows.AddFirst(previous.Node); var due=now-previous.Persisted>=TimeSpan.FromMinutes(1); active[code]=(previous.Node,signature,due?now:previous.Persisted); if(due||escalated)Persist(updated); return; } if(active.Remove(code,out var replaced)) {replaced.Node.Value=replaced.Node.Value with{Superseded=true,ClosedAt=now};Persist(replaced.Node.Value);} var entry=new ActivityEntry(now,kind,message){Code=code,Severity=severity,FirstDiagnostic=diagnostic,LastDiagnostic=diagnostic}; var node=Add(entry);active[code]=(node,signature,now);Persist(entry); } } public bool Resolve(string code,string kind,string message) { lock(sync) { if(!active.Remove(code,out var previous))return false; var entry=previous.Node.Value with{ClosedAt=Now,Resolved=true,Message=message,Kind=kind}; previous.Node.Value=entry;rows.Remove(previous.Node);rows.AddFirst(previous.Node);Persist(entry);return true; } } public void Flush(){lock(sync)foreach(var value in active.Values)Persist(value.Node.Value);} private LinkedListNode Add(ActivityEntry entry) { var node=rows.AddFirst(entry); while(rows.Count>300) { var last=rows.Last!;rows.RemoveLast(); if(last.Value.Code.Length>0)Persist(last.Value); if(last.Value.Code.Length>0&&active.TryGetValue(last.Value.Code,out var current)&&ReferenceEquals(current.Node,last))active.Remove(last.Value.Code); } return node; } private void Persist(ActivityEntry entry) {try{persist?.Invoke(entry);}catch(IOException){}catch(UnauthorizedAccessException){}} }