QrRapido/Services/Monitoring/MongoDbMonitoringService.cs
Ricardo Carneiro b54aa295ac
All checks were successful
Deploy QR Rapido / test (push) Successful in 44s
Deploy QR Rapido / build-and-push (push) Successful in 12m59s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m24s
fix: add Node.js to Docker build stage for frontend compilation
- Install Node.js 18.x in Docker build stage
- Add MongoDB DataProtection for Swarm compatibility
- Enables shared authentication keys across multiple replicas

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 16:25:54 -03:00

347 lines
16 KiB
C#

using MongoDB.Bson;
using MongoDB.Driver;
using QRRapidoApp.Data;
using System.Text.Json;
namespace QRRapidoApp.Services.Monitoring
{
public class MongoDbMonitoringService : BackgroundService
{
private readonly ILogger<MongoDbMonitoringService> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceProvider _serviceProvider;
private readonly string _applicationName;
// Configuration values
private readonly int _intervalMinutes;
private readonly long _databaseSizeWarningMB;
private readonly long _databaseSizeErrorMB;
private readonly long _growthRateWarningMBPerHour;
private readonly bool _includeCollectionStats;
private readonly List<string> _collectionsToMonitor;
// Monitoring state
private long _previousDatabaseSizeMB = 0;
private DateTime _previousMeasurement = DateTime.UtcNow;
private readonly Dictionary<string, CollectionSnapshot> _previousCollectionStats = new();
public MongoDbMonitoringService(
ILogger<MongoDbMonitoringService> logger,
IConfiguration configuration,
IServiceProvider serviceProvider)
{
_logger = logger;
_configuration = configuration;
_serviceProvider = serviceProvider;
_applicationName = configuration["ApplicationName"] ?? "QRRapido";
// Load configuration
_intervalMinutes = configuration.GetValue<int>("MongoDbMonitoring:IntervalMinutes", 5);
_databaseSizeWarningMB = configuration.GetValue<long>("MongoDbMonitoring:DatabaseSizeWarningMB", 1024);
_databaseSizeErrorMB = configuration.GetValue<long>("MongoDbMonitoring:DatabaseSizeErrorMB", 5120);
_growthRateWarningMBPerHour = configuration.GetValue<long>("MongoDbMonitoring:GrowthRateWarningMBPerHour", 100);
_includeCollectionStats = configuration.GetValue<bool>("MongoDbMonitoring:IncludeCollectionStats", true);
_collectionsToMonitor = configuration.GetSection("MongoDbMonitoring:CollectionsToMonitor").Get<List<string>>()
?? new List<string> { "Users", "QRCodeHistory", "AdFreeSessions" };
_logger.LogInformation("MongoDbMonitoringService initialized for {ApplicationName} - Interval: {IntervalMinutes}min, Warning: {WarningMB}MB, Error: {ErrorMB}MB",
_applicationName, _intervalMinutes, _databaseSizeWarningMB, _databaseSizeErrorMB);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("MongoDbMonitoringService started for {ApplicationName}", _applicationName);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await MonitorMongoDbAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in MongoDbMonitoringService for {ApplicationName}", _applicationName);
}
await Task.Delay(TimeSpan.FromMinutes(_intervalMinutes), stoppingToken);
}
_logger.LogInformation("MongoDbMonitoringService stopped for {ApplicationName}", _applicationName);
}
private async Task MonitorMongoDbAsync()
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetService<MongoDbContext>();
if (context?.Database == null)
{
_logger.LogWarning("MongoDB context not available for monitoring in {ApplicationName} - skipping this cycle", _applicationName);
return;
}
try
{
var now = DateTime.UtcNow;
// Get database statistics
var dbStats = await GetDatabaseStatsAsync(context);
var collectionStats = new List<CollectionStatistics>();
if (_includeCollectionStats)
{
collectionStats = await GetCollectionStatsAsync(context);
}
// Calculate growth rate
var growthSincePreviousMB = 0.0;
var growthRateMBPerHour = 0.0;
if (_previousDatabaseSizeMB > 0)
{
growthSincePreviousMB = dbStats.DatabaseSizeMB - _previousDatabaseSizeMB;
var timeDelta = (now - _previousMeasurement).TotalHours;
if (timeDelta > 0)
{
growthRateMBPerHour = growthSincePreviousMB / timeDelta;
}
}
_previousDatabaseSizeMB = (long)dbStats.DatabaseSizeMB;
_previousMeasurement = now;
// Determine status
var status = DetermineDatabaseStatus(dbStats.DatabaseSizeMB, growthRateMBPerHour);
// Log structured MongoDB metrics
using (_logger.BeginScope(new Dictionary<string, object>
{
["ApplicationName"] = _applicationName,
["MongoDbMonitoring"] = true,
["DatabaseName"] = dbStats.DatabaseName,
["DatabaseSizeMB"] = Math.Round(dbStats.DatabaseSizeMB, 2),
["DatabaseSizeGB"] = Math.Round(dbStats.DatabaseSizeMB / 1024.0, 3),
["GrowthSincePreviousMB"] = Math.Round(growthSincePreviousMB, 2),
["GrowthRateMBPerHour"] = Math.Round(growthRateMBPerHour, 2),
["DocumentCount"] = dbStats.DocumentCount,
["IndexSizeMB"] = Math.Round(dbStats.IndexSizeMB, 2),
["MongoDbVersion"] = dbStats.Version,
["Collections"] = collectionStats.Select(c => new
{
name = c.Name,
documentCount = c.DocumentCount,
sizeMB = Math.Round(c.SizeMB, 2),
indexSizeMB = Math.Round(c.IndexSizeMB, 2),
avgDocSizeBytes = c.AvgDocSizeBytes
}),
["Status"] = status
}))
{
var logLevel = status switch
{
"Error" => LogLevel.Error,
"Warning" => LogLevel.Warning,
_ => LogLevel.Information
};
_logger.Log(logLevel,
"MongoDB monitoring - Database: {Database}, Size: {SizeMB:F1}MB ({SizeGB:F2}GB), Growth: +{GrowthMB:F1}MB ({GrowthRate:F1}MB/h), Documents: {Documents:N0}, Status: {Status}",
dbStats.DatabaseName, dbStats.DatabaseSizeMB, dbStats.DatabaseSizeMB / 1024.0,
growthSincePreviousMB, growthRateMBPerHour, dbStats.DocumentCount, status);
}
// Check for alerts
await CheckDatabaseAlertsAsync(dbStats, growthRateMBPerHour, collectionStats);
// Update collection tracking
UpdateCollectionTracking(collectionStats);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to monitor MongoDB for {ApplicationName}", _applicationName);
}
}
private async Task<DatabaseStatistics> GetDatabaseStatsAsync(MongoDbContext context)
{
var command = new BsonDocument("dbStats", 1);
var result = await context.Database!.RunCommandAsync<BsonDocument>(command);
var databaseName = context.Database!.DatabaseNamespace.DatabaseName;
var dataSize = result.GetValue("dataSize", BsonValue.Create(0)).AsDouble;
var indexSize = result.GetValue("indexSize", BsonValue.Create(0)).AsDouble;
var totalSize = dataSize + indexSize;
var documentCount = result.GetValue("objects", BsonValue.Create(0)).ToInt64();
// Get MongoDB version
var serverStatus = await context.Database!.RunCommandAsync<BsonDocument>(new BsonDocument("serverStatus", 1));
var version = serverStatus.GetValue("version", BsonValue.Create("unknown")).AsString;
return new DatabaseStatistics
{
DatabaseName = databaseName,
DatabaseSizeMB = totalSize / (1024 * 1024),
IndexSizeMB = indexSize / (1024 * 1024),
DocumentCount = documentCount,
Version = version
};
}
private async Task<List<CollectionStatistics>> GetCollectionStatsAsync(MongoDbContext context)
{
var collectionStats = new List<CollectionStatistics>();
var collections = await context.Database!.ListCollectionNamesAsync();
var collectionList = await collections.ToListAsync();
foreach (var collectionName in collectionList.Where(c => ShouldMonitorCollection(c)))
{
try
{
var command = new BsonDocument("collStats", collectionName);
var result = await context.Database!.RunCommandAsync<BsonDocument>(command);
var size = GetDoubleValue(result, "size");
var totalIndexSize = GetDoubleValue(result, "totalIndexSize");
var count = result.GetValue("count", BsonValue.Create(0)).ToInt64();
var avgObjSize = GetDoubleValue(result, "avgObjSize");
collectionStats.Add(new CollectionStatistics
{
Name = collectionName,
SizeMB = size / (1024 * 1024),
IndexSizeMB = totalIndexSize / (1024 * 1024),
DocumentCount = count,
AvgDocSizeBytes = (long)avgObjSize
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get stats for collection {CollectionName} in {ApplicationName}",
collectionName, _applicationName);
}
}
return collectionStats.OrderByDescending(c => c.SizeMB).ToList();
}
private static double GetDoubleValue(BsonDocument document, string fieldName)
{
var value = document.GetValue(fieldName, BsonValue.Create(0));
return value.BsonType switch
{
BsonType.Double => value.AsDouble,
BsonType.Int32 => (double)value.AsInt32,
BsonType.Int64 => (double)value.AsInt64,
BsonType.Decimal128 => (double)value.AsDecimal128,
_ => 0.0
};
}
private bool ShouldMonitorCollection(string collectionName)
{
return _collectionsToMonitor.Any(monitored =>
string.Equals(monitored, collectionName, StringComparison.OrdinalIgnoreCase));
}
private string DetermineDatabaseStatus(double databaseSizeMB, double growthRateMBPerHour)
{
if (databaseSizeMB > _databaseSizeErrorMB)
{
return "Error";
}
if (databaseSizeMB > _databaseSizeWarningMB ||
Math.Abs(growthRateMBPerHour) > _growthRateWarningMBPerHour)
{
return "Warning";
}
return "Healthy";
}
private async Task CheckDatabaseAlertsAsync(DatabaseStatistics dbStats, double growthRate, List<CollectionStatistics> collections)
{
// Database size alerts
if (dbStats.DatabaseSizeMB > _databaseSizeErrorMB)
{
_logger.LogError("ALERT: Database size critical for {ApplicationName} - {SizeMB:F1}MB exceeds error threshold of {ThresholdMB}MB",
_applicationName, dbStats.DatabaseSizeMB, _databaseSizeErrorMB);
}
else if (dbStats.DatabaseSizeMB > _databaseSizeWarningMB)
{
_logger.LogWarning("ALERT: Database size warning for {ApplicationName} - {SizeMB:F1}MB exceeds warning threshold of {ThresholdMB}MB",
_applicationName, dbStats.DatabaseSizeMB, _databaseSizeWarningMB);
}
// Growth rate alerts
if (Math.Abs(growthRate) > _growthRateWarningMBPerHour)
{
var growthType = growthRate > 0 ? "growth" : "shrinkage";
_logger.LogWarning("ALERT: High database {GrowthType} rate for {ApplicationName} - {GrowthRate:F1}MB/hour exceeds threshold of {ThresholdMB}MB/hour",
growthType, _applicationName, Math.Abs(growthRate), _growthRateWarningMBPerHour);
}
// Collection-specific alerts
foreach (var collection in collections.Take(5)) // Top 5 largest collections
{
if (collection.SizeMB > 100) // Alert for collections over 100MB
{
_logger.LogInformation("Large collection detected in {ApplicationName} - {CollectionName}: {SizeMB:F1}MB with {DocumentCount:N0} documents",
_applicationName, collection.Name, collection.SizeMB, collection.DocumentCount);
}
}
}
private void UpdateCollectionTracking(List<CollectionStatistics> currentStats)
{
foreach (var stat in currentStats)
{
if (_previousCollectionStats.ContainsKey(stat.Name))
{
var previous = _previousCollectionStats[stat.Name];
var documentGrowth = stat.DocumentCount - previous.DocumentCount;
var sizeGrowthMB = stat.SizeMB - previous.SizeMB;
if (documentGrowth > 1000 || sizeGrowthMB > 10) // Significant growth
{
_logger.LogInformation("Collection growth detected in {ApplicationName} - {CollectionName}: +{DocumentGrowth} documents, +{SizeGrowthMB:F1}MB",
_applicationName, stat.Name, documentGrowth, sizeGrowthMB);
}
}
_previousCollectionStats[stat.Name] = new CollectionSnapshot
{
DocumentCount = stat.DocumentCount,
SizeMB = stat.SizeMB,
LastMeasurement = DateTime.UtcNow
};
}
}
}
public class DatabaseStatistics
{
public string DatabaseName { get; set; } = string.Empty;
public double DatabaseSizeMB { get; set; }
public double IndexSizeMB { get; set; }
public long DocumentCount { get; set; }
public string Version { get; set; } = string.Empty;
}
public class CollectionStatistics
{
public string Name { get; set; } = string.Empty;
public double SizeMB { get; set; }
public double IndexSizeMB { get; set; }
public long DocumentCount { get; set; }
public long AvgDocSizeBytes { get; set; }
}
public class CollectionSnapshot
{
public long DocumentCount { get; set; }
public double SizeMB { get; set; }
public DateTime LastMeasurement { get; set; }
}
}