diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs new file mode 100644 index 0000000000..18705af5b9 --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs @@ -0,0 +1,85 @@ +namespace ServiceControl.Audit.Persistence.RavenDB.CustomChecks +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.CustomChecks; + using NServiceBus.Logging; + using RavenDB; + + class CheckFreeDiskSpace(DatabaseConfiguration databaseConfiguration) : CustomCheck("ServiceControl.Audit database", "Storage space", TimeSpan.FromMinutes(5)) + { + public override Task PerformCheck(CancellationToken cancellationToken = default) + { + if (Logger.IsDebugEnabled) + { + Logger.Debug($"Check ServiceControl data drive space remaining custom check starting. Threshold {percentageThreshold:P0}"); + } + + if (!databaseConfiguration.ServerConfiguration.UseEmbeddedServer) + { + return CheckResult.Pass; + } + + if (dataPathRoot is null) + { + throw new Exception($"Unable to find the root of the data path {databaseConfiguration.ServerConfiguration.DbPath}"); + } + + var dataDriveInfo = new DriveInfo(dataPathRoot); + var availableFreeSpace = (decimal)dataDriveInfo.AvailableFreeSpace; + var totalSpace = (decimal)dataDriveInfo.TotalSize; + + var percentRemaining = (decimal)dataDriveInfo.AvailableFreeSpace / dataDriveInfo.TotalSize; + + if (Logger.IsDebugEnabled) + { + Logger.Debug($"Free space: {availableFreeSpace:N0}B | Total: {totalSpace:N0}B | Percent remaining {percentRemaining:P1}"); + } + + return percentRemaining > percentageThreshold + ? CheckResult.Pass + : CheckResult.Failed($"{percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'."); + } + + public static int Parse(IDictionary settings) + { + if (!settings.TryGetValue(RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey, out var thresholdValue)) + { + thresholdValue = $"{DataSpaceRemainingThresholdDefault}"; + } + + string message; + if (!int.TryParse(thresholdValue, out var threshold)) + { + message = $"{RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey} must be an integer."; + Logger.Fatal(message); + throw new Exception(message); + } + + if (threshold < 0) + { + message = $"{RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey} is invalid, minimum value is 0."; + Logger.Fatal(message); + throw new Exception(message); + } + + if (threshold > 100) + { + message = $"{RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey} is invalid, maximum value is 100."; + Logger.Fatal(message); + throw new Exception(message); + } + + return threshold; + } + + readonly string dataPathRoot = Path.GetPathRoot(databaseConfiguration.ServerConfiguration.DbPath); + readonly decimal percentageThreshold = databaseConfiguration.DataSpaceRemainingThreshold / 100m; + + public const int DataSpaceRemainingThresholdDefault = 20; + static readonly ILog Logger = LogManager.GetLogger(typeof(CheckFreeDiskSpace)); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForAuditIngestion.cs b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForAuditIngestion.cs deleted file mode 100644 index 66ab8b5209..0000000000 --- a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForAuditIngestion.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace ServiceControl.Audit.Persistence.RavenDB.CustomChecks -{ - using System; - using System.IO; - using System.Threading; - using System.Threading.Tasks; - using NServiceBus.CustomChecks; - using NServiceBus.Logging; - using ServiceControl.Audit.Persistence.RavenDB; - - class CheckMinimumStorageRequiredForAuditIngestion : CustomCheck - { - public CheckMinimumStorageRequiredForAuditIngestion(State stateHolder, DatabaseConfiguration databaseConfiguration) - : base("Audit Message Ingestion Process", "ServiceControl.Audit Health", TimeSpan.FromSeconds(5)) - { - this.stateHolder = stateHolder; - this.databaseConfiguration = databaseConfiguration; - } - - public override Task PerformCheck(CancellationToken cancellationToken = default) - { - var percentageThreshold = databaseConfiguration.MinimumStorageLeftRequiredForIngestion / 100m; - - var dataPathRoot = Path.GetPathRoot(databaseConfiguration.ServerConfiguration.DbPath); - if (dataPathRoot == null) - { - stateHolder.CanIngestMore = true; - return successResult; - } - - Logger.Debug($"Check ServiceControl data drive space starting. Threshold {percentageThreshold:P0}"); - - var dataDriveInfo = new DriveInfo(dataPathRoot); - var availableFreeSpace = (decimal)dataDriveInfo.AvailableFreeSpace; - var totalSpace = (decimal)dataDriveInfo.TotalSize; - - var percentRemaining = (decimal)dataDriveInfo.AvailableFreeSpace / dataDriveInfo.TotalSize; - - if (Logger.IsDebugEnabled) - { - Logger.Debug($"Free space: {availableFreeSpace} | Total: {totalSpace} | Percent remaining {percentRemaining:P0}"); - } - - if (percentRemaining > percentageThreshold) - { - stateHolder.CanIngestMore = true; - return successResult; - } - - var message = $"Audit message ingestion stopped! {percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'. This is less than {percentageThreshold}% - the minimal required space configured. The threshold can be set using the {RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey} configuration setting."; - Logger.Warn(message); - stateHolder.CanIngestMore = false; - return CheckResult.Failed(message); - } - - readonly State stateHolder; - readonly DatabaseConfiguration databaseConfiguration; - static Task successResult = Task.FromResult(CheckResult.Pass); - static readonly ILog Logger = LogManager.GetLogger(typeof(CheckMinimumStorageRequiredForAuditIngestion)); - - public class State - { - public bool CanIngestMore { get; set; } = true; - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs new file mode 100644 index 0000000000..6d2fccbc38 --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs @@ -0,0 +1,90 @@ +namespace ServiceControl.Audit.Persistence.RavenDB.CustomChecks +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.CustomChecks; + using NServiceBus.Logging; + using RavenDB; + + class CheckMinimumStorageRequiredForIngestion(MinimumRequiredStorageState stateHolder, DatabaseConfiguration databaseConfiguration) : CustomCheck("Audit Message Ingestion Process", "ServiceControl.Audit Health", TimeSpan.FromSeconds(5)) + { + public override Task PerformCheck(CancellationToken cancellationToken = default) + { + var percentageThreshold = databaseConfiguration.MinimumStorageLeftRequiredForIngestion / 100m; + + if (Logger.IsDebugEnabled) + { + Logger.Debug($"Check ServiceControl data drive space starting. Threshold {percentageThreshold:P0}"); + } + + // Should be checking UseEmbeddedServer but need to check DbPath instead for the ATT hack to work + if (string.IsNullOrEmpty(databaseConfiguration.ServerConfiguration.DbPath)) + { + stateHolder.CanIngestMore = true; + return SuccessResult; + } + + var dataPathRoot = Path.GetPathRoot(databaseConfiguration.ServerConfiguration.DbPath) ?? throw new Exception($"Unable to find the root of the data path {databaseConfiguration.ServerConfiguration.DbPath}"); + + var dataDriveInfo = new DriveInfo(dataPathRoot); + var availableFreeSpace = (decimal)dataDriveInfo.AvailableFreeSpace; + var totalSpace = (decimal)dataDriveInfo.TotalSize; + + var percentRemaining = (decimal)dataDriveInfo.AvailableFreeSpace / dataDriveInfo.TotalSize; + + if (Logger.IsDebugEnabled) + { + Logger.Debug($"Free space: {availableFreeSpace} | Total: {totalSpace} | Percent remaining {percentRemaining:P0}"); + } + + if (percentRemaining > percentageThreshold) + { + stateHolder.CanIngestMore = true; + return SuccessResult; + } + + var message = $"Audit message ingestion stopped! {percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'. This is less than {percentageThreshold}% - the minimal required space configured. The threshold can be set using the {RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey} configuration setting."; + Logger.Warn(message); + stateHolder.CanIngestMore = false; + return CheckResult.Failed(message); + } + + public static int Parse(IDictionary settings) + { + if (!settings.TryGetValue(RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey, out var thresholdValue)) + { + thresholdValue = $"{MinimumStorageLeftRequiredForIngestionDefault}"; + } + + if (!int.TryParse(thresholdValue, out var threshold)) + { + var message = $"{RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey} must be an integer."; + Logger.Fatal(message); + throw new Exception(message); + } + + if (threshold < 0) + { + var message = $"{RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey} is invalid, minimum value is 0."; + Logger.Fatal(message); + throw new Exception(message); + } + + if (threshold > 100) + { + var message = $"{RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey} is invalid, maximum value is 100."; + Logger.Fatal(message); + throw new Exception(message); + } + + return threshold; + } + + public const int MinimumStorageLeftRequiredForIngestionDefault = 5; + static readonly Task SuccessResult = Task.FromResult(CheckResult.Pass); + static readonly ILog Logger = LogManager.GetLogger(typeof(CheckMinimumStorageRequiredForIngestion)); + } +} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexLag.cs b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexLag.cs index d56b0b60d2..ba9af287e7 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexLag.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexLag.cs @@ -1,23 +1,17 @@ -namespace ServiceControl +namespace ServiceControl.Audit.Persistence.RavenDB.CustomChecks { using System; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; - using Audit.Persistence.RavenDB; using NServiceBus.CustomChecks; using NServiceBus.Logging; using Raven.Client.Documents.Operations; + using ServiceControl.Audit.Persistence.RavenDB; - class CheckRavenDBIndexLag : CustomCheck + class CheckRavenDBIndexLag(IRavenDocumentStoreProvider documentStoreProvider) : CustomCheck("Audit Database Index Lag", "ServiceControl.Audit Health", TimeSpan.FromMinutes(5)) { - public CheckRavenDBIndexLag(IRavenDocumentStoreProvider documentStoreProvider) - : base("Audit Database Index Lag", "ServiceControl.Audit Health", TimeSpan.FromMinutes(5)) - { - this.documentStoreProvider = documentStoreProvider; - } - public override async Task PerformCheck(CancellationToken cancellationToken = default) { var store = documentStoreProvider.GetDocumentStore(); @@ -84,7 +78,5 @@ static void CreateDiagnosticsLogEntry(DatabaseStatistics statistics, IndexInform static readonly TimeSpan IndexLagThresholdWarning = TimeSpan.FromMinutes(1); static readonly TimeSpan IndexLagThresholdError = TimeSpan.FromMinutes(10); static readonly ILog Log = LogManager.GetLogger(); - - readonly IRavenDocumentStoreProvider documentStoreProvider; } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/DatabaseConfiguration.cs b/src/ServiceControl.Audit.Persistence.RavenDB/DatabaseConfiguration.cs index f1cb948d5e..05f9258a31 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/DatabaseConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/DatabaseConfiguration.cs @@ -3,43 +3,35 @@ using System; using Sparrow.Json; - public class DatabaseConfiguration + public class DatabaseConfiguration( + string name, + int expirationProcessTimerInSeconds, + bool enableFullTextSearch, + TimeSpan auditRetentionPeriod, + int maxBodySizeToStore, + int dataSpaceRemainingThreshold, + int minimumStorageLeftRequiredForIngestion, + ServerConfiguration serverConfiguration, + TimeSpan bulkInsertCommitTimeout) { - public DatabaseConfiguration(string name, - int expirationProcessTimerInSeconds, - bool enableFullTextSearch, - TimeSpan auditRetentionPeriod, - int maxBodySizeToStore, - int minimumStorageLeftRequiredForIngestion, - ServerConfiguration serverConfiguration, - TimeSpan bulkInsertCommitTimeout) - { - Name = name; - ExpirationProcessTimerInSeconds = expirationProcessTimerInSeconds; - EnableFullTextSearch = enableFullTextSearch; - AuditRetentionPeriod = auditRetentionPeriod; - MaxBodySizeToStore = maxBodySizeToStore; - ServerConfiguration = serverConfiguration; - BulkInsertCommitTimeout = bulkInsertCommitTimeout; - MinimumStorageLeftRequiredForIngestion = minimumStorageLeftRequiredForIngestion; - } - - public string Name { get; } - - public int ExpirationProcessTimerInSeconds { get; } - - public bool EnableFullTextSearch { get; } + public string Name { get; } = name; + + public int ExpirationProcessTimerInSeconds { get; } = expirationProcessTimerInSeconds; + + public bool EnableFullTextSearch { get; } = enableFullTextSearch; public Func FindClrType { get; } - public ServerConfiguration ServerConfiguration { get; } + public ServerConfiguration ServerConfiguration { get; } = serverConfiguration; + + public TimeSpan AuditRetentionPeriod { get; } = auditRetentionPeriod; - public TimeSpan AuditRetentionPeriod { get; } + public int MaxBodySizeToStore { get; } = maxBodySizeToStore; - public int MaxBodySizeToStore { get; } + public int DataSpaceRemainingThreshold { get; } = dataSpaceRemainingThreshold; - public int MinimumStorageLeftRequiredForIngestion { get; internal set; } //Setting for ATT only + public int MinimumStorageLeftRequiredForIngestion { get; internal set; } = minimumStorageLeftRequiredForIngestion; //Setting for ATT only - public TimeSpan BulkInsertCommitTimeout { get; } + public TimeSpan BulkInsertCommitTimeout { get; } = bulkInsertCommitTimeout; } } diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/MinimumRequiredStorageState.cs b/src/ServiceControl.Audit.Persistence.RavenDB/MinimumRequiredStorageState.cs new file mode 100644 index 0000000000..5259d8f0ca --- /dev/null +++ b/src/ServiceControl.Audit.Persistence.RavenDB/MinimumRequiredStorageState.cs @@ -0,0 +1,7 @@ +namespace ServiceControl.Audit.Persistence.RavenDB +{ + public class MinimumRequiredStorageState + { + public bool CanIngestMore { get; set; } = true; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistence.cs b/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistence.cs index 6f416fe8ca..75081a0547 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistence.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistence.cs @@ -2,7 +2,6 @@ { using Microsoft.Extensions.DependencyInjection; using Persistence.UnitOfWork; - using RavenDB.CustomChecks; using UnitOfWork; class RavenPersistence(DatabaseConfiguration databaseConfiguration) : IPersistence @@ -14,7 +13,7 @@ public void AddPersistence(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); } public void AddInstaller(IServiceCollection services) => ConfigureLifecycle(services, databaseConfiguration); diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceConfiguration.cs b/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceConfiguration.cs index 6ff26714ff..12a1e7a5e7 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceConfiguration.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; + using CustomChecks; using NServiceBus.Logging; public class RavenPersistenceConfiguration : IPersistenceConfiguration @@ -17,6 +18,7 @@ public class RavenPersistenceConfiguration : IPersistenceConfiguration public const string RavenDbLogLevelKey = "RavenDBLogLevel"; public const string MinimumStorageLeftRequiredForIngestionKey = "MinimumStorageLeftRequiredForIngestion"; public const string BulkInsertCommitTimeoutInSecondsKey = "BulkInsertCommitTimeoutInSeconds"; + public const string DataSpaceRemainingThresholdKey = "DataSpaceRemainingThreshold"; public IEnumerable ConfigurationKeys => new[]{ DatabaseNameKey, @@ -26,6 +28,7 @@ public class RavenPersistenceConfiguration : IPersistenceConfiguration ExpirationProcessTimerInSecondsKey, LogPathKey, RavenDbLogLevelKey, + DataSpaceRemainingThresholdKey, MinimumStorageLeftRequiredForIngestionKey, BulkInsertCommitTimeoutInSecondsKey }; @@ -91,15 +94,8 @@ internal static DatabaseConfiguration GetDatabaseConfiguration(PersistenceSettin serverConfiguration = new ServerConfiguration(dbPath, serverUrl, logPath, logsMode); } - if (!settings.PersisterSpecificSettings.TryGetValue(MinimumStorageLeftRequiredForIngestionKey, out var minimumStorageLeftRequiredForIngestionKey)) - { - minimumStorageLeftRequiredForIngestionKey = "5"; - } - - if (!int.TryParse(minimumStorageLeftRequiredForIngestionKey, out var minimumStorageLeftRequiredForIngestion)) - { - throw new InvalidOperationException($"{MinimumStorageLeftRequiredForIngestionKey} must be an integer."); - } + var dataSpaceRemainingThreshold = CheckFreeDiskSpace.Parse(settings.PersisterSpecificSettings); + var minimumStorageLeftRequiredForIngestion = CheckMinimumStorageRequiredForIngestion.Parse(settings.PersisterSpecificSettings); var expirationProcessTimerInSeconds = GetExpirationProcessTimerInSeconds(settings); @@ -111,6 +107,7 @@ internal static DatabaseConfiguration GetDatabaseConfiguration(PersistenceSettin settings.EnableFullTextSearchOnBodies, settings.AuditRetentionPeriod, settings.MaxBodySizeToStore, + dataSpaceRemainingThreshold, minimumStorageLeftRequiredForIngestion, serverConfiguration, bulkInsertTimeout); @@ -182,4 +179,4 @@ static string GetLogPath(PersistenceSettings settings) const int ExpirationProcessTimerInSecondsDefault = 600; const int BulkInsertCommitTimeoutInSecondsDefault = 60; } -} \ No newline at end of file +} diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/UnitOfWork/RavenAuditUnitOfWorkFactory.cs b/src/ServiceControl.Audit.Persistence.RavenDB/UnitOfWork/RavenAuditUnitOfWorkFactory.cs index b404ac935d..738c4a42ba 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/UnitOfWork/RavenAuditUnitOfWorkFactory.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/UnitOfWork/RavenAuditUnitOfWorkFactory.cs @@ -5,19 +5,14 @@ using Persistence.UnitOfWork; using Raven.Client.Documents.BulkInsert; using RavenDB; - using ServiceControl.Audit.Persistence.RavenDB.CustomChecks; - class RavenAuditIngestionUnitOfWorkFactory : IAuditIngestionUnitOfWorkFactory + class RavenAuditIngestionUnitOfWorkFactory( + IRavenDocumentStoreProvider documentStoreProvider, + IRavenSessionProvider sessionProvider, + DatabaseConfiguration databaseConfiguration, + MinimumRequiredStorageState customCheckState) + : IAuditIngestionUnitOfWorkFactory { - public RavenAuditIngestionUnitOfWorkFactory(IRavenDocumentStoreProvider documentStoreProvider, IRavenSessionProvider sessionProvider, - DatabaseConfiguration databaseConfiguration, CheckMinimumStorageRequiredForAuditIngestion.State customCheckState) - { - this.documentStoreProvider = documentStoreProvider; - this.sessionProvider = sessionProvider; - this.databaseConfiguration = databaseConfiguration; - this.customCheckState = customCheckState; - } - public IAuditIngestionUnitOfWork StartNew(int batchSize) { var timedCancellationSource = new CancellationTokenSource(databaseConfiguration.BulkInsertCommitTimeout); @@ -29,14 +24,6 @@ public IAuditIngestionUnitOfWork StartNew(int batchSize) ); } - public bool CanIngestMore() - { - return customCheckState.CanIngestMore; - } - - readonly IRavenDocumentStoreProvider documentStoreProvider; - readonly IRavenSessionProvider sessionProvider; - readonly DatabaseConfiguration databaseConfiguration; - readonly CheckMinimumStorageRequiredForAuditIngestion.State customCheckState; + public bool CanIngestMore() => customCheckState.CanIngestMore; } } diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ApprovalFiles/CustomCheckTests.VerifyCustomChecks.approved.txt b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ApprovalFiles/CustomCheckTests.VerifyCustomChecks.approved.txt index 1395d49c0b..2dd77f440a 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ApprovalFiles/CustomCheckTests.VerifyCustomChecks.approved.txt +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ApprovalFiles/CustomCheckTests.VerifyCustomChecks.approved.txt @@ -1,2 +1,3 @@ ServiceControl.Audit Health: Audit Database Index Lag -ServiceControl.Audit Health: Audit Message Ingestion Process \ No newline at end of file +ServiceControl.Audit Health: Audit Message Ingestion Process +Storage space: ServiceControl.Audit database \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs index 77c312a2ec..d45b4ec9d1 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using NServiceBus.CustomChecks; using NUnit.Framework; using Raven.Client.Documents; using Raven.Client.Documents.BulkInsert; @@ -15,17 +16,24 @@ class PersistenceTestsConfiguration { - public IAuditDataStore AuditDataStore { get; protected set; } - public IFailedAuditStorage FailedAuditStorage { get; protected set; } - public IBodyStorage BodyStorage { get; set; } - public IAuditIngestionUnitOfWorkFactory AuditIngestionUnitOfWorkFactory { get; protected set; } + public IAuditDataStore AuditDataStore { get; private set; } + + public IFailedAuditStorage FailedAuditStorage { get; private set; } + + public IBodyStorage BodyStorage { get; private set; } + + public IAuditIngestionUnitOfWorkFactory AuditIngestionUnitOfWorkFactory { get; private set; } + + public IDocumentStore DocumentStore { get; private set; } + + public IServiceProvider ServiceProvider => host.Services; + + public string Name => "RavenDB"; public async Task Configure(Action setSettings) { var config = new RavenPersistenceConfiguration(); - var hostBuilder = Host.CreateApplicationBuilder(); - var persistenceSettings = new PersistenceSettings(TimeSpan.FromHours(1), true, 100000); setSettings(persistenceSettings); @@ -57,6 +65,16 @@ public async Task Configure(Action setSettings) persistence.AddPersistence(hostBuilder.Services); persistence.AddInstaller(hostBuilder.Services); + var assembly = typeof(RavenPersistenceConfiguration).Assembly; + + foreach (var type in assembly.DefinedTypes) + { + if (type.IsAssignableTo(typeof(ICustomCheck))) + { + hostBuilder.Services.AddTransient(typeof(ICustomCheck), type); + } + } + host = hostBuilder.Build(); await host.StartAsync(); @@ -92,9 +110,6 @@ public async Task Cleanup() host.Dispose(); } - public string Name => "RavenDB"; - - public IDocumentStore DocumentStore { get; private set; } string databaseName; IHost host; diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/SharedEmbeddedServer.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/SharedEmbeddedServer.cs index a67510e187..58f82595d8 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/SharedEmbeddedServer.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/SharedEmbeddedServer.cs @@ -35,7 +35,7 @@ public static async Task GetInstance(CancellationToken cancell var logsMode = "Operations"; var serverUrl = $"http://localhost:{PortUtility.FindAvailablePort(33334)}"; - embeddedDatabase = EmbeddedDatabase.Start(new DatabaseConfiguration("audit", 60, true, TimeSpan.FromMinutes(5), 120000, 5, new ServerConfiguration(dbPath, serverUrl, logPath, logsMode), TimeSpan.FromSeconds(60)), new ApplicationLifetime(new NullLogger())); + embeddedDatabase = EmbeddedDatabase.Start(new DatabaseConfiguration("audit", 60, true, TimeSpan.FromMinutes(5), 120000, 5, 5, new ServerConfiguration(dbPath, serverUrl, logPath, logsMode), TimeSpan.FromSeconds(60)), new ApplicationLifetime(new NullLogger())); //make sure that the database is up using var documentStore = await embeddedDatabase.Connect(cancellationToken); diff --git a/src/ServiceControl.Audit.Persistence.Tests/CustomCheckTests.cs b/src/ServiceControl.Audit.Persistence.Tests/CustomCheckTests.cs index 12cfefbb26..c50e624880 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/CustomCheckTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/CustomCheckTests.cs @@ -1,8 +1,8 @@ namespace ServiceControl.Audit.Persistence.Tests { using System; - using System.Collections.Generic; using System.Linq; + using Microsoft.Extensions.DependencyInjection; using NServiceBus.CustomChecks; using NUnit.Framework; using Particular.Approvals; @@ -15,29 +15,10 @@ class CustomCheckTests : PersistenceTestFixture // If any changes have been made to custom checks, this may break customer integration subscribers. Approver.Verify( string.Join(Environment.NewLine, - from check in GetCustomChecks() + from check in ServiceProvider.GetServices() orderby check.Category, check.Id select $"{check.Category}: {check.Id}" ) ); - - IEnumerable GetCustomChecks() - { - var customCheckTypes = DataStore.GetType().Assembly - .GetTypes() - .Where(t => t.IsAbstract == false && typeof(ICustomCheck).IsAssignableFrom(t)); - - var settings = new object(); - - foreach (var customCheckType in customCheckTypes) - { - var constructor = customCheckType.GetConstructors().Single(); - var constructorParameters = constructor.GetParameters() - .Select(p => p.ParameterType.Name == "Settings" ? settings : null) - .ToArray(); - var instance = (ICustomCheck)constructor.Invoke(constructorParameters); - yield return instance; - } - } } } diff --git a/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs b/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs index 9d1ffed132..4edecd97a8 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs @@ -4,44 +4,64 @@ using System.Threading.Tasks; using Auditing.BodyStorage; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using NServiceBus.CustomChecks; using ServiceControl.Audit.Persistence.InMemory; using UnitOfWork; - partial class PersistenceTestsConfiguration + class PersistenceTestsConfiguration { - public IAuditDataStore AuditDataStore { get; protected set; } - public IFailedAuditStorage FailedAuditStorage { get; protected set; } - public IBodyStorage BodyStorage { get; protected set; } - public IAuditIngestionUnitOfWorkFactory AuditIngestionUnitOfWorkFactory { get; protected set; } + public IAuditDataStore AuditDataStore { get; private set; } - public Task Configure(Action setSettings) + public IFailedAuditStorage FailedAuditStorage { get; private set; } + + public IBodyStorage BodyStorage { get; private set; } + + public IAuditIngestionUnitOfWorkFactory AuditIngestionUnitOfWorkFactory { get; private set; } + + public IServiceProvider ServiceProvider => host.Services; + + public string Name => "InMemory"; + + public async Task Configure(Action setSettings) { var config = new InMemoryPersistenceConfiguration(); - var serviceCollection = new ServiceCollection(); + var hostBuilder = Host.CreateApplicationBuilder(); var settings = new PersistenceSettings(TimeSpan.FromHours(1), true, 100000); setSettings(settings); + var persistence = config.Create(settings); - persistence.AddPersistence(serviceCollection); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - AuditDataStore = serviceProvider.GetRequiredService(); - FailedAuditStorage = serviceProvider.GetRequiredService(); - BodyStorage = serviceProvider.GetService(); - AuditIngestionUnitOfWorkFactory = serviceProvider.GetRequiredService(); - return Task.CompletedTask; - } + persistence.AddPersistence(hostBuilder.Services); + persistence.AddInstaller(hostBuilder.Services); - public Task CompleteDBOperation() - { - return Task.CompletedTask; + var assembly = typeof(InMemoryPersistenceConfiguration).Assembly; + + foreach (var type in assembly.DefinedTypes) + { + if (type.IsAssignableTo(typeof(ICustomCheck))) + { + hostBuilder.Services.AddTransient(typeof(ICustomCheck), type); + } + } + + host = hostBuilder.Build(); + await host.StartAsync(); + + AuditDataStore = host.Services.GetRequiredService(); + FailedAuditStorage = host.Services.GetRequiredService(); + BodyStorage = host.Services.GetService(); + AuditIngestionUnitOfWorkFactory = host.Services.GetRequiredService(); } - public Task Cleanup() + public Task CompleteDBOperation() => Task.CompletedTask; + + public async Task Cleanup() { - return Task.CompletedTask; + await host.StopAsync(); + host.Dispose(); } - public string Name => "InMemory"; + IHost host; } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs b/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs index cf358d9a31..207c098bc6 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs @@ -61,6 +61,8 @@ protected string GetManifestPath() protected IAuditIngestionUnitOfWork StartAuditUnitOfWork(int batchSize) => AuditIngestionUnitOfWorkFactory.StartNew(batchSize); + protected IServiceProvider ServiceProvider => configuration.ServiceProvider; + protected PersistenceTestsConfiguration configuration; } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj b/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj index 18062df884..3ccc7e39fc 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj +++ b/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt index eeed019adb..a382abd062 100644 --- a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt +++ b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt @@ -27,7 +27,6 @@ "ServiceName": "Particular.ServiceControl.Audit", "TransportConnectionString": null, "MaximumConcurrencyLevel": 32, - "DataSpaceRemainingThreshold": 20, "ServiceControlQueueAddress": "Particular.ServiceControl", "TimeToRestartAuditIngestionAfterFailure": "00:01:00", "EnableFullTextSearchOnBodies": true diff --git a/src/ServiceControl.Audit/Infrastructure/CheckFreeDiskSpace.cs b/src/ServiceControl.Audit/Infrastructure/CheckFreeDiskSpace.cs deleted file mode 100644 index 61ce8a2a45..0000000000 --- a/src/ServiceControl.Audit/Infrastructure/CheckFreeDiskSpace.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace ServiceControl.Audit.Infrastructure -{ - using System; - using System.IO; - using System.Threading; - using System.Threading.Tasks; - using NServiceBus.CustomChecks; - using NServiceBus.Logging; - using ServiceControl.Configuration; - - class CheckFreeDiskSpace : CustomCheck - { - public CheckFreeDiskSpace(Settings.Settings settings) : base("ServiceControl.Audit database", "Storage space", TimeSpan.FromMinutes(5)) - { - dataPath = SettingsReader.Read(Settings.Settings.SettingsRootNamespace, "DbPath"); - percentageThreshold = settings.DataSpaceRemainingThreshold / 100m; - Logger.Debug($"Check ServiceControl data drive space remaining custom check starting. Threshold {percentageThreshold:P0}"); - } - - public override Task PerformCheck(CancellationToken cancellationToken = default) - { - // TODO This custom check is problematic: - // This could be null for the following reasons: - // 1. DbPath is empty because using in-memory - // 2. DbPath is empty because using external RavenDB - // 3. DbPath is empty because using default location - // This should only passing automatically for the first two cases. - // However, there isn't a good way to determine which case we're in with the info this check currently has access to. - if (string.IsNullOrEmpty(dataPath)) - { - return CheckResult.Pass; - } - - var dataPathRoot = Path.GetPathRoot(dataPath) ?? throw new Exception($"Unable to find the root of the data path {dataPath}"); - - var dataDriveInfo = new DriveInfo(dataPathRoot); - var availableFreeSpace = (decimal)dataDriveInfo.AvailableFreeSpace; - var totalSpace = (decimal)dataDriveInfo.TotalSize; - - var percentRemaining = (decimal)dataDriveInfo.AvailableFreeSpace / dataDriveInfo.TotalSize; - - if (Logger.IsDebugEnabled) - { - Logger.Debug($"Free space: {availableFreeSpace} | Total: {totalSpace} | Percent remaining {percentRemaining:P0}"); - } - - if (percentRemaining > percentageThreshold) - { - return CheckResult.Pass; - } - - var message = $"{percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'."; - - Logger.Warn(message); - return CheckResult.Failed(message); - } - - readonly string dataPath; - decimal percentageThreshold; - static readonly ILog Logger = LogManager.GetLogger(typeof(CheckFreeDiskSpace)); - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs b/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs index a308be179a..7d257da8e7 100644 --- a/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs @@ -42,7 +42,6 @@ public Settings(string serviceName = null, string transportType = null, string p AuditRetentionPeriod = GetAuditRetentionPeriod(); Port = SettingsReader.Read(SettingsRootNamespace, "Port", 44444); MaximumConcurrencyLevel = SettingsReader.Read(SettingsRootNamespace, "MaximumConcurrencyLevel", 32); - DataSpaceRemainingThreshold = GetDataSpaceRemainingThreshold(); ServiceControlQueueAddress = SettingsReader.Read(SettingsRootNamespace, "ServiceControlQueueAddress"); TimeToRestartAuditIngestionAfterFailure = GetTimeToRestartAuditIngestionAfterFailure(); EnableFullTextSearchOnBodies = SettingsReader.Read(SettingsRootNamespace, "EnableFullTextSearchOnBodies", true); @@ -146,8 +145,6 @@ public int MaxBodySizeToStore public string TransportConnectionString { get; set; } public int MaximumConcurrencyLevel { get; set; } - public int DataSpaceRemainingThreshold { get; set; } - public string ServiceControlQueueAddress { get; set; } public TimeSpan TimeToRestartAuditIngestionAfterFailure { get; set; } @@ -279,27 +276,6 @@ static string Subscope(string address) return $"{queue}.log@{machine}"; } - int GetDataSpaceRemainingThreshold() - { - string message; - var threshold = SettingsReader.Read(SettingsRootNamespace, "DataSpaceRemainingThreshold", DataSpaceRemainingThresholdDefault); - if (threshold < 0) - { - message = $"{nameof(DataSpaceRemainingThreshold)} is invalid, minimum value is 0."; - logger.Fatal(message); - throw new Exception(message); - } - - if (threshold > 100) - { - message = $"{nameof(DataSpaceRemainingThreshold)} is invalid, maximum value is 100."; - logger.Fatal(message); - throw new Exception(message); - } - - return threshold; - } - void TryLoadLicenseFromConfig() => LicenseFileText = SettingsReader.Read(SettingsRootNamespace, "LicenseText"); // logger is intentionally not static to prevent it from being initialized before LoggingConfigurator.ConfigureLogging has been called @@ -311,6 +287,5 @@ int GetDataSpaceRemainingThreshold() public static readonly SettingsRootNamespace SettingsRootNamespace = new("ServiceControl.Audit"); const int MaxBodySizeToStoreDefault = 102400; //100 kb - const int DataSpaceRemainingThresholdDefault = 20; } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs index 649f4017c4..c90bce9807 100644 --- a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs +++ b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.Operations +namespace ServiceControl.Persistence.RavenDB.CustomChecks { using System; using System.IO; @@ -6,24 +6,25 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using Persistence.RavenDB; + using ServiceControl.Persistence.RavenDB; - class CheckFreeDiskSpace : CustomCheck + class CheckFreeDiskSpace(RavenPersisterSettings settings) : CustomCheck("ServiceControl database", "Storage space", TimeSpan.FromMinutes(5)) { - public CheckFreeDiskSpace(RavenPersisterSettings settings) : base("ServiceControl database", "Storage space", TimeSpan.FromMinutes(5)) - { - dataPath = settings.DatabasePath; - percentageThreshold = settings.DataSpaceRemainingThreshold / 100m; - Logger.Debug($"Check ServiceControl data drive space remaining custom check starting. Threshold {percentageThreshold:P0}"); - } - public override Task PerformCheck(CancellationToken cancellationToken = default) { - var dataPathRoot = Path.GetPathRoot(dataPath); + if (Logger.IsDebugEnabled) + { + Logger.Debug($"Check ServiceControl data drive space remaining custom check starting. Threshold {percentageThreshold:P0}"); + } + + if (!settings.UseEmbeddedServer) + { + return CheckResult.Pass; + } - if (dataPathRoot == null) + if (dataPathRoot is null) { - throw new Exception($"Unable to find the root of the data path {dataPath}"); + throw new Exception($"Unable to find the root of the data path {settings.DatabasePath}"); } var dataDriveInfo = new DriveInfo(dataPathRoot); @@ -63,8 +64,8 @@ public static void Validate(RavenPersisterSettings settings) } } - readonly string dataPath; - readonly decimal percentageThreshold; + readonly string dataPathRoot = Path.GetPathRoot(settings.DatabasePath); + readonly decimal percentageThreshold = settings.DataSpaceRemainingThreshold / 100m; public const int DataSpaceRemainingThresholdDefault = 20; static readonly ILog Logger = LogManager.GetLogger(typeof(CheckFreeDiskSpace)); diff --git a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs index 9915aa551b..053a41cff7 100644 --- a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs +++ b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.Operations +namespace ServiceControl.Persistence.RavenDB.CustomChecks { using System; using System.IO; @@ -6,33 +6,28 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using Persistence.RavenDB; using ServiceControl.Persistence; + using ServiceControl.Persistence.RavenDB; - class CheckMinimumStorageRequiredForIngestion : CustomCheck + class CheckMinimumStorageRequiredForIngestion(MinimumRequiredStorageState stateHolder, RavenPersisterSettings settings) : CustomCheck("Message Ingestion Process", "ServiceControl Health", TimeSpan.FromSeconds(5)) { - public CheckMinimumStorageRequiredForIngestion( - MinimumRequiredStorageState stateHolder, - RavenPersisterSettings settings) - : base("Message Ingestion Process", "ServiceControl Health", TimeSpan.FromSeconds(5)) - { - this.stateHolder = stateHolder; - this.settings = settings; - - dataPathRoot = Path.GetPathRoot(settings.DatabasePath); - } - public override Task PerformCheck(CancellationToken cancellationToken = default) { - percentageThreshold = settings.MinimumStorageLeftRequiredForIngestion / 100m; + var percentageThreshold = settings.MinimumStorageLeftRequiredForIngestion / 100m; - if (dataPathRoot == null) + if (Logger.IsDebugEnabled) + { + Logger.Debug($"Check ServiceControl data drive space starting. Threshold {percentageThreshold:P0}"); + } + + // Should be checking UseEmbeddedServer but need to check DatabasePath instead for the ATT hack to work + if (string.IsNullOrEmpty(settings.DatabasePath)) { stateHolder.CanIngestMore = true; return SuccessResult; } - Logger.Debug($"Check ServiceControl data drive space starting. Threshold {percentageThreshold:P0}"); + var dataPathRoot = Path.GetPathRoot(settings.DatabasePath) ?? throw new Exception($"Unable to find the root of the data path {settings.DatabasePath}"); var dataDriveInfo = new DriveInfo(dataPathRoot); var availableFreeSpace = (decimal)dataDriveInfo.AvailableFreeSpace; @@ -59,32 +54,24 @@ public override Task PerformCheck(CancellationToken cancellationTok public static void Validate(RavenPersisterSettings settings) { - int threshold = settings.MinimumStorageLeftRequiredForIngestion; + var threshold = settings.MinimumStorageLeftRequiredForIngestion; - string message; if (threshold < 0) { - message = $"{RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} is invalid, minimum value is 0."; + var message = $"{RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} is invalid, minimum value is 0."; Logger.Fatal(message); throw new Exception(message); } if (threshold > 100) { - message = $"{RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} is invalid, maximum value is 100."; + var message = $"{RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} is invalid, maximum value is 100."; Logger.Fatal(message); throw new Exception(message); } } public const int MinimumStorageLeftRequiredForIngestionDefault = 5; - - readonly MinimumRequiredStorageState stateHolder; - readonly RavenPersisterSettings settings; - readonly string dataPathRoot; - - decimal percentageThreshold; - static readonly Task SuccessResult = Task.FromResult(CheckResult.Pass); static readonly ILog Logger = LogManager.GetLogger(typeof(CheckMinimumStorageRequiredForIngestion)); } diff --git a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexErrors.cs b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexErrors.cs index 2cdd727469..cb3f817ba2 100644 --- a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexErrors.cs +++ b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexErrors.cs @@ -1,4 +1,4 @@ -namespace ServiceControl +namespace ServiceControl.Persistence.RavenDB.CustomChecks { using System; using System.Linq; @@ -7,12 +7,10 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using Persistence.RavenDB; - using Raven.Client.Documents; using Raven.Client.Documents.Operations.Indexes; + using ServiceControl.Persistence.RavenDB; - class CheckRavenDBIndexErrors(IRavenDocumentStoreProvider documentStoreProvider) : CustomCheck("Error Database Index Errors", - "ServiceControl Health", TimeSpan.FromMinutes(5)) + class CheckRavenDBIndexErrors(IRavenDocumentStoreProvider documentStoreProvider) : CustomCheck("Error Database Index Errors", "ServiceControl Health", TimeSpan.FromMinutes(5)) { public override async Task PerformCheck(CancellationToken cancellationToken = default) { diff --git a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexLag.cs b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexLag.cs index 655214e150..662ddcdadb 100644 --- a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexLag.cs +++ b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckRavenDBIndexLag.cs @@ -1,4 +1,4 @@ -namespace ServiceControl +namespace ServiceControl.Persistence.RavenDB.CustomChecks { using System; using System.Linq; @@ -7,9 +7,8 @@ using System.Threading.Tasks; using NServiceBus.CustomChecks; using NServiceBus.Logging; - using Persistence.RavenDB; - using Raven.Client.Documents; using Raven.Client.Documents.Operations; + using ServiceControl.Persistence.RavenDB; using CustomCheck = NServiceBus.CustomChecks.CustomCheck; class CheckRavenDBIndexLag(IRavenDocumentStoreProvider documentStoreProvider) : CustomCheck("Error Database Index Lag", "ServiceControl Health", TimeSpan.FromMinutes(5)) diff --git a/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs b/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs index 6e5a4323ef..aef02607b8 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Persistence.RavenDB { + using CustomChecks; using MessageRedirects; using Microsoft.Extensions.DependencyInjection; using NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions; @@ -8,7 +9,6 @@ using ServiceControl.CustomChecks; using ServiceControl.Infrastructure.RavenDB.Subscriptions; using ServiceControl.MessageFailures; - using ServiceControl.Operations; using ServiceControl.Operations.BodyStorage; using ServiceControl.Operations.BodyStorage.RavenAttachments; using ServiceControl.Persistence.MessageRedirects; diff --git a/src/ServiceControl.Persistence.RavenDB/RavenPersistenceConfiguration.cs b/src/ServiceControl.Persistence.RavenDB/RavenPersistenceConfiguration.cs index dc33a09549..a13ecd1a31 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenPersistenceConfiguration.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenPersistenceConfiguration.cs @@ -4,7 +4,7 @@ using System.IO; using System.Reflection; using Configuration; - using ServiceControl.Operations; + using CustomChecks; class RavenPersistenceConfiguration : IPersistenceConfiguration { diff --git a/src/ServiceControl.Persistence.RavenDB/RavenPersisterSettings.cs b/src/ServiceControl.Persistence.RavenDB/RavenPersisterSettings.cs index 1ebaf4adeb..12bc02323d 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenPersisterSettings.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenPersisterSettings.cs @@ -1,6 +1,6 @@ using System; -using ServiceControl.Operations; using ServiceControl.Persistence; +using ServiceControl.Persistence.RavenDB.CustomChecks; class RavenPersisterSettings : PersistenceSettings { diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/ApprovalFiles/CustomCheckTests.VerifyCustomChecks.approved.txt b/src/ServiceControl.Persistence.Tests.RavenDB/ApprovalFiles/CustomCheckTests.VerifyCustomChecks.approved.txt new file mode 100644 index 0000000000..1230037975 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDB/ApprovalFiles/CustomCheckTests.VerifyCustomChecks.approved.txt @@ -0,0 +1,5 @@ +Health: Saga Audit Destination +ServiceControl Health: Error Database Index Errors +ServiceControl Health: Error Database Index Lag +ServiceControl Health: Message Ingestion Process +Storage space: ServiceControl database \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/Archiving/ArchiveGroupTests.cs b/src/ServiceControl.Persistence.Tests.RavenDB/Archiving/ArchiveGroupTests.cs index 4b4c1235ee..433bbe6367 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/Archiving/ArchiveGroupTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDB/Archiving/ArchiveGroupTests.cs @@ -5,8 +5,6 @@ using Microsoft.Extensions.DependencyInjection; using NServiceBus.Testing; using NUnit.Framework; - using PersistenceTests; - using ServiceControl.Infrastructure.DomainEvents; using ServiceControl.Recoverability; [TestFixture] @@ -48,7 +46,7 @@ public async Task ArchiveGroup_skips_over_empty_batches_but_still_completes() await session.SaveChangesAsync(); } - var handler = GetRequiredService(); // See this.CreateHostBuilder + var handler = ServiceProvider.GetRequiredService(); // See this.CreateHostBuilder var context = new TestableMessageHandlerContext(); var message = new ArchiveAllInGroup { GroupId = groupId }; @@ -96,7 +94,7 @@ public async Task ArchiveGroup_GetGroupDetails_doesnt_fail_with_invalid_groupId( await session.SaveChangesAsync(); } - var handler = GetRequiredService(); // See this.CreateHostBuilder + var handler = ServiceProvider.GetRequiredService(); // See this.CreateHostBuilder var context = new TestableMessageHandlerContext(); var message = new ArchiveAllInGroup { GroupId = groupId + "Invalid" }; diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/BodyStorage/RavenAttachmentsBodyStorageTests.cs b/src/ServiceControl.Persistence.Tests.RavenDB/BodyStorage/RavenAttachmentsBodyStorageTests.cs index cd217f67f7..890a0648dc 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/BodyStorage/RavenAttachmentsBodyStorageTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDB/BodyStorage/RavenAttachmentsBodyStorageTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; using NServiceBus; using NServiceBus.Transport; using NUnit.Framework; @@ -33,7 +34,7 @@ async Task RunTest(Func, string> getIdToQuery) var contentType = "NotImportant"; var endpointName = "EndpointName"; var body = BitConverter.GetBytes(0xDEADBEEF); - var ingestionFactory = GetRequiredService(); + var ingestionFactory = ServiceProvider.GetRequiredService(); var headers = new Dictionary { diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/Operations/FailedErrorImportCustomCheckTests.cs b/src/ServiceControl.Persistence.Tests.RavenDB/Operations/FailedErrorImportCustomCheckTests.cs index 7d7bbeefbc..908eacc48f 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/Operations/FailedErrorImportCustomCheckTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDB/Operations/FailedErrorImportCustomCheckTests.cs @@ -21,7 +21,7 @@ public async Task Pass_if_no_failed_imports() { await DocumentStore.ExecuteIndexAsync(new FailedErrorImportIndex()); - var customCheck = GetRequiredService(); + var customCheck = ServiceProvider.GetRequiredService(); var result = await customCheck.PerformCheck(); @@ -46,7 +46,7 @@ public async Task Fail_if_failed_imports() DocumentStore.WaitForIndexing(); - var customCheck = GetRequiredService(); + var customCheck = ServiceProvider.GetRequiredService(); var result = await customCheck.PerformCheck(); diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/Recoverability/ErrorMessageDataStoreTests.cs b/src/ServiceControl.Persistence.Tests.RavenDB/Recoverability/ErrorMessageDataStoreTests.cs index cd287527f4..03ef727bb0 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/Recoverability/ErrorMessageDataStoreTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDB/Recoverability/ErrorMessageDataStoreTests.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using ServiceControl.MessageFailures; using ServiceControl.Operations; @@ -60,7 +61,7 @@ public async Task GetStore() CompleteDatabaseOperation(); - store = GetRequiredService(); + store = ServiceProvider.GetRequiredService(); } async Task GenerateAndSaveFailedMessage() diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/Unarchiving/UnarchiveGroupTests.cs b/src/ServiceControl.Persistence.Tests.RavenDB/Unarchiving/UnarchiveGroupTests.cs index 2058945f13..7b0b24b7bc 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/Unarchiving/UnarchiveGroupTests.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDB/Unarchiving/UnarchiveGroupTests.cs @@ -46,7 +46,7 @@ public async Task UnarchiveGroup_skips_over_empty_batches_but_still_completes() await session.SaveChangesAsync(); } - var handler = GetRequiredService(); // See this.CreateHostBuilder + var handler = ServiceProvider.GetRequiredService(); // See this.CreateHostBuilder var context = new TestableMessageHandlerContext(); var message = new UnarchiveAllInGroup { GroupId = groupId }; @@ -94,7 +94,7 @@ public async Task UnarchiveGroup_GetGroupDetails_doesnt_fail_with_invalid_groupI await session.SaveChangesAsync(); } - var handler = GetRequiredService(); // See this.CreateHostBuilder + var handler = ServiceProvider.GetRequiredService(); // See this.CreateHostBuilder var context = new TestableMessageHandlerContext(); var message = new UnarchiveAllInGroup { GroupId = groupId + "Invalid" }; diff --git a/src/ServiceControl.Persistence.Tests/CustomCheckTests.cs b/src/ServiceControl.Persistence.Tests/CustomCheckTests.cs new file mode 100644 index 0000000000..744f57e34b --- /dev/null +++ b/src/ServiceControl.Persistence.Tests/CustomCheckTests.cs @@ -0,0 +1,24 @@ +namespace ServiceControl.PersistenceTests +{ + using System; + using System.Linq; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus.CustomChecks; + using NUnit.Framework; + using Particular.Approvals; + + class CustomCheckTests : PersistenceTestBase + { + [Test] + public void VerifyCustomChecks() => + // HINT: Custom checks are documented on the docs site and Id and Category are published in integration events + // If any changes have been made to custom checks, this may break customer integration subscribers. + Approver.Verify( + string.Join(Environment.NewLine, + from check in ServiceProvider.GetServices() + orderby check.Category, check.Id + select $"{check.Category}: {check.Id}" + ) + ); + } +} diff --git a/src/ServiceControl.Persistence.Tests/EnsureSettingsInContainer.cs b/src/ServiceControl.Persistence.Tests/EnsureSettingsInContainer.cs index 9773b566ea..1a8841b484 100644 --- a/src/ServiceControl.Persistence.Tests/EnsureSettingsInContainer.cs +++ b/src/ServiceControl.Persistence.Tests/EnsureSettingsInContainer.cs @@ -1,5 +1,6 @@ namespace ServiceControl.PersistenceTests { + using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using ServiceControl.Persistence; @@ -8,14 +9,14 @@ public sealed class EnsureSettingsInContainer : PersistenceTestBase [Test] public void CheckForBothTypes() { - // Persistence implementation must register singleton as base type as some compoennts need to inject that - var baseSettings = GetRequiredService(); + // Persistence implementation must register singleton as base type as some components need to inject that + var baseSettings = ServiceProvider.GetRequiredService(); var actualType = baseSettings.GetType(); Assert.AreNotEqual(actualType, typeof(PersistenceSettings)); // Persistence implementation must also register the same singleton as the persister-specific type - var settingsAsActualType = GetRequiredService(actualType); + var settingsAsActualType = ServiceProvider.GetRequiredService(actualType); Assert.AreSame(baseSettings, settingsAsActualType); } } diff --git a/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs b/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs index 7a974dda74..1daab33c52 100644 --- a/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs +++ b/src/ServiceControl.Persistence.Tests/PersistenceTestBase.cs @@ -1,10 +1,8 @@ using System; using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using NServiceBus; using NServiceBus.Settings; @@ -90,10 +88,7 @@ protected PersistenceSettings PersistenceSettings protected IDocumentStore DocumentStore { get; private set; } protected IRavenSessionProvider SessionProvider { get; private set; } - - protected T GetRequiredService() => host.Services.GetRequiredService(); - protected object GetRequiredService(Type serviceType) => host.Services.GetRequiredService(serviceType); - + protected IServiceProvider ServiceProvider => host.Services; protected Action RegisterServices { get; set; } = _ => { }; protected void CompleteDatabaseOperation() => DocumentStore.WaitForIndexing(); @@ -144,17 +139,17 @@ protected void BlockToInspectDatabase() Debugger.Break(); } - protected IErrorMessageDataStore ErrorStore => GetRequiredService(); - protected IRetryDocumentDataStore RetryStore => GetRequiredService(); - protected IBodyStorage BodyStorage => GetRequiredService(); - protected IRetryBatchesDataStore RetryBatchesStore => GetRequiredService(); - protected IErrorMessageDataStore ErrorMessageDataStore => GetRequiredService(); - protected IMessageRedirectsDataStore MessageRedirectsDataStore => GetRequiredService(); - protected IMonitoringDataStore MonitoringDataStore => GetRequiredService(); - protected IIngestionUnitOfWorkFactory UnitOfWorkFactory => GetRequiredService(); - protected ICustomChecksDataStore CustomChecks => GetRequiredService(); - protected IArchiveMessages ArchiveMessages => GetRequiredService(); - protected IIngestionUnitOfWorkFactory IngestionUnitOfWorkFactory => GetRequiredService(); - protected IEventLogDataStore EventLogDataStore => GetRequiredService(); - protected IRetryDocumentDataStore RetryDocumentDataStore => GetRequiredService(); + protected IErrorMessageDataStore ErrorStore => ServiceProvider.GetRequiredService(); + protected IRetryDocumentDataStore RetryStore => ServiceProvider.GetRequiredService(); + protected IBodyStorage BodyStorage => ServiceProvider.GetRequiredService(); + protected IRetryBatchesDataStore RetryBatchesStore => ServiceProvider.GetRequiredService(); + protected IErrorMessageDataStore ErrorMessageDataStore => ServiceProvider.GetRequiredService(); + protected IMessageRedirectsDataStore MessageRedirectsDataStore => ServiceProvider.GetRequiredService(); + protected IMonitoringDataStore MonitoringDataStore => ServiceProvider.GetRequiredService(); + protected IIngestionUnitOfWorkFactory UnitOfWorkFactory => ServiceProvider.GetRequiredService(); + protected ICustomChecksDataStore CustomChecks => ServiceProvider.GetRequiredService(); + protected IArchiveMessages ArchiveMessages => ServiceProvider.GetRequiredService(); + protected IIngestionUnitOfWorkFactory IngestionUnitOfWorkFactory => ServiceProvider.GetRequiredService(); + protected IEventLogDataStore EventLogDataStore => ServiceProvider.GetRequiredService(); + protected IRetryDocumentDataStore RetryDocumentDataStore => ServiceProvider.GetRequiredService(); } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs b/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs index ae72e68669..f0fe64b6f8 100644 --- a/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs +++ b/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs @@ -27,7 +27,7 @@ sealed class EditMessageTests : PersistenceTestBase .AddTransient(); [SetUp] - public void Setup() => handler = GetRequiredService(); + public void Setup() => handler = ServiceProvider.GetRequiredService(); [Test] public async Task Should_discard_edit_when_failed_message_not_exists() diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt index 9db0b6043b..17eb857f3c 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt @@ -43,6 +43,5 @@ "MaximumConcurrencyLevel": 10, "RetryHistoryDepth": 10, "RemoteInstances": [], - "DataSpaceRemainingThreshold": 20, "DisableHealthChecks": false } \ No newline at end of file diff --git a/src/ServiceControl/App.config b/src/ServiceControl/App.config index 27afc99c1d..b999c5b963 100644 --- a/src/ServiceControl/App.config +++ b/src/ServiceControl/App.config @@ -22,7 +22,6 @@ These settings are only here so that we can debug ServiceControl while developin - diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index e35f191470..004688c0cf 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -59,7 +59,6 @@ public class Settings AllowMessageEditing = SettingsReader.Read(SettingsRootNamespace, "AllowMessageEditing"); NotificationsFilter = SettingsReader.Read(SettingsRootNamespace, "NotificationsFilter"); RemoteInstances = GetRemoteInstances().ToArray(); - DataSpaceRemainingThreshold = GetDataSpaceRemainingThreshold(); TimeToRestartErrorIngestionAfterFailure = GetTimeToRestartErrorIngestionAfterFailure(); DisableExternalIntegrationsPublishing = SettingsReader.Read(SettingsRootNamespace, "DisableExternalIntegrationsPublishing", false); @@ -178,8 +177,6 @@ public TimeSpan HeartbeatGracePeriod public RemoteInstanceSetting[] RemoteInstances { get; set; } - public int DataSpaceRemainingThreshold { get; set; } - public bool DisableHealthChecks { get; set; } public ITransportCustomization LoadTransportCustomization() @@ -386,27 +383,6 @@ static string Subscope(string address) return $"{queue}.log@{machine}"; } - int GetDataSpaceRemainingThreshold() - { - string message; - var threshold = SettingsReader.Read(SettingsRootNamespace, "DataSpaceRemainingThreshold", DataSpaceRemainingThresholdDefault); - if (threshold < 0) - { - message = $"{nameof(DataSpaceRemainingThreshold)} is invalid, minimum value is 0."; - logger.Fatal(message); - throw new Exception(message); - } - - if (threshold > 100) - { - message = $"{nameof(DataSpaceRemainingThreshold)} is invalid, maximum value is 100."; - logger.Fatal(message); - throw new Exception(message); - } - - return threshold; - } - void LoadErrorIngestionSettings() { var serviceBusRootNamespace = new SettingsRootNamespace("ServiceBus"); @@ -440,7 +416,5 @@ void LoadErrorIngestionSettings() public const string DEFAULT_SERVICE_NAME = "Particular.ServiceControl"; public static readonly SettingsRootNamespace SettingsRootNamespace = new("ServiceControl"); - - const int DataSpaceRemainingThresholdDefault = 20; } } \ No newline at end of file