Make AdminLogsSystem an IoC manager (#8492)

* Make log not entity system

* Fixes
This commit is contained in:
wrexbe
2022-05-28 23:41:17 -07:00
committed by GitHub
parent 0f99a0dd1d
commit 1e0babbd50
79 changed files with 653 additions and 572 deletions

View File

@@ -7,7 +7,7 @@ using Prometheus;
namespace Content.Server.Administration.Logs;
public sealed partial class AdminLogSystem
public sealed partial class AdminLogManager
{
private const int MaxRoundsCached = 3;
private const int LogListInitialSize = 30_000;
@@ -26,10 +26,10 @@ public sealed partial class AdminLogSystem
"How many logs are in cache.");
// TODO ADMIN LOGS cache previous {MaxRoundsCached} rounds on startup
private void CacheNewRound()
public void CacheNewRound()
{
List<SharedAdminLog> list;
var oldestRound = CurrentRoundId - MaxRoundsCached;
var oldestRound = _currentRoundId - MaxRoundsCached;
if (_roundsLogCache.Remove(oldestRound, out var oldestList))
{
@@ -41,7 +41,7 @@ public sealed partial class AdminLogSystem
list = new List<SharedAdminLog>(LogListInitialSize);
}
_roundsLogCache.Add(CurrentRoundId, list);
_roundsLogCache.Add(_currentRoundId, list);
CacheRoundCount.Set(_roundsLogCache.Count);
}
@@ -61,14 +61,14 @@ public sealed partial class AdminLogSystem
private void CacheLog(SharedAdminLog log)
{
// TODO ADMIN LOGS remove redundant data and don't do a dictionary lookup per log
var cache = _roundsLogCache[CurrentRoundId];
var cache = _roundsLogCache[_currentRoundId];
cache.Add(log);
CacheLogCount.Set(cache.Count);
}
private void CacheLogs(IEnumerable<SharedAdminLog> logs)
{
var cache = _roundsLogCache[CurrentRoundId];
var cache = _roundsLogCache[_currentRoundId];
cache.AddRange(logs);
CacheLogCount.Set(cache.Count);
}

View File

@@ -7,7 +7,7 @@ using Robust.Server.Player;
namespace Content.Server.Administration.Logs;
public sealed partial class AdminLogSystem
public sealed partial class AdminLogManager
{
private static readonly JsonNamingPolicy NamingPolicy = JsonNamingPolicy.CamelCase;

View File

@@ -0,0 +1,360 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.GameTicking;
using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Prometheus;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
namespace Content.Server.Administration.Logs;
public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogManager
{
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
public const string SawmillId = "admin.logs";
private static readonly Histogram DatabaseUpdateTime = Metrics.CreateHistogram(
"admin_logs_database_time",
"Time used to send logs to the database in ms",
new HistogramConfiguration
{
Buckets = Histogram.LinearBuckets(0, 0.5, 20)
});
private static readonly Gauge Queue = Metrics.CreateGauge(
"admin_logs_queue",
"How many logs are in the queue.");
private static readonly Gauge PreRoundQueue = Metrics.CreateGauge(
"admin_logs_pre_round_queue",
"How many logs are in the pre-round queue.");
private static readonly Gauge QueueCapReached = Metrics.CreateGauge(
"admin_logs_queue_cap_reached",
"Number of times the log queue cap has been reached in a round.");
private static readonly Gauge PreRoundQueueCapReached = Metrics.CreateGauge(
"admin_logs_queue_cap_reached",
"Number of times the pre-round log queue cap has been reached in a round.");
private static readonly Gauge LogsSent = Metrics.CreateGauge(
"admin_logs_sent",
"Amount of logs sent to the database in a round.");
// Init only
private ISawmill _sawmill = default!;
// CVars
private bool _metricsEnabled;
private bool _enabled;
private TimeSpan _queueSendDelay;
private int _queueMax;
private int _preRoundQueueMax;
// Per update
private TimeSpan _nextUpdateTime;
private readonly ConcurrentQueue<QueuedLog> _logQueue = new();
private readonly ConcurrentQueue<QueuedLog> _preRoundLogQueue = new();
// Per round
private int _currentRoundId;
private int _currentLogId;
private int NextLogId => Interlocked.Increment(ref _currentLogId);
private GameRunLevel _runLevel = GameRunLevel.PreRoundLobby;
public void Initialize()
{
_sawmill = _logManager.GetSawmill(SawmillId);
InitializeJson();
_configuration.OnValueChanged(CVars.MetricsEnabled,
value => _metricsEnabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsEnabled,
value => _enabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueSendDelay,
value => _queueSendDelay = TimeSpan.FromSeconds(value), true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueMax,
value => _queueMax = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsPreRoundQueueMax,
value => _preRoundQueueMax = value, true);
if (_metricsEnabled)
{
PreRoundQueueCapReached.Set(0);
QueueCapReached.Set(0);
LogsSent.Set(0);
}
}
public async Task Shutdown()
{
if (!_logQueue.IsEmpty)
{
await SaveLogs();
}
}
public async void Update()
{
if (_runLevel == GameRunLevel.PreRoundLobby)
{
await PreRoundUpdate();
return;
}
var count = _logQueue.Count;
Queue.Set(count);
var preRoundCount = _preRoundLogQueue.Count;
PreRoundQueue.Set(preRoundCount);
if (count + preRoundCount == 0)
{
return;
}
if (_timing.RealTime >= _nextUpdateTime)
{
await SaveLogs();
return;
}
if (count >= _queueMax)
{
if (_metricsEnabled)
{
QueueCapReached.Inc();
}
_sawmill.Warning($"Maximum cap of {_queueMax} reached for admin logs.");
await SaveLogs();
}
}
private async Task PreRoundUpdate()
{
var preRoundCount = _preRoundLogQueue.Count;
PreRoundQueue.Set(preRoundCount);
if (preRoundCount < _preRoundQueueMax)
{
return;
}
if (_metricsEnabled)
{
PreRoundQueueCapReached.Inc();
}
_sawmill.Warning($"Maximum cap of {_preRoundQueueMax} reached for pre-round admin logs.");
await SaveLogs();
}
private async Task SaveLogs()
{
_nextUpdateTime = _timing.RealTime.Add(_queueSendDelay);
// TODO ADMIN LOGS array pool
var copy = new List<QueuedLog>(_logQueue.Count + _preRoundLogQueue.Count);
copy.AddRange(_logQueue);
_logQueue.Clear();
Queue.Set(0);
if (_runLevel == GameRunLevel.PreRoundLobby && !_preRoundLogQueue.IsEmpty)
{
_sawmill.Error($"Dropping {_preRoundLogQueue.Count} pre-round logs. Current cap: {_preRoundQueueMax}");
}
else
{
foreach (var queued in _preRoundLogQueue)
{
queued.Log.RoundId = _currentRoundId;
CacheLog(queued);
}
copy.AddRange(_preRoundLogQueue);
}
_preRoundLogQueue.Clear();
PreRoundQueue.Set(0);
// ship the logs to Azkaban
var task = Task.Run(async () =>
{
await _db.AddAdminLogs(copy);
});
_sawmill.Debug($"Saving {copy.Count} admin logs.");
if (_metricsEnabled)
{
LogsSent.Inc(copy.Count);
using (DatabaseUpdateTime.NewTimer())
{
await task;
return;
}
}
await task;
}
public void RoundStarting(int id)
{
_currentRoundId = id;
CacheNewRound();
}
public void RunLevelChanged(GameRunLevel level)
{
_runLevel = level;
if (level == GameRunLevel.PreRoundLobby)
{
Interlocked.Exchange(ref _currentLogId, 0);
if (_metricsEnabled)
{
PreRoundQueueCapReached.Set(0);
QueueCapReached.Set(0);
LogsSent.Set(0);
}
}
}
private async void Add(LogType type, LogImpact impact, string message, JsonDocument json, HashSet<Guid> players, Dictionary<int, string?> entities)
{
var logId = NextLogId;
var date = DateTime.UtcNow;
var log = new AdminLog
{
Id = logId,
RoundId = _currentRoundId,
Type = type,
Impact = impact,
Date = date,
Message = message,
Json = json,
Players = new List<AdminLogPlayer>(players.Count)
};
var queued = new QueuedLog(log, entities);
foreach (var id in players)
{
var player = new AdminLogPlayer
{
LogId = logId,
PlayerUserId = id
};
log.Players.Add(player);
}
if (_runLevel == GameRunLevel.PreRoundLobby)
{
_preRoundLogQueue.Enqueue(queued);
}
else
{
_logQueue.Enqueue(queued);
CacheLog(log);
}
}
public override void Add(LogType type, LogImpact impact, ref LogStringHandler handler)
{
if (!_enabled)
{
handler.ToStringAndClear();
return;
}
var (json, players, entities) = ToJson(handler.Values);
var message = handler.ToStringAndClear();
Add(type, impact, message, json, players, entities);
}
public override void Add(LogType type, ref LogStringHandler handler)
{
Add(type, LogImpact.Medium, ref handler);
}
public async Task<List<SharedAdminLog>> All(LogFilter? filter = null)
{
if (TrySearchCache(filter, out var results))
{
return results;
}
var initialSize = Math.Min(filter?.Limit ?? 0, 1000);
var list = new List<SharedAdminLog>(initialSize);
await foreach (var log in _db.GetAdminLogs(filter).WithCancellation(filter?.CancellationToken ?? default))
{
list.Add(log);
}
return list;
}
public IAsyncEnumerable<string> AllMessages(LogFilter? filter = null)
{
return _db.GetAdminLogMessages(filter);
}
public IAsyncEnumerable<JsonDocument> AllJson(LogFilter? filter = null)
{
return _db.GetAdminLogsJson(filter);
}
public Task<Round> Round(int roundId)
{
return _db.GetRound(roundId);
}
public Task<List<SharedAdminLog>> CurrentRoundLogs(LogFilter? filter = null)
{
filter ??= new LogFilter();
filter.Round = _currentRoundId;
return All(filter);
}
public IAsyncEnumerable<string> CurrentRoundMessages(LogFilter? filter = null)
{
filter ??= new LogFilter();
filter.Round = _currentRoundId;
return AllMessages(filter);
}
public IAsyncEnumerable<JsonDocument> CurrentRoundJson(LogFilter? filter = null)
{
filter ??= new LogFilter();
filter.Round = _currentRoundId;
return AllJson(filter);
}
public Task<Round> CurrentRound()
{
return Round(_currentRoundId);
}
}

View File

@@ -1,367 +1,31 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.GameTicking;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Events;
using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Prometheus;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Reflection;
namespace Content.Server.Administration.Logs;
public sealed partial class AdminLogSystem : SharedAdminLogSystem
/// <summary>
/// For system events that the manager needs to know about.
/// <see cref="IAdminLogManager"/> for admin log usage.
/// </summary>
public sealed class AdminLogSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
public const string SawmillId = "admin.logs";
private static readonly Histogram DatabaseUpdateTime = Metrics.CreateHistogram(
"admin_logs_database_time",
"Time used to send logs to the database in ms",
new HistogramConfiguration
{
Buckets = Histogram.LinearBuckets(0, 0.5, 20)
});
private static readonly Gauge Queue = Metrics.CreateGauge(
"admin_logs_queue",
"How many logs are in the queue.");
private static readonly Gauge PreRoundQueue = Metrics.CreateGauge(
"admin_logs_pre_round_queue",
"How many logs are in the pre-round queue.");
private static readonly Gauge QueueCapReached = Metrics.CreateGauge(
"admin_logs_queue_cap_reached",
"Number of times the log queue cap has been reached in a round.");
private static readonly Gauge PreRoundQueueCapReached = Metrics.CreateGauge(
"admin_logs_queue_cap_reached",
"Number of times the pre-round log queue cap has been reached in a round.");
private static readonly Gauge LogsSent = Metrics.CreateGauge(
"admin_logs_sent",
"Amount of logs sent to the database in a round.");
// Init only
private ISawmill _sawmill = default!;
// CVars
private bool _metricsEnabled;
private bool _enabled;
private TimeSpan _queueSendDelay;
private int _queueMax;
private int _preRoundQueueMax;
// Per update
private float _accumulatedFrameTime;
private readonly ConcurrentQueue<QueuedLog> _logQueue = new();
private readonly ConcurrentQueue<QueuedLog> _preRoundLogQueue = new();
// Per round
private int CurrentRoundId => _gameTicker.RoundId;
private int _currentLogId;
private int NextLogId => Interlocked.Increment(ref _currentLogId);
[Dependency] private readonly IAdminLogManager _adminLogs = default!;
public override void Initialize()
{
base.Initialize();
_sawmill = _logManager.GetSawmill(SawmillId);
InitializeJson();
_configuration.OnValueChanged(CVars.MetricsEnabled,
value => _metricsEnabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsEnabled,
value => _enabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueSendDelay,
value => _queueSendDelay = TimeSpan.FromSeconds(value), true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueMax,
value => _queueMax = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsPreRoundQueueMax,
value => _preRoundQueueMax = value, true);
if (_metricsEnabled)
{
PreRoundQueueCapReached.Set(0);
QueueCapReached.Set(0);
LogsSent.Set(0);
}
SubscribeLocalEvent<RoundStartingEvent>(RoundStarting);
SubscribeLocalEvent<GameRunLevelChangedEvent>(RunLevelChanged);
SubscribeLocalEvent<RoundStartingEvent>(ev => _adminLogs.RoundStarting(ev.Id));
SubscribeLocalEvent<GameRunLevelChangedEvent>(ev => _adminLogs.RunLevelChanged(ev.New));
}
public override async void Shutdown()
public override void Update(float frameTime)
{
base.Shutdown();
if (!_logQueue.IsEmpty)
{
await SaveLogs();
}
_adminLogs.Update();
}
public override async void Update(float frameTime)
public override void Shutdown()
{
if (_gameTicker.RunLevel == GameRunLevel.PreRoundLobby)
{
await PreRoundUpdate();
return;
}
var count = _logQueue.Count;
Queue.Set(count);
var preRoundCount = _preRoundLogQueue.Count;
PreRoundQueue.Set(preRoundCount);
if (count + preRoundCount == 0)
{
return;
}
if (_accumulatedFrameTime >= _queueSendDelay.TotalSeconds)
{
await SaveLogs();
return;
}
if (count >= _queueMax)
{
if (_metricsEnabled)
{
QueueCapReached.Inc();
}
_sawmill.Warning($"Maximum cap of {_queueMax} reached for admin logs.");
await SaveLogs();
return;
}
_accumulatedFrameTime += frameTime;
}
private async Task PreRoundUpdate()
{
var preRoundCount = _preRoundLogQueue.Count;
PreRoundQueue.Set(preRoundCount);
if (preRoundCount < _preRoundQueueMax)
{
return;
}
if (_metricsEnabled)
{
PreRoundQueueCapReached.Inc();
}
_sawmill.Warning($"Maximum cap of {_preRoundQueueMax} reached for pre-round admin logs.");
await SaveLogs();
}
private async Task SaveLogs()
{
_accumulatedFrameTime = 0f;
// TODO ADMIN LOGS array pool
var copy = new List<QueuedLog>(_logQueue.Count + _preRoundLogQueue.Count);
copy.AddRange(_logQueue);
_logQueue.Clear();
Queue.Set(0);
if (_gameTicker.RunLevel == GameRunLevel.PreRoundLobby)
{
_sawmill.Error($"Dropping {_preRoundLogQueue.Count} pre-round logs. Current cap: {_preRoundQueueMax}");
}
else
{
foreach (var queued in _preRoundLogQueue)
{
queued.Log.RoundId = _gameTicker.RoundId;
CacheLog(queued);
}
copy.AddRange(_preRoundLogQueue);
}
_preRoundLogQueue.Clear();
PreRoundQueue.Set(0);
// ship the logs to Azkaban
var task = Task.Run(async () =>
{
await _db.AddAdminLogs(copy);
});
_sawmill.Debug($"Saving {copy.Count} admin logs.");
if (_metricsEnabled)
{
LogsSent.Inc(copy.Count);
using (DatabaseUpdateTime.NewTimer())
{
await task;
return;
}
}
await task;
}
private void RoundStarting(RoundStartingEvent ev)
{
CacheNewRound();
}
private void RunLevelChanged(GameRunLevelChangedEvent ev)
{
if (ev.New == GameRunLevel.PreRoundLobby)
{
Interlocked.Exchange(ref _currentLogId, 0);
if (_metricsEnabled)
{
PreRoundQueueCapReached.Set(0);
QueueCapReached.Set(0);
LogsSent.Set(0);
}
}
}
private async void Add(LogType type, LogImpact impact, string message, JsonDocument json, HashSet<Guid> players, Dictionary<int, string?> entities)
{
var logId = NextLogId;
var date = DateTime.UtcNow;
var log = new AdminLog
{
Id = logId,
RoundId = CurrentRoundId,
Type = type,
Impact = impact,
Date = date,
Message = message,
Json = json,
Players = new List<AdminLogPlayer>(players.Count)
};
var queued = new QueuedLog(log, entities);
foreach (var id in players)
{
var player = new AdminLogPlayer
{
LogId = logId,
PlayerUserId = id
};
log.Players.Add(player);
}
if (_gameTicker.RunLevel == GameRunLevel.PreRoundLobby)
{
_preRoundLogQueue.Enqueue(queued);
}
else
{
_logQueue.Enqueue(queued);
CacheLog(log);
}
}
public override void Add(LogType type, LogImpact impact, ref LogStringHandler handler)
{
if (!_enabled)
{
handler.ToStringAndClear();
return;
}
var (json, players, entities) = ToJson(handler.Values);
var message = handler.ToStringAndClear();
Add(type, impact, message, json, players, entities);
}
public override void Add(LogType type, ref LogStringHandler handler)
{
Add(type, LogImpact.Medium, ref handler);
}
public async Task<List<SharedAdminLog>> All(LogFilter? filter = null)
{
if (TrySearchCache(filter, out var results))
{
return results;
}
var initialSize = Math.Min(filter?.Limit ?? 0, 1000);
var list = new List<SharedAdminLog>(initialSize);
await foreach (var log in _db.GetAdminLogs(filter).WithCancellation(filter?.CancellationToken ?? default))
{
list.Add(log);
}
return list;
}
public IAsyncEnumerable<string> AllMessages(LogFilter? filter = null)
{
return _db.GetAdminLogMessages(filter);
}
public IAsyncEnumerable<JsonDocument> AllJson(LogFilter? filter = null)
{
return _db.GetAdminLogsJson(filter);
}
public Task<Round> Round(int roundId)
{
return _db.GetRound(roundId);
}
public Task<List<SharedAdminLog>> CurrentRoundLogs(LogFilter? filter = null)
{
filter ??= new LogFilter();
filter.Round = CurrentRoundId;
return All(filter);
}
public IAsyncEnumerable<string> CurrentRoundMessages(LogFilter? filter = null)
{
filter ??= new LogFilter();
filter.Round = CurrentRoundId;
return AllMessages(filter);
}
public IAsyncEnumerable<JsonDocument> CurrentRoundJson(LogFilter? filter = null)
{
filter ??= new LogFilter();
filter.Round = CurrentRoundId;
return AllJson(filter);
}
public Task<Round> CurrentRound()
{
return Round(CurrentRoundId);
_adminLogs.Shutdown();
}
}

View File

@@ -16,12 +16,12 @@ namespace Content.Server.Administration.Logs;
public sealed class AdminLogsEui : BaseEui
{
[Dependency] private readonly IAdminLogManager _adminLogs = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
private readonly ISawmill _sawmill;
private readonly AdminLogSystem _logSystem;
private int _clientBatchSize;
private bool _isLoading = true;
@@ -33,12 +33,10 @@ public sealed class AdminLogsEui : BaseEui
{
IoCManager.InjectDependencies(this);
_sawmill = _logManager.GetSawmill(AdminLogSystem.SawmillId);
_sawmill = _logManager.GetSawmill(AdminLogManager.SawmillId);
_configuration.OnValueChanged(CCVars.AdminLogsClientBatchSize, ClientBatchSizeChanged, true);
_logSystem = EntitySystem.Get<AdminLogSystem>();
_filter = new LogFilter
{
CancellationToken = _logSendCancellation.Token,
@@ -146,7 +144,7 @@ public sealed class AdminLogsEui : BaseEui
await Task.Run(async () =>
{
logs = await _logSystem.All(_filter);
logs = await _adminLogs.All(_filter);
}, _filter.CancellationToken);
if (logs.Count > 0)
@@ -186,7 +184,7 @@ public sealed class AdminLogsEui : BaseEui
_isLoading = true;
StateDirty();
var round = await Task.Run(() => _logSystem.Round(roundId));
var round = await Task.Run(() => _adminLogs.Round(roundId));
var players = round.Players
.ToDictionary(player => player.UserId, player => player.LastSeenUserName);

View File

@@ -0,0 +1,26 @@
using System.Text.Json;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.GameTicking;
using Content.Shared.Administration.Logs;
namespace Content.Server.Administration.Logs;
public interface IAdminLogManager : ISharedAdminLogManager
{
void Initialize();
Task Shutdown();
void Update();
void RoundStarting(int id);
void RunLevelChanged(GameRunLevel level);
Task<List<SharedAdminLog>> All(LogFilter? filter = null);
IAsyncEnumerable<string> AllMessages(LogFilter? filter = null);
IAsyncEnumerable<JsonDocument> AllJson(LogFilter? filter = null);
Task<Round> Round(int roundId);
Task<List<SharedAdminLog>> CurrentRoundLogs(LogFilter? filter = null);
IAsyncEnumerable<string> CurrentRoundMessages(LogFilter? filter = null);
IAsyncEnumerable<JsonDocument> CurrentRoundJson(LogFilter? filter = null);
Task<Round> CurrentRound();
}