diff --git a/Content.Server/Administration/Managers/AdminManager.Metrics.cs b/Content.Server/Administration/Managers/AdminManager.Metrics.cs new file mode 100644 index 00000000000..2fea931f1b9 --- /dev/null +++ b/Content.Server/Administration/Managers/AdminManager.Metrics.cs @@ -0,0 +1,98 @@ +using System.Diagnostics.Metrics; +using System.Runtime.InteropServices; +using Content.Server.Afk; +using Robust.Server.DataMetrics; + +namespace Content.Server.Administration.Managers; + +// Handles metrics reporting for active admin count and such. + +public sealed partial class AdminManager +{ + private Dictionary? _adminOnlineCounts; + + private const int SentinelRankId = -1; + + [Dependency] private readonly IMetricsManager _metrics = default!; + [Dependency] private readonly IAfkManager _afkManager = default!; + [Dependency] private readonly IMeterFactory _meterFactory = default!; + + private void InitializeMetrics() + { + _metrics.UpdateMetrics += MetricsOnUpdateMetrics; + + var meter = _meterFactory.Create("SS14.AdminManager"); + + meter.CreateObservableGauge( + "admins_online_count", + MeasureAdminCount, + null, + "The count of online admins"); + } + + private void MetricsOnUpdateMetrics() + { + _sawmill.Verbose("Updating metrics"); + + var dict = new Dictionary(); + + foreach (var (session, reg) in _admins) + { + var rankId = reg.RankId ?? SentinelRankId; + + ref var counts = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, rankId, out _); + + if (reg.Data.Active) + { + if (_afkManager.IsAfk(session)) + counts.afk += 1; + else + counts.active += 1; + } + else + { + counts.deadminned += 1; + } + } + + // Neither prometheus-net nor dotnet-counters seem to handle stuff well if we STOP returning measurements. + // i.e. if the last admin with a rank disconnects. + // So if we have EVER reported a rank, always keep reporting it. + if (_adminOnlineCounts != null) + { + foreach (var rank in _adminOnlineCounts.Keys) + { + CollectionsMarshal.GetValueRefOrAddDefault(dict, rank, out _); + } + } + + // Make sure "no rank" is always available. Avoid "no data". + CollectionsMarshal.GetValueRefOrAddDefault(dict, SentinelRankId, out _); + + _adminOnlineCounts = dict; + } + + private IEnumerable> MeasureAdminCount() + { + if (_adminOnlineCounts == null) + yield break; + + foreach (var (rank, (active, afk, deadminned)) in _adminOnlineCounts) + { + yield return new Measurement( + active, + new KeyValuePair("state", "active"), + new KeyValuePair("rank", rank == SentinelRankId ? "none" : rank.ToString())); + + yield return new Measurement( + afk, + new KeyValuePair("state", "afk"), + new KeyValuePair("rank", rank == SentinelRankId ? "none" : rank.ToString())); + + yield return new Measurement( + deadminned, + new KeyValuePair("state", "deadminned"), + new KeyValuePair("rank", rank == SentinelRankId ? "none" : rank.ToString())); + } + } +} diff --git a/Content.Server/Administration/Managers/AdminManager.cs b/Content.Server/Administration/Managers/AdminManager.cs index 4eaa08fe9dd..b1cca46e63f 100644 --- a/Content.Server/Administration/Managers/AdminManager.cs +++ b/Content.Server/Administration/Managers/AdminManager.cs @@ -23,7 +23,7 @@ namespace Content.Server.Administration.Managers { - public sealed class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation + public sealed partial class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IServerDbManager _dbManager = default!; @@ -34,6 +34,7 @@ public sealed class AdminManager : IAdminManager, IPostInjectInit, IConGroupCont [Dependency] private readonly IServerConsoleHost _consoleHost = default!; [Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly ToolshedManager _toolshed = default!; + [Dependency] private readonly ILogManager _logManager = default!; private readonly Dictionary _admins = new(); private readonly HashSet _promotedPlayers = new(); @@ -49,6 +50,8 @@ public sealed class AdminManager : IAdminManager, IPostInjectInit, IConGroupCont private readonly AdminCommandPermissions _commandPermissions = new(); private readonly AdminCommandPermissions _toolshedCommandPermissions = new(); + private ISawmill _sawmill = default!; + public bool IsAdmin(ICommonSession session, bool includeDeAdmin = false) { return GetAdminData(session, includeDeAdmin) != null; @@ -181,6 +184,8 @@ public void ReloadAdminsWithRank(int rankId) public void Initialize() { + _sawmill = _logManager.GetSawmill("admin"); + _netMgr.RegisterNetMessage(); // Cache permissions for loaded console commands with the requisite attributes. @@ -234,6 +239,8 @@ public void Initialize() } _toolshed.ActivePermissionController = this; + + InitializeMetrics(); } public void PromoteHost(ICommonSession player)