334 lines
15 KiB
C#
334 lines
15 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 = 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<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; }
|
|
}
|
|
} |