1
0

AdminLogManager.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. using System.Collections.Concurrent;
  2. using System.Text.Json;
  3. using System.Threading;
  4. using System.Threading.Tasks;
  5. using Content.Server.Database;
  6. using Content.Server.GameTicking;
  7. using Content.Shared.Administration.Logs;
  8. using Content.Shared.CCVar;
  9. using Content.Shared.Database;
  10. using Prometheus;
  11. using Robust.Shared;
  12. using Robust.Shared.Configuration;
  13. using Robust.Shared.Reflection;
  14. using Robust.Shared.Timing;
  15. namespace Content.Server.Administration.Logs;
  16. public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogManager
  17. {
  18. [Dependency] private readonly IConfigurationManager _configuration = default!;
  19. [Dependency] private readonly IEntityManager _entityManager = default!;
  20. [Dependency] private readonly ILogManager _logManager = default!;
  21. [Dependency] private readonly IServerDbManager _db = default!;
  22. [Dependency] private readonly IGameTiming _timing = default!;
  23. [Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
  24. [Dependency] private readonly IReflectionManager _reflection = default!;
  25. [Dependency] private readonly IDependencyCollection _dependencies = default!;
  26. public const string SawmillId = "admin.logs";
  27. private static readonly Histogram DatabaseUpdateTime = Metrics.CreateHistogram(
  28. "admin_logs_database_time",
  29. "Time used to send logs to the database in ms",
  30. new HistogramConfiguration
  31. {
  32. Buckets = Histogram.LinearBuckets(0, 0.5, 20)
  33. });
  34. private static readonly Gauge Queue = Metrics.CreateGauge(
  35. "admin_logs_queue",
  36. "How many logs are in the queue.");
  37. private static readonly Gauge PreRoundQueue = Metrics.CreateGauge(
  38. "admin_logs_pre_round_queue",
  39. "How many logs are in the pre-round queue.");
  40. private static readonly Gauge QueueCapReached = Metrics.CreateGauge(
  41. "admin_logs_queue_cap_reached",
  42. "Number of times the log queue cap has been reached in a round.");
  43. private static readonly Gauge PreRoundQueueCapReached = Metrics.CreateGauge(
  44. "admin_logs_queue_cap_reached",
  45. "Number of times the pre-round log queue cap has been reached in a round.");
  46. private static readonly Gauge LogsSent = Metrics.CreateGauge(
  47. "admin_logs_sent",
  48. "Amount of logs sent to the database in a round.");
  49. // Init only
  50. private ISawmill _sawmill = default!;
  51. // CVars
  52. private bool _metricsEnabled;
  53. private bool _enabled;
  54. private TimeSpan _queueSendDelay;
  55. private int _queueMax;
  56. private int _preRoundQueueMax;
  57. private int _dropThreshold;
  58. // Per update
  59. private TimeSpan _nextUpdateTime;
  60. private readonly ConcurrentQueue<AdminLog> _logQueue = new();
  61. private readonly ConcurrentQueue<AdminLog> _preRoundLogQueue = new();
  62. // Per round
  63. private int _currentRoundId;
  64. private int _currentLogId;
  65. private int NextLogId => Interlocked.Increment(ref _currentLogId);
  66. private GameRunLevel _runLevel = GameRunLevel.PreRoundLobby;
  67. // 1 when saving, 0 otherwise
  68. private int _savingLogs;
  69. private int _logsDropped;
  70. public void Initialize()
  71. {
  72. _sawmill = _logManager.GetSawmill(SawmillId);
  73. InitializeJson();
  74. _configuration.OnValueChanged(CVars.MetricsEnabled,
  75. value => _metricsEnabled = value, true);
  76. _configuration.OnValueChanged(CCVars.AdminLogsEnabled,
  77. value => _enabled = value, true);
  78. _configuration.OnValueChanged(CCVars.AdminLogsQueueSendDelay,
  79. value => _queueSendDelay = TimeSpan.FromSeconds(value), true);
  80. _configuration.OnValueChanged(CCVars.AdminLogsQueueMax,
  81. value => _queueMax = value, true);
  82. _configuration.OnValueChanged(CCVars.AdminLogsPreRoundQueueMax,
  83. value => _preRoundQueueMax = value, true);
  84. _configuration.OnValueChanged(CCVars.AdminLogsDropThreshold,
  85. value => _dropThreshold = value, true);
  86. if (_metricsEnabled)
  87. {
  88. PreRoundQueueCapReached.Set(0);
  89. QueueCapReached.Set(0);
  90. LogsSent.Set(0);
  91. }
  92. }
  93. public async Task Shutdown()
  94. {
  95. if (!_logQueue.IsEmpty)
  96. {
  97. await SaveLogs();
  98. }
  99. }
  100. public async void Update()
  101. {
  102. if (_runLevel == GameRunLevel.PreRoundLobby)
  103. {
  104. await PreRoundUpdate();
  105. return;
  106. }
  107. var count = _logQueue.Count;
  108. Queue.Set(count);
  109. var preRoundCount = _preRoundLogQueue.Count;
  110. PreRoundQueue.Set(preRoundCount);
  111. if (count + preRoundCount == 0)
  112. {
  113. return;
  114. }
  115. if (_timing.RealTime >= _nextUpdateTime)
  116. {
  117. await TrySaveLogs();
  118. return;
  119. }
  120. if (count >= _queueMax)
  121. {
  122. if (_metricsEnabled)
  123. {
  124. QueueCapReached.Inc();
  125. }
  126. await TrySaveLogs();
  127. }
  128. }
  129. private async Task PreRoundUpdate()
  130. {
  131. var preRoundCount = _preRoundLogQueue.Count;
  132. PreRoundQueue.Set(preRoundCount);
  133. if (preRoundCount < _preRoundQueueMax)
  134. {
  135. return;
  136. }
  137. if (_metricsEnabled)
  138. {
  139. PreRoundQueueCapReached.Inc();
  140. }
  141. await TrySaveLogs();
  142. }
  143. private async Task TrySaveLogs()
  144. {
  145. if (Interlocked.Exchange(ref _savingLogs, 1) == 1)
  146. return;
  147. try
  148. {
  149. await SaveLogs();
  150. }
  151. finally
  152. {
  153. Interlocked.Exchange(ref _savingLogs, 0);
  154. }
  155. }
  156. private async Task SaveLogs()
  157. {
  158. _nextUpdateTime = _timing.RealTime.Add(_queueSendDelay);
  159. // TODO ADMIN LOGS array pool
  160. var copy = new List<AdminLog>(_logQueue.Count + _preRoundLogQueue.Count);
  161. copy.AddRange(_logQueue);
  162. if (_logQueue.Count >= _queueMax)
  163. {
  164. _sawmill.Warning($"In-round cap of {_queueMax} reached for admin logs.");
  165. }
  166. var dropped = Interlocked.Exchange(ref _logsDropped, 0);
  167. if (dropped > 0)
  168. {
  169. _sawmill.Error($"Dropped {dropped} logs. Current max threshold: {_dropThreshold}");
  170. }
  171. if (_runLevel == GameRunLevel.PreRoundLobby && !_preRoundLogQueue.IsEmpty)
  172. {
  173. _sawmill.Error($"Dropping {_preRoundLogQueue.Count} pre-round logs. Current cap: {_preRoundQueueMax}");
  174. }
  175. else
  176. {
  177. foreach (var log in _preRoundLogQueue)
  178. {
  179. log.RoundId = _currentRoundId;
  180. CacheLog(log);
  181. }
  182. copy.AddRange(_preRoundLogQueue);
  183. }
  184. _logQueue.Clear();
  185. Queue.Set(0);
  186. _preRoundLogQueue.Clear();
  187. PreRoundQueue.Set(0);
  188. var task = _db.AddAdminLogs(copy);
  189. _sawmill.Debug($"Saving {copy.Count} admin logs.");
  190. if (_metricsEnabled)
  191. {
  192. LogsSent.Inc(copy.Count);
  193. using (DatabaseUpdateTime.NewTimer())
  194. {
  195. await task;
  196. return;
  197. }
  198. }
  199. await task;
  200. }
  201. public void RoundStarting(int id)
  202. {
  203. _currentRoundId = id;
  204. CacheNewRound();
  205. }
  206. public void RunLevelChanged(GameRunLevel level)
  207. {
  208. _runLevel = level;
  209. if (level == GameRunLevel.PreRoundLobby)
  210. {
  211. Interlocked.Exchange(ref _currentLogId, 0);
  212. if (!_preRoundLogQueue.IsEmpty)
  213. {
  214. // This technically means that you could get pre-round logs from
  215. // a previous round passed onto the next one
  216. // If this happens please file a complaint with your nearest lottery
  217. foreach (var log in _preRoundLogQueue)
  218. {
  219. log.Id = NextLogId;
  220. }
  221. }
  222. if (_metricsEnabled)
  223. {
  224. PreRoundQueueCapReached.Set(0);
  225. QueueCapReached.Set(0);
  226. LogsSent.Set(0);
  227. }
  228. }
  229. }
  230. private void Add(LogType type, LogImpact impact, string message, JsonDocument json, HashSet<Guid> players)
  231. {
  232. var preRound = _runLevel == GameRunLevel.PreRoundLobby;
  233. var count = preRound ? _preRoundLogQueue.Count : _logQueue.Count;
  234. if (count >= _dropThreshold)
  235. {
  236. Interlocked.Increment(ref _logsDropped);
  237. return;
  238. }
  239. var log = new AdminLog
  240. {
  241. Id = NextLogId,
  242. RoundId = _currentRoundId,
  243. Type = type,
  244. Impact = impact,
  245. Date = DateTime.UtcNow,
  246. Message = message,
  247. Json = json,
  248. Players = new List<AdminLogPlayer>(players.Count)
  249. };
  250. foreach (var id in players)
  251. {
  252. var player = new AdminLogPlayer
  253. {
  254. LogId = log.Id,
  255. PlayerUserId = id
  256. };
  257. log.Players.Add(player);
  258. }
  259. if (preRound)
  260. {
  261. _preRoundLogQueue.Enqueue(log);
  262. }
  263. else
  264. {
  265. _logQueue.Enqueue(log);
  266. CacheLog(log);
  267. }
  268. }
  269. public override void Add(LogType type, LogImpact impact, ref LogStringHandler handler)
  270. {
  271. if (!_enabled)
  272. {
  273. handler.ToStringAndClear();
  274. return;
  275. }
  276. var (json, players) = ToJson(handler.Values);
  277. var message = handler.ToStringAndClear();
  278. Add(type, impact, message, json, players);
  279. }
  280. public override void Add(LogType type, ref LogStringHandler handler)
  281. {
  282. Add(type, LogImpact.Medium, ref handler);
  283. }
  284. public async Task<List<SharedAdminLog>> All(LogFilter? filter = null, Func<List<SharedAdminLog>>? listProvider = null)
  285. {
  286. if (TrySearchCache(filter, out var results))
  287. {
  288. return results;
  289. }
  290. var initialSize = Math.Min(filter?.Limit ?? 0, 1000);
  291. List<SharedAdminLog> list;
  292. if (listProvider != null)
  293. {
  294. list = listProvider();
  295. list.EnsureCapacity(initialSize);
  296. }
  297. else
  298. {
  299. list = new List<SharedAdminLog>(initialSize);
  300. }
  301. await foreach (var log in _db.GetAdminLogs(filter).WithCancellation(filter?.CancellationToken ?? default))
  302. {
  303. list.Add(log);
  304. }
  305. return list;
  306. }
  307. public IAsyncEnumerable<string> AllMessages(LogFilter? filter = null)
  308. {
  309. return _db.GetAdminLogMessages(filter);
  310. }
  311. public IAsyncEnumerable<JsonDocument> AllJson(LogFilter? filter = null)
  312. {
  313. return _db.GetAdminLogsJson(filter);
  314. }
  315. public Task<Round> Round(int roundId)
  316. {
  317. return _db.GetRound(roundId);
  318. }
  319. public Task<List<SharedAdminLog>> CurrentRoundLogs(LogFilter? filter = null)
  320. {
  321. filter ??= new LogFilter();
  322. filter.Round = _currentRoundId;
  323. return All(filter);
  324. }
  325. public IAsyncEnumerable<string> CurrentRoundMessages(LogFilter? filter = null)
  326. {
  327. filter ??= new LogFilter();
  328. filter.Round = _currentRoundId;
  329. return AllMessages(filter);
  330. }
  331. public IAsyncEnumerable<JsonDocument> CurrentRoundJson(LogFilter? filter = null)
  332. {
  333. filter ??= new LogFilter();
  334. filter.Round = _currentRoundId;
  335. return AllJson(filter);
  336. }
  337. public Task<Round> CurrentRound()
  338. {
  339. return Round(_currentRoundId);
  340. }
  341. public Task<int> CountLogs(int round)
  342. {
  343. return _db.CountAdminLogs(round);
  344. }
  345. }