using MongoDB.Bson; using MongoDB.Driver; using QRRapidoApp.Data; using System.Text.Json; namespace QRRapidoApp.Services.Monitoring { public class MongoDbMonitoringService : BackgroundService { private readonly ILogger _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 _collectionsToMonitor; // Monitoring state private long _previousDatabaseSizeMB = 0; private DateTime _previousMeasurement = DateTime.UtcNow; private readonly Dictionary _previousCollectionStats = new(); public MongoDbMonitoringService( ILogger logger, IConfiguration configuration, IServiceProvider serviceProvider) { _logger = logger; _configuration = configuration; _serviceProvider = serviceProvider; _applicationName = configuration["ApplicationName"] ?? "QRRapido"; // Load configuration _intervalMinutes = configuration.GetValue("MongoDbMonitoring:IntervalMinutes", 5); _databaseSizeWarningMB = configuration.GetValue("MongoDbMonitoring:DatabaseSizeWarningMB", 1024); _databaseSizeErrorMB = configuration.GetValue("MongoDbMonitoring:DatabaseSizeErrorMB", 5120); _growthRateWarningMBPerHour = configuration.GetValue("MongoDbMonitoring:GrowthRateWarningMBPerHour", 100); _includeCollectionStats = configuration.GetValue("MongoDbMonitoring:IncludeCollectionStats", true); _collectionsToMonitor = configuration.GetSection("MongoDbMonitoring:CollectionsToMonitor").Get>() ?? new List { "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(); 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(); 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 { ["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 GetDatabaseStatsAsync(MongoDbContext context) { var command = new BsonDocument("dbStats", 1); var result = await context.Database!.RunCommandAsync(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(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> GetCollectionStatsAsync(MongoDbContext context) { var collectionStats = new List(); 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(command); var size = result.GetValue("size", BsonValue.Create(0)).AsDouble; var totalIndexSize = result.GetValue("totalIndexSize", BsonValue.Create(0)).AsDouble; var count = result.GetValue("count", BsonValue.Create(0)).ToInt64(); var avgObjSize = result.GetValue("avgObjSize", BsonValue.Create(0)).AsDouble; 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 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 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 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; } } }