Files
SocialPub/PrivaPub/Infrastructure/Statistics/Rollup.cs
T

132 lines
5.2 KiB
C#
Raw Normal View History

2026-10-03 11:30:14 +02:00
using PrivaPub.Models.Statistics;
namespace PrivaPub.Infrastructure.Statistics
{
public sealed class HostFold
{
public Dictionary<string, long> Counters { get; } = new();
public Dictionary<string, long> PublicCounters { get; } = new();
public Dictionary<string, long> Latency { get; } = new();
public Dictionary<string, long> Waits { get; } = new();
public Dictionary<string, long> Bytes { get; } = new();
public HashSet<string> Accounts { get; } = new(StringComparer.Ordinal);
}
public sealed class DayFold
{
public Dictionary<string, HostFold> Hosts { get; } = new(StringComparer.Ordinal);
public HashSet<string> Accounts { get; } = new(StringComparer.Ordinal);
}
public static class Rollup
{
static readonly HashSet<string> PublicActivities = new(StringComparer.Ordinal)
{
"Create", "Update", "Delete", "Announce", "Like", "EmojiReact", "Dislike", "Follow", "Accept", "Reject", "Undo", "Move", "Add", "Remove",
"Join", "Leave", "QuoteRequest"
};
static readonly HashSet<string> PublicObjects = new(StringComparer.Ordinal)
{
"Note", "Article", "Page", "Question", "Video", "Audio", "Image", "Event", "Document", "Tombstone", "Person", "Group", "Service",
"Application", "Organization", "Create", "Update", "Delete", "Announce", "Like", "Dislike", "Follow", "Undo", "EmojiReact"
};
static readonly HashSet<string> HealthPurposes = new(StringComparer.Ordinal) { "actor", "key", "object" };
public static DayFold Fold(IEnumerable<InteractionEvent> events)
{
var day = new DayFold();
foreach (var e in events)
{
var host = e.Host ?? Interactions.Unknown;
if (!day.Hosts.TryGetValue(host, out var fold))
day.Hosts[host] = fold = new HostFold();
Count(fold, e);
if (e.ActorHash != default)
{
fold.Accounts.Add(e.ActorHash);
day.Accounts.Add(e.ActorHash);
}
}
return day;
}
static void Count(HostFold fold, InteractionEvent e)
{
var channel = e.Channel ?? Interactions.Other;
switch (channel)
{
case Interactions.Receive:
Add(fold.Counters, Key(channel, e.Activity, e.Status?.ToString(System.Globalization.CultureInfo.InvariantCulture), e.Reason));
if (e.Signature != default)
Add(fold.Counters, Key("sig", e.Signature));
break;
case Interactions.In or Interactions.Out:
Add(fold.Counters, Key(channel, e.Activity, e.Object, e.Outcome, e.Reason));
if (e.Audience != default)
Add(fold.Counters, Key("aud", channel, e.Audience));
if (e.LocalKind != default)
Add(fold.Counters, Key("kind", channel, e.LocalKind));
PublicActivity(fold, e, channel);
break;
case Interactions.Http:
Add(fold.Counters, Key(channel, e.Purpose, e.Outcome, e.Reason));
break;
default:
Add(fold.Counters, Key(channel, e.Outcome, e.Reason));
break;
}
if (Health(e) is { } health)
Add(fold.PublicCounters, Key("health", health));
foreach (var feature in e.Features ?? new List<string>())
{
Add(fold.Counters, Key("feat", feature));
if (e.Audience == Interactions.Public)
Add(fold.PublicCounters, Key("feat", feature));
}
var timing = channel == Interactions.Http ? Key(channel, e.Purpose) : channel;
if (e.LatencyMs is { } latency)
Add(fold.Latency, Key(timing, Interactions.Latency(latency)));
if (e.WaitMs is { } wait)
Add(fold.Waits, Key(channel, Interactions.Wait(wait)));
if (e.Bytes is { } bytes && bytes > 0)
Add(fold.Bytes, timing, bytes);
}
static void PublicActivity(HostFold fold, InteractionEvent e, string channel)
{
if (e.Audience is not (Interactions.Public or Interactions.Unlisted or Interactions.None)
|| e.Activity == default || !PublicActivities.Contains(e.Activity))
return;
var outcome = channel == Interactions.Out
? e.Outcome == Interactions.Ok ? "delivered" : e.Outcome is Interactions.Retry or Interactions.Dead or Interactions.Failed ? "failed" : default
: e.Outcome == Interactions.Accepted ? "accepted" : e.Outcome is Interactions.Dropped or Interactions.Rejected ? "dropped" : default;
if (outcome == default)
return;
var type = e.Object == default ? Interactions.Unknown : PublicObjects.Contains(e.Object) ? e.Object : Interactions.Other;
Add(fold.PublicCounters, Key(channel, e.Activity, type, outcome));
}
static string Health(InteractionEvent e) => e.Channel switch
{
Interactions.Out when e.Outcome == Interactions.Ok || e.Outcome == Interactions.Dead && e.Status is >= 400 and < 500 => "ok",
Interactions.Out when e.Outcome is Interactions.Retry or Interactions.Failed
|| e.Outcome == Interactions.Dead && (e.Status >= 500 || e.Reason is "timeout" or "network") => "failed",
Interactions.Http when HealthPurposes.Contains(e.Purpose ?? string.Empty) =>
e.Outcome is Interactions.Ok or Interactions.Refused ? "ok" : e.Outcome == Interactions.Failed ? "failed" : default,
_ => default
};
public static string Key(params string[] segments)
{
var count = segments.Length;
while (count > 1 && segments[count - 1] == default)
count--;
return string.Join(':', segments.Take(count).Select(s => (s ?? Interactions.Unknown).Replace('.', '_').Replace('$', '_')));
}
static void Add(Dictionary<string, long> counters, string key, long by = 1) =>
counters[key] = counters.GetValueOrDefault(key) + by;
}
}