XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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}":"");
}

/// <summary>One visible record per active failure, retaining its first/last observation and diagnostics.</summary>
public sealed class ActivityLog(Func<DateTimeOffset>? utcNow=null,Action<ActivityEntry>? persist=null)
{
    private readonly object sync=new();
    private readonly LinkedList<ActivityEntry> rows=new();
    private readonly Dictionary<string,(LinkedListNode<ActivityEntry> Node,string Signature,DateTimeOffset Persisted)> active=new(StringComparer.Ordinal);
    private DateTimeOffset Now=>utcNow?.Invoke()??DateTimeOffset.UtcNow;
    public IReadOnlyList<ActivityEntry> 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<ActivityEntry> 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){}}
}