mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: add periodic server stats reporting and logging
- Introduced ServerStatsReport and ServerStatsCollector for tracking server activity. - Implemented ServerStatsReportService to generate and persist stats reports periodically. - Added configuration options for stats reporting in appsettings and .env.example. - Enhanced logging in ChannelsController and ModerationController to include stats-related actions. - Updated database context and migrations to support new ServerStatsReport entity. - Adjusted logging levels in IrcGatewayService and ChatService for better performance.
This commit is contained in:
@@ -29,6 +29,7 @@ public class ChannelsController : ControllerBase
|
||||
private readonly IChatService _chatService;
|
||||
private readonly IMessageEncryptionService _encryption;
|
||||
private readonly UploadLimits _uploadLimits;
|
||||
private readonly ILogger<ChannelsController> _logger;
|
||||
|
||||
public ChannelsController(
|
||||
IChannelService channelService,
|
||||
@@ -38,7 +39,8 @@ public class ChannelsController : ControllerBase
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IChatService chatService,
|
||||
IMessageEncryptionService encryption,
|
||||
UploadLimits uploadLimits)
|
||||
UploadLimits uploadLimits,
|
||||
ILogger<ChannelsController> logger)
|
||||
{
|
||||
_channelService = channelService;
|
||||
_db = db;
|
||||
@@ -48,6 +50,7 @@ public class ChannelsController : ControllerBase
|
||||
_chatService = chatService;
|
||||
_encryption = encryption;
|
||||
_uploadLimits = uploadLimits;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -289,6 +292,12 @@ public class ChannelsController : ControllerBase
|
||||
});
|
||||
attachmentDtos.Add(new AttachmentDto(kind, url, file.FileName, file.Length,
|
||||
_encryption.EncryptNullable(previewPlain)));
|
||||
|
||||
// Filename is client-encrypted ciphertext in E2E rooms — never log it there.
|
||||
var loggedName = channelDto.IsEncrypted ? "[encrypted]" : file.FileName;
|
||||
_logger.LogInformation(
|
||||
"{User} uploaded {Kind} '{FileName}' ({Size} bytes) to '{Channel}': {Url}",
|
||||
usernameClaim, kind, loggedName, file.Length, channelName, url);
|
||||
}
|
||||
|
||||
var dbContent = _encryption.EncryptDatabaseEnabled ? _encryption.Encrypt(content) : content;
|
||||
@@ -446,6 +455,10 @@ public class ChannelsController : ControllerBase
|
||||
_db.Messages.Add(message);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation(
|
||||
"{User} uploaded Image '{FileName}' ({Size} bytes) from URL to '{Channel}': {Url} (source: {Source})",
|
||||
usernameClaim, fileName, imageBytes.Length, channelName, attachmentUrl, request.Url);
|
||||
|
||||
var messageDto = new MessageDto(
|
||||
message.Id,
|
||||
_encryption.Encrypt(string.Empty),
|
||||
|
||||
@@ -4,6 +4,7 @@ using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
using EchoHub.Server.Data;
|
||||
using EchoHub.Server.Services;
|
||||
using EchoHub.Server.Services.Stats;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
@@ -22,19 +23,25 @@ public class ModerationController : ControllerBase
|
||||
private readonly PresenceTracker _presenceTracker;
|
||||
private readonly FileStorageService _fileStorage;
|
||||
private readonly IEnumerable<IChatBroadcaster> _broadcasters;
|
||||
private readonly ServerStatsCollector _statsCollector;
|
||||
private readonly ILogger<ModerationController> _logger;
|
||||
|
||||
public ModerationController(
|
||||
EchoHubDbContext db,
|
||||
IChatService chatService,
|
||||
PresenceTracker presenceTracker,
|
||||
FileStorageService fileStorage,
|
||||
IEnumerable<IChatBroadcaster> broadcasters)
|
||||
IEnumerable<IChatBroadcaster> broadcasters,
|
||||
ServerStatsCollector statsCollector,
|
||||
ILogger<ModerationController> logger)
|
||||
{
|
||||
_db = db;
|
||||
_chatService = chatService;
|
||||
_presenceTracker = presenceTracker;
|
||||
_fileStorage = fileStorage;
|
||||
_broadcasters = broadcasters;
|
||||
_statsCollector = statsCollector;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("role")]
|
||||
@@ -56,9 +63,14 @@ public class ModerationController : ControllerBase
|
||||
if (request.Role >= caller!.Role)
|
||||
return BadRequest(new ErrorResponse("Cannot assign a role equal to or above your own."));
|
||||
|
||||
var previousRole = target.Role;
|
||||
target.Role = request.Role;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Role change: {Actor} set {Target} from {OldRole} to {NewRole}",
|
||||
caller!.Username, target.Username, previousRole, request.Role);
|
||||
|
||||
return Ok(new { Message = $"{target.Username} is now {request.Role}." });
|
||||
}
|
||||
|
||||
@@ -86,6 +98,11 @@ public class ModerationController : ControllerBase
|
||||
var reason = request?.Reason ?? "You have been kicked from the server.";
|
||||
await ForceDisconnectAndCleanupAsync(target.Username, reason);
|
||||
|
||||
_statsCollector.RecordKick();
|
||||
_logger.LogInformation(
|
||||
"Kick: {Actor} kicked {Target} (reason: {Reason})",
|
||||
caller!.Username, target.Username, request?.Reason ?? "none");
|
||||
|
||||
return Ok(new { Message = $"{target.Username} has been kicked." });
|
||||
}
|
||||
|
||||
@@ -111,13 +128,18 @@ public class ModerationController : ControllerBase
|
||||
var reason = request?.Reason ?? "You have been banned from this server.";
|
||||
await ForceDisconnectAndCleanupAsync(target.Username, reason);
|
||||
|
||||
_statsCollector.RecordBan();
|
||||
_logger.LogWarning(
|
||||
"Ban: {Actor} banned {Target} (reason: {Reason})",
|
||||
caller!.Username, target.Username, request?.Reason ?? "none");
|
||||
|
||||
return Ok(new { Message = $"{target.Username} has been banned." });
|
||||
}
|
||||
|
||||
[HttpPost("unban/{username}")]
|
||||
public async Task<IActionResult> UnbanUser(string username)
|
||||
{
|
||||
var (_, error) = await GetCallerAsync(ServerRole.Admin);
|
||||
var (caller, error) = await GetCallerAsync(ServerRole.Admin);
|
||||
if (error is not null) return error;
|
||||
|
||||
var target = await _db.Users.FirstOrDefaultAsync(u => u.Username == username.ToLowerInvariant());
|
||||
@@ -127,6 +149,8 @@ public class ModerationController : ControllerBase
|
||||
target.IsBanned = false;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Unban: {Actor} unbanned {Target}", caller!.Username, target.Username);
|
||||
|
||||
return Ok(new { Message = $"{target.Username} has been unbanned." });
|
||||
}
|
||||
|
||||
@@ -149,6 +173,12 @@ public class ModerationController : ControllerBase
|
||||
: null;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Mute: {Actor} muted {Target} ({Duration}, reason: {Reason})",
|
||||
caller!.Username, target.Username,
|
||||
request?.DurationMinutes is > 0 ? $"{request.DurationMinutes}m" : "indefinite",
|
||||
request?.Reason ?? "none");
|
||||
|
||||
var durationText = request?.DurationMinutes is > 0 ? $" for {request.DurationMinutes} minutes" : "";
|
||||
return Ok(new { Message = $"{target.Username} has been muted{durationText}." });
|
||||
}
|
||||
@@ -156,7 +186,7 @@ public class ModerationController : ControllerBase
|
||||
[HttpPost("unmute/{username}")]
|
||||
public async Task<IActionResult> UnmuteUser(string username)
|
||||
{
|
||||
var (_, error) = await GetCallerAsync(ServerRole.Mod);
|
||||
var (caller, error) = await GetCallerAsync(ServerRole.Mod);
|
||||
if (error is not null) return error;
|
||||
|
||||
var target = await _db.Users.FirstOrDefaultAsync(u => u.Username == username.ToLowerInvariant());
|
||||
@@ -167,6 +197,8 @@ public class ModerationController : ControllerBase
|
||||
target.MutedUntil = null;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Unmute: {Actor} unmuted {Target}", caller!.Username, target.Username);
|
||||
|
||||
return Ok(new { Message = $"{target.Username} has been unmuted." });
|
||||
}
|
||||
|
||||
@@ -219,13 +251,19 @@ public class ModerationController : ControllerBase
|
||||
|
||||
await BroadcastToAllAsync(b => b.SendMessageDeletedAsync(channelName, messageId));
|
||||
|
||||
// Only moderator removals of another user's message are noteworthy; self-deletes are routine.
|
||||
if (!isOwnMessage)
|
||||
_logger.LogInformation(
|
||||
"Message removed: {Actor} deleted {Author}'s message {MessageId} in '{Channel}'",
|
||||
caller.Username, message.SenderUsername, messageId, channelName);
|
||||
|
||||
return Ok(new { Message = "Message deleted." });
|
||||
}
|
||||
|
||||
[HttpDelete("channels/{channel}/nuke")]
|
||||
public async Task<IActionResult> NukeChannel(string channel)
|
||||
{
|
||||
var (_, error) = await GetCallerAsync(ServerRole.Mod);
|
||||
var (caller, error) = await GetCallerAsync(ServerRole.Mod);
|
||||
if (error is not null) return error;
|
||||
|
||||
var channelName = channel.ToLowerInvariant().Trim();
|
||||
@@ -251,6 +289,10 @@ public class ModerationController : ControllerBase
|
||||
|
||||
await BroadcastToAllAsync(b => b.SendChannelNukedAsync(channelName));
|
||||
|
||||
_logger.LogWarning(
|
||||
"Channel nuked: {Actor} cleared {Count} messages from '{Channel}'",
|
||||
caller!.Username, messages.Count, channelName);
|
||||
|
||||
return Ok(new { Message = $"All messages in #{channelName} have been cleared." });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user