diff --git a/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs b/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs index 4ac7b5bb05..3f9392685a 100644 --- a/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs @@ -10,11 +10,13 @@ using AuditThroughput; using Contracts; using Infrastructure; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using NuGet.Versioning; using NUnit.Framework; using ServiceControl.Transports.BrokerThroughput; +using Shared; [TestFixture] class AuditThroughputCollectorHostedService_Tests : ThroughputCollectorTestFixture @@ -34,11 +36,19 @@ public async Task Should_handle_no_audit_remotes() CancellationToken token = tokenSource.Token; var fakeTimeProvider = new FakeTimeProvider(); var auditQuery = new AuditQuery_NoAuditRemotes(); + var emptyConfig = new ConfigurationBuilder().Build(); using var auditThroughputCollectorHostedService = new AuditThroughputCollectorHostedService( - NullLogger.Instance, configuration.ThroughputSettings, DataStore, - auditQuery, fakeTimeProvider) - { DelayStart = TimeSpan.Zero }; + NullLogger.Instance, + configuration.ThroughputSettings, + DataStore, + auditQuery, + fakeTimeProvider, + new PlatformEndpointHelper(new ServiceControlSettings(emptyConfig)) + ) + { + DelayStart = TimeSpan.Zero + }; //Act await auditThroughputCollectorHostedService.StartAsync(token); @@ -64,10 +74,17 @@ public async Task Should_handle_exceptions_in_try_block_and_continue() var fakeTimeProvider = new FakeTimeProvider(); var auditQuery = new AuditQuery_ThrowingAnExceptionOnKnownEndpointsCall(); + var emptyConfig = new ConfigurationBuilder().Build(); + using var auditThroughputCollectorHostedService = new AuditThroughputCollectorHostedService( NullLogger.Instance, configuration.ThroughputSettings, DataStore, - auditQuery, fakeTimeProvider) - { DelayStart = TimeSpan.Zero }; + auditQuery, + fakeTimeProvider, + new PlatformEndpointHelper(new ServiceControlSettings(emptyConfig)) + ) + { + DelayStart = TimeSpan.Zero + }; //Act await auditThroughputCollectorHostedService.StartAsync(token); @@ -91,11 +108,17 @@ public async Task Should_handle_cancellation_token_gracefully() var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); CancellationToken token = tokenSource.Token; var fakeTimeProvider = new FakeTimeProvider(); + var emptyConfig = new ConfigurationBuilder().Build(); using var auditThroughputCollectorHostedService = new AuditThroughputCollectorHostedService( NullLogger.Instance, configuration.ThroughputSettings, DataStore, - configuration.AuditQuery, fakeTimeProvider) - { DelayStart = TimeSpan.Zero }; + configuration.AuditQuery, + fakeTimeProvider, + new PlatformEndpointHelper(new ServiceControlSettings(emptyConfig)) + ) + { + DelayStart = TimeSpan.Zero + }; //Act await auditThroughputCollectorHostedService.StartAsync(token); @@ -116,11 +139,17 @@ public async Task Should_sanitize_endpoint_name() string endpointName = "e$ndpoint&1"; var auditQuery = new AuditQuery_WithOneEndpoint(endpointName, 0, DateOnly.FromDateTime(DateTime.UtcNow)); string endpointNameSanitized = "e-ndpoint-1"; + var emptyConfig = new ConfigurationBuilder().Build(); using var auditThroughputCollectorHostedService = new AuditThroughputCollectorHostedService( NullLogger.Instance, configuration.ThroughputSettings, DataStore, - auditQuery, fakeTimeProvider, new BrokerThroughputQuery_WithSanitization()) - { DelayStart = TimeSpan.Zero }; + auditQuery, + fakeTimeProvider, + new PlatformEndpointHelper(new ServiceControlSettings(emptyConfig)) + ) + { + DelayStart = TimeSpan.Zero + }; //Act await auditThroughputCollectorHostedService.StartAsync(token); @@ -152,11 +181,16 @@ public async Task Should_not_add_the_same_endpoint_throughput_if_runs_twice_on_t var throughputDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)); long throughputCount = 5; var auditQuery = new AuditQuery_WithOneEndpoint(endpointName, throughputCount, throughputDate); + var emptyConfig = new ConfigurationBuilder().Build(); using var auditThroughputCollectorHostedService = new AuditThroughputCollectorHostedService( NullLogger.Instance, configuration.ThroughputSettings, DataStore, - auditQuery: auditQuery, fakeTimeProvider) - { DelayStart = TimeSpan.Zero }; + auditQuery: auditQuery, + fakeTimeProvider, + new PlatformEndpointHelper(new ServiceControlSettings(emptyConfig))) + { + DelayStart = TimeSpan.Zero + }; //Act await auditThroughputCollectorHostedService.StartAsync(token1); @@ -224,7 +258,11 @@ public Task> GetKnownEndpoints(CancellationT public Task TestAuditConnection(CancellationToken cancellationToken) => Task.FromResult( - new ConnectionSettingsTestResult { ConnectionSuccessful = true, ConnectionErrorMessages = [] }); + new ConnectionSettingsTestResult + { + ConnectionSuccessful = true, + ConnectionErrorMessages = [] + }); public bool InstanceParameter { get; set; } } @@ -245,9 +283,16 @@ public AuditQuery_WithOneEndpoint(string endpointName, long throughputCount, Dat public Task> GetAuditCountForEndpoint(string endpointUrlName, CancellationToken cancellationToken) { - var auditCount = new AuditCount { UtcDate = ThroughputDate, Count = ThroughputCount }; + var auditCount = new AuditCount + { + UtcDate = ThroughputDate, + Count = ThroughputCount + }; - return Task.FromResult(new List { auditCount }.AsEnumerable()); + return Task.FromResult(new List + { + auditCount + }.AsEnumerable()); } public Task> GetAuditRemotes(CancellationToken cancellationToken) => @@ -255,13 +300,21 @@ public Task> GetAuditRemotes(CancellationToken c public Task> GetKnownEndpoints(CancellationToken cancellationToken) { - var scEndpoint = new ServiceControlEndpoint { Name = EndpointName, HeartbeatsEnabled = true }; + var scEndpoint = new ServiceControlEndpoint + { + Name = EndpointName, + HeartbeatsEnabled = true + }; return Task.FromResult>([scEndpoint]); } public Task TestAuditConnection(CancellationToken cancellationToken) => Task.FromResult( - new ConnectionSettingsTestResult { ConnectionSuccessful = true, ConnectionErrorMessages = [] }); + new ConnectionSettingsTestResult + { + ConnectionSuccessful = true, + ConnectionErrorMessages = [] + }); string EndpointName { get; } long ThroughputCount { get; } diff --git a/src/Particular.LicensingComponent.UnitTests/BrokerThroughputCollectorHostedServiceTests.cs b/src/Particular.LicensingComponent.UnitTests/BrokerThroughputCollectorHostedServiceTests.cs index 93c91a24fd..074a20b6f0 100644 --- a/src/Particular.LicensingComponent.UnitTests/BrokerThroughputCollectorHostedServiceTests.cs +++ b/src/Particular.LicensingComponent.UnitTests/BrokerThroughputCollectorHostedServiceTests.cs @@ -9,11 +9,13 @@ namespace Particular.LicensingComponent.UnitTests; using System.Threading.Tasks; using BrokerThroughput; using Contracts; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using NUnit.Framework; using Persistence.InMemory; using ServiceControl.Transports.BrokerThroughput; +using Shared; [TestFixture] class BrokerThroughputCollectorHostedServiceTests @@ -24,13 +26,21 @@ public async Task EnsuringRepeatedEndpointsSanitizedNameContainsPostfix() var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); CancellationToken token = tokenSource.Token; + var configuration = new ConfigurationBuilder().Build(); var dataStore = new InMemoryLicensingDataStore(); + using var brokerThroughputCollectorHostedService = new BrokerThroughputCollectorHostedService( NullLogger.Instance, new MockedBrokerThroughputQuery(), new ThroughputSettings(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty), - dataStore, TimeProvider.System) - { DelayStart = TimeSpan.Zero }; + dataStore, + TimeProvider.System, + new PlatformEndpointHelper(new ServiceControlSettings(configuration)), + configuration + ) + { + DelayStart = TimeSpan.Zero + }; await brokerThroughputCollectorHostedService.StartAsync(token); await (brokerThroughputCollectorHostedService.ExecuteTask! ?? Task.CompletedTask); @@ -38,8 +48,14 @@ public async Task EnsuringRepeatedEndpointsSanitizedNameContainsPostfix() IEnumerable sanitizedNames = endpoints.Select(endpoint => endpoint.SanitizedName); IEnumerable queueNames = endpoints.Select(endpoint => endpoint.Id.Name); - Assert.That(sanitizedNames, Is.EquivalentTo(new[] { "sales", "sales1", "marketing", "customer" })); - Assert.That(queueNames, Is.EquivalentTo(new[] { "sales@one", "sales@two", "marketing", "customer" })); + Assert.That(sanitizedNames, Is.EquivalentTo(new[] + { + "sales", "sales1", "marketing", "customer" + })); + Assert.That(queueNames, Is.EquivalentTo(new[] + { + "sales@one", "sales@two", "marketing", "customer" + })); } [Test] @@ -57,12 +73,20 @@ await dataStore.RecordEndpointThroughput("marketing", ThroughputSource.Broker, await dataStore.RecordEndpointThroughput("customer", ThroughputSource.Broker, new List([new EndpointDailyThroughput(lastCollectionDate, 100)]), token); var mockedBrokerThroughputQueryThatRecordsDate = new MockedBrokerThroughputQueryThatRecordsDate(); + var emptyConfig = new ConfigurationBuilder().Build(); + using var brokerThroughputCollectorHostedService = new BrokerThroughputCollectorHostedService( NullLogger.Instance, mockedBrokerThroughputQueryThatRecordsDate, new ThroughputSettings(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty), - dataStore, TimeProvider.System) - { DelayStart = TimeSpan.Zero }; + dataStore, + TimeProvider.System, + new PlatformEndpointHelper(new ServiceControlSettings(emptyConfig)), + emptyConfig + ) + { + DelayStart = TimeSpan.Zero + }; await brokerThroughputCollectorHostedService.StartAsync(token); await (brokerThroughputCollectorHostedService.ExecuteTask! ?? Task.CompletedTask); @@ -81,12 +105,20 @@ public async Task EnsuringExceptionsAreHandledAndThroughputCollectorKeepsGoing() var dataStore = new InMemoryLicensingDataStore(); var fakeTimeProvider = new FakeTimeProvider(); var mockedBrokerThroughputQueryThatThrowsExceptions = new MockedBrokerThroughputQueryThatThrowsExceptions(); + var emptyConfig = new ConfigurationBuilder().Build(); + using var brokerThroughputCollectorHostedService = new BrokerThroughputCollectorHostedService( NullLogger.Instance, mockedBrokerThroughputQueryThatThrowsExceptions, new ThroughputSettings(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty), - dataStore, fakeTimeProvider) - { DelayStart = TimeSpan.Zero }; + dataStore, + fakeTimeProvider, + new PlatformEndpointHelper(new ServiceControlSettings(emptyConfig)), + emptyConfig + ) + { + DelayStart = TimeSpan.Zero + }; await brokerThroughputCollectorHostedService.StartAsync(token); await Task.Run(async () => @@ -112,12 +144,20 @@ public async Task EnsuringHostedServiceStopCleanly() CancellationToken token = tokenSource.Token; var dataStore = new InMemoryLicensingDataStore(); + var emptyConfig = new ConfigurationBuilder().Build(); + using var brokerThroughputCollectorHostedService = new BrokerThroughputCollectorHostedService( NullLogger.Instance, new MockedBrokerThroughputQuery(), new ThroughputSettings(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty), - dataStore, TimeProvider.System) - { DelayStart = TimeSpan.Zero }; + dataStore, + TimeProvider.System, + new PlatformEndpointHelper(new ServiceControlSettings(emptyConfig)), + emptyConfig + ) + { + DelayStart = TimeSpan.Zero + }; await brokerThroughputCollectorHostedService.StartAsync(token); await Task.Delay(TimeSpan.FromSeconds(2), token); await brokerThroughputCollectorHostedService.StopAsync(token); @@ -197,8 +237,14 @@ public async IAsyncEnumerable GetThroughputPerDay(IBrokerQueue public async IAsyncEnumerable GetQueueNames( [EnumeratorCancellation] CancellationToken cancellationToken) { - yield return new DefaultBrokerQueue("sales@one") { SanitizedName = "sales" }; - yield return new DefaultBrokerQueue("sales@two") { SanitizedName = "sales" }; + yield return new DefaultBrokerQueue("sales@one") + { + SanitizedName = "sales" + }; + yield return new DefaultBrokerQueue("sales@two") + { + SanitizedName = "sales" + }; yield return new DefaultBrokerQueue("marketing"); yield return new DefaultBrokerQueue("customer"); diff --git a/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs b/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs index 56c9c08ad4..fc9764af80 100644 --- a/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs @@ -11,9 +11,11 @@ namespace Particular.LicensingComponent.UnitTests; using Approvals; using Contracts; using Infrastructure; +using Microsoft.Extensions.Configuration; using MonitoringThroughput; using NUnit.Framework; using ServiceControl.Transports.BrokerThroughput; +using Shared; [TestFixture] class MonitoringService_Tests : ThroughputCollectorTestFixture @@ -82,7 +84,13 @@ public async Task Should_sanitize_endpoint_name() EndpointThroughputData = new EndpointThroughputData[] { new() { Name = endpointName, Throughput = 15 } } }; - var monitoringService = new MonitoringService(DataStore, new BrokerThroughputQuery_WithSanitization()); + var emptyConfig = new ConfigurationBuilder().Build(); + + var monitoringService = new MonitoringService( + DataStore, + new ServiceControlSettings(emptyConfig), + new BrokerThroughputQuery_WithSanitization() + ); byte[] messageBytes = JsonSerializer.SerializeToUtf8Bytes(message); await monitoringService.RecordMonitoringThroughput(messageBytes, default); string endpointNameSanitized = "e-ndpoint-1"; @@ -90,7 +98,7 @@ public async Task Should_sanitize_endpoint_name() // Act Endpoint foundEndpoint = await DataStore.GetEndpoint(endpointName, ThroughputSource.Monitoring, default); - // Assert + // Assert Assert.That(foundEndpoint, Is.Not.Null, $"Expected endpoint {endpointName} not found."); Assert.That(foundEndpoint.SanitizedName, Is.EqualTo(endpointNameSanitized), $"Endpoint {endpointName} name not sanitized correctly."); diff --git a/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs b/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs index 91c90dbb95..ad2df00564 100644 --- a/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs +++ b/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs @@ -13,7 +13,9 @@ public class AuditThroughputCollectorHostedService( ILicensingDataStore dataStore, IAuditQuery auditQuery, TimeProvider timeProvider, - IBrokerThroughputQuery? brokerThroughputQuery = null) : BackgroundService + PlatformEndpointHelper platformEndpointHelper, + IBrokerThroughputQuery? brokerThroughputQuery = null + ) : BackgroundService { public TimeSpan DelayStart { get; set; } = TimeSpan.FromSeconds(40); public static List AuditQueues { get; set; } = []; @@ -98,7 +100,7 @@ Endpoint ConvertToEndpoint(ServiceControlEndpoint scEndpoint) EndpointIndicators = [EndpointIndicator.KnownEndpoint.ToString()] }; - if (PlatformEndpointHelper.IsPlatformEndpoint(scEndpoint.Name, throughputSettings)) + if (platformEndpointHelper.IsPlatformEndpoint(scEndpoint.Name, throughputSettings)) { endpoint.EndpointIndicators.Append(EndpointIndicator.PlatformEndpoint.ToString()); } diff --git a/src/Particular.LicensingComponent/BrokerThroughput/BrokerThroughputCollectorHostedService.cs b/src/Particular.LicensingComponent/BrokerThroughput/BrokerThroughputCollectorHostedService.cs index 497f7eb2f3..e56e50d465 100644 --- a/src/Particular.LicensingComponent/BrokerThroughput/BrokerThroughputCollectorHostedService.cs +++ b/src/Particular.LicensingComponent/BrokerThroughput/BrokerThroughputCollectorHostedService.cs @@ -2,6 +2,7 @@ namespace Particular.LicensingComponent.BrokerThroughput; using System.Collections.ObjectModel; using Contracts; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Persistence; @@ -9,22 +10,22 @@ namespace Particular.LicensingComponent.BrokerThroughput; using ServiceControl.Transports.BrokerThroughput; using Shared; + + public class BrokerThroughputCollectorHostedService( ILogger logger, IBrokerThroughputQuery brokerThroughputQuery, ThroughputSettings throughputSettings, ILicensingDataStore dataStore, - TimeProvider timeProvider) - : BackgroundService + TimeProvider timeProvider, + PlatformEndpointHelper platformEndpointHelper, + IConfiguration configuration +): BackgroundService { public TimeSpan DelayStart { get; set; } = TimeSpan.FromSeconds(40); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - static ReadOnlyDictionary LoadBrokerSettingValues(IEnumerable brokerKeys) - => new(brokerKeys.Select(pair => KeyValuePair.Create(pair.Key, SettingsReader.Read(ThroughputSettings.SettingsNamespace, pair.Key))) - .Where(pair => !string.IsNullOrEmpty(pair.Value)).ToDictionary()); - brokerThroughputQuery.Initialize(LoadBrokerSettingValues(brokerThroughputQuery.Settings)); if (brokerThroughputQuery.HasInitialisationErrors(out var errorMessage)) @@ -59,6 +60,15 @@ static ReadOnlyDictionary LoadBrokerSettingValues(IEnumerable LoadBrokerSettingValues(IEnumerable brokerKeys) + { + var section = configuration.GetSection(ThroughputSettings.SettingsNamespace.Root); + + return new ReadOnlyDictionary(brokerKeys.Select(pair => KeyValuePair.Create(pair.Key, section.GetValue(pair.Key))) + .Where(pair => !string.IsNullOrEmpty(pair.Value)) + .ToDictionary()); + } + async Task GatherThroughput(CancellationToken stoppingToken) { logger.LogInformation("Gathering throughput from broker"); @@ -68,7 +78,7 @@ async Task GatherThroughput(CancellationToken stoppingToken) await foreach (var queueName in brokerThroughputQuery.GetQueueNames(stoppingToken)) { - if (PlatformEndpointHelper.IsPlatformEndpoint(queueName.SanitizedName, throughputSettings)) + if (platformEndpointHelper.IsPlatformEndpoint(queueName.SanitizedName, throughputSettings)) { continue; } diff --git a/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs b/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs index bddab8ab3c..b06f8d703c 100644 --- a/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs +++ b/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs @@ -8,7 +8,11 @@ using ServiceControl.Transports.BrokerThroughput; using Shared; -public class MonitoringService(ILicensingDataStore dataStore, IBrokerThroughputQuery? brokerThroughputQuery = null) +public class MonitoringService( + ILicensingDataStore dataStore, + ServiceControlSettings serviceControlSettings, + IBrokerThroughputQuery? brokerThroughputQuery = null +) { public async Task RecordMonitoringThroughput(byte[] throughputMessage, CancellationToken cancellationToken) { @@ -61,7 +65,7 @@ public async Task TestMonitoringConnection(Cancell diagnostics.AppendLine("No throughput from Monitoring recorded in the last 30 days"); connectionTestResult.ConnectionSuccessful = false; } - diagnostics.AppendLine($"Listening on queue {ServiceControlSettings.ServiceControlThroughputDataQueue}"); + diagnostics.AppendLine($"Listening on queue {serviceControlSettings.ServiceControlThroughputDataQueue}"); connectionTestResult.Diagnostics = diagnostics.ToString(); diff --git a/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringThroughputHostedService.cs b/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringThroughputHostedService.cs index 2081075b87..cb2a34f1db 100644 --- a/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringThroughputHostedService.cs +++ b/src/Particular.LicensingComponent/MonitoringThroughput/MonitoringThroughputHostedService.cs @@ -6,7 +6,13 @@ namespace Particular.LicensingComponent.MonitoringThroughput; using ServiceControl.Transports; using Shared; -class MonitoringThroughputHostedService(ITransportCustomization transportCustomization, TransportSettings transportSettings, ILogger logger, MonitoringService monitoringService) : IHostedService +class MonitoringThroughputHostedService( + ITransportCustomization transportCustomization, + TransportSettings transportSettings, + ILogger logger, + MonitoringService monitoringService , + ServiceControlSettings serviceControlSettings + ) : IHostedService { TransportInfrastructure? transportInfrastructure; @@ -26,8 +32,8 @@ public async Task StartAsync(CancellationToken cancellationToken) { logger.LogInformation("Starting {ServiceName}", nameof(MonitoringThroughputHostedService)); - transportInfrastructure = await transportCustomization.CreateTransportInfrastructure(ServiceControlSettings.ServiceControlThroughputDataQueue, transportSettings, Handle, (_, __) => Task.FromResult(ErrorHandleResult.Handled), (_, __) => Task.CompletedTask); - await transportInfrastructure.Receivers[ServiceControlSettings.ServiceControlThroughputDataQueue].StartReceive(cancellationToken); + transportInfrastructure = await transportCustomization.CreateTransportInfrastructure(serviceControlSettings.ServiceControlThroughputDataQueue, transportSettings, Handle, (_, __) => Task.FromResult(ErrorHandleResult.Handled), (_, __) => Task.CompletedTask); + await transportInfrastructure.Receivers[serviceControlSettings.ServiceControlThroughputDataQueue].StartReceive(cancellationToken); } public async Task StopAsync(CancellationToken cancellationToken) @@ -36,7 +42,7 @@ public async Task StopAsync(CancellationToken cancellationToken) if (transportInfrastructure != null) { - await transportInfrastructure.Receivers[ServiceControlSettings.ServiceControlThroughputDataQueue].StopReceive(cancellationToken); + await transportInfrastructure.Receivers[serviceControlSettings.ServiceControlThroughputDataQueue].StopReceive(cancellationToken); await transportInfrastructure.Shutdown(cancellationToken); } } diff --git a/src/Particular.LicensingComponent/Shared/PlatformEndpointHelper.cs b/src/Particular.LicensingComponent/Shared/PlatformEndpointHelper.cs index a59f20524a..45ab862daf 100644 --- a/src/Particular.LicensingComponent/Shared/PlatformEndpointHelper.cs +++ b/src/Particular.LicensingComponent/Shared/PlatformEndpointHelper.cs @@ -3,18 +3,16 @@ using Contracts; using Particular.LicensingComponent.AuditThroughput; - public static class PlatformEndpointHelper + public class PlatformEndpointHelper(ServiceControlSettings serviceControlSettings) { - public static bool IsPlatformEndpoint(string endpointName, ThroughputSettings throughputSettings) - { - return endpointName.Equals(throughputSettings.ErrorQueue, StringComparison.OrdinalIgnoreCase) - || endpointName.Equals(throughputSettings.ServiceControlQueue, StringComparison.OrdinalIgnoreCase) - || endpointName.EndsWith(".Timeouts", StringComparison.OrdinalIgnoreCase) - || endpointName.EndsWith(".TimeoutsDispatcher", StringComparison.OrdinalIgnoreCase) - || endpointName.StartsWith($"{throughputSettings.ServiceControlQueue}.", StringComparison.OrdinalIgnoreCase) - || endpointName.Equals(ServiceControlSettings.ServiceControlThroughputDataQueue, StringComparison.OrdinalIgnoreCase) - || endpointName.StartsWith("Particular.Monitoring", StringComparison.OrdinalIgnoreCase) - || AuditThroughputCollectorHostedService.AuditQueues.Any(a => endpointName.Equals(a, StringComparison.OrdinalIgnoreCase)); - } + public bool IsPlatformEndpoint(string endpointName, ThroughputSettings throughputSettings) => + endpointName.Equals(throughputSettings.ErrorQueue, StringComparison.OrdinalIgnoreCase) + || endpointName.Equals(throughputSettings.ServiceControlQueue, StringComparison.OrdinalIgnoreCase) + || endpointName.EndsWith(".Timeouts", StringComparison.OrdinalIgnoreCase) + || endpointName.EndsWith(".TimeoutsDispatcher", StringComparison.OrdinalIgnoreCase) + || endpointName.StartsWith($"{throughputSettings.ServiceControlQueue}.", StringComparison.OrdinalIgnoreCase) + || endpointName.Equals(serviceControlSettings.ServiceControlThroughputDataQueue, StringComparison.OrdinalIgnoreCase) + || endpointName.StartsWith("Particular.Monitoring", StringComparison.OrdinalIgnoreCase) + || AuditThroughputCollectorHostedService.AuditQueues.Any(a => endpointName.Equals(a, StringComparison.OrdinalIgnoreCase)); } } diff --git a/src/Particular.LicensingComponent/Shared/ServiceControlSettings.cs b/src/Particular.LicensingComponent/Shared/ServiceControlSettings.cs index 2b54b3f5bf..92dffe4f30 100644 --- a/src/Particular.LicensingComponent/Shared/ServiceControlSettings.cs +++ b/src/Particular.LicensingComponent/Shared/ServiceControlSettings.cs @@ -1,17 +1,18 @@ namespace Particular.LicensingComponent.Shared { + using Microsoft.Extensions.Configuration; using Particular.LicensingComponent.Contracts; - using ServiceControl.Configuration; - public static class ServiceControlSettings + public class ServiceControlSettings(IConfiguration configuration) { - public static readonly string MessageTransport = "ServiceControl"; - - public static string ServiceControlThroughputDataQueueSetting = "ServiceControlThroughputDataQueue"; - public static string ServiceControlThroughputDataQueue = SettingsReader.Read(ThroughputSettings.SettingsNamespace, ServiceControlThroughputDataQueueSetting, "ServiceControl.ThroughputData"); + public const string MessageTransport = "ServiceControl"; + public const string ServiceControlThroughputDataQueueSetting = "ServiceControlThroughputDataQueue"; + public string ServiceControlThroughputDataQueue => configuration + .GetSection(ThroughputSettings.SettingsNamespace.Root) + .GetValue(ServiceControlThroughputDataQueueSetting, "ServiceControl.ThroughputData") ?? "ServiceControl.ThroughputData"; static string MonitoringQueue = $"Monitoring/{ServiceControlThroughputDataQueueSetting}"; - static string MonitoringQueueDescription = $"Queue to send monitoring throughput data to for processing by ServiceControl. This setting only needs to be specified if the Monitoring instance is not hosted in the same machine as the Error instance is running on."; + static string MonitoringQueueDescription = "Queue to send monitoring throughput data to for processing by ServiceControl. This setting only needs to be specified if the Monitoring instance is not hosted in the same machine as the Error instance is running on."; public static List GetServiceControlConnectionSettings() { @@ -23,4 +24,4 @@ public static List GetMonitoringConnectionSettings( return [new ThroughputConnectionSetting(MonitoringQueue, MonitoringQueueDescription)]; } } -} +} \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs index bcc703e4ff..48d09554e7 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/When_a_message_fails_to_import.cs @@ -34,8 +34,8 @@ public async Task It_can_be_reimported() SetSettings = settings => { - settings.ForwardErrorMessages = true; - settings.ErrorLogQueue = Conventions.EndpointNamingConvention(typeof(ErrorLogSpy)); + settings.ServiceControl.ForwardErrorMessages = true; + settings.ServiceBus.ErrorLogQueue = Conventions.EndpointNamingConvention(typeof(ErrorLogSpy)); }; var runResult = await Define() diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/StartupModeTests.cs b/src/ServiceControl.AcceptanceTests.RavenDB/StartupModeTests.cs index 02f868009a..1275379724 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/StartupModeTests.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/StartupModeTests.cs @@ -4,6 +4,7 @@ using System.Runtime.Loader; using System.Threading.Tasks; using Hosting.Commands; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using NServiceBus; @@ -22,16 +23,19 @@ class StartupModeTests : AcceptanceTest [SetUp] public async Task InitializeSettings() { + PersistenceFactory.AssemblyLoadContextResolver = static _ => AssemblyLoadContext.Default; + var transportIntegration = new ConfigureEndpointLearningTransport(); - settings = new Settings( - transportType: transportIntegration.TypeName, - forwardErrorMessages: false, - errorRetentionPeriod: TimeSpan.FromDays(1), - persisterType: "RavenDB") + settings = new Settings { - TransportConnectionString = transportIntegration.ConnectionString, - AssemblyLoadContextResolver = static _ => AssemblyLoadContext.Default + ServiceControl = + { + TransportType = transportIntegration.TypeName, + ErrorRetentionPeriod = TimeSpan.FromDays(1), + PersistenceType = "RavenDB", + ConnectionString = transportIntegration.ConnectionString, + } }; configuration = new AcceptanceTestStorageConfiguration(); @@ -47,7 +51,8 @@ public async Task CanRunMaintenanceMode() // ideally we'd be using the MaintenanceModeCommand here but that indefinitely blocks due to the RunAsync // not terminating. var hostBuilder = Host.CreateApplicationBuilder(); - hostBuilder.Services.AddPersistence(settings, maintenanceMode: true); + settings.ServiceControl.MaintenanceMode = true; + hostBuilder.AddPersistence(); // TODO: Configuration needs to be initialized using var host = hostBuilder.Build(); await host.StartAsync(); @@ -56,7 +61,7 @@ public async Task CanRunMaintenanceMode() [Test] public async Task CanRunImportFailedMessagesMode() - => await new TestableImportFailedErrorsCommand().Execute(new HostArguments(Array.Empty()), settings); + => await new TestableImportFailedErrorsCommand().Execute(new HostArguments([], maintenanceMode: false)); class TestableImportFailedErrorsCommand() : ImportFailedErrorsCommand() { @@ -65,7 +70,10 @@ protected override EndpointConfiguration CreateEndpointConfiguration(Settings se var configuration = base.CreateEndpointConfiguration(settings); //HINT: we want to exclude this assembly to prevent loading features that are part of the acceptance testing framework - var thisAssembly = new[] { typeof(StartupModeTests).Assembly.GetName().Name }; + var thisAssembly = new[] + { + typeof(StartupModeTests).Assembly.GetName().Name + }; configuration.AssemblyScanner().ExcludeAssemblies(thisAssembly); diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_a_custom_check_fails.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_a_custom_check_fails.cs index fb261e8ffd..bea5fa2469 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_a_custom_check_fails.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_a_custom_check_fails.cs @@ -44,7 +44,7 @@ public class MyContext : ScenarioContext; public class EndpointWithFailingCustomCheck : EndpointConfigurationBuilder { - public EndpointWithFailingCustomCheck() => EndpointSetup(c => { c.ReportCustomChecksTo(Settings.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); }); + public EndpointWithFailingCustomCheck() => EndpointSetup(c => { c.ReportCustomChecksTo(PrimaryOptions.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); }); class FailingCustomCheck() : CustomCheck("MyCustomCheckId", "MyCategory") { diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_a_periodic_custom_check_fails.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_a_periodic_custom_check_fails.cs index f6ee080c59..886bce425d 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_a_periodic_custom_check_fails.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_a_periodic_custom_check_fails.cs @@ -124,7 +124,7 @@ protected override async Task OnStart(IMessageSession session, CancellationToken public class WithCustomCheck : EndpointConfigurationBuilder { - public WithCustomCheck() => EndpointSetup(c => { c.ReportCustomChecksTo(Settings.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); }); + public WithCustomCheck() => EndpointSetup(c => { c.ReportCustomChecksTo(PrimaryOptions.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); }); class FailingCustomCheck(MyContext context) : NServiceBus.CustomChecks.CustomCheck("MyCustomCheckId", "MyCategory", TimeSpan.FromSeconds(5)) diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs index 40358da9ad..ef3648c1c1 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs @@ -20,8 +20,8 @@ public void SetupIngestion() { SetSettings = s => { - s.TimeToRestartErrorIngestionAfterFailure = TimeSpan.FromSeconds(1); - s.DisableHealthChecks = false; + s.ServiceControl.TimeToRestartErrorIngestionAfterFailure = TimeSpan.FromSeconds(1); + s.ServiceControl.DisableHealthChecks = false; }; } @@ -97,7 +97,7 @@ public class Sender : EndpointConfigurationBuilder public Sender() => EndpointSetup(c => { - c.ReportCustomChecksTo(Settings.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); + c.ReportCustomChecksTo(PrimaryOptions.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); c.NoRetries(); }); diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_custom_check_events_are_triggered.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_custom_check_events_are_triggered.cs index 01e03450cc..447e0ba614 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_custom_check_events_are_triggered.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_custom_check_events_are_triggered.cs @@ -44,7 +44,7 @@ public class MyContext : ScenarioContext public class EndpointWithCustomCheck : EndpointConfigurationBuilder { - public EndpointWithCustomCheck() => EndpointSetup(c => c.ReportCustomChecksTo(Settings.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1))); + public EndpointWithCustomCheck() => EndpointSetup(c => c.ReportCustomChecksTo(PrimaryOptions.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1))); public class EventuallyFailingCustomCheck() : CustomCheck("EventuallyFailingCustomCheck", "Testing", TimeSpan.FromSeconds(1)) diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_email_notifications_are_enabled.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_email_notifications_are_enabled.cs index 99472bb501..997f503dc1 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_email_notifications_are_enabled.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_email_notifications_are_enabled.cs @@ -28,8 +28,8 @@ public async Task Should_send_custom_check_status_change_emails() SetSettings = settings => { - settings.NotificationsFilter = "MyCustomCheckId#Other custom check"; - settings.EmailDropFolder = emailDropPath; + settings.ServiceControl.NotificationsFilter = "MyCustomCheckId#Other custom check"; + settings.ServiceControl.EmailDropFolder = emailDropPath; }; CustomizeHostBuilder = hostBuilder => hostBuilder.Services.AddHostedService(); @@ -87,7 +87,7 @@ public class MyContext : ScenarioContext public class EndpointWithFailingCustomCheck : EndpointConfigurationBuilder { - public EndpointWithFailingCustomCheck() => EndpointSetup(c => { c.ReportCustomChecksTo(Settings.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); }); + public EndpointWithFailingCustomCheck() => EndpointSetup(c => { c.ReportCustomChecksTo(PrimaryOptions.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); }); class FailingCustomCheck() : CustomCheck("MyCustomCheckId", "MyCategory") { diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_reporting_custom_check_with_signalr.cs b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_reporting_custom_check_with_signalr.cs index 2f97446ea2..e92d89ac4f 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_reporting_custom_check_with_signalr.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_reporting_custom_check_with_signalr.cs @@ -93,7 +93,7 @@ protected override Task OnStart(IMessageSession session, CancellationToken cance class EndpointWithCustomCheck : EndpointConfigurationBuilder { - public EndpointWithCustomCheck() => EndpointSetup(c => { c.ReportCustomChecksTo(Settings.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); }); + public EndpointWithCustomCheck() => EndpointSetup(c => { c.ReportCustomChecksTo(PrimaryOptions.DEFAULT_INSTANCE_NAME, TimeSpan.FromSeconds(1)); }); public class EventuallyFailingCustomCheck() : CustomCheck("EventuallyFailingCustomCheck", "Testing", TimeSpan.FromSeconds(1)) diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_a_custom_check_fails.cs b/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_a_custom_check_fails.cs index e4fd5e617f..f5828c5870 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_a_custom_check_fails.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_a_custom_check_fails.cs @@ -69,10 +69,10 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(MessageFailed).Assembly, Settings.DEFAULT_INSTANCE_NAME); + routing.RouteToEndpoint(typeof(MessageFailed).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); }, publisherMetadata => { - publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); + publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class CustomCheckFailedHandler(MyContext testContext) : IHandleMessages diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_a_custom_check_succeeds.cs b/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_a_custom_check_succeeds.cs index 7c4e08ac52..3d00242500 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_a_custom_check_succeeds.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_a_custom_check_succeeds.cs @@ -64,10 +64,10 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(MessageFailed).Assembly, Settings.DEFAULT_INSTANCE_NAME); + routing.RouteToEndpoint(typeof(MessageFailed).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); }, publisherMetadata => { - publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); + publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class CustomCheckSucceededHandler(MyContext testContext) : IHandleMessages diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_heartbeat_is_restored.cs b/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_heartbeat_is_restored.cs index 68335bc42a..b7b0f455a5 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_heartbeat_is_restored.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_heartbeat_is_restored.cs @@ -64,8 +64,8 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(MessageFailed).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(MessageFailed).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class FailureHandler(MyContext testContext) : IHandleMessages { diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_heartbeat_loss_is_detected.cs b/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_heartbeat_loss_is_detected.cs index df7dadf9f8..c12bfb8f99 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_heartbeat_loss_is_detected.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/ExternalIntegration/When_heartbeat_loss_is_detected.cs @@ -66,8 +66,8 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(MessageFailed).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(MessageFailed).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class FailureHandler(MyContext testContext) : IHandleMessages { diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_critical_error_is_triggered.cs b/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_critical_error_is_triggered.cs index 02edba8357..59b5c5e5bb 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_critical_error_is_triggered.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_critical_error_is_triggered.cs @@ -24,7 +24,7 @@ public async Task Service_control_is_not_killed_and_error_is_reported_via_custom SetSettings = settings => { - settings.DisableHealthChecks = false; + settings.ServiceControl.DisableHealthChecks = false; }; EventLogItem entry = null; diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_custom_check_fails.cs b/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_custom_check_fails.cs index 42cd740f21..2a4f8d5d77 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_custom_check_fails.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/InternalCustomChecks/When_a_custom_check_fails.cs @@ -32,7 +32,7 @@ public async Task Should_result_in_a_custom_check_failed_event() { SetSettings = settings => { - settings.DisableHealthChecks = false; + settings.ServiceControl.DisableHealthChecks = false; }; CustomizeHostBuilder = builder => diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_is_removed.cs b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_is_removed.cs index 0c0ba6a88c..75be349b19 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_is_removed.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_is_removed.cs @@ -64,7 +64,7 @@ class Context : ScenarioContext; public class StartingEndpoint : EndpointConfigurationBuilder { - public StartingEndpoint() => EndpointSetup(c => c.SendHeartbeatTo(Settings.DEFAULT_INSTANCE_NAME, TimeSpan.FromHours(1))); + public StartingEndpoint() => EndpointSetup(c => c.SendHeartbeatTo(PrimaryOptions.DEFAULT_INSTANCE_NAME, TimeSpan.FromHours(1))); } } } \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_starts_up.cs b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_starts_up.cs index f60472145f..c42187ea5b 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_starts_up.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_starts_up.cs @@ -45,7 +45,7 @@ public class StartingEndpoint : EndpointConfigurationBuilder public StartingEndpoint() => EndpointSetup(c => { - c.SendHeartbeatTo(Settings.DEFAULT_INSTANCE_NAME); + c.SendHeartbeatTo(PrimaryOptions.DEFAULT_INSTANCE_NAME); c.GetSettings().Set("ServiceControl.CustomHostIdentifier", hostIdentifier); c.UniquelyIdentifyRunningInstance().UsingCustomIdentifier(hostIdentifier); diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_with_heartbeat_plugin_starts_up.cs b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_with_heartbeat_plugin_starts_up.cs index 0290332a39..f257560fda 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_with_heartbeat_plugin_starts_up.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_endpoint_with_heartbeat_plugin_starts_up.cs @@ -61,7 +61,7 @@ public class MyContext : ScenarioContext; public class StartingEndpoint : EndpointConfigurationBuilder { - public StartingEndpoint() => EndpointSetup(c => c.SendHeartbeatTo(Settings.DEFAULT_INSTANCE_NAME)); + public StartingEndpoint() => EndpointSetup(c => c.SendHeartbeatTo(PrimaryOptions.DEFAULT_INSTANCE_NAME)); } } } \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_unmonitored_endpoint_is_marked_as_monitored.cs b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_unmonitored_endpoint_is_marked_as_monitored.cs index ac8b4bbd72..a00516265d 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/When_an_unmonitored_endpoint_is_marked_as_monitored.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/When_an_unmonitored_endpoint_is_marked_as_monitored.cs @@ -33,7 +33,7 @@ await Define() var options = new SendOptions(); options.DoNotEnforceBestPractices(); - options.SetDestination(Settings.DEFAULT_INSTANCE_NAME); + options.SetDestination(PrimaryOptions.DEFAULT_INSTANCE_NAME); return bus.Send(new NewEndpointDetected { diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/When_unmonitored_endpoint_starts_to_sends_heartbeats.cs b/src/ServiceControl.AcceptanceTests/Monitoring/When_unmonitored_endpoint_starts_to_sends_heartbeats.cs index 1476656f47..e05ddb4c6c 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/When_unmonitored_endpoint_starts_to_sends_heartbeats.cs +++ b/src/ServiceControl.AcceptanceTests/Monitoring/When_unmonitored_endpoint_starts_to_sends_heartbeats.cs @@ -29,7 +29,7 @@ await Define() var options = new SendOptions(); options.DoNotEnforceBestPractices(); - options.SetDestination(Settings.DEFAULT_INSTANCE_NAME); + options.SetDestination(PrimaryOptions.DEFAULT_INSTANCE_NAME); return bus.Send(new NewEndpointDetected { @@ -87,7 +87,7 @@ public class WithoutHeartbeat : EndpointConfigurationBuilder public class WithHeartbeat : EndpointConfigurationBuilder { - public WithHeartbeat() => EndpointSetup(c => { c.SendHeartbeatTo(Settings.DEFAULT_INSTANCE_NAME); }).CustomEndpointName(EndpointName); + public WithHeartbeat() => EndpointSetup(c => { c.SendHeartbeatTo(PrimaryOptions.DEFAULT_INSTANCE_NAME); }).CustomEndpointName(EndpointName); } } } \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_archived.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_archived.cs index f902d9485a..1fd181fe63 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_archived.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_archived.cs @@ -60,8 +60,8 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(FailedMessagesArchived).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(FailedMessagesArchived).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class FailureHandler(Context testContext) : IHandleMessages { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_resolved_by_retry.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_resolved_by_retry.cs index cce8c03e3f..5faba93b68 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_resolved_by_retry.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_resolved_by_retry.cs @@ -58,8 +58,8 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(FailedMessagesArchived).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(FailedMessagesArchived).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class FailureHandler(Context testContext) : IHandleMessages { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_resolved_manually.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_resolved_manually.cs index 49aea81ae0..7f81f59e77 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_resolved_manually.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_resolved_manually.cs @@ -69,8 +69,8 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(MessageFailureResolvedManually).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(MessageFailureResolvedManually).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class FailureHandler(Context testContext) : IHandleMessages { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_unarchived.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_unarchived.cs index b2b24b2460..ba756f3423 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_unarchived.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_message_is_unarchived.cs @@ -77,8 +77,8 @@ public ExternalProcessor() EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(FailedMessagesUnArchived).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(FailedMessagesUnArchived).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); } public class FailureHandler(Context testContext) : IHandleMessages diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_group_is_archived.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_group_is_archived.cs index 89535b6be2..37cab754c4 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_group_is_archived.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_group_is_archived.cs @@ -75,8 +75,8 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(FailedMessagesArchived).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(FailedMessagesArchived).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class FailureHandler(Context testContext) : IHandleMessages { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_message_has_failed_detected.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_message_has_failed_detected.cs index 6fe1140291..c876dd8429 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_message_has_failed_detected.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_message_has_failed_detected.cs @@ -75,8 +75,8 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(MessageFailed).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(MessageFailed).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class FailureHandler(MyContext testContext) : IHandleMessages { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs index c4401d3d3d..8122fab8c1 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_encountered_an_error.cs @@ -100,8 +100,8 @@ public ExternalProcessor() => EndpointSetup(c => { var routing = c.ConfigureRouting(); - routing.RouteToEndpoint(typeof(MessageFailed).Assembly, Settings.DEFAULT_INSTANCE_NAME); - }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(Settings.DEFAULT_INSTANCE_NAME); }); + routing.RouteToEndpoint(typeof(MessageFailed).Assembly, PrimaryOptions.DEFAULT_INSTANCE_NAME); + }, publisherMetadata => { publisherMetadata.RegisterPublisherFor(PrimaryOptions.DEFAULT_INSTANCE_NAME); }); public class FailureHandler(MyContext testContext) : IHandleMessages { diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_errors_with_same_uniqueid_are_imported.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_errors_with_same_uniqueid_are_imported.cs index 7da463830a..c8b3a85771 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_errors_with_same_uniqueid_are_imported.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_errors_with_same_uniqueid_are_imported.cs @@ -24,7 +24,7 @@ public async Task The_import_should_deduplicate_on_TimeOfFailure() { var criticalErrorExecuted = false; - SetSettings = settings => { settings.MaximumConcurrencyLevel = 10; }; + SetSettings = settings => { settings.ServiceControl.MaximumConcurrencyLevel = 10; }; CustomConfiguration = config => { config.DefineCriticalErrorAction((_, __) => diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/When_single_message_fails_in_batch.cs b/src/ServiceControl.AcceptanceTests/Recoverability/When_single_message_fails_in_batch.cs index 99ccbe4258..9a48946d74 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/When_single_message_fails_in_batch.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/When_single_message_fails_in_batch.cs @@ -30,7 +30,7 @@ public async Task Should_import_all_messages() SetSettings = settings => { - settings.MaximumConcurrencyLevel = maximumConcurrencyLevel; + settings.ServiceControl.MaximumConcurrencyLevel = maximumConcurrencyLevel; }; await Define(ctx => diff --git a/src/ServiceControl.AcceptanceTests/RootControllerTests.cs b/src/ServiceControl.AcceptanceTests/RootControllerTests.cs index a9f1f61898..604e34f229 100644 --- a/src/ServiceControl.AcceptanceTests/RootControllerTests.cs +++ b/src/ServiceControl.AcceptanceTests/RootControllerTests.cs @@ -19,13 +19,13 @@ public async Task Should_gather_remote_data() SetSettings = settings => { - settings.RemoteInstances = + settings.ServiceControl.RemoteInstanceSettings = [ - new RemoteInstanceSetting(settings.RootUrl), - new RemoteInstanceSetting(settings.RootUrl) + new RemoteInstanceSetting(settings.ServiceControl.RootUrl), + new RemoteInstanceSetting(settings.ServiceControl.RootUrl) ]; - serviceName = settings.InstanceName; - baseAddress = settings.RootUrl; + serviceName = settings.ServiceControl.InstanceName; + baseAddress = settings.ServiceControl.RootUrl; }; JsonArray config = null; diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs index 8862856c1e..3958400bed 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs @@ -70,7 +70,7 @@ public async Task Teardown() } #pragma warning disable IDE0060 // Remove unused parameter - protected void ExecuteWhen(Func execute, Func action, string instanceName = Settings.DEFAULT_INSTANCE_NAME) + protected void ExecuteWhen(Func execute, Func action, string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME) #pragma warning restore IDE0060 // Remove unused parameter { var timeout = TimeSpan.FromSeconds(1); diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/HttpClientServiceCollectionExtensions.cs b/src/ServiceControl.AcceptanceTests/TestSupport/HttpClientServiceCollectionExtensions.cs index d82ed852a3..8833f1a276 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/HttpClientServiceCollectionExtensions.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/HttpClientServiceCollectionExtensions.cs @@ -13,7 +13,7 @@ public static void AddHttpClientDefaultsOverrides(this IServiceCollection servic services.AddKeyedSingleton>("Forwarding", (provider, _) => () => provider.GetRequiredService().CreateHandler()); services.AddSingleton(p => new HttpMessageInvoker(p.GetRequiredKeyedService>("Forwarding")())); - foreach (var remoteInstance in settings.RemoteInstances) + foreach (var remoteInstance in settings.ServiceControl.RemoteInstanceSettings) { services.AddKeyedSingleton>(remoteInstance.InstanceId, (provider, _) => () => provider.GetRequiredService().CreateHandler()); var remoteInstanceHttpClientBuilder = services.AddHttpClient(remoteInstance.InstanceId); diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/ReportSuccessfulRetryToServiceControl.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ReportSuccessfulRetryToServiceControl.cs index dd2cd5603a..fa5ac5d4d7 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/ReportSuccessfulRetryToServiceControl.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/ReportSuccessfulRetryToServiceControl.cs @@ -16,7 +16,7 @@ public async Task Invoke(IIncomingPhysicalMessageContext context, Func AssemblyLoadContext.Default; + + + var settings = new Settings { - InstanceName = instanceName, - AllowMessageEditing = true, - ForwardErrorMessages = false, - TransportConnectionString = transportToUse.ConnectionString, - ProcessRetryBatchesFrequency = TimeSpan.FromSeconds(2), - TimeToRestartErrorIngestionAfterFailure = TimeSpan.FromSeconds(2), - MaximumConcurrencyLevel = 2, - DisableHealthChecks = true, - MessageFilter = messageContext => + Logging = + { + LogLevel = logLevel.ToString(), + LogPath = logPath + }, + ServiceControl = { - var headers = messageContext.Headers; - var id = messageContext.NativeMessageId; - var logger = LoggerUtil.CreateStaticLogger(loggingSettings.LogLevel); - headers.TryGetValue(Headers.MessageId, out var originalMessageId); - logger.LogDebug("OnMessage for message '{MessageId}'({OriginalMessageId})", id, originalMessageId ?? string.Empty); - - //Do not filter out CC, SA and HB messages as they can't be stamped - if (headers.TryGetValue(Headers.EnclosedMessageTypes, out var messageTypes) - && (messageTypes.StartsWith("ServiceControl.Contracts") || messageTypes.StartsWith("ServiceControl.EndpointPlugin"))) + TransportType = transportToUse.TypeName, + PersistenceType = persistenceToUse.PersistenceType, + ErrorRetentionPeriod = TimeSpan.FromDays(10), + InstanceName = instanceName, + AllowMessageEditing = true, + ForwardErrorMessages = false, + ConnectionString = transportToUse.ConnectionString, + ProcessRetryBatchesFrequency = TimeSpan.FromSeconds(2), + TimeToRestartErrorIngestionAfterFailure = TimeSpan.FromSeconds(2), + MaximumConcurrencyLevel = 2, + DisableHealthChecks = true, + MessageFilter = messageContext => { - return false; - } + var headers = messageContext.Headers; + var id = messageContext.NativeMessageId; + var logger = LoggerUtil.CreateStaticLogger(logLevel); + headers.TryGetValue(Headers.MessageId, out var originalMessageId); + logger.LogDebug("OnMessage for message '{MessageId}'({OriginalMessageId})", id, originalMessageId ?? string.Empty); + + //Do not filter out CC, SA and HB messages as they can't be stamped + if (headers.TryGetValue(Headers.EnclosedMessageTypes, out var messageTypes) + && (messageTypes.StartsWith("ServiceControl.Contracts") || messageTypes.StartsWith("ServiceControl.EndpointPlugin"))) + { + return false; + } + + //Do not filter out subscribe messages as they can't be stamped + if (headers.TryGetValue(Headers.MessageIntent, out var intent) + && intent == MessageIntent.Subscribe.ToString()) + { + return false; + } + + var currentSession = context.TestRunId.ToString(); + if (!headers.TryGetValue("SC.SessionID", out var session) || session != currentSession) + { + logger.LogDebug("Discarding message '{MessageId}'({OriginalMessageId}) because it's session id is '{SessionId}' instead of '{CurrentSessionId}'", id, originalMessageId ?? string.Empty, session, currentSession); + return true; + } - //Do not filter out subscribe messages as they can't be stamped - if (headers.TryGetValue(Headers.MessageIntent, out var intent) - && intent == MessageIntent.Subscribe.ToString()) - { return false; } - - var currentSession = context.TestRunId.ToString(); - if (!headers.TryGetValue("SC.SessionID", out var session) || session != currentSession) - { - logger.LogDebug("Discarding message '{MessageId}'({OriginalMessageId}) because it's session id is '{SessionId}' instead of '{CurrentSessionId}'", id, originalMessageId ?? string.Empty, session, currentSession); - return true; - } - - return false; - }, - AssemblyLoadContextResolver = static _ => AssemblyLoadContext.Default + } }; await persistenceToUse.CustomizeSettings(settings); @@ -104,7 +118,7 @@ async Task InitializeServiceControl(ScenarioContext context) using (new DiagnosticTimer($"Creating infrastructure for {instanceName}")) { var setupCommand = new SetupCommand(); - await setupCommand.Execute(new HostArguments([]), settings); + await setupCommand.Execute(new HostArguments([], maintenanceMode: false)); } var configuration = new EndpointConfiguration(instanceName); @@ -153,6 +167,6 @@ public override async Task Stop(CancellationToken cancellationToken = default) readonly Action setSettings; readonly Action customConfiguration; readonly Action hostBuilderCustomization; - readonly string instanceName = Settings.DEFAULT_INSTANCE_NAME; + readonly string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME; } } \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/WebApplicationBuilderExtensions.cs b/src/ServiceControl.AcceptanceTests/TestSupport/WebApplicationBuilderExtensions.cs index 3e7b59add5..7b9eac5f80 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/WebApplicationBuilderExtensions.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/WebApplicationBuilderExtensions.cs @@ -15,7 +15,7 @@ public static void AddServiceControlTesting(this WebApplicationBuilder hostBuild // CustomizeHostBuilder = builder => builder.ConfigureServices((hostContext, services) => services.AddHostedService()); hostBuilder.Logging.AddScenarioContextLogging(); - hostBuilder.WebHost.UseTestServer(options => options.BaseAddress = new Uri(settings.RootUrl)); + hostBuilder.WebHost.UseTestServer(options => options.BaseAddress = new Uri(settings.ServiceControl.RootUrl)); // This facilitates receiving the test server anywhere where DI is available hostBuilder.Services.AddSingleton(provider => (TestServer)provider.GetRequiredService()); diff --git a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index e0b41effe6..9cd9c46468 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -2,7 +2,6 @@ namespace ServiceControl.Audit.AcceptanceTests.TestSupport { using System; using System.Collections.Generic; - using System.Configuration; using System.IO; using System.Net.Http; using System.Runtime.Loader; @@ -16,12 +15,14 @@ namespace ServiceControl.Audit.AcceptanceTests.TestSupport using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NServiceBus; using NServiceBus.AcceptanceTesting; using NServiceBus.AcceptanceTesting.Support; using ServiceControl.Infrastructure; + using ConfigurationManager = System.Configuration.ConfigurationManager; public class ServiceControlComponentRunner( ITransportIntegration transportToUse, @@ -44,10 +45,30 @@ async Task InitializeServiceControl(ScenarioContext context) var logPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(logPath); - var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace, defaultLevel: LogLevel.Debug, logPath: logPath); + + var loggingSettings = new LoggingSettings + { + LogLevel = LogLevel.Debug, + LogPath = logPath + }; + LoggerUtil.ActiveLoggers = Loggers.Test; - settings = new Settings(transportToUse.TypeName, persistenceToUse.PersistenceType, loggingSettings) + var configuration = new ConfigurationBuilder() + // .AddInMemoryCollection(new Dictionary + // { + // { "ServiceBus:ConnectionString", "Endpoint=sb://test.servicebus.windows.net/" }, + // { "ServiceBus:Topology", """{"Topics": [{"Name": "test-topic"}]}""" }, + // { "Logging:LogLevel:Default", "Debug" } + // }) + .Build(); + + settings = new Settings( + configuration: configuration, + transportType: transportToUse.TypeName, + persisterType: persistenceToUse.PersistenceType, + loggingSettings: loggingSettings + ) { InstanceName = instanceName, TransportConnectionString = transportToUse.ConnectionString, @@ -101,13 +122,13 @@ async Task InitializeServiceControl(ScenarioContext context) using (new DiagnosticTimer($"Creating infrastructure for {instanceName}")) { var setupCommand = new SetupCommand(); - await setupCommand.Execute(new HostArguments([]), settings); + await setupCommand.Execute(new HostArguments([], settings), settings); } - var configuration = new EndpointConfiguration(instanceName); - configuration.CustomizeServiceControlAuditEndpointTesting(context); + var endpointConfiguration = new EndpointConfiguration(instanceName); + endpointConfiguration.CustomizeServiceControlAuditEndpointTesting(context); - customConfiguration(configuration); + customConfiguration(endpointConfiguration); using (new DiagnosticTimer($"Starting ServiceControl {instanceName}")) { @@ -127,7 +148,7 @@ async Task InitializeServiceControl(ScenarioContext context) }; context.Logs.Enqueue(logitem); return criticalErrorContext.Stop(cancellationToken); - }, settings, configuration); + }, settings, endpointConfiguration); hostBuilder.AddServiceControlAuditApi(); diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistenceConfiguration.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistenceConfiguration.cs index ee4b78acff..f881f68de6 100644 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistenceConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryPersistenceConfiguration.cs @@ -1,13 +1,12 @@ namespace ServiceControl.Audit.Persistence.InMemory { using System.Collections.Generic; + using Microsoft.Extensions.Configuration; - public class InMemoryPersistenceConfiguration : IPersistenceConfiguration + public sealed class InMemoryPersistenceConfiguration : IPersistenceConfiguration { public string Name => "InMemory"; - public IEnumerable ConfigurationKeys => new string[0]; - - public IPersistence Create(PersistenceSettings settings) => new InMemoryPersistence(settings); + public IPersistence Create(PersistenceSettings settings, IConfiguration configuration) => new InMemoryPersistence(settings); } } diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs index e7757101a2..319d18dd42 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs @@ -1,10 +1,10 @@ namespace ServiceControl.Audit.Persistence.RavenDB.CustomChecks { using System; - using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NServiceBus.CustomChecks; using RavenDB; @@ -37,9 +37,10 @@ public override Task PerformCheck(CancellationToken cancellationTok : CheckResult.Failed($"{percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'."); } - public static int Parse(IDictionary settings, ILogger logger) + public static int Parse(IConfiguration configuration, ILogger logger) { - if (!settings.TryGetValue(RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey, out var thresholdValue)) + var thresholdValue = configuration[RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey]; + if (string.IsNullOrWhiteSpace(thresholdValue)) { thresholdValue = $"{DataSpaceRemainingThresholdDefault}"; } diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs index 58fb1ed984..a3a8c48f46 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs @@ -5,6 +5,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NServiceBus.CustomChecks; using RavenDB; @@ -44,18 +45,9 @@ public override Task PerformCheck(CancellationToken cancellationTok return CheckResult.Failed($"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."); } - public static int Parse(IDictionary settings) + public static int Parse(IConfiguration configuration) { - if (!settings.TryGetValue(RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey, out var thresholdValue)) - { - thresholdValue = $"{MinimumStorageLeftRequiredForIngestionDefault}"; - } - - if (!int.TryParse(thresholdValue, out var threshold)) - { - Logger.LogCritical("{RavenPersistenceConfigurationMinimumStorageLeftRequiredForIngestionKey} must be an integer", RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey); - throw new Exception($"{RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey} must be an integer."); - } + int threshold = configuration.GetValue(RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey,MinimumStorageLeftRequiredForIngestionDefault); if (threshold < 0) { diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceConfiguration.cs b/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceConfiguration.cs index eced9e35b2..3c05b32bc4 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceConfiguration.cs @@ -5,17 +5,61 @@ using System.IO; using System.Reflection; using CustomChecks; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ServiceControl.Infrastructure; + // + // class RavenPersistenceSettings + // { + // // public const string DatabaseNameKey = "RavenDB/DatabaseName"; + // // public const string DatabasePathKey = "DbPath"; + // // public const string ConnectionStringKey = "RavenDB/ConnectionString"; + // // public const string ClientCertificatePathKey = "RavenDB/ClientCertificatePath"; + // // public const string ClientCertificateBase64Key = "RavenDB/ClientCertificateBase64"; + // // public const string ClientCertificatePasswordKey = "RavenDB/ClientCertificatePassword"; + // // public const string DatabaseMaintenancePortKey = "DatabaseMaintenancePort"; + // // public const string ExpirationProcessTimerInSecondsKey = "ExpirationProcessTimerInSeconds"; + // // public const string LogPathKey = "LogPath"; + // // public const string RavenDbLogLevelKey = "RavenDBLogLevel"; + // // public const string MinimumStorageLeftRequiredForIngestionKey = "MinimumStorageLeftRequiredForIngestion"; + // // public const string BulkInsertCommitTimeoutInSecondsKey = "BulkInsertCommitTimeoutInSeconds"; + // // public const string DataSpaceRemainingThresholdKey = "DataSpaceRemainingThreshold"; + // + // private string DbPath { get; set; } = string.Empty; + // public string DatabasePath => DbPath; + // + // private int DatabaseMaintenancePort { get; set; } + // public int MaintenancePort => DatabaseMaintenancePort; + // + // public int ExpirationProcessTimerInSeconds { get; set; } + // public string LogPath { get; set; } = string.Empty; + // public string RavenDBLogLevel { get; set; } = string.Empty; + // public long MinimumStorageLeftRequiredForIngestion { get; set; } + // public int BulkInsertCommitTimeoutInSeconds { get; set; } + // public double DataSpaceRemainingThreshold { get; set; } + // + // public RavenDBSettings RavenDB { get; set; } = new(); + // + // class RavenDBSettings + // { + // public string DatabaseName { get; set; } = string.Empty; + // public string ConnectionString { get; set; } = string.Empty; + // public string ClientCertificatePath { get; set; } = string.Empty; + // public string ClientCertificateBase64 { get; set; } = string.Empty; + // public string ClientCertificatePassword { get; set; } = string.Empty; + // } + // + // } + public class RavenPersistenceConfiguration : IPersistenceConfiguration { - public const string DatabaseNameKey = "RavenDB/DatabaseName"; + public const string DatabaseNameKey = "RavenDB:DatabaseName"; public const string DatabasePathKey = "DbPath"; - public const string ConnectionStringKey = "RavenDB/ConnectionString"; - public const string ClientCertificatePathKey = "RavenDB/ClientCertificatePath"; - public const string ClientCertificateBase64Key = "RavenDB/ClientCertificateBase64"; - public const string ClientCertificatePasswordKey = "RavenDB/ClientCertificatePassword"; + public const string ConnectionStringKey = "RavenDB:ConnectionString"; + public const string ClientCertificatePathKey = "RavenDB:ClientCertificatePath"; + public const string ClientCertificateBase64Key = "RavenDB:ClientCertificateBase64"; + public const string ClientCertificatePasswordKey = "RavenDB:ClientCertificatePassword"; public const string DatabaseMaintenancePortKey = "DatabaseMaintenancePort"; public const string ExpirationProcessTimerInSecondsKey = "ExpirationProcessTimerInSeconds"; public const string LogPathKey = "LogPath"; @@ -42,47 +86,61 @@ public class RavenPersistenceConfiguration : IPersistenceConfiguration public string Name => "RavenDB"; - public IPersistence Create(PersistenceSettings settings) + public IPersistence Create(PersistenceSettings settings, IConfiguration configuration) { - var databaseConfiguration = GetDatabaseConfiguration(settings); + var databaseConfiguration = GetDatabaseConfiguration(settings, configuration); return new RavenPersistence(databaseConfiguration); } - internal static DatabaseConfiguration GetDatabaseConfiguration(PersistenceSettings settings) + + + internal static DatabaseConfiguration GetDatabaseConfiguration( + PersistenceSettings settings, + IConfiguration configuration + ) { - if (!settings.PersisterSpecificSettings.TryGetValue(DatabaseNameKey, out var databaseName)) + var databaseName = configuration[DatabaseNameKey]; + if (string.IsNullOrWhiteSpace(databaseName)) { databaseName = "audit"; } ServerConfiguration serverConfiguration; - if (settings.PersisterSpecificSettings.TryGetValue(ConnectionStringKey, out var connectionString)) + var connectionString = configuration[ConnectionStringKey]; + if (!string.IsNullOrWhiteSpace(connectionString)) { - if (settings.PersisterSpecificSettings.ContainsKey(DatabasePathKey)) + var databasePath = configuration[DatabasePathKey]; + if (!string.IsNullOrWhiteSpace(databasePath)) { throw new InvalidOperationException($"{ConnectionStringKey} and {DatabasePathKey} cannot be specified at the same time."); } serverConfiguration = new ServerConfiguration(connectionString); - if (settings.PersisterSpecificSettings.TryGetValue(ClientCertificatePathKey, out var clientCertificatePath)) + var clientCertificatePath = configuration[ClientCertificatePathKey]; + if (!string.IsNullOrWhiteSpace(clientCertificatePath)) { serverConfiguration.ClientCertificatePath = clientCertificatePath; } - if (settings.PersisterSpecificSettings.TryGetValue(ClientCertificateBase64Key, out var clientCertificateBase64)) + + var clientCertificateBase64 = configuration[ClientCertificateBase64Key]; + if (!string.IsNullOrWhiteSpace(clientCertificateBase64)) { serverConfiguration.ClientCertificateBase64 = clientCertificateBase64; } - if (settings.PersisterSpecificSettings.TryGetValue(ClientCertificatePasswordKey, out var clientCertificatePassword)) + + var clientCertificatePassword = configuration[ClientCertificatePasswordKey]; + if (!string.IsNullOrWhiteSpace(clientCertificatePassword)) { serverConfiguration.ClientCertificatePassword = clientCertificatePassword; } } else { - if (!settings.PersisterSpecificSettings.TryGetValue(DatabasePathKey, out var dbPath)) + var dbPath = configuration[DatabasePathKey]; + if (string.IsNullOrWhiteSpace(dbPath)) { // SC installer always populates DBPath in app.config on installation/change/upgrade so this will only be used when // debugging or if the entry is removed manually. In those circumstances default to the folder containing the exe @@ -90,7 +148,8 @@ internal static DatabaseConfiguration GetDatabaseConfiguration(PersistenceSettin dbPath = Path.Combine(Path.GetDirectoryName(assemblyLocation), ".db"); } - if (!settings.PersisterSpecificSettings.TryGetValue(DatabaseMaintenancePortKey, out var databaseMaintenancePortString)) + var databaseMaintenancePortString = configuration[DatabaseMaintenancePortKey]; + if (string.IsNullOrWhiteSpace(databaseMaintenancePortString)) { throw new InvalidOperationException($"{DatabaseMaintenancePortKey} must be specified when using embedded server."); } @@ -102,11 +161,12 @@ internal static DatabaseConfiguration GetDatabaseConfiguration(PersistenceSettin var serverUrl = $"http://localhost:{databaseMaintenancePort}"; - var logPath = GetLogPath(settings); + var logPath = GetLogPath(configuration); var logsMode = "Operations"; - if (settings.PersisterSpecificSettings.TryGetValue(RavenDbLogLevelKey, out var ravenDbLogLevel)) + var ravenDbLogLevel = configuration[RavenDbLogLevelKey]; + if (!string.IsNullOrWhiteSpace(ravenDbLogLevel)) { logsMode = RavenDbLogLevelToLogsModeMapper.Map(ravenDbLogLevel, Logger); } @@ -114,12 +174,12 @@ internal static DatabaseConfiguration GetDatabaseConfiguration(PersistenceSettin serverConfiguration = new ServerConfiguration(dbPath, serverUrl, logPath, logsMode); } - var dataSpaceRemainingThreshold = CheckFreeDiskSpace.Parse(settings.PersisterSpecificSettings, Logger); - var minimumStorageLeftRequiredForIngestion = CheckMinimumStorageRequiredForIngestion.Parse(settings.PersisterSpecificSettings); + var dataSpaceRemainingThreshold = CheckFreeDiskSpace.Parse(configuration, Logger); + var minimumStorageLeftRequiredForIngestion = CheckMinimumStorageRequiredForIngestion.Parse(configuration); - var expirationProcessTimerInSeconds = GetExpirationProcessTimerInSeconds(settings); + var expirationProcessTimerInSeconds = GetExpirationProcessTimerInSeconds(configuration); - var bulkInsertTimeout = TimeSpan.FromSeconds(GetBulkInsertCommitTimeout(settings)); + var bulkInsertTimeout = TimeSpan.FromSeconds(GetBulkInsertCommitTimeout(configuration)); return new DatabaseConfiguration( databaseName, @@ -133,11 +193,12 @@ internal static DatabaseConfiguration GetDatabaseConfiguration(PersistenceSettin bulkInsertTimeout); } - static int GetExpirationProcessTimerInSeconds(PersistenceSettings settings) + static int GetExpirationProcessTimerInSeconds(IConfiguration configuration) { var expirationProcessTimerInSeconds = ExpirationProcessTimerInSecondsDefault; - if (settings.PersisterSpecificSettings.TryGetValue(ExpirationProcessTimerInSecondsKey, out var expirationProcessTimerInSecondsString)) + var expirationProcessTimerInSecondsString = configuration[ExpirationProcessTimerInSecondsKey]; + if (!string.IsNullOrWhiteSpace(expirationProcessTimerInSecondsString)) { expirationProcessTimerInSeconds = int.Parse(expirationProcessTimerInSecondsString); } @@ -159,11 +220,12 @@ static int GetExpirationProcessTimerInSeconds(PersistenceSettings settings) return expirationProcessTimerInSeconds; } - static int GetBulkInsertCommitTimeout(PersistenceSettings settings) + static int GetBulkInsertCommitTimeout(IConfiguration configuration) { var bulkInsertCommitTimeoutInSeconds = BulkInsertCommitTimeoutInSecondsDefault; - if (settings.PersisterSpecificSettings.TryGetValue(BulkInsertCommitTimeoutInSecondsKey, out var bulkInsertCommitTimeoutString)) + var bulkInsertCommitTimeoutString = configuration[BulkInsertCommitTimeoutInSecondsKey]; + if (!string.IsNullOrWhiteSpace(bulkInsertCommitTimeoutString)) { bulkInsertCommitTimeoutInSeconds = int.Parse(bulkInsertCommitTimeoutString); } @@ -185,9 +247,10 @@ static int GetBulkInsertCommitTimeout(PersistenceSettings settings) return bulkInsertCommitTimeoutInSeconds; } - static string GetLogPath(PersistenceSettings settings) + static string GetLogPath(IConfiguration configuration) { - if (!settings.PersisterSpecificSettings.TryGetValue(LogPathKey, out var logPath)) + var logPath = configuration[LogPathKey]; + if (string.IsNullOrWhiteSpace(logPath)) { // SC installer always populates LogPath in app.config on installation/change/upgrade so this will only be used when // debugging or if the entry is removed manually. In those circumstances default to the folder containing the exe @@ -202,4 +265,4 @@ static string GetLogPath(PersistenceSettings settings) const int BulkInsertCommitTimeoutInSecondsDefault = 60; static readonly ILogger Logger = LoggerUtil.CreateStaticLogger(); } -} +} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ConfigurationValidationTests.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ConfigurationValidationTests.cs index 855ce8573b..7bde00e2bf 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ConfigurationValidationTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ConfigurationValidationTests.cs @@ -1,6 +1,8 @@ namespace ServiceControl.UnitTests { using System; + using System.Collections.Generic; + using Microsoft.Extensions.Configuration; using NUnit.Framework; using ServiceControl.Audit.Persistence; using ServiceControl.Audit.Persistence.RavenDB; @@ -12,9 +14,14 @@ public void Should_apply_persistence_settings() { var settings = BuildSettings(); - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.ConnectionStringKey] = "connection string"; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["RavenPersistenceConfiguration.ConnectionStringKey"] = "connection string", + }) + .Build(); - var configuration = RavenPersistenceConfiguration.GetDatabaseConfiguration(settings); + var configuration = RavenPersistenceConfiguration.GetDatabaseConfiguration(settings, config); Assert.Multiple(() => { @@ -30,9 +37,14 @@ public void Should_support_external_server() var settings = BuildSettings(); var connectionString = "http://someserver:44444"; - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.ConnectionStringKey] = connectionString; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [RavenPersistenceConfiguration.ConnectionStringKey] = connectionString, + }) + .Build(); - var configuration = RavenPersistenceConfiguration.GetDatabaseConfiguration(settings); + var configuration = RavenPersistenceConfiguration.GetDatabaseConfiguration(settings,config); Assert.Multiple(() => { @@ -48,10 +60,16 @@ public void Should_support_embedded_server() var dpPath = "c://db-path"; var logPath = "c://log-path"; - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabasePathKey] = dpPath; - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabaseMaintenancePortKey] = "11111"; - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.LogPathKey] = logPath; - var configuration = RavenPersistenceConfiguration.GetDatabaseConfiguration(settings); + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [RavenPersistenceConfiguration.DatabasePathKey] = dpPath, + [RavenPersistenceConfiguration.DatabaseMaintenancePortKey] = "11111", + [RavenPersistenceConfiguration.LogPathKey] = logPath, + }) + .Build(); + + var configuration = RavenPersistenceConfiguration.GetDatabaseConfiguration(settings, config); Assert.Multiple(() => { @@ -68,9 +86,14 @@ public void Should_throw_if_port_is_missing() var settings = BuildSettings(); var dpPath = "c://some-path"; - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabasePathKey] = dpPath; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [RavenPersistenceConfiguration.DatabasePathKey] = dpPath, + }) + .Build(); - Assert.Throws(() => RavenPersistenceConfiguration.GetDatabaseConfiguration(settings)); + Assert.Throws(() => RavenPersistenceConfiguration.GetDatabaseConfiguration(settings, config)); } [Test] @@ -79,18 +102,25 @@ public void Should_throw_if_port_is_not_an_integer() var settings = BuildSettings(); var dpPath = "c://some-path"; - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabasePathKey] = dpPath; - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabaseMaintenancePortKey] = "not an int"; - Assert.Throws(() => RavenPersistenceConfiguration.GetDatabaseConfiguration(settings)); + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [RavenPersistenceConfiguration.DatabasePathKey] = dpPath, + [RavenPersistenceConfiguration.DatabaseMaintenancePortKey] = "not an int", + }) + .Build(); + + Assert.Throws(() => RavenPersistenceConfiguration.GetDatabaseConfiguration(settings, config)); } [Test] public void Should_throw_if_no_path_or_connection_string_is_configured() { var settings = BuildSettings(); + var config = new ConfigurationBuilder().Build(); - Assert.Throws(() => RavenPersistenceConfiguration.GetDatabaseConfiguration(settings)); + Assert.Throws(() => RavenPersistenceConfiguration.GetDatabaseConfiguration(settings, config)); } [Test] @@ -98,10 +128,16 @@ public void Should_throw_if_both_path_or_connection_string_is_configured() { var settings = BuildSettings(); - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabasePathKey] = "path"; - settings.PersisterSpecificSettings[RavenPersistenceConfiguration.ConnectionStringKey] = "connection string"; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [RavenPersistenceConfiguration.DatabasePathKey] = "path", + [RavenPersistenceConfiguration.ConnectionStringKey] = "connection string", + }) + .Build(); + - Assert.Throws(() => RavenPersistenceConfiguration.GetDatabaseConfiguration(settings)); + Assert.Throws(() => RavenPersistenceConfiguration.GetDatabaseConfiguration(settings, config)); } PersistenceSettings BuildSettings() diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/EmbeddedLifecycleTests.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/EmbeddedLifecycleTests.cs index 95194198ed..ec091da6ac 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/EmbeddedLifecycleTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/EmbeddedLifecycleTests.cs @@ -15,15 +15,15 @@ class EmbeddedLifecycleTests : PersistenceTestFixture public override async Task Setup() { - SetSettings = s => + SetSettings = (s,d) => { dbPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Tests", "Embedded"); logPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); var databaseMaintenancePort = PortUtility.FindAvailablePort(33335); - s.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabasePathKey] = dbPath; - s.PersisterSpecificSettings[RavenPersistenceConfiguration.LogPathKey] = logPath; - s.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabaseMaintenancePortKey] = databaseMaintenancePort.ToString(); + d[RavenPersistenceConfiguration.DatabasePathKey] = dbPath; + d[RavenPersistenceConfiguration.LogPathKey] = logPath; + d[RavenPersistenceConfiguration.DatabaseMaintenancePortKey] = databaseMaintenancePort.ToString(); }; //make sure to stop the global instance first diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs index 05a121cd16..77b3a4edb8 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/PersistenceTestsConfiguration.cs @@ -1,8 +1,10 @@ namespace ServiceControl.Audit.Persistence.Tests { using System; + using System.Collections.Generic; using System.IO; using System.Threading.Tasks; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NServiceBus.CustomChecks; @@ -30,27 +32,27 @@ class PersistenceTestsConfiguration public string Name => "RavenDB"; - public async Task Configure(Action setSettings) + public async Task Configure(Action> setSettings) { var config = new RavenPersistenceConfiguration(); var hostBuilder = Host.CreateApplicationBuilder(); var persistenceSettings = new PersistenceSettings(TimeSpan.FromHours(1), true, 100000); + var configSettings = new Dictionary(); + setSettings(persistenceSettings, configSettings); - setSettings(persistenceSettings); - - if (!persistenceSettings.PersisterSpecificSettings.ContainsKey(RavenPersistenceConfiguration.DatabasePathKey)) + if (!configSettings.ContainsKey(RavenPersistenceConfiguration.DatabasePathKey)) { var instance = await SharedEmbeddedServer.GetInstance(); - persistenceSettings.PersisterSpecificSettings[RavenPersistenceConfiguration.ConnectionStringKey] = instance.ServerUrl; + configSettings[RavenPersistenceConfiguration.ConnectionStringKey] = instance.ServerUrl; } - if (!persistenceSettings.PersisterSpecificSettings.ContainsKey(RavenPersistenceConfiguration.LogPathKey)) + if (!configSettings.ContainsKey(RavenPersistenceConfiguration.LogPathKey)) { - persistenceSettings.PersisterSpecificSettings[RavenPersistenceConfiguration.LogPathKey] = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Logs"); + configSettings[RavenPersistenceConfiguration.LogPathKey] = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Logs"); } - if (persistenceSettings.PersisterSpecificSettings.TryGetValue(RavenPersistenceConfiguration.DatabaseNameKey, out var configuredDatabaseName)) + if (configSettings.TryGetValue(RavenPersistenceConfiguration.DatabaseNameKey, out var configuredDatabaseName)) { databaseName = configuredDatabaseName; } @@ -58,10 +60,19 @@ public async Task Configure(Action setSettings) { databaseName = Guid.NewGuid().ToString(); - persistenceSettings.PersisterSpecificSettings[RavenPersistenceConfiguration.DatabaseNameKey] = databaseName; + configSettings[RavenPersistenceConfiguration.DatabaseNameKey] = databaseName; } - var persistence = config.Create(persistenceSettings); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "ServiceBus:ConnectionString", "Endpoint=sb://test.servicebus.windows.net/" }, + { "ServiceBus:Topology", """{"Topics": [{"Name": "test-topic"}]}""" }, + { "Logging:LogLevel:Default", "Debug" } + }) + .Build(); + + var persistence = config.Create(persistenceSettings, configuration); persistence.AddPersistence(hostBuilder.Services); persistence.AddInstaller(hostBuilder.Services); diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/RetentionTests.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/RetentionTests.cs index 9fef1ed7db..5681a2a0fb 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/RetentionTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/RetentionTests.cs @@ -15,10 +15,10 @@ class RetentionTests : PersistenceTestFixture { public override Task Setup() { - SetSettings = s => + SetSettings = (s,keys) => { s.AuditRetentionPeriod = TimeSpan.FromSeconds(2); - s.PersisterSpecificSettings["ExpirationProcessTimerInSeconds"] = 3.ToString(); + keys["ExpirationProcessTimerInSeconds"] = 3.ToString(); }; return base.Setup(); } diff --git a/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs b/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs index 4d89044420..5b5b78c921 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs @@ -14,7 +14,7 @@ class AuditTests : PersistenceTestFixture { public override Task Setup() { - SetSettings = s => + SetSettings = (s,d) => { s.MaxBodySizeToStore = MAX_BODY_SIZE; }; diff --git a/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs b/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs index 4edecd97a8..cee955202f 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/InMemory/PersistenceTestsConfiguration.cs @@ -1,10 +1,13 @@ namespace ServiceControl.Audit.Persistence.Tests { using System; + using System.Collections.Generic; using System.Threading.Tasks; using Auditing.BodyStorage; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Primitives; using NServiceBus.CustomChecks; using ServiceControl.Audit.Persistence.InMemory; using UnitOfWork; @@ -23,15 +26,16 @@ class PersistenceTestsConfiguration public string Name => "InMemory"; - public async Task Configure(Action setSettings) + public async Task Configure(Action> setSettings) { var config = new InMemoryPersistenceConfiguration(); var hostBuilder = Host.CreateApplicationBuilder(); var settings = new PersistenceSettings(TimeSpan.FromHours(1), true, 100000); - setSettings(settings); + setSettings(settings, null); // TODO: new Dictionary(); - var persistence = config.Create(settings); + var configuration = new ConfigurationBuilder().Build(); + var persistence = config.Create(settings, configuration); persistence.AddPersistence(hostBuilder.Services); persistence.AddInstaller(hostBuilder.Services); diff --git a/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs b/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs index eab82cd25c..1050b66505 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/PersistenceTestFixture.cs @@ -1,6 +1,8 @@ namespace ServiceControl.Audit.Persistence.Tests { using System; + using System.Collections; + using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -13,7 +15,7 @@ [TestFixture] abstract class PersistenceTestFixture { - public Action SetSettings = _ => { }; + public Action> SetSettings = (_,_) => { }; [SetUp] public virtual Task Setup() diff --git a/src/ServiceControl.Audit.Persistence/IPersistenceConfiguration.cs b/src/ServiceControl.Audit.Persistence/IPersistenceConfiguration.cs index 360b26c94f..dcd342f7b8 100644 --- a/src/ServiceControl.Audit.Persistence/IPersistenceConfiguration.cs +++ b/src/ServiceControl.Audit.Persistence/IPersistenceConfiguration.cs @@ -1,13 +1,11 @@ namespace ServiceControl.Audit.Persistence { - using System.Collections.Generic; + using Microsoft.Extensions.Configuration; public interface IPersistenceConfiguration { string Name { get; } - IEnumerable ConfigurationKeys { get; } - - IPersistence Create(PersistenceSettings settings); + IPersistence Create(PersistenceSettings settings, IConfiguration configuration); } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence/PersistenceSettings.cs b/src/ServiceControl.Audit.Persistence/PersistenceSettings.cs index b5b51676de..5b80165dea 100644 --- a/src/ServiceControl.Audit.Persistence/PersistenceSettings.cs +++ b/src/ServiceControl.Audit.Persistence/PersistenceSettings.cs @@ -2,29 +2,17 @@ { using System; using System.Collections.Generic; + using Microsoft.Extensions.Configuration; - public class PersistenceSettings + public sealed class PersistenceSettings( + TimeSpan auditRetentionPeriod, + bool enableFullTextSearchOnBodies, + int maxBodySizeToStore + ) { - public PersistenceSettings( - TimeSpan auditRetentionPeriod, - bool enableFullTextSearchOnBodies, - int maxBodySizeToStore) - { - AuditRetentionPeriod = auditRetentionPeriod; - EnableFullTextSearchOnBodies = enableFullTextSearchOnBodies; - MaxBodySizeToStore = maxBodySizeToStore; - - PersisterSpecificSettings = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - public bool MaintenanceMode { get; set; } - - public TimeSpan AuditRetentionPeriod { get; set; } - - public bool EnableFullTextSearchOnBodies { get; set; } - - public int MaxBodySizeToStore { get; set; } - - public IDictionary PersisterSpecificSettings { get; } + public TimeSpan AuditRetentionPeriod { get; set; } = auditRetentionPeriod; + public bool EnableFullTextSearchOnBodies { get; set; } = enableFullTextSearchOnBodies; + public int MaxBodySizeToStore { get; set; } = maxBodySizeToStore; } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.UnitTests/API/APIApprovals.cs b/src/ServiceControl.Audit.UnitTests/API/APIApprovals.cs index 60663693db..fb764e4f54 100644 --- a/src/ServiceControl.Audit.UnitTests/API/APIApprovals.cs +++ b/src/ServiceControl.Audit.UnitTests/API/APIApprovals.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; + using Microsoft.Extensions.Configuration; using NUnit.Framework; using Particular.Approvals; @@ -21,8 +22,19 @@ class APIApprovals [Test] public void RootPathValue() { - var httpContext = new DefaultHttpContext { Request = { Scheme = "http", Host = new HostString("localhost") } }; - var actionContext = new ActionContext { HttpContext = httpContext, RouteData = new RouteData(), ActionDescriptor = new ControllerActionDescriptor() }; + var httpContext = new DefaultHttpContext + { + Request = + { + Scheme = "http", Host = new HostString("localhost") + } + }; + var actionContext = new ActionContext + { + HttpContext = httpContext, + RouteData = new RouteData(), + ActionDescriptor = new ControllerActionDescriptor() + }; var controllerContext = new ControllerContext(actionContext); var settings = CreateTestSettings(); @@ -48,9 +60,9 @@ public void HttpApiRoutes() var type = method.DeclaringType; var httpMethods = method.GetCustomAttributes(true) .OfType() - .SelectMany(att => att.HttpMethods.Select(m => m)) - .Distinct() - .OrderBy(httpMethod => httpMethod) + .SelectMany(att => att.HttpMethods.Select(m => m)) + .Distinct() + .OrderBy(httpMethod => httpMethod) .ToArray(); if (!httpMethods.Any()) @@ -76,6 +88,7 @@ public void HttpApiRoutes() { builder.AppendLine($"{item.HttpMethods} /{item.Route} => {item.MethodSignature}"); } + var httpApi = builder.ToString(); Console.Write(httpApi); @@ -128,6 +141,10 @@ public void PlatformSampleSettings() Approver.Verify(settings); } - static Settings CreateTestSettings() => new("LearningTransport", "InMemory"); + static Settings CreateTestSettings() => new( + configuration: new ConfigurationBuilder().Build(), + transportType: "LearningTransport", + persisterType: "InMemory" + ); } } \ No newline at end of file diff --git a/src/ServiceControl.Audit.UnitTests/Infrastructure/When_instance_is_setup.cs b/src/ServiceControl.Audit.UnitTests/Infrastructure/When_instance_is_setup.cs index f01c4595ea..13e47c43a3 100644 --- a/src/ServiceControl.Audit.UnitTests/Infrastructure/When_instance_is_setup.cs +++ b/src/ServiceControl.Audit.UnitTests/Infrastructure/When_instance_is_setup.cs @@ -8,6 +8,7 @@ using Audit.Infrastructure.Hosting; using Audit.Infrastructure.Hosting.Commands; using Audit.Infrastructure.Settings; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NServiceBus; using NServiceBus.Transport; @@ -21,20 +22,29 @@ public async Task Should_provision_queues() { var manifest = new TransportManifest { - Definitions = [new TransportManifestDefinition - { - Name = "FakeTransport", - Location = AppContext.BaseDirectory, - AssemblyName = Assembly.GetExecutingAssembly().GetName().Name, - TypeName = typeof(FakeTransport).AssemblyQualifiedName - }] + Definitions = + [ + new TransportManifestDefinition + { + Name = "FakeTransport", + Location = AppContext.BaseDirectory, + AssemblyName = Assembly.GetExecutingAssembly().GetName().Name, + TypeName = typeof(FakeTransport).AssemblyQualifiedName + } + ] }; TransportManifestLibrary.TransportManifests.Add(manifest); var instanceInputQueueName = "SomeInstanceQueue"; - var settings = new Settings("FakeTransport", "InMemory") + var emptyConfig = new ConfigurationBuilder().Build(); + + var settings = new Settings( + configuration: emptyConfig, + transportType: "FakeTransport", + persisterType: "InMemory" + ) { InstanceName = instanceInputQueueName, ForwardAuditMessages = true, @@ -42,14 +52,11 @@ public async Task Should_provision_queues() }; var setupCommand = new SetupCommand(); - await setupCommand.Execute(new HostArguments([]), settings); + await setupCommand.Execute(new HostArguments([], settings), settings); Assert.That(FakeTransport.QueuesCreated, Is.EquivalentTo(new[] { - instanceInputQueueName, - $"{instanceInputQueueName}.Errors", - settings.AuditQueue, - settings.AuditLogQueue + instanceInputQueueName, $"{instanceInputQueueName}.Errors", settings.AuditQueue, settings.AuditLogQueue })); } } @@ -88,6 +95,7 @@ public Task CreateTransportInfrastructure(string name, OnMessage onMessage = null, OnError onError = null, Func onCriticalError = null, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) => throw new NotImplementedException(); + public string ToTransportQualifiedQueueName(string queueName) => queueName; } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs b/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs index d65a66697f..9247c5ac75 100644 --- a/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl.Audit/HostApplicationBuilderExtensions.cs @@ -28,13 +28,16 @@ static class HostApplicationBuilderExtensions { static readonly string InstanceVersion = FileVersionInfo.GetVersionInfo(typeof(HostApplicationBuilderExtensions).Assembly.Location).ProductVersion; - public static void AddServiceControlAudit(this IHostApplicationBuilder builder, + public static void AddServiceControlAudit( + this IHostApplicationBuilder builder, Func onCriticalError, Settings settings, EndpointConfiguration configuration) { + var section = builder.Configuration.GetSection(Settings.SectionName); + var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings); - var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings); + var persistenceSettings = PersistenceConfigurationFactory.BuildPersistenceSettings(settings, section); RecordStartup(settings, configuration, persistenceConfiguration); @@ -65,7 +68,7 @@ public static void AddServiceControlAudit(this IHostApplicationBuilder builder, // directly and to make things more complex of course the order of registration still matters ;) services.AddSingleton(provider => new Lazy(provider.GetRequiredService)); - services.AddPersistence(persistenceSettings, persistenceConfiguration); + services.AddPersistence(persistenceSettings, persistenceConfiguration, section); NServiceBusFactory.Configure(settings, transportCustomization, transportSettings, onCriticalError, configuration); builder.UseNServiceBus(configuration); @@ -87,9 +90,12 @@ public static void AddServiceControlAudit(this IHostApplicationBuilder builder, public static void AddServiceControlAuditInstallers(this IHostApplicationBuilder builder, Settings settings) { + var section = builder.Configuration.GetSection(Settings.SectionName); + var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings); - var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings); - builder.Services.AddInstaller(persistenceSettings, persistenceConfiguration); + var persistenceSettings = PersistenceConfigurationFactory.BuildPersistenceSettings(settings, section); + + builder.Services.AddInstaller(persistenceSettings, persistenceConfiguration, section); } public static void AddMetrics(this IHostApplicationBuilder builder, Settings settings) diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs index 4a92ce6b41..e333e62511 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/MaintenanceModeCommand.cs @@ -12,12 +12,15 @@ class MaintenanceModeCommand : AbstractCommand public override async Task Execute(HostArguments args, Settings settings) { var persistenceConfiguration = PersistenceConfigurationFactory.LoadPersistenceConfiguration(settings); - var persistenceSettings = persistenceConfiguration.BuildPersistenceSettings(settings); + + var hostBuilder = Host.CreateApplicationBuilder(); + hostBuilder.Configuration.AddLegacyAppSettings(); + + var persistenceSettings = PersistenceConfigurationFactory.BuildPersistenceSettings(settings, hostBuilder.Configuration); persistenceSettings.MaintenanceMode = true; - var hostBuilder = Host.CreateApplicationBuilder(); - hostBuilder.Services.AddPersistence(persistenceSettings, persistenceConfiguration); + hostBuilder.Services.AddPersistence(persistenceSettings, persistenceConfiguration, hostBuilder.Configuration); if (WindowsServiceHelpers.IsWindowsService()) { diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/HostArguments.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/HostArguments.cs index 5221dcdc0a..abdac54b72 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/HostArguments.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/HostArguments.cs @@ -9,9 +9,9 @@ namespace ServiceControl.Audit.Infrastructure.Hosting class HostArguments { - public HostArguments(string[] args) + public HostArguments(string[] args, Settings settings) { - if (SettingsReader.Read(Settings.SettingsRootNamespace, "MaintenanceMode")) + if (settings.MaintenanceMode) // TODO: Hack.... { args = [.. args, "-m"]; } diff --git a/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs b/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs index dd409f0334..3badf81e5d 100644 --- a/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs @@ -1,38 +1,54 @@ namespace ServiceControl.Audit.Infrastructure.Settings { using System; - using System.Configuration; using System.Runtime.Loader; using System.Text.Json.Serialization; using Configuration; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NLog.Common; using NServiceBus.Transport; using ServiceControl.Infrastructure; using Transports; + using ConfigurationManager = System.Configuration.ConfigurationManager; public class Settings { - public Settings(string transportType = null, string persisterType = null, LoggingSettings loggingSettings = null) + // TODO: All kinds of validation happening but validation and deserialization should be split + public Settings( + IConfiguration configuration, + string transportType = null, + string persisterType = null, + LoggingSettings loggingSettings = null + ) { - LoggingSettings = loggingSettings ?? new(SettingsRootNamespace); + // TODO: Consider GetRequiredSection ? + IConfiguration serviceBusSection = configuration.GetSection(SectionNameServiceBus); + IConfiguration serviceControlAuditsection = configuration.GetSection(SectionName); + IConfiguration serviceControlSection = configuration.GetSection(SectionNameServiceControl); + + LoggingSettings = loggingSettings; // TODO: ?? new(); // Overwrite the instance name if it is specified in ENVVAR, reg, or config file -- LEGACY SETTING NAME - InstanceName = SettingsReader.Read(SettingsRootNamespace, "InternalQueueName", InstanceName); + InstanceName = serviceControlAuditsection.GetValue("InternalQueueName", InstanceName); // Overwrite the instance name if it is specified in ENVVAR, reg, or config file - InstanceName = SettingsReader.Read(SettingsRootNamespace, "InstanceName", InstanceName); + InstanceName = serviceControlAuditsection.GetValue("InstanceName", InstanceName); - TransportType = transportType ?? SettingsReader.Read(SettingsRootNamespace, "TransportType"); + TransportType = transportType ?? serviceControlAuditsection.GetValue("TransportType"); - PersistenceType = persisterType ?? SettingsReader.Read(SettingsRootNamespace, "PersistenceType"); + PersistenceType = persisterType ?? serviceControlAuditsection.GetValue("PersistenceType"); - TransportConnectionString = GetConnectionString(); + TransportConnectionString = GetConnectionString(serviceControlAuditsection); - LoadAuditQueueInformation(); + LoadAuditQueueInformation( + serviceControlAuditsection, + serviceBusSection, + serviceControlSection + ); //TODO: Ugly, extract to its own settings class - ForwardAuditMessages = GetForwardAuditMessages(); - AuditRetentionPeriod = GetAuditRetentionPeriod(); + ForwardAuditMessages = GetForwardAuditMessages(serviceControlAuditsection); + AuditRetentionPeriod = GetAuditRetentionPeriod(serviceControlAuditsection); if (AppEnvironment.RunningInContainer) { @@ -41,44 +57,53 @@ public Settings(string transportType = null, string persisterType = null, Loggin } else { - Hostname = SettingsReader.Read(SettingsRootNamespace, "Hostname", "localhost"); - Port = SettingsReader.Read(SettingsRootNamespace, "Port", 44444); + Hostname = serviceControlAuditsection.GetValue("Hostname", "localhost"); + Port = serviceControlAuditsection.GetValue("Port", 44444); } - MaximumConcurrencyLevel = SettingsReader.Read(SettingsRootNamespace, "MaximumConcurrencyLevel"); - ServiceControlQueueAddress = SettingsReader.Read(SettingsRootNamespace, "ServiceControlQueueAddress"); - TimeToRestartAuditIngestionAfterFailure = GetTimeToRestartAuditIngestionAfterFailure(); - EnableFullTextSearchOnBodies = SettingsReader.Read(SettingsRootNamespace, "EnableFullTextSearchOnBodies", true); - ShutdownTimeout = SettingsReader.Read(SettingsRootNamespace, "ShutdownTimeout", ShutdownTimeout); + MaximumConcurrencyLevel = serviceControlAuditsection.GetValue("MaximumConcurrencyLevel"); + ServiceControlQueueAddress = serviceControlAuditsection.GetValue("ServiceControlQueueAddress"); + TimeToRestartAuditIngestionAfterFailure = GetTimeToRestartAuditIngestionAfterFailure(serviceControlAuditsection); + EnableFullTextSearchOnBodies = serviceControlAuditsection.GetValue("EnableFullTextSearchOnBodies", true); + ShutdownTimeout = serviceControlAuditsection.GetValue("ShutdownTimeout", ShutdownTimeout); AssemblyLoadContextResolver = static assemblyPath => new PluginAssemblyLoadContext(assemblyPath); + + PrintMetrics = serviceControlAuditsection.GetValue("PrintMetrics"); + OtlpEndpointUrl = serviceControlAuditsection.GetValue(nameof(OtlpEndpointUrl)); + VirtualDirectory = serviceControlAuditsection.GetValue("VirtualDirectory", string.Empty); + maxBodySizeToStore = serviceControlAuditsection.GetValue("MaxBodySizeToStore", MaxBodySizeToStoreDefault); + ValidateConfiguration = serviceControlAuditsection.GetValue("ValidateConfig", true); + + Name = serviceControlAuditsection.GetValue("Name", "ServiceControl.Audit"); + Description = serviceControlAuditsection.GetValue("Description", "The audit backend for the Particular Service Platform"); + MaintenanceMode = serviceControlAuditsection.GetValue("MaintenanceMode", false); } - void LoadAuditQueueInformation() + void LoadAuditQueueInformation( + IConfiguration serviceControlAuditsection, + IConfiguration serviceBusSection, + IConfiguration serviceControlSection + ) { - var serviceBusRootNamespace = new SettingsRootNamespace("ServiceBus"); - AuditQueue = SettingsReader.Read(serviceBusRootNamespace, "AuditQueue", "audit"); + AuditQueue = serviceBusSection.GetValue("AuditQueue", "audit"); if (string.IsNullOrEmpty(AuditQueue)) { throw new Exception("ServiceBus/AuditQueue value is required to start the instance"); } - if (!SettingsReader.TryRead(SettingsRootNamespace, "IngestAuditMessages", out bool ingestAuditMessages)) - { - // Backwards compatibility - var serviceControlNamespace = new SettingsRootNamespace("ServiceControl"); - ingestAuditMessages = SettingsReader.Read(serviceControlNamespace, "IngestAuditMessages", true); - } - - IngestAuditMessages = ingestAuditMessages; + IngestAuditMessages = serviceControlAuditsection.GetValue("IngestAuditMessages") + // Backwards compatibility + // TODO: How far does this go back? + ?? serviceControlSection.GetValue("IngestAuditMessages", true); if (IngestAuditMessages == false) { logger.LogInformation("Audit ingestion disabled"); } - AuditLogQueue = SettingsReader.Read(serviceBusRootNamespace, "AuditLogQueue", null); + AuditLogQueue = serviceBusSection.GetValue("AuditLogQueue", null); if (AuditLogQueue == null) { @@ -87,15 +112,14 @@ void LoadAuditQueueInformation() } } - [JsonIgnore] - public Func AssemblyLoadContextResolver { get; set; } + [JsonIgnore] public Func AssemblyLoadContextResolver { get; set; } public LoggingSettings LoggingSettings { get; } //HINT: acceptance tests only public Func MessageFilter { get; set; } - public bool ValidateConfiguration => SettingsReader.Read(SettingsRootNamespace, "ValidateConfig", true); + public bool ValidateConfiguration { get; } public string RootUrl { @@ -116,10 +140,10 @@ public string RootUrl public int Port { get; set; } - public bool PrintMetrics => SettingsReader.Read(SettingsRootNamespace, "PrintMetrics"); - public string OtlpEndpointUrl { get; set; } = SettingsReader.Read(SettingsRootNamespace, nameof(OtlpEndpointUrl)); + public bool PrintMetrics { get; set; } + public string OtlpEndpointUrl { get; set; } public string Hostname { get; private set; } - public string VirtualDirectory => SettingsReader.Read(SettingsRootNamespace, "VirtualDirectory", string.Empty); + public string VirtualDirectory { get; set; } public string TransportType { get; private set; } @@ -179,10 +203,10 @@ public TransportSettings ToTransportSettings() return transportSettings; } - TimeSpan GetTimeToRestartAuditIngestionAfterFailure() + TimeSpan GetTimeToRestartAuditIngestionAfterFailure(IConfiguration section) { string message; - var valueRead = SettingsReader.Read(SettingsRootNamespace, "TimeToRestartAuditIngestionAfterFailure"); + var valueRead = section.GetValue("TimeToRestartAuditIngestionAfterFailure"); if (valueRead == null) { return TimeSpan.FromSeconds(60); @@ -214,9 +238,9 @@ TimeSpan GetTimeToRestartAuditIngestionAfterFailure() return result; } - static bool GetForwardAuditMessages() + static bool GetForwardAuditMessages(IConfiguration serviceControlAuditsection) { - var forwardAuditMessages = SettingsReader.Read(SettingsRootNamespace, "ForwardAuditMessages"); + var forwardAuditMessages = serviceControlAuditsection.GetValue("ForwardAuditMessages"); if (forwardAuditMessages.HasValue) { return forwardAuditMessages.Value; @@ -225,9 +249,9 @@ static bool GetForwardAuditMessages() return false; } - static string GetConnectionString() + static string GetConnectionString(IConfiguration serviceControlAuditsection) { - var settingsValue = SettingsReader.Read(SettingsRootNamespace, "ConnectionString"); + var settingsValue = serviceControlAuditsection.GetValue("ConnectionString"); if (settingsValue != null) { return settingsValue; @@ -237,10 +261,10 @@ static string GetConnectionString() return connectionStringSettings?.ConnectionString; } - TimeSpan GetAuditRetentionPeriod() + TimeSpan GetAuditRetentionPeriod(IConfiguration serviceControlAuditsection) { string message; - var valueRead = SettingsReader.Read(SettingsRootNamespace, "AuditRetentionPeriod"); + var valueRead = serviceControlAuditsection.GetValue("AuditRetentionPeriod"); if (valueRead == null) { // SCMU actually defaults to 7 days, as does Dockerfile, but a change to same-up everything should be done in a major @@ -291,11 +315,18 @@ static string Subscope(string address) // logger is intentionally not static to prevent it from being initialized before LoggingConfigurator.ConfigureLogging has been called readonly ILogger logger = LoggerUtil.CreateStaticLogger(); - int maxBodySizeToStore = SettingsReader.Read(SettingsRootNamespace, "MaxBodySizeToStore", MaxBodySizeToStoreDefault); + int maxBodySizeToStore; public const string DEFAULT_INSTANCE_NAME = "Particular.ServiceControl.Audit"; - public static readonly SettingsRootNamespace SettingsRootNamespace = new("ServiceControl.Audit"); + public const string SectionName = "ServiceControl.Audit"; + public const string SectionNameServiceBus = "ServiceBus"; + public const string SectionNameServiceControl = "ServiceControl"; const int MaxBodySizeToStoreDefault = 102400; //100 kb + + public string Name { get; } + public string Description { get; } + + public bool MaintenanceMode { get; } } } \ No newline at end of file diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/RootController.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/RootController.cs index 159e8c6d49..332dde10ea 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/RootController.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/RootController.cs @@ -30,8 +30,8 @@ public OkObjectResult Urls() EndpointsMessagesUrl = baseUrl + "endpoints/{name}/messages/{?page,per_page,direction,sort}", AuditCountUrl = baseUrl + "endpoints/{name}/audit-count", - Name = SettingsReader.Read(Settings.SettingsRootNamespace, "Name", "ServiceControl.Audit"), - Description = SettingsReader.Read(Settings.SettingsRootNamespace, "Description", "The audit backend for the Particular Service Platform"), + Name = settings.Name, + Description = settings.Description, Configuration = baseUrl + "configuration" }; diff --git a/src/ServiceControl.Audit/Persistence/PersistenceConfigurationFactory.cs b/src/ServiceControl.Audit/Persistence/PersistenceConfigurationFactory.cs index d79ee76fe0..71194ce134 100644 --- a/src/ServiceControl.Audit/Persistence/PersistenceConfigurationFactory.cs +++ b/src/ServiceControl.Audit/Persistence/PersistenceConfigurationFactory.cs @@ -3,6 +3,7 @@ namespace ServiceControl.Audit.Persistence using System; using System.IO; using Configuration; + using Microsoft.Extensions.Configuration; using ServiceControl.Audit.Infrastructure.Settings; static class PersistenceConfigurationFactory @@ -16,6 +17,7 @@ public static IPersistenceConfiguration LoadPersistenceConfiguration(Settings se var loadContext = settings.AssemblyLoadContextResolver(assemblyPath); var customizationType = Type.GetType(persistenceManifest.TypeName, loadContext.LoadFromAssemblyName, null, true); + // TODO: Why not just a big switch and have all types accessible by referencing all persisters? return (IPersistenceConfiguration)Activator.CreateInstance(customizationType); } catch (Exception e) @@ -24,19 +26,16 @@ public static IPersistenceConfiguration LoadPersistenceConfiguration(Settings se } } - public static PersistenceSettings BuildPersistenceSettings(this IPersistenceConfiguration persistenceConfiguration, Settings settings) + public static PersistenceSettings BuildPersistenceSettings( + Settings settings, + IConfiguration configuration // TODO: Remove this dependency + ) { - var persistenceSettings = new PersistenceSettings(settings.AuditRetentionPeriod, settings.EnableFullTextSearchOnBodies, settings.MaxBodySizeToStore); - - foreach (var key in persistenceConfiguration.ConfigurationKeys) - { - var value = SettingsReader.Read(Settings.SettingsRootNamespace, key, null); - if (!string.IsNullOrWhiteSpace(value)) - { - persistenceSettings.PersisterSpecificSettings[key] = value; - } - } - + var persistenceSettings = new PersistenceSettings( + auditRetentionPeriod: settings.AuditRetentionPeriod, + enableFullTextSearchOnBodies: settings.EnableFullTextSearchOnBodies, + maxBodySizeToStore: settings.MaxBodySizeToStore + ); return persistenceSettings; } } diff --git a/src/ServiceControl.Audit/Persistence/PersistenceServiceCollectionExtensions.cs b/src/ServiceControl.Audit/Persistence/PersistenceServiceCollectionExtensions.cs index 7da28426fa..32339052f2 100644 --- a/src/ServiceControl.Audit/Persistence/PersistenceServiceCollectionExtensions.cs +++ b/src/ServiceControl.Audit/Persistence/PersistenceServiceCollectionExtensions.cs @@ -1,22 +1,27 @@ namespace ServiceControl.Audit.Persistence { + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; static class PersistenceServiceCollectionExtensions { public static void AddPersistence(this IServiceCollection services, PersistenceSettings persistenceSettings, - IPersistenceConfiguration persistenceConfiguration) + IPersistenceConfiguration persistenceConfiguration, + IConfiguration configuration + ) { - var persistence = persistenceConfiguration.Create(persistenceSettings); + var persistence = persistenceConfiguration.Create(persistenceSettings, configuration); persistence.AddPersistence(services); } public static void AddInstaller(this IServiceCollection services, PersistenceSettings persistenceSettings, - IPersistenceConfiguration persistenceConfiguration) + IPersistenceConfiguration persistenceConfiguration, + IConfiguration configuration + ) { - var persistence = persistenceConfiguration.Create(persistenceSettings); + var persistence = persistenceConfiguration.Create(persistenceSettings, configuration); persistence.AddInstaller(services); } } diff --git a/src/ServiceControl.Audit/Program.cs b/src/ServiceControl.Audit/Program.cs index dbe5268e93..bc73690907 100644 --- a/src/ServiceControl.Audit/Program.cs +++ b/src/ServiceControl.Audit/Program.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ServiceControl.Audit.Infrastructure.Hosting; using ServiceControl.Audit.Infrastructure.Hosting.Commands; @@ -11,7 +12,15 @@ try { - var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); + var bootstrapConfig = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddLegacyAppSettings() + .AddEnvironmentVariables() + .Build(); + + var serviceControlAuditSection = bootstrapConfig.GetSection(Settings.SectionName); + + var loggingSettings = LoggingSettingsFactory.Create(serviceControlAuditSection); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); @@ -27,7 +36,12 @@ ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - var arguments = new HostArguments(args); + var settings = new Settings( + bootstrapConfig, + loggingSettings: loggingSettings + ); + + var arguments = new HostArguments(args, settings); if (arguments.Help) { @@ -35,8 +49,6 @@ return 0; } - var settings = new Settings(loggingSettings: loggingSettings); - await new CommandRunner(arguments.Command).Execute(arguments, settings); return 0; @@ -52,6 +64,7 @@ LoggingConfigurator.ConfigureNLog("bootstrap.${shortdate}.txt", "./", NLog.LogLevel.Fatal); NLog.LogManager.GetCurrentClassLogger().Fatal(ex, "Unrecoverable error"); } + throw; } finally diff --git a/src/ServiceControl.Configuration/ExeConfiguration.cs b/src/ServiceControl.Configuration/ExeConfiguration.cs index 2f0b0f3c5b..519f526021 100644 --- a/src/ServiceControl.Configuration/ExeConfiguration.cs +++ b/src/ServiceControl.Configuration/ExeConfiguration.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Configuration { + using System; using System.Configuration; using System.IO; using System.Linq; @@ -10,6 +11,7 @@ public static class ExeConfiguration { // ConfigurationManager on .NET is looking for {assembly}.dll.config files, but all previous versions of ServiceControl will have {assembly}.exe.config instead. // This code reads in the exe.config files and adds all the values into the ConfigurationManager's collections. + [Obsolete("TODO: Check if this is still needed if all settings or can be integrared in the LegacyAppSettingsConfigurationProvider or maybe even a new provider")] public static void PopulateAppSettings(Assembly assembly) { var location = Path.GetDirectoryName(assembly.Location); diff --git a/src/ServiceControl.Configuration/LegacyAppSettingsConfigurationProvider.cs b/src/ServiceControl.Configuration/LegacyAppSettingsConfigurationProvider.cs new file mode 100644 index 0000000000..6096bddc9e --- /dev/null +++ b/src/ServiceControl.Configuration/LegacyAppSettingsConfigurationProvider.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using ConfigurationManager = System.Configuration.ConfigurationManager; + +public class LegacyAppSettingsConfigurationProvider : ConfigurationProvider +{ + public override void Load() + { + var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var key in ConfigurationManager.AppSettings.AllKeys) + { + if (key is null) + { + continue; + } + + var normalizedKey = key.Replace('/', ':'); + data[normalizedKey] = ConfigurationManager.AppSettings[key]; + } + + Data = data; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Configuration/LegacyAppSettingsConfigurationSource.cs b/src/ServiceControl.Configuration/LegacyAppSettingsConfigurationSource.cs new file mode 100644 index 0000000000..93b089b150 --- /dev/null +++ b/src/ServiceControl.Configuration/LegacyAppSettingsConfigurationSource.cs @@ -0,0 +1,7 @@ +using Microsoft.Extensions.Configuration; + +public class LegacyAppSettingsConfigurationSource : IConfigurationSource +{ + public IConfigurationProvider Build(IConfigurationBuilder builder) => + new LegacyAppSettingsConfigurationProvider(); +} \ No newline at end of file diff --git a/src/ServiceControl.Configuration/LegacyAppSettingsExtensions.cs b/src/ServiceControl.Configuration/LegacyAppSettingsExtensions.cs new file mode 100644 index 0000000000..78b18ac526 --- /dev/null +++ b/src/ServiceControl.Configuration/LegacyAppSettingsExtensions.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Configuration; + +public static class LegacyAppSettingsExtensions +{ + public static IConfigurationBuilder AddLegacyAppSettings(this IConfigurationBuilder builder) + { + builder.Add(new LegacyAppSettingsConfigurationSource()); + return builder; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj b/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj index 302ef7cbea..20581072ea 100644 --- a/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj +++ b/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj @@ -10,4 +10,13 @@ + + + ..\..\..\..\.nuget\packages\microsoft.extensions.configuration\8.0.0\lib\net8.0\Microsoft.Extensions.Configuration.dll + + + ..\..\..\..\.nuget\packages\microsoft.extensions.configuration.abstractions\8.0.0\lib\net8.0\Microsoft.Extensions.Configuration.Abstractions.dll + + + diff --git a/src/ServiceControl.Configuration/SettingsReader.cs b/src/ServiceControl.Configuration/SettingsReader.cs index be3249f4e0..dd0ff3ad23 100644 --- a/src/ServiceControl.Configuration/SettingsReader.cs +++ b/src/ServiceControl.Configuration/SettingsReader.cs @@ -1,13 +1,17 @@ namespace ServiceControl.Configuration; +using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +[Obsolete("Use the new ConfigurationBinder instead.", true)] public static class SettingsReader { + [Obsolete("Use the new ConfigurationBinder instead.", true)] public static T Read(SettingsRootNamespace settingsNamespace, string name, T defaultValue = default) => TryRead(settingsNamespace, name, out var value) ? value : defaultValue; + [Obsolete("Use the new ConfigurationBinder instead.", true)] public static bool TryRead(SettingsRootNamespace settingsNamespace, string name, [NotNullWhen(true)] out T value) { if (EnvironmentVariableSettingsReader.TryRead(settingsNamespace, name, out var envValue)) diff --git a/src/ServiceControl.Configuration/SettingsReaderConfigurationProvider.cs b/src/ServiceControl.Configuration/SettingsReaderConfigurationProvider.cs new file mode 100644 index 0000000000..d8284864ee --- /dev/null +++ b/src/ServiceControl.Configuration/SettingsReaderConfigurationProvider.cs @@ -0,0 +1,173 @@ +namespace ServiceControl.Configuration; + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Configuration; +using Microsoft.Win32; +using ConfigurationManager = System.Configuration.ConfigurationManager; + +public sealed class SettingsReaderConfigurationProvider : ConfigurationProvider +{ + public override void Load() + { + var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + LoadFromRegistry(data); + } + + if (!AppEnvironment.RunningInContainer) + { + LoadFromConfigFile(data); + } + + LoadFromEnvironmentVariables(data); + + Data = data; + } + + void LoadFromEnvironmentVariables(Dictionary data) + { + foreach (var settingsNamespace in KnownNamespaces) + { + // Get all environment variables + var allEnvVars = Environment.GetEnvironmentVariables(); + + foreach (System.Collections.DictionaryEntry entry in allEnvVars) + { + var envKey = entry.Key.ToString(); + if (string.IsNullOrEmpty(envKey)) + { + continue; + } + + var envValue = entry.Value?.ToString(); + if (envValue == null) + { + continue; + } + + // Try to match this env var to a namespaced key + var normalizedEnvKey = envKey.Replace("__", ":"); + + // Check if it matches the pattern: NAMESPACE_NAME or just NAME (for allowed namespaces) + var namespacedPattern = $"{settingsNamespace.Root.ToUpperInvariant()}_"; + var configKey = string.Empty; + + if (normalizedEnvKey.StartsWith(namespacedPattern, StringComparison.OrdinalIgnoreCase)) + { + // Extract the setting name after namespace + var settingName = normalizedEnvKey.Substring(namespacedPattern.Length); + configKey = $"{settingsNamespace.Root}:{settingName.Replace('_', ':')}"; + } + else if (CanSetWithoutNamespaceList.Contains(settingsNamespace)) + { + // For allowed namespaces, also check without namespace prefix + configKey = $"{settingsNamespace.Root}:{normalizedEnvKey.Replace('_', ':')}"; + } + + if (!string.IsNullOrEmpty(configKey)) + { + var expandedValue = Environment.ExpandEnvironmentVariables(envValue); + data[configKey] = expandedValue; + } + } + } + } + + void LoadFromConfigFile(Dictionary data) + { + foreach (var key in ConfigurationManager.AppSettings.AllKeys) + { + if (key == null) + { + continue; + } + + // ConfigFile uses "/" separator, convert to ":" for configuration system + var normalizedKey = key.Replace('/', ':'); + var value = ConfigurationManager.AppSettings[key]; + + if (value != null) + { + var expandedValue = Environment.ExpandEnvironmentVariables(value); + data[normalizedKey] = expandedValue; + } + } + } + +#pragma warning disable CA1416 + void LoadFromRegistry(Dictionary data) + { + foreach (var settingsNamespace in KnownNamespaces) + { + var regPath = @"SOFTWARE\ParticularSoftware\" + settingsNamespace.Root.Replace("/", "\\"); + + try + { + if (Environment.Is64BitOperatingSystem) + { + LoadFromRegistryView(data, regPath, settingsNamespace, RegistryView.Registry64); + LoadFromRegistryView(data, regPath, settingsNamespace, RegistryView.Registry32); + } + else + { + LoadFromRegistryView(data, regPath, settingsNamespace, RegistryView.Default); + } + } + catch (Exception) + { + // Intentionally swallow exceptions to allow fallback behavior + } + } + } + + void LoadFromRegistryView(Dictionary data, string regPath, SettingsRootNamespace settingsNamespace, RegistryView view) + { + try + { + var rootKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view); + using var registryKey = rootKey.OpenSubKey(regPath); + + if (registryKey == null) + { + return; + } + + foreach (var valueName in registryKey.GetValueNames()) + { + var value = registryKey.GetValue(valueName); + if (value != null) + { + var configKey = $"{settingsNamespace.Root}:{valueName}"; + // Only add if not already present (to respect priority from higher-priority views) + if (!data.ContainsKey(configKey)) + { + data[configKey] = value.ToString(); + } + } + } + } + catch (Exception) + { + // Intentionally swallow exceptions + } + } +#pragma warning restore CA1416 + + static readonly HashSet CanSetWithoutNamespaceList = + [ + new("ServiceControl"), + new("ServiceControl.Audit"), + new("Monitoring") + ]; + + static readonly SettingsRootNamespace[] KnownNamespaces = + [ + new("ServiceControl"), + new("ServiceControl.Audit"), + new("Monitoring") + ]; +} \ No newline at end of file diff --git a/src/ServiceControl.Configuration/SettingsReaderConfigurationSource.cs b/src/ServiceControl.Configuration/SettingsReaderConfigurationSource.cs new file mode 100644 index 0000000000..5cd1f0b8d8 --- /dev/null +++ b/src/ServiceControl.Configuration/SettingsReaderConfigurationSource.cs @@ -0,0 +1,9 @@ +namespace ServiceControl.Configuration; + +using Microsoft.Extensions.Configuration; + +public class SettingsReaderConfigurationSource : IConfigurationSource +{ + public IConfigurationProvider Build(IConfigurationBuilder builder) => + new SettingsReaderConfigurationProvider(); +} \ No newline at end of file diff --git a/src/ServiceControl.Infrastructure/LoggerOptionsExtensions.cs b/src/ServiceControl.Infrastructure/LoggerOptionsExtensions.cs new file mode 100644 index 0000000000..59c11c7f75 --- /dev/null +++ b/src/ServiceControl.Infrastructure/LoggerOptionsExtensions.cs @@ -0,0 +1,86 @@ +namespace ServiceControl.Infrastructure; + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Logging; + +public static class LoggerOptionsExtensions +{ + public static LoggingSettings ToLoggingSettings(this LoggingOptions src) + { + ArgumentNullException.ThrowIfNull(src); + + LoggingSettings dst = new(); + + var activeLoggers = Loggers.None; + if (src.LoggingProviders.Contains("NLog")) + { + activeLoggers |= Loggers.NLog; + } + + if (src.LoggingProviders.Contains("Seq")) + { + activeLoggers |= Loggers.Seq; + if (!string.IsNullOrWhiteSpace(src.SeqAddress)) + { + LoggerUtil.SeqAddress = src.SeqAddress; + } + } + + if (src.LoggingProviders.Contains("Otlp")) + { + activeLoggers |= Loggers.Otlp; + } + + //this defaults to NLog because historically that was the default, and we don't want to break existing installs that don't have the config key to define loggingProviders + LoggerUtil.ActiveLoggers = activeLoggers == Loggers.None + ? Loggers.NLog + : activeLoggers; + + dst.LogLevel = InitializeLogLevel(src.LogLevel, dst.LogLevel); + dst.LogPath = Environment.ExpandEnvironmentVariables(src.LogPath ?? DefaultLogLocation()); + + static LogLevel InitializeLogLevel(string levelText, LogLevel defaultLevel) + { + if (string.IsNullOrWhiteSpace(levelText)) + { + return defaultLevel; + } + + return ParseLogLevel(levelText, defaultLevel); + } + + return dst; + } + + // SC installer always populates LogPath in app.config on installation/change/upgrade so this will only be used when + // debugging or if the entry is removed manually. In those circumstances default to the folder containing the exe + static string DefaultLogLocation() => Path.Combine(AppContext.BaseDirectory, ".logs"); + + // This is not a complete mapping of NLog levels, just the ones that are different. + static readonly Dictionary NLogAliases = new(StringComparer.OrdinalIgnoreCase) + { + ["info"] = LogLevel.Information, + ["warn"] = LogLevel.Warning, + ["fatal"] = LogLevel.Critical, + ["off"] = LogLevel.None + }; + + static LogLevel ParseLogLevel(string value, LogLevel defaultLevel) + { + if (Enum.TryParse(value, ignoreCase: true, out LogLevel parsedLevel)) + { + return parsedLevel; + } + + if (NLogAliases.TryGetValue(value.Trim(), out parsedLevel)) + { + return parsedLevel; + } + + LoggerUtil.CreateStaticLogger().LogWarning("Failed to parse {LogLevelKey} setting. Defaulting to {DefaultLevel}", nameof(LoggingOptions.LogLevel), defaultLevel); + + return defaultLevel; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Infrastructure/LoggingConfigurator.cs b/src/ServiceControl.Infrastructure/LoggingConfigurator.cs index f86f776bac..b06d93f0be 100644 --- a/src/ServiceControl.Infrastructure/LoggingConfigurator.cs +++ b/src/ServiceControl.Infrastructure/LoggingConfigurator.cs @@ -14,8 +14,10 @@ namespace ServiceControl.Infrastructure public static class LoggingConfigurator { - public static void ConfigureLogging(LoggingSettings loggingSettings) + public static void ConfigureLogging(LoggingOptions options) { + var loggingSettings = options.ToLoggingSettings(); + //used for loggers outside of ServiceControl (i.e. transports and core) to use the logger factory defined here LogManager.UseFactory(new ExtensionsLoggerFactory(LoggerFactory.Create(configure => configure.ConfigureLogging(loggingSettings.LogLevel)))); diff --git a/src/ServiceControl.Infrastructure/LoggingSettings.cs b/src/ServiceControl.Infrastructure/LoggingSettings.cs index e92e29e7e5..62226046fd 100644 --- a/src/ServiceControl.Infrastructure/LoggingSettings.cs +++ b/src/ServiceControl.Infrastructure/LoggingSettings.cs @@ -1,91 +1,17 @@ namespace ServiceControl.Infrastructure; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.Extensions.Logging; -using ServiceControl.Configuration; -public class LoggingSettings +public record LoggingOptions { - public LoggingSettings(SettingsRootNamespace rootNamespace, LogLevel defaultLevel = LogLevel.Information, string logPath = null) - { - var loggingProviders = (SettingsReader.Read(rootNamespace, loggingProvidersKey) ?? "").Split(","); - var activeLoggers = Loggers.None; - if (loggingProviders.Contains("NLog")) - { - activeLoggers |= Loggers.NLog; - } - if (loggingProviders.Contains("Seq")) - { - activeLoggers |= Loggers.Seq; - var seqAddress = SettingsReader.Read(rootNamespace, seqAddressKey); - if (!string.IsNullOrWhiteSpace(seqAddress)) - { - LoggerUtil.SeqAddress = seqAddress; - } - } - if (loggingProviders.Contains("Otlp")) - { - activeLoggers |= Loggers.Otlp; - } - //this defaults to NLog because historically that was the default, and we don't want to break existing installs that don't have the config key to define loggingProviders - LoggerUtil.ActiveLoggers = activeLoggers == Loggers.None ? Loggers.NLog : activeLoggers; + public string LogLevel { get; set; } = "Information"; + public string LogPath { get; set; } + public string LoggingProviders { get; set; } = "NLog"; + public string SeqAddress { get; set; } +} - LogLevel = InitializeLogLevel(rootNamespace, defaultLevel); - LogPath = SettingsReader.Read(rootNamespace, logPathKey, Environment.ExpandEnvironmentVariables(logPath ?? DefaultLogLocation())); - } - - public LogLevel LogLevel { get; } - - public string LogPath { get; } - - static LogLevel InitializeLogLevel(SettingsRootNamespace rootNamespace, LogLevel defaultLevel) - { - var levelText = SettingsReader.Read(rootNamespace, logLevelKey); - - if (string.IsNullOrWhiteSpace(levelText)) - { - return defaultLevel; - } - - return ParseLogLevel(levelText, defaultLevel); - } - - // SC installer always populates LogPath in app.config on installation/change/upgrade so this will only be used when - // debugging or if the entry is removed manually. In those circumstances default to the folder containing the exe - static string DefaultLogLocation() => Path.Combine(AppContext.BaseDirectory, ".logs"); - - // This is not a complete mapping of NLog levels, just the ones that are different. - static readonly Dictionary NLogAliases = - new(StringComparer.OrdinalIgnoreCase) - { - ["info"] = LogLevel.Information, - ["warn"] = LogLevel.Warning, - ["fatal"] = LogLevel.Critical, - ["off"] = LogLevel.None - }; - - static LogLevel ParseLogLevel(string value, LogLevel defaultLevel) - { - if (Enum.TryParse(value, ignoreCase: true, out LogLevel parsedLevel)) - { - return parsedLevel; - } - - if (NLogAliases.TryGetValue(value.Trim(), out parsedLevel)) - { - return parsedLevel; - } - - LoggerUtil.CreateStaticLogger().LogWarning("Failed to parse {LogLevelKey} setting. Defaulting to {DefaultLevel}", logLevelKey, defaultLevel); - - return defaultLevel; - } - - const string logLevelKey = "LogLevel"; - const string logPathKey = "LogPath"; - const string loggingProvidersKey = "LoggingProviders"; - const string seqAddressKey = "SeqAddress"; -} \ No newline at end of file +public class LoggingSettings // TODO: Register +{ + public LogLevel LogLevel { get; set; } = LogLevel.Information; + public string LogPath { get; set; } +} diff --git a/src/ServiceControl.Infrastructure/LoggingSettingsFactory.cs b/src/ServiceControl.Infrastructure/LoggingSettingsFactory.cs new file mode 100644 index 0000000000..49fe66f426 --- /dev/null +++ b/src/ServiceControl.Infrastructure/LoggingSettingsFactory.cs @@ -0,0 +1 @@ +// TODO DELETE \ No newline at end of file diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs b/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs index 59ca54e8bd..e317c78be1 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/PerformanceTests.cs @@ -10,6 +10,7 @@ using Infrastructure; using Infrastructure.Api; using Messaging; + using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; using QueueLength; using Timings; @@ -26,17 +27,18 @@ class PerformanceTests : AcceptanceTest retriesStore = new RetriesStore(); queueLengthStore = new QueueLengthStore(); - var settings = new Settings(transportType: "Unknown") { EndpointUptimeGracePeriod = TimeSpan.FromMinutes(5) }; + var settings = new Settings + { + TransportType = "Unknown", + EndpointUptimeGracePeriod = TimeSpan.FromMinutes(5) + }; activityTracker = new EndpointInstanceActivityTracker(settings, TimeProvider.System); messageTypeRegistry = new MessageTypeRegistry(); var breakdownProviders = new IProvideBreakdown[] { - criticalTimeStore, - processingTimeStore, - retriesStore, - queueLengthStore + criticalTimeStore, processingTimeStore, retriesStore, queueLengthStore }; var endpointMetricsApi = new EndpointMetricsApi(breakdownProviders, endpointRegistry, activityTracker, @@ -64,9 +66,7 @@ public async Task GetMonitoredEndpointsQueryTest(int numberOfEndpoints, int numb var reporters = new[] { - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => criticalTimeStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))), - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => processingTimeStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))), - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => retriesStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))) + BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => criticalTimeStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))), BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => processingTimeStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))), BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => retriesStore.Store(e, i, EndpointMessageType.Unknown(i.EndpointName))) }.SelectMany(i => i).ToArray(); var histogram = CreateTimeHistogram(); @@ -123,9 +123,7 @@ public async Task GetMonitoredSingleEndpointQueryTest(int numberOfInstances, int var reporters = new[] { - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => criticalTimeStore.Store(e, i, getter())), - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => processingTimeStore.Store(e, i, getter())), - BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => retriesStore.Store(e, i, getter())) + BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => criticalTimeStore.Store(e, i, getter())), BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => processingTimeStore.Store(e, i, getter())), BuildReporters(sendReportEvery, numberOfEntriesInReport, instances, source, (e, i) => retriesStore.Store(e, i, getter())) }.SelectMany(i => i).ToArray(); var histogram = CreateTimeHistogram(); diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index 00c545bd97..115c5331e2 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -35,8 +35,9 @@ class ServiceControlComponentRunner( async Task InitializeServiceControl(ScenarioContext context) { LoggerUtil.ActiveLoggers = Loggers.Test; - settings = new Settings(transportType: transportToUse.TypeName) + settings = new Settings { + TransportType = transportToUse.TypeName, ConnectionString = transportToUse.ConnectionString, HttpHostName = "localhost", OnMessage = (id, headers, body, @continue) => diff --git a/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs b/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs index b55672e51d..ca634d1401 100644 --- a/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl.Monitoring/HostApplicationBuilderExtensions.cs @@ -19,6 +19,7 @@ namespace ServiceControl.Monitoring; using NServiceBus.Configuration.AdvancedExtensibility; using NServiceBus.Features; using NServiceBus.Transport; +using Particular.LicensingComponent.Shared; using QueueLength; using ServiceControl.Infrastructure; using Timings; @@ -26,10 +27,16 @@ namespace ServiceControl.Monitoring; public static class HostApplicationBuilderExtensions { - public static void AddServiceControlMonitoring(this IHostApplicationBuilder hostBuilder, - Func onCriticalError, Settings settings, - EndpointConfiguration endpointConfiguration) + public static void AddServiceControlMonitoring( + this IHostApplicationBuilder hostBuilder, + Func onCriticalError, + Settings settings, + EndpointConfiguration endpointConfiguration + ) { + var section = hostBuilder.Configuration.GetSection(Settings.SectionName); + //hostBuilder.Services.ConfigureOptions(); + hostBuilder.Services.AddLogging(); hostBuilder.Logging.ConfigureLogging(settings.LoggingSettings.LogLevel); @@ -52,6 +59,8 @@ public static void AddServiceControlMonitoring(this IHostApplicationBuilder host services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.RegisterAsSelfAndImplementedInterfaces(); services.RegisterAsSelfAndImplementedInterfaces(); diff --git a/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs b/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs index 340c12fc08..8610cc7f13 100644 --- a/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs @@ -1,9 +1,12 @@ namespace ServiceControl.Monitoring { using System.Threading.Tasks; + using Configuration; using Infrastructure; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; using NServiceBus; class RunCommand : AbstractCommand @@ -13,6 +16,8 @@ public override async Task Execute(HostArguments args, Settings settings) var endpointConfiguration = new EndpointConfiguration(settings.InstanceName); var hostBuilder = WebApplication.CreateBuilder(); + hostBuilder.Configuration.AddLegacyAppSettings(); + hostBuilder.AddServiceControlMonitoring((_, __) => Task.CompletedTask, settings, endpointConfiguration); hostBuilder.AddServiceControlMonitoringApi(); diff --git a/src/ServiceControl.Monitoring/Program.cs b/src/ServiceControl.Monitoring/Program.cs index e8dce81512..b443d621ac 100644 --- a/src/ServiceControl.Monitoring/Program.cs +++ b/src/ServiceControl.Monitoring/Program.cs @@ -4,14 +4,23 @@ using ServiceControl.Configuration; using ServiceControl.Infrastructure; using ServiceControl.Monitoring; +using Microsoft.Extensions.Configuration; ILogger logger = null; try { - var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); + var bootstrapConfig = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddLegacyAppSettings() + .AddEnvironmentVariables() + .Build(); + + var section = bootstrapConfig.GetSection(Settings.SectionName); + + var loggingSettings = LoggingSettingsFactory.Create(section); LoggingConfigurator.ConfigureLogging(loggingSettings); - logger = LoggerUtil.CreateStaticLogger(typeof(Program)); + logger = LoggerUtil.CreateStaticLogger(); AppDomain.CurrentDomain.UnhandledException += (s, e) => logger.LogError(e.ExceptionObject as Exception, "Unhandled exception was caught"); @@ -27,7 +36,11 @@ var arguments = new HostArguments(args); - var settings = new Settings(loggingSettings: loggingSettings); + var settings = new Settings( + LoggerUtil.CreateStaticLogger(), + section, + loggingSettings + ); await new CommandRunner(arguments.Command).Execute(arguments, settings); diff --git a/src/ServiceControl.Monitoring/ServiceControl.Monitoring.csproj b/src/ServiceControl.Monitoring/ServiceControl.Monitoring.csproj index 1a9b705469..85c88fa391 100644 --- a/src/ServiceControl.Monitoring/ServiceControl.Monitoring.csproj +++ b/src/ServiceControl.Monitoring/ServiceControl.Monitoring.csproj @@ -14,6 +14,7 @@ + diff --git a/src/ServiceControl.Monitoring/Settings.cs b/src/ServiceControl.Monitoring/Settings.cs index 3412307042..5074aefbaf 100644 --- a/src/ServiceControl.Monitoring/Settings.cs +++ b/src/ServiceControl.Monitoring/Settings.cs @@ -2,50 +2,52 @@ namespace ServiceControl.Monitoring { using System; using System.Collections.Generic; - using System.Configuration; using System.Runtime.Loader; using System.Text.Json.Serialization; using System.Threading.Tasks; using Configuration; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Logging; using ServiceControl.Infrastructure; using Transports; + using ConfigurationManager = System.Configuration.ConfigurationManager; public class Settings { - public Settings(LoggingSettings loggingSettings = null, string transportType = null) + public Settings() { } + + public Settings(ILogger logger, IConfigurationSection section, LoggingSettings loggingSettings, string transportType = null) { - LoggingSettings = loggingSettings ?? new(SettingsRootNamespace); + LoggingSettings = loggingSettings; - // Overwrite the instance name if it is specified in ENVVAR, reg, or config file - InstanceName = SettingsReader.Read(SettingsRootNamespace, "InstanceName", InstanceName); + InstanceName = section.GetValue("InstanceName", DEFAULT_INSTANCE_NAME); - TransportType = SettingsReader.Read(SettingsRootNamespace, "TransportType", transportType); + TransportType = section.GetValue("TransportType", transportType); - ConnectionString = GetConnectionString(); - ErrorQueue = SettingsReader.Read(SettingsRootNamespace, "ErrorQueue", "error"); + ConnectionString = GetConnectionString(logger, section); + ErrorQueue = section.GetValue("ErrorQueue", "error"); if (AppEnvironment.RunningInContainer) { HttpHostName = "*"; HttpPort = "33633"; - } else { - HttpHostName = SettingsReader.Read(SettingsRootNamespace, "HttpHostname"); - HttpPort = SettingsReader.Read(SettingsRootNamespace, "HttpPort"); + HttpHostName = section.GetValue("HttpHostname"); + HttpPort = section.GetValue("HttpPort"); } - EndpointUptimeGracePeriod = TimeSpan.Parse(SettingsReader.Read(SettingsRootNamespace, "EndpointUptimeGracePeriod", "00:00:40")); - MaximumConcurrencyLevel = SettingsReader.Read(SettingsRootNamespace, "MaximumConcurrencyLevel"); - ServiceControlThroughputDataQueue = SettingsReader.Read(SettingsRootNamespace, "ServiceControlThroughputDataQueue", "ServiceControl.ThroughputData"); - ShutdownTimeout = SettingsReader.Read(SettingsRootNamespace, "ShutdownTimeout", ShutdownTimeout); + EndpointUptimeGracePeriod = section.GetValue("EndpointUptimeGracePeriod", TimeSpan.FromSeconds(40)); + MaximumConcurrencyLevel = section.GetValue("MaximumConcurrencyLevel"); + ServiceControlThroughputDataQueue = section.GetValue("ServiceControlThroughputDataQueue", "ServiceControl.ThroughputData"); + ShutdownTimeout = section.GetValue("ShutdownTimeout", TimeSpan.FromSeconds(5)); AssemblyLoadContextResolver = static assemblyPath => new PluginAssemblyLoadContext(assemblyPath); } - [JsonIgnore] - public Func AssemblyLoadContextResolver { get; set; } + + [JsonIgnore] public Func AssemblyLoadContextResolver { get; set; } public LoggingSettings LoggingSettings { get; } @@ -73,7 +75,7 @@ public Settings(LoggingSettings loggingSettings = null, string transportType = n // restrictive hosting platform, which is Linux containers. Linux // containers allow for a maximum of 10 seconds. We set it to 5 to // allow for cancellation and logging to take place - public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + public TimeSpan ShutdownTimeout { get; set; } public TransportSettings ToTransportSettings() { @@ -89,21 +91,26 @@ public TransportSettings ToTransportSettings() return transportSettings; } - static string GetConnectionString() + static string GetConnectionString(ILogger logger, IConfigurationSection section) { - var settingsValue = SettingsReader.Read(SettingsRootNamespace, "ConnectionString"); + var settingsValue = section.GetValue("ConnectionString"); // Intentionally NOT using .GetConnectionString() for legacy reasons if (settingsValue != null) { return settingsValue; } var connectionStringSettings = ConfigurationManager.ConnectionStrings["NServiceBus/Transport"]; + if (connectionStringSettings != null) + { + logger.LogWarning($"Connection string resolved from legacy `NServiceBus/Transport` connection string. Migrate to `{SectionName}/{nameof(ConnectionString)}`"); + } return connectionStringSettings?.ConnectionString; } internal Func, byte[], Func, Task> OnMessage { get; set; } = (messageId, headers, body, next) => next(); public const string DEFAULT_INSTANCE_NAME = "Particular.Monitoring"; - public static readonly SettingsRootNamespace SettingsRootNamespace = new("Monitoring"); + public const string SectionName = "Monitoring"; + } } \ No newline at end of file diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs index f618503256..e3d397e55f 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs @@ -19,7 +19,7 @@ namespace ServiceControl.MultiInstance.AcceptanceTests [TestFixture] abstract class AcceptanceTest : NServiceBusAcceptanceTest, IAcceptanceTestInfrastructureProviderMultiInstance { - protected static string ServiceControlInstanceName { get; } = Settings.DEFAULT_INSTANCE_NAME; + protected static string ServiceControlInstanceName { get; } = PrimaryOptions.DEFAULT_INSTANCE_NAME; protected static string ServiceControlAuditInstanceName { get; } = Audit.Infrastructure.Settings.Settings.DEFAULT_INSTANCE_NAME; public Dictionary HttpClients => serviceControlRunnerBehavior.HttpClients; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Infrastructure/When_remote_instance_is_not_reachable.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Infrastructure/When_remote_instance_is_not_reachable.cs index ccc269fdb5..0f41baf88e 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Infrastructure/When_remote_instance_is_not_reachable.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Infrastructure/When_remote_instance_is_not_reachable.cs @@ -24,11 +24,11 @@ public async Task Should_not_fail(bool disableHealthChecks) var remoteInstanceSetting = new RemoteInstanceSetting("http://localhost:12121"); CustomServiceControlPrimarySettings = settings => { - var currentSetting = settings.RemoteInstances[0]; - settings.RemoteInstances = [currentSetting, remoteInstanceSetting]; + var currentSetting = settings.ServiceControl.RemoteInstanceSettings[0]; + settings.ServiceControl.RemoteInstanceSettings = [currentSetting, remoteInstanceSetting]; // Toggle the health checks because the behavior should not depend on the health checks running or not running - settings.DisableHealthChecks = disableHealthChecks; + settings.ServiceControl.DisableHealthChecks = disableHealthChecks; settings.PersisterSpecificSettings.OverrideCustomCheckRepeatTime = TimeSpan.FromSeconds(2); }; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingSameMessageMultipleTimes.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingSameMessageMultipleTimes.cs index 9083dbe760..6206590542 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingSameMessageMultipleTimes.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingSameMessageMultipleTimes.cs @@ -27,7 +27,7 @@ public enum RetryType [TestCase(new[] { RetryType.Edit, RetryType.Edit, RetryType.NoEdit })] public async Task WithMixOfRetryTypes(RetryType[] retryTypes) { - CustomServiceControlPrimarySettings = s => { s.AllowMessageEditing = true; }; + CustomServiceControlPrimarySettings = s => { s.ServiceControl.AllowMessageEditing = true; }; await Define(context => context.NumberOfRetriesToComplete = retryTypes.Length) .WithEndpoint(b => diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingWithEdit.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingWithEdit.cs index d2d01d95ce..504eddbe66 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingWithEdit.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/WhenRetryingWithEdit.cs @@ -17,7 +17,7 @@ class WhenRetryingWithEdit : WhenRetrying [Test] public async Task ShouldCreateNewMessageAndResolveEditedMessage() { - CustomServiceControlPrimarySettings = s => { s.AllowMessageEditing = true; }; + CustomServiceControlPrimarySettings = s => { s.ServiceControl.AllowMessageEditing = true; }; await Define() .WithEndpoint(b => diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_issuing_retry_by_specifying_instance_id.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_issuing_retry_by_specifying_instance_id.cs index ad41f84d7b..f2e63bb025 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_issuing_retry_by_specifying_instance_id.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/Recoverability/When_issuing_retry_by_specifying_instance_id.cs @@ -22,7 +22,7 @@ public async Task Should_be_work() string addressOfItself = null; // instead of setting up a multiple crazy instances we just use the current instance and rely on it forwarding the instance call to itself - CustomServiceControlPrimarySettings = s => { addressOfItself = s.ApiUrl; }; + CustomServiceControlPrimarySettings = s => { addressOfItself = s.ServiceControl.ApiUrl; }; FailedMessage failure; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/HttpExtensionsMultiinstance.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/HttpExtensionsMultiinstance.cs index d09497c90a..c3b0439098 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/HttpExtensionsMultiinstance.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/HttpExtensionsMultiinstance.cs @@ -17,37 +17,37 @@ static IAcceptanceTestInfrastructureProvider ToHttpExtension(this IAcceptanceTes SerializerOptions = providerMultiInstance.SerializerOptions[instanceName], }; - public static Task Put(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, Func requestHasFailed = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task Put(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, Func requestHasFailed = null, string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME) where T : class { return providerMultiInstance.ToHttpExtension(instanceName).Put(url, payload, requestHasFailed); } - public static Task GetRaw(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, string instanceName = Settings.DEFAULT_INSTANCE_NAME) + public static Task GetRaw(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME) { return providerMultiInstance.ToHttpExtension(instanceName).GetRaw(url); } - public static Task> TryGetMany(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task> TryGetMany(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME) where T : class { return providerMultiInstance.ToHttpExtension(instanceName).TryGetMany(url, condition); } - public static Task Patch(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task Patch(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME) where T : class { return providerMultiInstance.ToHttpExtension(instanceName).Patch(url, payload); } - public static Task> TryGet(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task> TryGet(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME) where T : class { return providerMultiInstance.ToHttpExtension(instanceName).TryGet(url, condition); } - public static Task> TryGetSingle(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task> TryGetSingle(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, Predicate condition = null, string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME) where T : class { return providerMultiInstance.ToHttpExtension(instanceName).TryGetSingle(url, condition); } - public static Task Post(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, Func requestHasFailed = null, string instanceName = Settings.DEFAULT_INSTANCE_NAME) where T : class + public static Task Post(this IAcceptanceTestInfrastructureProviderMultiInstance providerMultiInstance, string url, T payload = null, Func requestHasFailed = null, string instanceName = PrimaryOptions.DEFAULT_INSTANCE_NAME) where T : class { return providerMultiInstance.ToHttpExtension(instanceName).Post(url, payload, requestHasFailed); } diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index 231c462e66..baaa8b259e 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -60,7 +60,7 @@ public async Task Initialize(RunDescriptor run) transportToUse, new AcceptanceTestStorageConfiguration(), auditSettings => { - auditSettings.ServiceControlQueueAddress = PrimaryInstanceSettings.DEFAULT_INSTANCE_NAME; + auditSettings.ServiceControlQueueAddress = PrimaryOptions.DEFAULT_INSTANCE_NAME; customServiceControlAuditSettings(auditSettings); SettingsPerInstance[AuditInstanceSettings.DEFAULT_INSTANCE_NAME] = auditSettings; }, auditEndpointConfiguration => @@ -92,9 +92,9 @@ public async Task Initialize(RunDescriptor run) transportToUse, new ServiceControl.AcceptanceTests.AcceptanceTestStorageConfiguration(), primarySettings => { - primarySettings.RemoteInstances = [auditInstance]; + primarySettings.ServiceControl.RemoteInstanceSettings = [auditInstance]; customServiceControlSettings(primarySettings); - SettingsPerInstance[PrimaryInstanceSettings.DEFAULT_INSTANCE_NAME] = primarySettings; + SettingsPerInstance[PrimaryOptions.DEFAULT_INSTANCE_NAME] = primarySettings; // TODO: Not sure! }, primaryEndpointConfiguration => { @@ -118,7 +118,7 @@ public async Task Initialize(RunDescriptor run) // For example one way to deal with this is to have a custom invoker that figures out the right target based on the base address // in the request URI. primaryHostBuilder.Services.AddKeyedSingleton("Forwarding", () => auditInstanceComponentRunner.InstanceTestServer.CreateHandler()); - foreach (var remoteInstance in ((PrimaryInstanceSettings)SettingsPerInstance[PrimaryInstanceSettings.DEFAULT_INSTANCE_NAME]).RemoteInstances) + foreach (var remoteInstance in ((PrimaryInstanceSettings)SettingsPerInstance[PrimaryOptions.DEFAULT_INSTANCE_NAME]).ServiceControl.RemoteInstanceSettings) { if (TestServerPerRemoteInstance.TryGetValue(remoteInstance.InstanceId, out var testServer)) { @@ -132,11 +132,11 @@ public async Task Initialize(RunDescriptor run) primaryHostBuilderCustomization(primaryHostBuilder); }); - typeof(ScenarioContext).GetProperty("CurrentEndpoint", BindingFlags.Static | BindingFlags.NonPublic)?.SetValue(run.ScenarioContext, PrimaryInstanceSettings.DEFAULT_INSTANCE_NAME); + typeof(ScenarioContext).GetProperty("CurrentEndpoint", BindingFlags.Static | BindingFlags.NonPublic)?.SetValue(run.ScenarioContext, PrimaryOptions.DEFAULT_INSTANCE_NAME); await primaryInstanceComponentRunner.Initialize(run); - HttpClients[PrimaryInstanceSettings.DEFAULT_INSTANCE_NAME] = primaryInstanceComponentRunner.HttpClient; - SerializerOptions[PrimaryInstanceSettings.DEFAULT_INSTANCE_NAME] = primaryInstanceComponentRunner.SerializerOptions; + HttpClients[PrimaryOptions.DEFAULT_INSTANCE_NAME] = primaryInstanceComponentRunner.HttpClient; + SerializerOptions[PrimaryOptions.DEFAULT_INSTANCE_NAME] = primaryInstanceComponentRunner.SerializerOptions; } public override async Task Stop(CancellationToken cancellationToken = default) diff --git a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs index cbfcfbcf8f..b69d5e2f70 100644 --- a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs +++ b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckFreeDiskSpace.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus.CustomChecks; using ServiceControl.Infrastructure; using ServiceControl.Persistence.RavenDB; @@ -38,21 +39,23 @@ public override Task PerformCheck(CancellationToken cancellationTok : CheckResult.Failed($"{percentRemaining:P0} disk space remaining on data drive '{dataDriveInfo.VolumeLabel} ({dataDriveInfo.RootDirectory})' on '{Environment.MachineName}'."); } - public static void Validate(RavenPersisterSettings settings) + public class Validation : IValidateOptions // TODO: Register!! { - var logger = LoggerUtil.CreateStaticLogger(); - var threshold = settings.DataSpaceRemainingThreshold; - - if (threshold < 0) + public ValidateOptionsResult Validate(string name, RavenPersisterSettings options) { - logger.LogCritical("{RavenPersistenceConfigurationDataSpaceRemainingThresholdKey} is invalid, minimum value is 0", RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey); - throw new Exception($"{RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey} is invalid, minimum value is 0."); - } + var threshold = options.DataSpaceRemainingThreshold; - if (threshold > 100) - { - logger.LogCritical("{RavenPersistenceConfigurationDataSpaceRemainingThresholdKey} is invalid, maximum value is 100", RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey); - throw new Exception($"{RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey} is invalid, maximum value is 100."); + if (threshold < 0) + { + return ValidateOptionsResult.Fail($"{RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey} is invalid, minimum value is 0."); + } + + if (threshold > 100) + { + return ValidateOptionsResult.Fail($"{RavenPersistenceConfiguration.DataSpaceRemainingThresholdKey} is invalid, maximum value is 100."); + } + + return ValidateOptionsResult.Success; } } diff --git a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs index dad26352d7..fe74c8d05f 100644 --- a/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs +++ b/src/ServiceControl.Persistence.RavenDB/CustomChecks/CheckMinimumStorageRequiredForIngestion.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus.CustomChecks; using ServiceControl.Infrastructure; using ServiceControl.Persistence; @@ -47,30 +48,33 @@ public override Task PerformCheck(CancellationToken cancellationTok dataDriveInfo.RootDirectory, Environment.MachineName, percentageThreshold, - RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey); + RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey); stateHolder.CanIngestMore = false; - return CheckResult.Failed($"Error 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 {RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} configuration setting."); + return CheckResult.Failed($"Error 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."); } - public static void Validate(RavenPersisterSettings settings) - { - var logger = LoggerUtil.CreateStaticLogger(); - var threshold = settings.MinimumStorageLeftRequiredForIngestion; + public const int MinimumStorageLeftRequiredForIngestionDefault = 5; + static readonly Task SuccessResult = Task.FromResult(CheckResult.Pass); - if (threshold < 0) - { - logger.LogCritical("{RavenBootstrapperMinimumStorageLeftRequiredForIngestionKey} is invalid, minimum value is 0", RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey); - throw new Exception($"{RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} is invalid, minimum value is 0."); - } - if (threshold > 100) + public sealed class Validation : IValidateOptions // TODO: Register!! + { + public ValidateOptionsResult Validate(string name, RavenPersisterSettings options) { - logger.LogCritical("{RavenBootstrapperMinimumStorageLeftRequiredForIngestionKey} is invalid, maximum value is 100", RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey); - throw new Exception($"{RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey} is invalid, maximum value is 100."); + var threshold = options.MinimumStorageLeftRequiredForIngestion; + + if (threshold < 0) + { + return ValidateOptionsResult.Fail($"{RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey} is invalid, minimum value is 0."); + } + + if (threshold > 100) + { + return ValidateOptionsResult.Fail($"{RavenPersistenceConfiguration.MinimumStorageLeftRequiredForIngestionKey} is invalid, maximum value is 100."); + } + + return ValidateOptionsResult.Success; } } - - public const int MinimumStorageLeftRequiredForIngestionDefault = 5; - static readonly Task SuccessResult = Task.FromResult(CheckResult.Pass); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDB/RavenBootstrapper.cs b/src/ServiceControl.Persistence.RavenDB/RavenBootstrapper.cs deleted file mode 100644 index bc6e294289..0000000000 --- a/src/ServiceControl.Persistence.RavenDB/RavenBootstrapper.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace ServiceControl.Persistence.RavenDB -{ - static class RavenBootstrapper - { - public const string DatabasePathKey = "DbPath"; - public const string HostNameKey = "HostName"; - public const string DatabaseMaintenancePortKey = "DatabaseMaintenancePort"; - public const string ExpirationProcessTimerInSecondsKey = "ExpirationProcessTimerInSeconds"; - public const string ConnectionStringKey = "RavenDB/ConnectionString"; - public const string ClientCertificatePathKey = "RavenDB/ClientCertificatePath"; - public const string ClientCertificateBase64Key = "RavenDB/ClientCertificateBase64"; - public const string ClientCertificatePasswordKey = "RavenDB/ClientCertificatePassword"; - public const string MinimumStorageLeftRequiredForIngestionKey = "MinimumStorageLeftRequiredForIngestion"; - public const string DatabaseNameKey = "RavenDB/DatabaseName"; - public const string LogsPathKey = "LogPath"; - public const string RavenDbLogLevelKey = "RavenDBLogLevel"; - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs b/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs index 93a8fe63b7..4aad71457c 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs @@ -3,6 +3,7 @@ namespace ServiceControl.Persistence.RavenDB; using CustomChecks; using MessageFailures; using MessageRedirects; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions; using Operations.BodyStorage; @@ -20,7 +21,7 @@ namespace ServiceControl.Persistence.RavenDB; class RavenPersistence(RavenPersisterSettings settings) : IPersistence { - public void AddPersistence(IServiceCollection services) + public void AddPersistence(IServiceCollection services, IConfiguration configuration) { ConfigureLifecycle(services); diff --git a/src/ServiceControl.Persistence.RavenDB/RavenPersistenceConfiguration.cs b/src/ServiceControl.Persistence.RavenDB/RavenPersistenceConfiguration.cs index c9d03685e7..67c9ebf3bc 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenPersistenceConfiguration.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenPersistenceConfiguration.cs @@ -5,10 +5,12 @@ using System.Reflection; using Configuration; using CustomChecks; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Options; using Particular.LicensingComponent.Contracts; using ServiceControl.Infrastructure; - class RavenPersistenceConfiguration : IPersistenceConfiguration + sealed class RavenPersistenceConfiguration(IConfiguration configuration) : IConfigureOptions { public const string DataSpaceRemainingThresholdKey = "DataSpaceRemainingThreshold"; const string AuditRetentionPeriodKey = "AuditRetentionPeriod"; @@ -17,46 +19,54 @@ class RavenPersistenceConfiguration : IPersistenceConfiguration const string ExternalIntegrationsDispatchingBatchSizeKey = "ExternalIntegrationsDispatchingBatchSize"; const string MaintenanceModeKey = "MaintenanceMode"; - public PersistenceSettings CreateSettings(SettingsRootNamespace settingsRootNamespace) - { - static T GetRequiredSetting(SettingsRootNamespace settingsRootNamespace, string key) - { - if (SettingsReader.TryRead(settingsRootNamespace, key, out var value)) - { - return value; - } + const string DatabasePathKey = "DbPath"; + const string HostNameKey = "HostName"; + const string DatabaseMaintenancePortKey = "DatabaseMaintenancePort"; + const string ExpirationProcessTimerInSecondsKey = "ExpirationProcessTimerInSeconds"; + const string ConnectionStringKey = "RavenDB:ConnectionString"; + const string ClientCertificatePathKey = "RavenDB:ClientCertificatePath"; + const string ClientCertificateBase64Key = "RavenDB:ClientCertificateBase64"; + const string ClientCertificatePasswordKey = "RavenDB:ClientCertificatePassword"; + public const string MinimumStorageLeftRequiredForIngestionKey = "MinimumStorageLeftRequiredForIngestion"; + const string DatabaseNameKey = "RavenDB:DatabaseName"; + const string LogsPathKey = "LogPath"; + const string RavenDbLogLevelKey = "RavenDBLogLevel"; - throw new Exception($"Setting {key} of type {typeof(T)} is required"); - } + public void Configure(RavenPersisterSettings options) + { + IConfigurationSection s = configuration.GetSection("ServiceControl"); - var ravenDbLogLevel = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.RavenDbLogLevelKey, "Warn"); + var ravenDbLogLevel = s.GetValue(RavenDbLogLevelKey, "Warn"); var logsMode = RavenDbLogLevelToLogsModeMapper.Map(ravenDbLogLevel, LoggerUtil.CreateStaticLogger()); - var settings = new RavenPersisterSettings - { - ConnectionString = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.ConnectionStringKey), - ClientCertificatePath = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.ClientCertificatePathKey), - ClientCertificateBase64 = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.ClientCertificateBase64Key), - ClientCertificatePassword = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.ClientCertificatePasswordKey), - DatabaseName = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.DatabaseNameKey, RavenPersisterSettings.DatabaseNameDefault), - DatabasePath = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.DatabasePathKey, DefaultDatabaseLocation()), - DatabaseMaintenancePort = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.DatabaseMaintenancePortKey, RavenPersisterSettings.DatabaseMaintenancePortDefault), - ExpirationProcessTimerInSeconds = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.ExpirationProcessTimerInSecondsKey, 600), - MinimumStorageLeftRequiredForIngestion = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.MinimumStorageLeftRequiredForIngestionKey, CheckMinimumStorageRequiredForIngestion.MinimumStorageLeftRequiredForIngestionDefault), - DataSpaceRemainingThreshold = SettingsReader.Read(settingsRootNamespace, DataSpaceRemainingThresholdKey, CheckFreeDiskSpace.DataSpaceRemainingThresholdDefault), - ErrorRetentionPeriod = GetRequiredSetting(settingsRootNamespace, ErrorRetentionPeriodKey), - EventsRetentionPeriod = SettingsReader.Read(settingsRootNamespace, EventsRetentionPeriodKey, TimeSpan.FromDays(14)), - AuditRetentionPeriod = SettingsReader.Read(settingsRootNamespace, AuditRetentionPeriodKey, TimeSpan.Zero), - ExternalIntegrationsDispatchingBatchSize = SettingsReader.Read(settingsRootNamespace, ExternalIntegrationsDispatchingBatchSizeKey, 100), - MaintenanceMode = SettingsReader.Read(settingsRootNamespace, MaintenanceModeKey, false), - LogPath = SettingsReader.Read(settingsRootNamespace, RavenBootstrapper.LogsPathKey, DefaultLogLocation()), - LogsMode = logsMode, - EnableFullTextSearchOnBodies = SettingsReader.Read(settingsRootNamespace, "EnableFullTextSearchOnBodies", true), - ThroughputDatabaseName = SettingsReader.Read(ThroughputSettings.SettingsNamespace, ThroughputSettings.DatabaseNameKey, ThroughputSettings.DefaultDatabaseName) - }; + options.ConnectionString = s.GetValue(ConnectionStringKey); + options.ClientCertificatePath = s.GetValue(ClientCertificatePathKey); + options.ClientCertificateBase64 = s.GetValue(ClientCertificateBase64Key); + options.ClientCertificatePassword = s.GetValue(ClientCertificatePasswordKey); + options.DatabaseName = s.GetValue(DatabaseNameKey, RavenPersisterSettings.DatabaseNameDefault); + options.DatabasePath = s.GetValue(DatabasePathKey, DefaultDatabaseLocation()); + options.DatabaseMaintenancePort = s.GetValue(DatabaseMaintenancePortKey, RavenPersisterSettings.DatabaseMaintenancePortDefault); + options.ExpirationProcessTimerInSeconds = s.GetValue(ExpirationProcessTimerInSecondsKey, RavenPersisterSettings.ExpirationProcessTimerInSecondsDefault); + options.MinimumStorageLeftRequiredForIngestion = s.GetValue(MinimumStorageLeftRequiredForIngestionKey, CheckMinimumStorageRequiredForIngestion.MinimumStorageLeftRequiredForIngestionDefault); + options.DataSpaceRemainingThreshold = s.GetValue(DataSpaceRemainingThresholdKey, CheckFreeDiskSpace.DataSpaceRemainingThresholdDefault); + options.ErrorRetentionPeriod = s.GetValue(ErrorRetentionPeriodKey); + options.EventsRetentionPeriod = s.GetValue(EventsRetentionPeriodKey, TimeSpan.FromDays(14)); + options.AuditRetentionPeriod = s.GetValue(AuditRetentionPeriodKey, TimeSpan.Zero); + options.ExternalIntegrationsDispatchingBatchSize = s.GetValue(ExternalIntegrationsDispatchingBatchSizeKey, 100); + options.MaintenanceMode = s.GetValue(MaintenanceModeKey, false); + options.LogPath = s.GetValue(LogsPathKey, DefaultLogLocation()); + options.LogsMode = logsMode; + options.EnableFullTextSearchOnBodies = s.GetValue("EnableFullTextSearchOnBodies", true); + + var licensingComponentSection = configuration.GetSection("LicensingComponent"); + + options.ThroughputDatabaseName = licensingComponentSection.GetValue(ThroughputSettings.DatabaseNameKey, ThroughputSettings.DefaultDatabaseName); + } - CheckFreeDiskSpace.Validate(settings); - CheckMinimumStorageRequiredForIngestion.Validate(settings); + public PersistenceSettings CreateSettings(SettingsRootNamespace settingsRootNamespace) + { + var settings = new RavenPersisterSettings(); + Configure(settings); return settings; } @@ -75,11 +85,15 @@ static string DefaultLogLocation() var assemblyLocation = Assembly.GetExecutingAssembly().Location; return Path.Combine(Path.GetDirectoryName(assemblyLocation), ".logs"); } + } - public IPersistence Create(PersistenceSettings settings) + class RavenPersisterSettingsValidation : IValidateOptions + { + public ValidateOptionsResult Validate(string name, RavenPersisterSettings options) { - var specificSettings = (RavenPersisterSettings)settings; - return new RavenPersistence(specificSettings); + return options.ErrorRetentionPeriod==TimeSpan.Zero + ? ValidateOptionsResult.Fail("ErrorRetentionPeriod must be set than 0") + : ValidateOptionsResult.Success; } } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDB/RavenPersisterSettings.cs b/src/ServiceControl.Persistence.RavenDB/RavenPersisterSettings.cs index 13ceb6b874..5f56eb9882 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenPersisterSettings.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenPersisterSettings.cs @@ -1,6 +1,10 @@ using System; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; using Particular.LicensingComponent.Contracts; using ServiceControl.Persistence; +using ServiceControl.Persistence.RavenDB; using ServiceControl.Persistence.RavenDB.CustomChecks; using ServiceControl.RavenDB; @@ -24,6 +28,7 @@ class RavenPersisterSettings : PersistenceSettings, IRavenClientCertificateInfo /// User provided external RavenDB instance connection string /// public string ConnectionString { get; set; } + public string ClientCertificatePath { get; set; } public string ClientCertificateBase64 { get; set; } public string ClientCertificatePassword { get; set; } diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/PersistenceTestsContext.cs b/src/ServiceControl.Persistence.Tests.RavenDB/PersistenceTestsContext.cs index 1bf7b44424..0330c9fb55 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/PersistenceTestsContext.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDB/PersistenceTestsContext.cs @@ -26,7 +26,7 @@ public async Task Setup(IHostApplicationBuilder hostBuilder) embeddedServer = await SharedEmbeddedServer.GetInstance(); - PersistenceSettings = new RavenPersisterSettings + var settings = new RavenPersisterSettings { AuditRetentionPeriod = retentionPeriod, ErrorRetentionPeriod = retentionPeriod, @@ -36,9 +36,11 @@ public async Task Setup(IHostApplicationBuilder hostBuilder) ThroughputDatabaseName = $"{databaseName}-throughput", }; - var persistence = new RavenPersistenceConfiguration().Create(PersistenceSettings); + PersistenceSettings = settings; - persistence.AddPersistence(hostBuilder.Services); + var persistence = new RavenPersistence(settings); + + persistence.AddPersistence(hostBuilder.Services, hostBuilder.Configuration); persistence.AddInstaller(hostBuilder.Services); } diff --git a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs index c0c730cf68..d0fd601b8d 100644 --- a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs +++ b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; + using Microsoft.Extensions.Options; using NServiceBus.Transport; using NUnit.Framework; using ServiceBus.Management.Infrastructure.Settings; @@ -67,7 +68,10 @@ public async Task When_the_dequeuer_is_created_then_the_error_address_is_cached( TransportAddress = "TestAddress" }; - var transportCustomization = new TestTransportCustomization { TransportInfrastructure = transportInfrastructure }; + var transportCustomization = new TestTransportCustomization + { + TransportInfrastructure = transportInfrastructure + }; var testReturnToSenderDequeuer = new TestReturnToSenderDequeuer(new ReturnToSender(ErrorStore, NullLogger.Instance), ErrorStore, domainEvents, "TestEndpoint", errorQueueNameCache, transportCustomization); @@ -187,8 +191,7 @@ public async Task When_there_is_one_poison_message_it_is_removed_from_batch_and_ //Continue trying until there is no exception -> poison message is removed from the batch c = true; } - } - while (c); + } while (c); var status = retryManager.GetStatusForRetryOperation("Test-group", RetryType.FailureGroup); @@ -239,9 +242,7 @@ async Task CreateAFailedMessageAndMarkAsPartOfRetryBatch(RetryingManager retryMa [ new FailedMessage.FailureGroup { - Id = groupId, - Title = groupId, - Type = groupId + Id = groupId, Title = groupId, Type = groupId } ], Status = FailedMessageStatus.Unresolved, @@ -331,9 +332,30 @@ class FakeApplicationLifetime : IHostApplicationLifetime class TestReturnToSenderDequeuer : ReturnToSenderDequeuer { - public TestReturnToSenderDequeuer(ReturnToSender returnToSender, IErrorMessageDataStore store, IDomainEvents domainEvents, string endpointName, - ErrorQueueNameCache cache, ITransportCustomization transportCustomization) - : base(returnToSender, store, domainEvents, transportCustomization, null, new Settings { InstanceName = endpointName }, cache, NullLogger.Instance) + public TestReturnToSenderDequeuer( + ReturnToSender returnToSender, + IErrorMessageDataStore store, + IDomainEvents domainEvents, + string endpointName, + ErrorQueueNameCache cache, + ITransportCustomization transportCustomization + ) + : base( + returnToSender, + store, + domainEvents, + transportCustomization, + null, + Options.Create(new Settings + { + ServiceControl = + { + InstanceName = endpointName + } + }), + cache, + NullLogger.Instance + ) { } @@ -356,6 +378,7 @@ public Task CreateTransportInfrastructure(string name, Func onCriticalError = null, NServiceBus.TransportTransactionMode preferredTransactionMode = NServiceBus.TransportTransactionMode.ReceiveOnly) => Task.FromResult(TransportInfrastructure); + public void CustomizeAuditEndpoint(NServiceBus.EndpointConfiguration endpointConfiguration, TransportSettings transportSettings) => throw new NotImplementedException(); public void CustomizeMonitoringEndpoint(NServiceBus.EndpointConfiguration endpointConfiguration, TransportSettings transportSettings) => throw new NotImplementedException(); public void CustomizePrimaryEndpoint(NServiceBus.EndpointConfiguration endpointConfiguration, TransportSettings transportSettings) => throw new NotImplementedException(); diff --git a/src/ServiceControl.Persistence/IPersistence.cs b/src/ServiceControl.Persistence/IPersistence.cs index 76fe7ef190..429971e657 100644 --- a/src/ServiceControl.Persistence/IPersistence.cs +++ b/src/ServiceControl.Persistence/IPersistence.cs @@ -1,10 +1,11 @@ namespace ServiceControl.Persistence { + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; public interface IPersistence { - void AddPersistence(IServiceCollection services); + void AddPersistence(IServiceCollection services, IConfiguration configuration); void AddInstaller(IServiceCollection services); } } diff --git a/src/ServiceControl.Persistence/IPersistenceConfiguration.cs b/src/ServiceControl.Persistence/IPersistenceConfiguration.cs index 5b2e794562..bc5da98c34 100644 --- a/src/ServiceControl.Persistence/IPersistenceConfiguration.cs +++ b/src/ServiceControl.Persistence/IPersistenceConfiguration.cs @@ -1,10 +1,9 @@ namespace ServiceControl.Persistence { - using Configuration; + using Microsoft.Extensions.Configuration; public interface IPersistenceConfiguration { - PersistenceSettings CreateSettings(SettingsRootNamespace settingsRootNamespace); - IPersistence Create(PersistenceSettings settings); + IPersistence Create(IConfiguration configuration); } } \ No newline at end of file diff --git a/src/ServiceControl.Transports.ASBS.Tests/TransportTestsConfiguration.cs b/src/ServiceControl.Transports.ASBS.Tests/TransportTestsConfiguration.cs index e5fd0f1e68..6a8168daa5 100644 --- a/src/ServiceControl.Transports.ASBS.Tests/TransportTestsConfiguration.cs +++ b/src/ServiceControl.Transports.ASBS.Tests/TransportTestsConfiguration.cs @@ -2,6 +2,7 @@ { using System; using System.Threading.Tasks; + using Microsoft.Extensions.Configuration; using Transports; using Transports.ASBS; @@ -13,7 +14,8 @@ partial class TransportTestsConfiguration public Task Configure() { - TransportCustomization = new ASBSTransportCustomization(); + var emptyConfig = new ConfigurationBuilder().Build(); + TransportCustomization = new ASBSTransportCustomization(emptyConfig); ConnectionString = Environment.GetEnvironmentVariable(ConnectionStringKey); if (string.IsNullOrEmpty(ConnectionString)) diff --git a/src/ServiceControl.Transports.ASBS/ASBSTransportCustomization.cs b/src/ServiceControl.Transports.ASBS/ASBSTransportCustomization.cs index 5a600a385f..0daed1639c 100644 --- a/src/ServiceControl.Transports.ASBS/ASBSTransportCustomization.cs +++ b/src/ServiceControl.Transports.ASBS/ASBSTransportCustomization.cs @@ -4,11 +4,12 @@ using System.Text.Json; using BrokerThroughput; using Configuration; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NServiceBus; using NServiceBus.Transport.AzureServiceBus; - public class ASBSTransportCustomization : TransportCustomization + public class ASBSTransportCustomization(IConfiguration configuration) : TransportCustomization { protected override void CustomizeTransportForPrimaryEndpoint(EndpointConfiguration endpointConfiguration, AzureServiceBusTransport transportDefinition, TransportSettings transportSettings) => transportDefinition.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; @@ -56,7 +57,6 @@ protected override void AddTransportForPrimaryCore(IServiceCollection services, var connectionSettings = ConnectionStringParser.Parse(transportSettings.ConnectionString); TopicTopology selectedTopology; - var serviceBusRootNamespace = new SettingsRootNamespace("ServiceControl.Transport.ASBS"); if (connectionSettings.TopicName != null) { //Bundle name provided -> use migration topology @@ -67,7 +67,8 @@ protected override void AddTransportForPrimaryCore(IServiceCollection services, { TopicToPublishTo = connectionSettings.TopicName, TopicToSubscribeOn = connectionSettings.TopicName, - EventsToMigrateMap = [ + EventsToMigrateMap = + [ "ServiceControl.Contracts.CustomCheckFailed", "ServiceControl.Contracts.CustomCheckSucceeded", "ServiceControl.Contracts.HeartbeatRestored", @@ -80,15 +81,13 @@ protected override void AddTransportForPrimaryCore(IServiceCollection services, ] }); } - else if (SettingsReader.TryRead(serviceBusRootNamespace, "Topology", out var topologyJson)) - { - //Load topology from json - selectedTopology = TopicTopology.FromOptions(JsonSerializer.Deserialize(topologyJson, TopologyOptionsSerializationContext.Default.TopologyOptions)); - } else { - //Default to topic-per-event topology - selectedTopology = TopicTopology.Default; + //Load topology from json + var topologyJson = configuration.GetSection(ServiceBusSectionName).GetValue("Topology"); + selectedTopology = string.IsNullOrWhiteSpace(topologyJson) + ? TopicTopology.Default + : TopicTopology.FromOptions(JsonSerializer.Deserialize(topologyJson, TopologyOptionsSerializationContext.Default.TopologyOptions)); } transportSettings.Set(selectedTopology); @@ -99,5 +98,7 @@ protected override void AddTransportForMonitoringCore(IServiceCollection service services.AddSingleton(); services.AddHostedService(provider => provider.GetRequiredService()); } + + const string ServiceBusSectionName = "ServiceControl.Transport.ASBS"; } } \ No newline at end of file diff --git a/src/ServiceControl.Transports.Learning/QueueLengthProvider.cs b/src/ServiceControl.Transports.Learning/QueueLengthProvider.cs index 93bc2fcc41..62e9f28ec3 100644 --- a/src/ServiceControl.Transports.Learning/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.Learning/QueueLengthProvider.cs @@ -39,6 +39,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } catch (Exception ex) { + await Task.Delay(5000, stoppingToken); logger.LogWarning(ex, "Problem getting learning transport queue length"); } } diff --git a/src/ServiceControl.Transports/TransportFactory.cs b/src/ServiceControl.Transports/TransportFactory.cs index 14051a6e09..2bd37763ca 100644 --- a/src/ServiceControl.Transports/TransportFactory.cs +++ b/src/ServiceControl.Transports/TransportFactory.cs @@ -2,16 +2,19 @@ namespace ServiceControl.Transports { using System; using System.IO; + using System.Runtime.Loader; + using Infrastructure; public static class TransportFactory { + internal static Func AssemblyLoadContextResolver { get; set; } = static (assemblyPath) => new PluginAssemblyLoadContext(assemblyPath); public static ITransportCustomization Create(TransportSettings settings) { try { var transportManifest = TransportManifestLibrary.Find(settings.TransportType); var assemblyPath = Path.Combine(transportManifest.Location, $"{transportManifest.AssemblyName}.dll"); - var loadContext = settings.AssemblyLoadContextResolver(assemblyPath); + var loadContext = AssemblyLoadContextResolver(assemblyPath); var customizationType = Type.GetType(transportManifest.TypeName, loadContext.LoadFromAssemblyName, null, true); return (ITransportCustomization)Activator.CreateInstance(customizationType); diff --git a/src/ServiceControl.Transports/TransportSettings.cs b/src/ServiceControl.Transports/TransportSettings.cs index eaa94cac91..6045ffeb17 100644 --- a/src/ServiceControl.Transports/TransportSettings.cs +++ b/src/ServiceControl.Transports/TransportSettings.cs @@ -6,6 +6,7 @@ public class TransportSettings : SettingsHolder { + [Obsolete("",true)] public Func AssemblyLoadContextResolver { get; set; } public string TransportType { get; set; } diff --git a/src/ServiceControl.UnitTests/API/APIApprovals.cs b/src/ServiceControl.UnitTests/API/APIApprovals.cs index 1a75ce026f..719f68ce0c 100644 --- a/src/ServiceControl.UnitTests/API/APIApprovals.cs +++ b/src/ServiceControl.UnitTests/API/APIApprovals.cs @@ -146,7 +146,7 @@ string RemoveDataStoreSettings(string json) var result = string.Empty; - var dataStoreSettings = new[] { nameof(Settings.PersistenceType) }; + var dataStoreSettings = new[] { nameof(PrimaryOptions.PersistenceType) }; foreach (var settingLine in settingsLines) { diff --git a/src/ServiceControl.UnitTests/Infrastructure/Settings/SettingsTests.cs b/src/ServiceControl.UnitTests/Infrastructure/Settings/SettingsTests.cs index 58ee716a5d..4285bc7b91 100644 --- a/src/ServiceControl.UnitTests/Infrastructure/Settings/SettingsTests.cs +++ b/src/ServiceControl.UnitTests/Infrastructure/Settings/SettingsTests.cs @@ -1,6 +1,5 @@ namespace ServiceControl.UnitTests.Infrastructure.Settings { - using System.Collections; using System.Collections.Generic; using NUnit.Framework; using ServiceBus.Management.Infrastructure.Settings; @@ -12,7 +11,7 @@ public class SettingsTests public void Should_read_RemoteInstances_from_serialized_json() { var configValue = """[{"api_uri":"http://instance1"},{"api_uri":"http://instance2"}]"""; - var remoteInstances = Settings.ParseRemoteInstances(configValue); + var remoteInstances = PrimaryOptionsPostConfiguration.ParseRemoteInstances(configValue); Assert.That( new[] { new RemoteInstanceSetting("http://instance1"), new RemoteInstanceSetting("http://instance2") }, diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 6189431088..35124aca10 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -26,8 +26,16 @@ public async Task Should_handle_cancellation_token_gracefully() var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore([]), new MockEndpointSettingsStore([]), - new MockEndpointInstanceMonitoring([]), new Settings { TrackInstancesInitialValue = true }, - fakeTimeProvider, NullLogger.Instance) + new MockEndpointInstanceMonitoring([]), new Settings + { + ServiceControl = + { + TrackInstancesInitialValue = true + } + }, + fakeTimeProvider, + NullLogger.Instance + ) { DelayStart = TimeSpan.Zero }; @@ -46,13 +54,25 @@ public async Task Should_delete_settings_from_endpoints_that_are_no_longer_live( CancellationToken token = tokenSource.Token; var fakeTimeProvider = new FakeTimeProvider(); var mockEndpointSettingsStore = new MockEndpointSettingsStore([ - new EndpointSettings { Name = "Sales", TrackInstances = true }, - new EndpointSettings { Name = "Orders", TrackInstances = false } + new EndpointSettings + { + Name = "Sales", TrackInstances = true + }, + new EndpointSettings + { + Name = "Orders", TrackInstances = false + } ]); var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore([]), mockEndpointSettingsStore, - new MockEndpointInstanceMonitoring([]), new Settings { TrackInstancesInitialValue = true }, + new MockEndpointInstanceMonitoring([]), new Settings + { + ServiceControl = + { + TrackInstancesInitialValue = true + } + }, fakeTimeProvider, NullLogger.Instance) { DelayStart = TimeSpan.Zero @@ -79,7 +99,13 @@ public async Task Should_set_the_default_for_settings_if_does_not_exist_already( []), mockEndpointSettingsStore, new MockEndpointInstanceMonitoring([]), - new Settings { TrackInstancesInitialValue = expectedTrackInstancesInitialValue }, + new Settings + { + ServiceControl = + { + TrackInstancesInitialValue = expectedTrackInstancesInitialValue + } + }, fakeTimeProvider, NullLogger.Instance) { DelayStart = TimeSpan.Zero @@ -104,14 +130,23 @@ public async Task Should_not_set_the_default_if_already_exists() const bool expectedTrackInstancesInitialValue = false; var fakeTimeProvider = new FakeTimeProvider(); var mockEndpointSettingsStore = new MockEndpointSettingsStore([ - new EndpointSettings { Name = string.Empty, TrackInstances = expectedTrackInstancesInitialValue } + new EndpointSettings + { + Name = string.Empty, TrackInstances = expectedTrackInstancesInitialValue + } ]); var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore( []), mockEndpointSettingsStore, new MockEndpointInstanceMonitoring([]), - new Settings { TrackInstancesInitialValue = expectedTrackInstancesInitialValue }, + new Settings + { + ServiceControl = + { + TrackInstancesInitialValue = expectedTrackInstancesInitialValue + } + }, fakeTimeProvider, NullLogger.Instance) { DelayStart = TimeSpan.Zero @@ -136,22 +171,48 @@ public async Task Guid instanceB = DeterministicGuid.MakeId(endpointName1, "B"); Guid instanceC = DeterministicGuid.MakeId(endpointName1, "C"); var mockMonitoringDataStore = new MockMonitoringDataStore( - [new KnownEndpoint { EndpointDetails = new EndpointDetails { Name = endpointName1 } }]); + [ + new KnownEndpoint + { + EndpointDetails = new EndpointDetails + { + Name = endpointName1 + } + } + ]); var mockEndpointInstanceMonitoring = new MockEndpointInstanceMonitoring([ - new EndpointsView { IsSendingHeartbeats = false, Name = endpointName1, Id = instanceA }, - new EndpointsView { IsSendingHeartbeats = false, Name = endpointName1, Id = instanceB }, - new EndpointsView { IsSendingHeartbeats = false, Name = endpointName1, Id = instanceC }, new EndpointsView { - IsSendingHeartbeats = true, - Name = endpointName1, - Id = DeterministicGuid.MakeId(endpointName1, "D") + IsSendingHeartbeats = false, Name = endpointName1, Id = instanceA + }, + new EndpointsView + { + IsSendingHeartbeats = false, Name = endpointName1, Id = instanceB + }, + new EndpointsView + { + IsSendingHeartbeats = false, Name = endpointName1, Id = instanceC + }, + new EndpointsView + { + IsSendingHeartbeats = true, Name = endpointName1, Id = DeterministicGuid.MakeId(endpointName1, "D") } ]); var service = new HeartbeatEndpointSettingsSyncHostedService( mockMonitoringDataStore, - new MockEndpointSettingsStore([new EndpointSettings { Name = endpointName1, TrackInstances = false }]), - mockEndpointInstanceMonitoring, new Settings { TrackInstancesInitialValue = true }, + new MockEndpointSettingsStore([ + new EndpointSettings + { + Name = endpointName1, TrackInstances = false + } + ]), + mockEndpointInstanceMonitoring, new Settings + { + ServiceControl = + { + TrackInstancesInitialValue = true + } + }, fakeTimeProvider, NullLogger.Instance) { DelayStart = TimeSpan.Zero @@ -177,20 +238,40 @@ public async Task const string endpointName1 = "Sales"; Guid instanceA = DeterministicGuid.MakeId(endpointName1, "A"); var mockMonitoringDataStore = new MockMonitoringDataStore( - [new KnownEndpoint { EndpointDetails = new EndpointDetails { Name = endpointName1 } }]); + [ + new KnownEndpoint + { + EndpointDetails = new EndpointDetails + { + Name = endpointName1 + } + } + ]); var mockEndpointInstanceMonitoring = new MockEndpointInstanceMonitoring([ - new EndpointsView { IsSendingHeartbeats = false, Name = endpointName1, Id = instanceA }, new EndpointsView { - IsSendingHeartbeats = true, - Name = endpointName1, - Id = DeterministicGuid.MakeId(endpointName1, "B") + IsSendingHeartbeats = false, Name = endpointName1, Id = instanceA + }, + new EndpointsView + { + IsSendingHeartbeats = true, Name = endpointName1, Id = DeterministicGuid.MakeId(endpointName1, "B") } ]); var service = new HeartbeatEndpointSettingsSyncHostedService( mockMonitoringDataStore, - new MockEndpointSettingsStore([new EndpointSettings { Name = endpointName1, TrackInstances = true }]), - mockEndpointInstanceMonitoring, new Settings { TrackInstancesInitialValue = true }, + new MockEndpointSettingsStore([ + new EndpointSettings + { + Name = endpointName1, TrackInstances = true + } + ]), + mockEndpointInstanceMonitoring, new Settings + { + ServiceControl = + { + TrackInstancesInitialValue = true + } + }, fakeTimeProvider, NullLogger.Instance) { DelayStart = TimeSpan.Zero diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs index e7133ba20a..b5fa98714f 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs @@ -79,7 +79,7 @@ public async Task> GetEndpointAuditCounts([FromQuery] PagingIn [HttpGet] public async Task Get(string id, [FromQuery(Name = "instance_id")] string instanceId) { - if (string.IsNullOrWhiteSpace(instanceId) || instanceId == settings.InstanceId) + if (string.IsNullOrWhiteSpace(instanceId) || instanceId == settings.ServiceControl.InstanceId) { var result = await bodyStorage.TryFetch(id); @@ -97,7 +97,7 @@ public async Task Get(string id, [FromQuery(Name = "instance_id") return File(result.Stream, result.ContentType ?? "text/*"); } - var remote = settings.RemoteInstances.SingleOrDefault(r => r.InstanceId == instanceId); + var remote = settings.ServiceControl.RemoteInstanceSettings.SingleOrDefault(r => r.InstanceId == instanceId); if (remote == null) { diff --git a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs index d9fc9cb535..c76970c86f 100644 --- a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs @@ -41,8 +41,8 @@ protected ScatterGatherApi(TDataStore store, Settings settings, IHttpClientFacto public async Task> Execute(TIn input, string pathAndQuery) { - var remotes = Settings.RemoteInstances; - var instanceId = Settings.InstanceId; + var remotes = Settings.ServiceControl.RemoteInstanceSettings; + var instanceId = Settings.ServiceControl.InstanceId; var tasks = new List>>(remotes.Length + 1) { LocalCall(input, instanceId) diff --git a/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs b/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs index a0b8df6089..bf242f3735 100644 --- a/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs +++ b/src/ServiceControl/Connection/RemotePlatformConnectionDetailsProvider.cs @@ -13,7 +13,7 @@ class RemotePlatformConnectionDetailsProvider(Settings settings, IHttpClientFact { public Task ProvideConnectionDetails(PlatformConnectionDetails connection) => Task.WhenAll( - settings.RemoteInstances + settings.ServiceControl.RemoteInstanceSettings .Select(remote => UpdateFromRemote(remote, connection)) ); diff --git a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecks.cs b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecks.cs index cc840c6e76..8b290b20aa 100644 --- a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecks.cs +++ b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecks.cs @@ -1,15 +1,9 @@ namespace ServiceControl.CustomChecks { - using System.Linq; - using Infrastructure.BackgroundTasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using NServiceBus.CustomChecks; - using NServiceBus.Hosting; using Operations; using SagaAudit; - using ServiceBus.Management.Infrastructure.Settings; static class InternalCustomChecks { @@ -19,14 +13,7 @@ public static IHostApplicationBuilder AddInternalCustomChecks(this IHostApplicat services.AddCustomCheck(); services.AddCustomCheck(); services.AddCustomCheck(); - - services.AddHostedService(provider => new InternalCustomChecksHostedService( - provider.GetServices().ToList(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService().InstanceName, - provider.GetRequiredService>())); + services.AddHostedService(provider => provider.GetRequiredService()); return hostBuilder; } } diff --git a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs index b75804e2d1..c50308a276 100644 --- a/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs +++ b/src/ServiceControl/CustomChecks/InternalCustomChecks/InternalCustomChecksHostedService.cs @@ -7,8 +7,10 @@ using Infrastructure.BackgroundTasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus.CustomChecks; using NServiceBus.Hosting; + using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Operations; class InternalCustomChecksHostedService( @@ -16,7 +18,7 @@ class InternalCustomChecksHostedService( HostInformation hostInfo, IAsyncTimer scheduler, CustomCheckResultProcessor checkResultProcessor, - string endpointName, + IOptions options, ILogger logger) : IHostedService { @@ -45,7 +47,7 @@ public async Task StopAsync(CancellationToken cancellationToken) { Host = hostInfo.DisplayName, HostId = hostInfo.HostId, - Name = endpointName + Name = options.Value.InstanceName }; IList managers = []; } diff --git a/src/ServiceControl/HostApplicationBuilderExtensions.cs b/src/ServiceControl/HostApplicationBuilderExtensions.cs index fbe99dada5..f021c50db4 100644 --- a/src/ServiceControl/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl/HostApplicationBuilderExtensions.cs @@ -3,6 +3,8 @@ namespace Particular.ServiceControl using System; using System.Diagnostics; using System.Runtime.InteropServices; + using System.Threading; + using System.Threading.Tasks; using global::ServiceControl.CustomChecks; using global::ServiceControl.ExternalIntegrations; using global::ServiceControl.Hosting; @@ -17,24 +19,30 @@ namespace Particular.ServiceControl using global::ServiceControl.Transports; using Licensing; using Microsoft.AspNetCore.HttpLogging; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.WindowsServices; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus; using NServiceBus.Configuration.AdvancedExtensibility; using NServiceBus.Transport; + using NuGet.Versioning; using ServiceBus.Management.Infrastructure; using ServiceBus.Management.Infrastructure.Installers; using ServiceBus.Management.Infrastructure.Settings; static class HostApplicationBuilderExtensions { - public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, Settings settings, EndpointConfiguration configuration) + public static void AddServiceControl( + this IHostApplicationBuilder hostBuilder, + Settings settings, + EndpointConfiguration endpointConfiguration + ) { - ArgumentNullException.ThrowIfNull(configuration); - RecordStartup(settings, configuration); + AddVersion(hostBuilder); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.UserInteractive && Debugger.IsAttached) { @@ -42,14 +50,14 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S } hostBuilder.Logging.ClearProviders(); - hostBuilder.Logging.ConfigureLogging(settings.LoggingSettings.LogLevel); + hostBuilder.Logging.ConfigureLogging(settings.Logging.ToLoggingSettings().LogLevel); var services = hostBuilder.Services; - var transportSettings = settings.ToTransportSettings(); + var transportSettings = settings.ServiceControl.ToTransportSettings(); var transportCustomization = TransportFactory.Create(transportSettings); transportCustomization.AddTransportForPrimary(services, transportSettings); - services.Configure(options => options.ShutdownTimeout = settings.ShutdownTimeout); + services.Configure(options => options.ShutdownTimeout = settings.ServiceControl.ShutdownTimeout); services.AddSingleton(); services.AddSingleton(); @@ -73,13 +81,19 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S services.AddSingleton(provider => new Lazy(provider.GetRequiredService)); services.AddLicenseCheck(); - services.AddPersistence(settings); - services.AddMetrics(settings.PrintMetrics); - NServiceBusFactory.Configure(settings, transportCustomization, transportSettings, configuration); - hostBuilder.UseNServiceBus(configuration); + hostBuilder.AddPersistence(); - if (!settings.DisableExternalIntegrationsPublishing) + services.AddMetrics(settings.ServiceControl.PrintMetrics); + + NServiceBusFactory.Configure(settings, transportCustomization, transportSettings, endpointConfiguration); + endpointConfiguration.GetSettings().AddStartupDiagnosticsSection("Startup", new + { + Settings = settings, + }); + hostBuilder.UseNServiceBus(endpointConfiguration); + + if (!settings.ServiceControl.DisableExternalIntegrationsPublishing) { hostBuilder.AddExternalIntegrationEvents(); } @@ -88,7 +102,7 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S hostBuilder.AddEmailNotifications(); hostBuilder.AddAsyncTimer(); - if (!settings.DisableHealthChecks) + if (!settings.ServiceControl.DisableHealthChecks) { hostBuilder.AddInternalCustomChecks(); } @@ -102,33 +116,41 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S hostBuilder.AddServiceControlComponents(settings, transportCustomization, ServiceControlMainInstance.Components); } - public static void AddServiceControlInstallers(this IHostApplicationBuilder hostApplicationBuilder, Settings settings) + static void AddVersion(IHostApplicationBuilder hostBuilder) => hostBuilder.Services.AddSingleton(NuGetVersion.Parse(FileVersionInfo.GetVersionInfo(typeof(HostApplicationBuilderExtensions).Assembly.Location).ProductVersion)); + + public static void AddServiceControlInstallers(this IHostApplicationBuilder hostApplicationBuilder) { - var persistence = PersistenceFactory.Create(settings); + var persistence = PersistenceFactory.Create(hostApplicationBuilder.Configuration); persistence.AddInstaller(hostApplicationBuilder.Services); } + } - static void RecordStartup(Settings settings, EndpointConfiguration endpointConfiguration) + public class RecordStartup( + ILogger logger, + IOptions primaryOptions, + IOptions loggingOptions, + NuGetVersion version + ) : BackgroundService + { + protected override Task ExecuteAsync(CancellationToken stoppingToken) { - var version = FileVersionInfo.GetVersionInfo(typeof(HostApplicationBuilderExtensions).Assembly.Location).ProductVersion; - - var startupMessage = $@" -------------------------------------------------------------- -ServiceControl Version: {version} -Audit Retention Period (optional): {settings.AuditRetentionPeriod} -Error Retention Period: {settings.ErrorRetentionPeriod} -Ingest Error Messages: {settings.IngestErrorMessages} -Forwarding Error Messages: {settings.ForwardErrorMessages} -ServiceControl Logging Level: {settings.LoggingSettings.LogLevel} -Selected Transport Customization: {settings.TransportType} --------------------------------------------------------------"; - - var logger = LoggerUtil.CreateStaticLogger(typeof(HostApplicationBuilderExtensions), settings.LoggingSettings.LogLevel); + var primary = primaryOptions.Value; + var logging = loggingOptions.Value; + + var startupMessage = $""" + ------------------------------------------------------------- + ServiceControl Version: {version} + Audit Retention Period (optional): {primary.AuditRetentionPeriod} + Error Retention Period: {primary.ErrorRetentionPeriod} + Ingest Error Messages: {primary.IngestErrorMessages} + Forwarding Error Messages: {primary.ForwardErrorMessages} + ServiceControl Logging Level: {logging.LogLevel} + Selected Transport Customization: {primary.TransportType} + ------------------------------------------------------------ + """; + logger.LogInformation(startupMessage); - endpointConfiguration.GetSettings().AddStartupDiagnosticsSection("Startup", new - { - Settings = settings, - }); + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/src/ServiceControl/Hosting/Commands/AbstractCommand.cs b/src/ServiceControl/Hosting/Commands/AbstractCommand.cs index 4178465847..fbb8747809 100644 --- a/src/ServiceControl/Hosting/Commands/AbstractCommand.cs +++ b/src/ServiceControl/Hosting/Commands/AbstractCommand.cs @@ -2,10 +2,9 @@ { using System.Threading.Tasks; using Particular.ServiceControl.Hosting; - using ServiceBus.Management.Infrastructure.Settings; abstract class AbstractCommand { - public abstract Task Execute(HostArguments args, Settings settings); + public abstract Task Execute(HostArguments args); } } \ No newline at end of file diff --git a/src/ServiceControl/Hosting/Commands/CommandRunner.cs b/src/ServiceControl/Hosting/Commands/CommandRunner.cs index f959364f85..d4e36a7bae 100644 --- a/src/ServiceControl/Hosting/Commands/CommandRunner.cs +++ b/src/ServiceControl/Hosting/Commands/CommandRunner.cs @@ -3,14 +3,13 @@ using System; using System.Threading.Tasks; using Particular.ServiceControl.Hosting; - using ServiceBus.Management.Infrastructure.Settings; class CommandRunner(Type commandType) { - public async Task Execute(HostArguments args, Settings settings) + public async Task Execute(HostArguments args) { var command = (AbstractCommand)Activator.CreateInstance(commandType); - await command.Execute(args, settings); + await command.Execute(args); } } } \ No newline at end of file diff --git a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs index 4adafa724f..2e7209c3d0 100644 --- a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs +++ b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Infrastructure.WebApi; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -16,15 +17,20 @@ class ImportFailedErrorsCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args) { - settings.IngestErrorMessages = false; - settings.RunRetryProcessor = false; - settings.DisableHealthChecks = true; + var hostBuilder = Host.CreateApplicationBuilder(); + hostBuilder.SetupApplicationConfiguration(); + hostBuilder.Services.Configure(s => + { + s.IngestErrorMessages = false; + s.RunRetryProcessor = false; + s.DisableHealthChecks = true; + }); - EndpointConfiguration endpointConfiguration = CreateEndpointConfiguration(settings); + var settings = hostBuilder.Configuration.Get(); // THIS WILL NOT RESOLVE SETTINGS WITH THE ABOVE VALUES !!!! TOOD: SHould really use a different type any settings used here. - var hostBuilder = Host.CreateApplicationBuilder(); + EndpointConfiguration endpointConfiguration = CreateEndpointConfiguration(settings); hostBuilder.AddServiceControl(settings, endpointConfiguration); hostBuilder.AddServiceControlApi(); @@ -52,7 +58,7 @@ public override async Task Execute(HostArguments args, Settings settings) protected virtual EndpointConfiguration CreateEndpointConfiguration(Settings settings) { - var endpointConfiguration = new EndpointConfiguration(settings.InstanceName); + var endpointConfiguration = new EndpointConfiguration(settings.ServiceControl.InstanceName); var assemblyScanner = endpointConfiguration.AssemblyScanner(); assemblyScanner.ExcludeAssemblies("ServiceControl.Plugin"); diff --git a/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs b/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs index f3ba85e132..1aa816e415 100644 --- a/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs +++ b/src/ServiceControl/Hosting/Commands/MaintenanceModeCommand.cs @@ -1,18 +1,20 @@ namespace ServiceControl.Hosting.Commands { using System.Threading.Tasks; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.WindowsServices; using Particular.ServiceControl.Hosting; using Persistence; using ServiceBus.Management.Infrastructure.Settings; - class MaintenanceModeCommand : AbstractCommand + sealed class MaintenanceModeCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args) { var hostBuilder = Host.CreateApplicationBuilder(); - hostBuilder.Services.AddPersistence(settings, maintenanceMode: true); + hostBuilder.SetupApplicationConfiguration(); + hostBuilder.AddPersistence(); if (WindowsServiceHelpers.IsWindowsService()) { diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index db658857de..65b4203d2c 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -3,29 +3,35 @@ using System.Threading.Tasks; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; using NServiceBus; + using NServiceBus.Configuration.AdvancedExtensibility; using Particular.ServiceControl; using Particular.ServiceControl.Hosting; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl; - class RunCommand : AbstractCommand + sealed class RunCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args) { - var endpointConfiguration = new EndpointConfiguration(settings.InstanceName); - var assemblyScanner = endpointConfiguration.AssemblyScanner(); - assemblyScanner.ExcludeAssemblies("ServiceControl.Plugin"); + var hostBuilder = WebApplication.CreateBuilder(); + var settings = hostBuilder.Configuration.Get(); - settings.RunCleanupBundle = true; + hostBuilder.SetupApplicationConfiguration(); + hostBuilder.Services.Configure(s => s.RunCleanupBundle = true); - var hostBuilder = WebApplication.CreateBuilder(); + var endpointConfiguration = new EndpointConfiguration(settings.ServiceControl.InstanceName); + + var assemblyScanner = endpointConfiguration.AssemblyScanner(); + assemblyScanner.ExcludeAssemblies("ServiceControl.Plugin"); hostBuilder.AddServiceControl(settings, endpointConfiguration); hostBuilder.AddServiceControlApi(); var app = hostBuilder.Build(); app.UseServiceControl(); - await app.RunAsync(settings.RootUrl); + await app.RunAsync(settings.ServiceControl.RootUrl); } } -} +} \ No newline at end of file diff --git a/src/ServiceControl/Hosting/Commands/SetupCommand.cs b/src/ServiceControl/Hosting/Commands/SetupCommand.cs index 3fdc2e7e11..df2bc0a53b 100644 --- a/src/ServiceControl/Hosting/Commands/SetupCommand.cs +++ b/src/ServiceControl/Hosting/Commands/SetupCommand.cs @@ -2,6 +2,7 @@ { using System.Runtime.InteropServices; using System.Threading.Tasks; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Particular.ServiceControl; @@ -11,12 +12,15 @@ using ServiceControl.Infrastructure; using Transports; - class SetupCommand : AbstractCommand + sealed class SetupCommand : AbstractCommand { - public override async Task Execute(HostArguments args, Settings settings) + public override async Task Execute(HostArguments args) { var hostBuilder = Host.CreateApplicationBuilder(); - hostBuilder.AddServiceControlInstallers(settings); + hostBuilder.SetupApplicationConfiguration(); + var settings = hostBuilder.Configuration.Get(); + + hostBuilder.AddServiceControlInstallers(); var componentSetupContext = new ComponentInstallationContext(); @@ -40,7 +44,7 @@ public override async Task Execute(HostArguments args, Settings settings) } else { - var transportSettings = settings.ToTransportSettings(); + var transportSettings = settings.ServiceControl.ToTransportSettings(); transportSettings.RunCustomChecks = false; var transportCustomization = TransportFactory.Create(transportSettings); diff --git a/src/ServiceControl/Hosting/HostArguments.cs b/src/ServiceControl/Hosting/HostArguments.cs index f2151d8968..932fe359f0 100644 --- a/src/ServiceControl/Hosting/HostArguments.cs +++ b/src/ServiceControl/Hosting/HostArguments.cs @@ -3,15 +3,14 @@ namespace Particular.ServiceControl.Hosting using System; using System.IO; using System.Reflection; - using global::ServiceControl.Configuration; using global::ServiceControl.Hosting.Commands; - using ServiceBus.Management.Infrastructure.Settings; + using Microsoft.Extensions.Configuration; class HostArguments { - public HostArguments(string[] args) + public HostArguments(string[] args, bool maintenanceMode) { - if (SettingsReader.Read(Settings.SettingsRootNamespace, "MaintenanceMode")) + if (maintenanceMode) { args = [.. args, "-m"]; } diff --git a/src/ServiceControl/HostingComponent.cs b/src/ServiceControl/HostingComponent.cs index df5d818048..6a7a4c9426 100644 --- a/src/ServiceControl/HostingComponent.cs +++ b/src/ServiceControl/HostingComponent.cs @@ -16,6 +16,6 @@ public override void Configure(Settings settings, ITransportCustomization transp services.AddSingleton(); } - public override void Setup(Settings settings, IComponentInstallationContext context, IHostApplicationBuilder hostBuilder) => context.CreateQueue(settings.InstanceName); + public override void Setup(Settings settings, IComponentInstallationContext context, IHostApplicationBuilder hostBuilder) => context.CreateQueue(settings.ServiceControl.InstanceName); } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs index f8af272eb4..303653d1ad 100644 --- a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs +++ b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs @@ -35,8 +35,8 @@ public Task GetUrls(string baseUrl, CancellationToken cancellationToke EndpointsMessagesUrl = baseUrl + "endpoints/{name}/messages/{?page,per_page,direction,sort}", AuditCountUrl = baseUrl + "endpoints/{name}/audit-count", - Name = SettingsReader.Read(Settings.SettingsRootNamespace, "Name", "ServiceControl"), - Description = SettingsReader.Read(Settings.SettingsRootNamespace, "Description", "The management backend for the Particular Service Platform"), + Name = settings.ServiceControl.Name, + Description = settings.ServiceControl.Description, LicenseStatus = license.IsValid ? "valid" : "invalid", LicenseDetails = baseUrl + "license", Configuration = baseUrl + "configuration", @@ -56,33 +56,33 @@ public Task GetConfig(CancellationToken cancellationToken) { Host = new { - settings.InstanceName, + settings.ServiceControl.InstanceName, Logging = new { - settings.LoggingSettings.LogPath, - LoggingLevel = settings.LoggingSettings.LogLevel + settings.Logging.LogPath, + LoggingLevel = settings.Logging.LogLevel } }, DataRetention = new { - settings.AuditRetentionPeriod, - settings.ErrorRetentionPeriod + settings.ServiceControl.AuditRetentionPeriod, + settings.ServiceControl.ErrorRetentionPeriod }, PerformanceTunning = new { - settings.ExternalIntegrationsDispatchingBatchSize + settings.ServiceControl.ExternalIntegrationsDispatchingBatchSize }, PersistenceSettings = settings.PersisterSpecificSettings, Transport = new { - settings.TransportType, - settings.ErrorLogQueue, - settings.ErrorQueue, - settings.ForwardErrorMessages + settings.ServiceControl.TransportType, + settings.ServiceBus.ErrorLogQueue, + settings.ServiceBus.ErrorQueue, + settings.ServiceControl.ForwardErrorMessages }, Plugins = new { - settings.HeartbeatGracePeriod + settings.ServiceControl.HeartbeatGracePeriod }, MassTransitConnector = connectorHeartbeatStatus.LastHeartbeat }; @@ -92,7 +92,7 @@ public Task GetConfig(CancellationToken cancellationToken) public async Task GetRemoteConfigs(CancellationToken cancellationToken = default) { - var remotes = settings.RemoteInstances; + var remotes = settings.ServiceControl.RemoteInstanceSettings; var tasks = remotes .Select(async remote => { diff --git a/src/ServiceControl/Infrastructure/NServiceBusFactory.cs b/src/ServiceControl/Infrastructure/NServiceBusFactory.cs index 72116beaae..affe83185d 100644 --- a/src/ServiceControl/Infrastructure/NServiceBusFactory.cs +++ b/src/ServiceControl/Infrastructure/NServiceBusFactory.cs @@ -32,10 +32,10 @@ public static void Configure(Settings.Settings settings, ITransportCustomization transportCustomization.CustomizePrimaryEndpoint(configuration, transportSettings); - configuration.GetSettings().Set(settings.LoggingSettings); - configuration.SetDiagnosticsPath(settings.LoggingSettings.LogPath); + configuration.GetSettings().Set(settings.Logging); + configuration.SetDiagnosticsPath(settings.Logging.LogPath); - if (settings.DisableExternalIntegrationsPublishing) + if (settings.ServiceControl.DisableExternalIntegrationsPublishing) { configuration.DisableFeature(); } diff --git a/src/ServiceControl/Infrastructure/Settings/ConfigureSettings.cs b/src/ServiceControl/Infrastructure/Settings/ConfigureSettings.cs new file mode 100644 index 0000000000..e0e9199025 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/ConfigureSettings.cs @@ -0,0 +1,21 @@ +namespace ServiceBus.Management.Infrastructure.Settings; + +using Microsoft.Extensions.Options; +using ServiceControl.Infrastructure; +using ServiceControl.Persistence; + +class ConfigureSettings( + IOptions logging, + IOptions primary, + IOptions serviceBus, + PersistenceSettings persistenceSettings +) : IConfigureOptions +{ + public void Configure(Settings options) + { + options.Logging = logging.Value; + options.ServiceBus = serviceBus.Value; + options.ServiceControl = primary.Value; + options.PersisterSpecificSettings = persistenceSettings; + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/HostBuilderExtensions.cs b/src/ServiceControl/Infrastructure/Settings/HostBuilderExtensions.cs new file mode 100644 index 0000000000..a3c03999dd --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/HostBuilderExtensions.cs @@ -0,0 +1,42 @@ +namespace ServiceBus.Management.Infrastructure.Settings; + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using ServiceControl.Infrastructure; + +static class HostBuilderExtensions +{ + public static void SetupApplicationConfiguration(this IHostApplicationBuilder hostBuilder) + { + hostBuilder.Configuration + .SetBasePath(AppContext.BaseDirectory) + .AddLegacyAppSettings() + .AddEnvironmentVariables(); + + hostBuilder.Services.AddOptions() + .Services + .ConfigureOptions(); + + hostBuilder.Services.AddOptions() + .Bind(hostBuilder.Configuration.GetSection(PrimaryOptions.SectionName)); + + hostBuilder.Services.AddOptions() + .Bind(hostBuilder.Configuration.GetSection(PrimaryOptions.SectionName)) + .ValidateDataAnnotations() + .ValidateOnStart() + .Services + .AddSingleton, SettingsValidation>() + .AddSingleton, PrimaryOptionsPostConfiguration>(); + + hostBuilder.Services.AddOptions() + .Bind(hostBuilder.Configuration.GetSection(ServiceBusOptions.SectionName)) + .ValidateDataAnnotations() + .ValidateOnStart() + .Services + .AddSingleton, ServiceBusValidation>() + .AddSingleton, ServiceBusOptionsPostConfiguration>(); + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/PrimaryOptions.cs b/src/ServiceControl/Infrastructure/Settings/PrimaryOptions.cs new file mode 100644 index 0000000000..3fbf8f2cda --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/PrimaryOptions.cs @@ -0,0 +1,76 @@ +namespace ServiceBus.Management.Infrastructure.Settings; + +using System; +using NServiceBus.Transport; +using Particular.LicensingComponent.Shared; +using ServiceControl.Transports; + +public class PrimaryOptions +{ + + public const string SectionName = "ServiceControl"; + + public string TransportType { get; set; } + public string PersistenceType { get; set; } + public bool MaintenanceMode { get; set; } + public ServiceControlSettings ServiceControlSettings { get; set; } + public string NotificationsFilter { get; set; } + public bool AllowMessageEditing { get; set; } + public Func MessageFilter { get; set; } //HINT: acceptance tests only + public string EmailDropFolder { get; set; } //HINT: acceptance tests only + [Obsolete("Likely not needed?", true)] public bool ValidateConfiguration { get; set; } = true; + public int ExternalIntegrationsDispatchingBatchSize { get; set; } = 100; + public bool DisableExternalIntegrationsPublishing { get; set; } = false; + public bool RunCleanupBundle { get; set; } + public string RootUrl { get; set; } + public string ApiUrl => $"{RootUrl}api"; + public string InstanceId { get; set; } + public string StorageUrl => $"{RootUrl}storage"; + public string StagingQueue => $"{InstanceName}.staging"; + public int Port { get; set; } = 33333; + public bool PrintMetrics { get; set; } + public string Hostname { get; set; } = "localhost"; + public string VirtualDirectory { get; set; } = string.Empty; + public TimeSpan HeartbeatGracePeriod { get; set; } = TimeSpan.FromSeconds(40); + public bool? ForwardErrorMessages { get; set; } + public bool IngestErrorMessages { get; set; } = true; + public bool RunRetryProcessor { get; set; } = true; + public TimeSpan? AuditRetentionPeriod { get; set; } + public TimeSpan ErrorRetentionPeriod { get; set; } + public TimeSpan EventsRetentionPeriod { get; set; } = TimeSpan.FromDays(14); + public string InstanceName { get; set; } = DEFAULT_INSTANCE_NAME; + public bool TrackInstancesInitialValue { get; set; } = true; + public string ConnectionString { get; set; } + public TimeSpan ProcessRetryBatchesFrequency { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan TimeToRestartErrorIngestionAfterFailure { get; set; } = TimeSpan.FromSeconds(60); + public int? MaximumConcurrencyLevel { get; set; } + public int RetryHistoryDepth { get; set; } = 10; + public string RemoteInstances { get; set; } + public RemoteInstanceSetting[] RemoteInstanceSettings { get; set; } + + public bool DisableHealthChecks { get; set; } + + // The default value is set to the maximum allowed time by the most + // restrictive hosting platform, which is Linux containers. Linux + // containers allow for a maximum of 10 seconds. We set it to 5 to + // allow for cancellation and logging to take place + public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + public string Name { get; set; } = "ServiceControl"; + public string Description { get; set; } = "The management backend for the Particular Service Platform"; + public string InternalQueueName { get; set; } + + public TransportSettings ToTransportSettings() + { + var transportSettings = new TransportSettings + { + EndpointName = InstanceName, + ConnectionString = ConnectionString, + MaxConcurrency = MaximumConcurrencyLevel, + RunCustomChecks = true, + TransportType = TransportType + }; + return transportSettings; + } + + public const string DEFAULT_INSTANCE_NAME = "Particular.ServiceControl"; +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/PrimaryOptionsPostConfiguration.cs b/src/ServiceControl/Infrastructure/Settings/PrimaryOptionsPostConfiguration.cs new file mode 100644 index 0000000000..536b0b7516 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/PrimaryOptionsPostConfiguration.cs @@ -0,0 +1,36 @@ +namespace ServiceBus.Management.Infrastructure.Settings; + +using System.Text.Json; +using Microsoft.Extensions.Options; +using ServiceControl.Configuration; +using ServiceControl.Infrastructure.Settings; +using ServiceControl.Infrastructure.WebApi; + +class PrimaryOptionsPostConfiguration : IPostConfigureOptions // TODO: Register +{ + public void PostConfigure(string name, PrimaryOptions options) + { + var suffix = string.Empty; + if (!string.IsNullOrEmpty(options.VirtualDirectory)) + { + suffix = $"{options.VirtualDirectory}/"; + } + + options.RootUrl = $"http://{options.Hostname}:{options.Port}/{suffix}"; + + if (AppEnvironment.RunningInContainer) + { + options.Hostname = "*"; + options.Port = 33333; + } + + options.ConnectionString ??= System.Configuration.ConfigurationManager.ConnectionStrings["NServiceBus/Transport"]?.ConnectionString; + options.InstanceId = InstanceIdGenerator.FromApiUrl(options.ApiUrl); + + options.RemoteInstanceSettings = string.IsNullOrEmpty(options.RemoteInstances) + ? [] + : ParseRemoteInstances(options.RemoteInstances); + } + + internal static RemoteInstanceSetting[] ParseRemoteInstances(string value) => JsonSerializer.Deserialize(value, SerializerOptions.Default) ?? []; +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/ServiceBusOptions.cs b/src/ServiceControl/Infrastructure/Settings/ServiceBusOptions.cs new file mode 100644 index 0000000000..5d28e74e05 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/ServiceBusOptions.cs @@ -0,0 +1,9 @@ +namespace ServiceBus.Management.Infrastructure.Settings; + +public class ServiceBusOptions +{ + public const string SectionName = "ServiceBus"; + + public string ErrorLogQueue { get; set; } + public string ErrorQueue { get; set; } = "error"; +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/ServiceBusOptionsPostConfiguration.cs b/src/ServiceControl/Infrastructure/Settings/ServiceBusOptionsPostConfiguration.cs new file mode 100644 index 0000000000..9514b34dc8 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/ServiceBusOptionsPostConfiguration.cs @@ -0,0 +1,31 @@ +namespace ServiceBus.Management.Infrastructure.Settings; + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +class ServiceBusOptionsPostConfiguration(ILogger logger) : IPostConfigureOptions // TODO: Register +{ + public void PostConfigure(string name, ServiceBusOptions options) + { + if (string.IsNullOrEmpty(options.ErrorLogQueue)) + { + logger.LogInformation("No settings found for audit log queue to import, default name will be used"); + options.ErrorLogQueue = Subscope(options.ErrorLogQueue); + } + } + + static string Subscope(string address) + { + var atIndex = address.IndexOf("@", StringComparison.InvariantCulture); + + if (atIndex <= -1) + { + return $"{address}.log"; + } + + var queue = address.Substring(0, atIndex); + var machine = address.Substring(atIndex + 1); + return $"{queue}.log@{machine}"; + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/ServiceBusValidation.cs b/src/ServiceControl/Infrastructure/Settings/ServiceBusValidation.cs new file mode 100644 index 0000000000..8bd86da1fa --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/ServiceBusValidation.cs @@ -0,0 +1,19 @@ +namespace ServiceBus.Management.Infrastructure.Settings; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +public class ServiceBusValidation( + ILogger logger // Intentionally using SETTINGS as logger name +) : IValidateOptions // TODO: Register +{ + public ValidateOptionsResult Validate(string name, ServiceBusOptions options) + { + if (string.IsNullOrEmpty(options.ErrorLogQueue)) + { + logger.LogInformation("No settings found for error log queue to import, default name will be used"); + } + + return ValidateOptionsResult.Success; + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index 772d33203d..2454d317b6 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -1,423 +1,12 @@ -namespace ServiceBus.Management.Infrastructure.Settings -{ - using System; - using System.Collections.Generic; - using System.Configuration; - using System.Linq; - using System.Runtime.Loader; - using System.Text.Json.Serialization; - using Microsoft.Extensions.Logging; - using NLog.Common; - using NServiceBus.Transport; - using ServiceControl.Configuration; - using ServiceControl.Infrastructure; - using ServiceControl.Infrastructure.Settings; - using ServiceControl.Infrastructure.WebApi; - using ServiceControl.Persistence; - using ServiceControl.Transports; - using JsonSerializer = System.Text.Json.JsonSerializer; - - public class Settings - { - public Settings( - string transportType = null, - string persisterType = null, - LoggingSettings loggingSettings = null, - bool? forwardErrorMessages = default, - TimeSpan? errorRetentionPeriod = default - ) - { - LoggingSettings = loggingSettings ?? new(SettingsRootNamespace); - - // Overwrite the instance name if it is specified in ENVVAR, reg, or config file -- LEGACY SETTING NAME - InstanceName = SettingsReader.Read(SettingsRootNamespace, "InternalQueueName", InstanceName); - - // Overwrite the instance name if it is specified in ENVVAR, reg, or config file - InstanceName = SettingsReader.Read(SettingsRootNamespace, "InstanceName", InstanceName); - - LoadErrorIngestionSettings(); - - TransportConnectionString = GetConnectionString(); - TransportType = transportType ?? SettingsReader.Read(SettingsRootNamespace, "TransportType"); - PersistenceType = persisterType ?? SettingsReader.Read(SettingsRootNamespace, "PersistenceType"); - AuditRetentionPeriod = GetAuditRetentionPeriod(); - ForwardErrorMessages = forwardErrorMessages ?? GetForwardErrorMessages(); - ErrorRetentionPeriod = errorRetentionPeriod ?? GetErrorRetentionPeriod(); - EventsRetentionPeriod = GetEventRetentionPeriod(); - - if (AppEnvironment.RunningInContainer) - { - Hostname = "*"; - Port = 33333; - } - else - { - Port = SettingsReader.Read(SettingsRootNamespace, "Port", 33333); - Hostname = SettingsReader.Read(SettingsRootNamespace, "Hostname", "localhost"); - } - - ProcessRetryBatchesFrequency = TimeSpan.FromSeconds(30); - MaximumConcurrencyLevel = SettingsReader.Read(SettingsRootNamespace, "MaximumConcurrencyLevel"); - RetryHistoryDepth = SettingsReader.Read(SettingsRootNamespace, "RetryHistoryDepth", 10); - AllowMessageEditing = SettingsReader.Read(SettingsRootNamespace, "AllowMessageEditing"); - NotificationsFilter = SettingsReader.Read(SettingsRootNamespace, "NotificationsFilter"); - RemoteInstances = GetRemoteInstances().ToArray(); - TimeToRestartErrorIngestionAfterFailure = GetTimeToRestartErrorIngestionAfterFailure(); - DisableExternalIntegrationsPublishing = SettingsReader.Read(SettingsRootNamespace, "DisableExternalIntegrationsPublishing", false); - TrackInstancesInitialValue = SettingsReader.Read(SettingsRootNamespace, "TrackInstancesInitialValue", true); - ShutdownTimeout = SettingsReader.Read(SettingsRootNamespace, "ShutdownTimeout", ShutdownTimeout); - AssemblyLoadContextResolver = static assemblyPath => new PluginAssemblyLoadContext(assemblyPath); - } - - [JsonIgnore] - public Func AssemblyLoadContextResolver { get; set; } - - public LoggingSettings LoggingSettings { get; } - - public string NotificationsFilter { get; set; } - - public bool AllowMessageEditing { get; set; } - - //HINT: acceptance tests only - public Func MessageFilter { get; set; } - - //HINT: acceptance tests only - public string EmailDropFolder { get; set; } - - public bool ValidateConfiguration => SettingsReader.Read(SettingsRootNamespace, "ValidateConfig", true); - - public int ExternalIntegrationsDispatchingBatchSize => SettingsReader.Read(SettingsRootNamespace, "ExternalIntegrationsDispatchingBatchSize", 100); - - public bool DisableExternalIntegrationsPublishing { get; set; } - - public bool RunCleanupBundle { get; set; } - - public string RootUrl - { - get - { - var suffix = string.Empty; - - if (!string.IsNullOrEmpty(VirtualDirectory)) - { - suffix = $"{VirtualDirectory}/"; - } - - return $"http://{Hostname}:{Port}/{suffix}"; - } - } - - public string ApiUrl => $"{RootUrl}api"; - - string _instanceId; - public string InstanceId - { - get - { - if (string.IsNullOrEmpty(_instanceId)) - { - _instanceId = InstanceIdGenerator.FromApiUrl(ApiUrl); - } - - return _instanceId; - } - } - - public string StorageUrl => $"{RootUrl}storage"; - - public string StagingQueue => $"{InstanceName}.staging"; - - public int Port { get; private set; } - - public PersistenceSettings PersisterSpecificSettings { get; set; } - - public bool PrintMetrics => SettingsReader.Read(SettingsRootNamespace, "PrintMetrics"); - public string Hostname { get; private set; } - public string VirtualDirectory => SettingsReader.Read(SettingsRootNamespace, "VirtualDirectory", string.Empty); - - public TimeSpan HeartbeatGracePeriod - { - get - { - try - { - return TimeSpan.Parse(SettingsReader.Read(SettingsRootNamespace, "HeartbeatGracePeriod", "00:00:40")); - } - catch (Exception ex) - { - logger.LogError(ex, "HeartbeatGracePeriod settings invalid. Defaulting HeartbeatGracePeriod to '00:00:40'"); - return TimeSpan.FromSeconds(40); - } - } - } - - public string TransportType { get; set; } - public string PersistenceType { get; private set; } - public string ErrorLogQueue { get; set; } - public string ErrorQueue { get; set; } - - public bool ForwardErrorMessages { get; set; } - - public bool IngestErrorMessages { get; set; } = true; - public bool RunRetryProcessor { get; set; } = true; - - public TimeSpan? AuditRetentionPeriod { get; set; } - - public TimeSpan ErrorRetentionPeriod { get; } - - public TimeSpan EventsRetentionPeriod { get; } - - public string InstanceName { get; init; } = DEFAULT_INSTANCE_NAME; - public bool TrackInstancesInitialValue { get; set; } - - public string TransportConnectionString { get; set; } - public TimeSpan ProcessRetryBatchesFrequency { get; set; } - public TimeSpan TimeToRestartErrorIngestionAfterFailure { get; set; } - public int? MaximumConcurrencyLevel { get; set; } +namespace ServiceBus.Management.Infrastructure.Settings; - public int RetryHistoryDepth { get; set; } +using ServiceControl.Infrastructure; +using ServiceControl.Persistence; - public RemoteInstanceSetting[] RemoteInstances { get; set; } - - public bool DisableHealthChecks { get; set; } - - // The default value is set to the maximum allowed time by the most - // restrictive hosting platform, which is Linux containers. Linux - // containers allow for a maximum of 10 seconds. We set it to 5 to - // allow for cancellation and logging to take place - public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); - - public string GetConnectionString() - { - var settingsValue = SettingsReader.Read(SettingsRootNamespace, "ConnectionString"); - if (settingsValue != null) - { - return settingsValue; - } - - var connectionStringSettings = ConfigurationManager.ConnectionStrings["NServiceBus/Transport"]; - return connectionStringSettings?.ConnectionString; - } - - public TransportSettings ToTransportSettings() - { - var transportSettings = new TransportSettings - { - EndpointName = InstanceName, - ConnectionString = TransportConnectionString, - MaxConcurrency = MaximumConcurrencyLevel, - RunCustomChecks = true, - TransportType = TransportType, - AssemblyLoadContextResolver = AssemblyLoadContextResolver - }; - return transportSettings; - } - - static bool GetForwardErrorMessages() - { - var forwardErrorMessages = SettingsReader.Read(SettingsRootNamespace, "ForwardErrorMessages"); - if (forwardErrorMessages.HasValue) - { - return forwardErrorMessages.Value; - } - - throw new Exception("ForwardErrorMessages settings is missing, please make sure it is included."); - } - - TimeSpan GetEventRetentionPeriod() - { - var valueRead = SettingsReader.Read(SettingsRootNamespace, "EventRetentionPeriod"); - if (valueRead != null) - { - if (TimeSpan.TryParse(valueRead, out var result)) - { - string message; - if (ValidateConfiguration && result < TimeSpan.FromHours(1)) - { - message = "EventRetentionPeriod settings is invalid, value should be minimum 1 hour"; - logger.LogCritical(message); - throw new Exception(message); - } - - if (ValidateConfiguration && result > TimeSpan.FromDays(200)) - { - message = "EventRetentionPeriod settings is invalid, value should be maximum 200 days"; - logger.LogCritical(message); - throw new Exception(message); - } - - return result; - } - } - - return TimeSpan.FromDays(14); - } - - TimeSpan GetErrorRetentionPeriod() - { - string message; - var valueRead = SettingsReader.Read(SettingsRootNamespace, "ErrorRetentionPeriod"); - if (valueRead == null) - { - message = "ErrorRetentionPeriod settings is missing, please make sure it is included"; - logger.LogCritical(message); - throw new Exception(message); - } - - if (TimeSpan.TryParse(valueRead, out var result)) - { - if (ValidateConfiguration && result < TimeSpan.FromDays(5)) - { - message = "ErrorRetentionPeriod settings is invalid, value should be minimum 5 days"; - logger.LogCritical(message); - throw new Exception(message); - } - - if (ValidateConfiguration && result > TimeSpan.FromDays(45)) - { - message = "ErrorRetentionPeriod settings is invalid, value should be maximum 45 days"; - logger.LogCritical(message); - throw new Exception(message); - } - } - else - { - message = "ErrorRetentionPeriod settings is invalid, please make sure it is a TimeSpan"; - logger.LogCritical(message); - throw new Exception(message); - } - - return result; - } - - TimeSpan? GetAuditRetentionPeriod() - { - string message; - var valueRead = SettingsReader.Read(SettingsRootNamespace, "AuditRetentionPeriod"); - if (valueRead == null) - { - return null; - } - - if (TimeSpan.TryParse(valueRead, out var result)) - { - if (ValidateConfiguration && result < TimeSpan.FromHours(1)) - { - message = "AuditRetentionPeriod settings is invalid, value should be minimum 1 hour."; - InternalLogger.Fatal(message); - throw new Exception(message); - } - - if (ValidateConfiguration && result > TimeSpan.FromDays(365)) - { - message = "AuditRetentionPeriod settings is invalid, value should be maximum 365 days."; - InternalLogger.Fatal(message); - throw new Exception(message); - } - } - else - { - message = "AuditRetentionPeriod settings is invalid, please make sure it is a TimeSpan."; - InternalLogger.Fatal(message); - throw new Exception(message); - } - - return result; - } - - TimeSpan GetTimeToRestartErrorIngestionAfterFailure() - { - string message; - var valueRead = SettingsReader.Read(SettingsRootNamespace, "TimeToRestartErrorIngestionAfterFailure"); - if (valueRead == null) - { - return TimeSpan.FromSeconds(60); - } - - if (TimeSpan.TryParse(valueRead, out var result)) - { - if (ValidateConfiguration && result < TimeSpan.FromSeconds(5)) - { - message = "TimeToRestartErrorIngestionAfterFailure setting is invalid, value should be minimum 5 seconds."; - InternalLogger.Fatal(message); - throw new Exception(message); - } - - if (ValidateConfiguration && result > TimeSpan.FromHours(1)) - { - message = "TimeToRestartErrorIngestionAfterFailure setting is invalid, value should be maximum 1 hour."; - InternalLogger.Fatal(message); - throw new Exception(message); - } - } - else - { - message = "TimeToRestartErrorIngestionAfterFailure setting is invalid, please make sure it is a TimeSpan."; - InternalLogger.Fatal(message); - throw new Exception(message); - } - - return result; - } - - static IList GetRemoteInstances() - { - var valueRead = SettingsReader.Read(SettingsRootNamespace, "RemoteInstances"); - if (!string.IsNullOrEmpty(valueRead)) - { - return ParseRemoteInstances(valueRead); - } - - return Array.Empty(); - } - - internal static RemoteInstanceSetting[] ParseRemoteInstances(string value) => - JsonSerializer.Deserialize(value, SerializerOptions.Default) ?? []; - - static string Subscope(string address) - { - var atIndex = address.IndexOf("@", StringComparison.InvariantCulture); - - if (atIndex <= -1) - { - return $"{address}.log"; - } - - var queue = address.Substring(0, atIndex); - var machine = address.Substring(atIndex + 1); - return $"{queue}.log@{machine}"; - } - - void LoadErrorIngestionSettings() - { - var serviceBusRootNamespace = new SettingsRootNamespace("ServiceBus"); - ErrorQueue = SettingsReader.Read(serviceBusRootNamespace, "ErrorQueue", "error"); - - if (string.IsNullOrEmpty(ErrorQueue)) - { - throw new Exception("ServiceBus/ErrorQueue requires a value to start the instance"); - } - - IngestErrorMessages = SettingsReader.Read(SettingsRootNamespace, "IngestErrorMessages", true); - - if (!IngestErrorMessages) - { - logger.LogInformation("Error ingestion disabled"); - } - - ErrorLogQueue = SettingsReader.Read(serviceBusRootNamespace, "ErrorLogQueue", null); - - if (ErrorLogQueue == null) - { - logger.LogInformation("No settings found for error log queue to import, default name will be used"); - ErrorLogQueue = Subscope(ErrorQueue); - } - } - - // logger is intentionally not static to prevent it from being initialized before LoggingConfigurator.ConfigureLogging has been called - readonly ILogger logger = LoggerUtil.CreateStaticLogger(); - - public const string DEFAULT_INSTANCE_NAME = "Particular.ServiceControl"; - public static readonly SettingsRootNamespace SettingsRootNamespace = new("ServiceControl"); - } +public class Settings +{ + public PrimaryOptions ServiceControl { get; set; } = new(); + public ServiceBusOptions ServiceBus { get; set; } = new(); + public LoggingOptions Logging { get; set; } = new(); + public PersistenceSettings PersisterSpecificSettings { get; set; } } \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/Settings/SettingsValidation.cs b/src/ServiceControl/Infrastructure/Settings/SettingsValidation.cs new file mode 100644 index 0000000000..c34126d177 --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/SettingsValidation.cs @@ -0,0 +1,85 @@ +namespace ServiceBus.Management.Infrastructure.Settings; + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +public class SettingsValidation( + ILogger logger // Intentionally using SETTINGS as logger name +) : IValidateOptions // TODO: Register +{ + public ValidateOptionsResult Validate(string name, PrimaryOptions options) + { + List failures = []; + + if (!options.ForwardErrorMessages.HasValue) + { + failures.Add("ForwardErrorMessages settings is missing, please make sure it is included."); + } + + // ErrorRetentionPeriod + + if (options.ErrorRetentionPeriod == TimeSpan.Zero) + { + failures.Add("ErrorRetentionPeriod settings is missing, please make sure it is included."); + } + else if (options.ErrorRetentionPeriod < TimeSpan.FromDays(5)) + { + failures.Add("ErrorRetentionPeriod settings is invalid, value should be minimum 5 days"); + } + else if (options.ErrorRetentionPeriod > TimeSpan.FromDays(45)) + { + failures.Add("ErrorRetentionPeriod settings is invalid, value should be maximum 45 days"); + } + + // EventRetentionPeriod + + if (options.EventsRetentionPeriod < TimeSpan.FromHours(1)) + { + failures.Add("EventRetentionPeriod settings is invalid, value should be minimum 1 hour"); + } + else if (options.EventsRetentionPeriod > TimeSpan.FromDays(200)) + { + failures.Add("EventRetentionPeriod settings is invalid, value should be maximum 200 days"); + } + + // AuditRetentionPeriod + + if (options.AuditRetentionPeriod < TimeSpan.FromHours(1)) + { + failures.Add("AuditRetentionPeriod settings is invalid, value should be minimum 1 hour"); + } + else if (options.AuditRetentionPeriod > TimeSpan.FromDays(365)) + { + failures.Add("AuditRetentionPeriod settings is invalid, value should be maximum 365 days"); + } + + // TimeToRestartErrorIngestionAfterFailure + + if (options.TimeToRestartErrorIngestionAfterFailure < TimeSpan.FromSeconds(5)) + { + failures.Add("TimeToRestartErrorIngestionAfterFailure settings is invalid, value should be minimum 5 seconds."); + } + else if (options.TimeToRestartErrorIngestionAfterFailure > TimeSpan.FromHours(1)) + { + failures.Add("TimeToRestartErrorIngestionAfterFailure settings is invalid, value should be maximum 1 hour."); + } + + // ConnectionString + + if (string.IsNullOrEmpty(options.ConnectionString)) + { + failures.Add("ConnectionString settings is missing."); + } + + if (!options.IngestErrorMessages) + { + logger.LogInformation("Error ingestion disabled"); + } + + return failures.Count == 0 + ? ValidateOptionsResult.Success + : ValidateOptionsResult.Fail(failures); + } +} \ No newline at end of file diff --git a/src/ServiceControl/Infrastructure/WebApi/RemoteInstanceServiceCollectionExtensions.cs b/src/ServiceControl/Infrastructure/WebApi/RemoteInstanceServiceCollectionExtensions.cs index 40dfd8645f..4ea99fff75 100644 --- a/src/ServiceControl/Infrastructure/WebApi/RemoteInstanceServiceCollectionExtensions.cs +++ b/src/ServiceControl/Infrastructure/WebApi/RemoteInstanceServiceCollectionExtensions.cs @@ -30,7 +30,7 @@ public static void AddHttpForwarding(this IServiceCollection services) public static void AddRemoteInstancesHttpClients(this IServiceCollection services, Settings settings) { - foreach (var remoteInstance in settings.RemoteInstances) + foreach (var remoteInstance in settings.ServiceControl.RemoteInstanceSettings) { var remoteClientBuilder = services.AddHttpClient(remoteInstance.InstanceId, client => { diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index 15539d9db0..c4897f5370 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -29,7 +29,7 @@ public async Task> License(bool refresh, string client ExpirationDate = activeLicense.Details.ExpirationDate?.ToString("O") ?? string.Empty, Status = activeLicense.IsValid ? "valid" : "invalid", LicenseType = activeLicense.Details.LicenseType ?? string.Empty, - InstanceName = settings.InstanceName ?? string.Empty, + InstanceName = settings.ServiceControl.InstanceName ?? string.Empty, LicenseStatus = activeLicense.Details.Status, LicenseExtensionUrl = connectorHeartbeatStatus.LastHeartbeat == null ? $"https://particular.net/extend-your-trial?p={clientName}" diff --git a/src/ServiceControl/Licensing/LicensingComponent.cs b/src/ServiceControl/Licensing/LicensingComponent.cs index 736bf79779..6c0eadda31 100644 --- a/src/ServiceControl/Licensing/LicensingComponent.cs +++ b/src/ServiceControl/Licensing/LicensingComponent.cs @@ -10,19 +10,25 @@ namespace Particular.ServiceControl; class LicensingComponent : ServiceControlComponent { - public override void Configure(Settings settings, ITransportCustomization transportCustomization, - IHostApplicationBuilder hostBuilder) + public override void Configure( + Settings settings, + ITransportCustomization transportCustomization, + IHostApplicationBuilder hostBuilder + ) { var licenseDetails = LicenseManager.FindLicense().Details; hostBuilder.AddLicensingComponent( - TransportManifestLibrary.Find(settings.TransportType)?.Name ?? settings.TransportType, - settings.ErrorQueue, - settings.InstanceName, + TransportManifestLibrary.Find(settings.ServiceControl.TransportType)?.Name ?? settings.ServiceControl.TransportType, + settings.ServiceBus.ErrorQueue, + settings.ServiceControl.InstanceName, licenseDetails.RegisteredTo, ServiceControlVersion.GetFileVersion()); } - public override void Setup(Settings settings, IComponentInstallationContext context, - IHostApplicationBuilder hostBuilder) => - context.CreateQueue(ServiceControlSettings.ServiceControlThroughputDataQueue); + public override void Setup( + Settings settings, + IComponentInstallationContext context, + IHostApplicationBuilder hostBuilder + ) => + context.CreateQueue(settings.ServiceControl.ServiceControlSettings.ServiceControlThroughputDataQueue); } \ No newline at end of file diff --git a/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs b/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs index 9c0413cbac..58d08fd645 100644 --- a/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs @@ -29,7 +29,7 @@ public class EditFailedMessagesController( [HttpPost] public async Task Edit(string failedMessageId, [FromBody] EditMessageModel edit) { - if (!settings.AllowMessageEditing) + if (!settings.ServiceControl.AllowMessageEditing) { logger.LogInformation("Message edit-retry has not been enabled"); return NotFound(); @@ -81,7 +81,7 @@ await session.SendLocal(new EditAndSend EditConfigurationModel GetEditConfiguration() => new() { - Enabled = settings.AllowMessageEditing, + Enabled = settings.ServiceControl.AllowMessageEditing, LockedHeaders = new[] { Headers.MessageId, diff --git a/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs b/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs index c486129df9..fdcccdc719 100644 --- a/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs @@ -27,13 +27,13 @@ public class RetryMessagesController( [HttpPost] public async Task RetryMessageBy([FromQuery(Name = "instance_id")] string instanceId, string failedMessageId) { - if (string.IsNullOrWhiteSpace(instanceId) || instanceId == settings.InstanceId) + if (string.IsNullOrWhiteSpace(instanceId) || instanceId == settings.ServiceControl.InstanceId) { await messageSession.SendLocal(m => m.FailedMessageId = failedMessageId); return Accepted(); } - var remote = settings.RemoteInstances.SingleOrDefault(r => r.InstanceId == instanceId); + var remote = settings.ServiceControl.RemoteInstanceSettings.SingleOrDefault(r => r.InstanceId == instanceId); if (remote == null) { diff --git a/src/ServiceControl/Monitoring/HeartbeatEndpointSettingsSyncHostedService.cs b/src/ServiceControl/Monitoring/HeartbeatEndpointSettingsSyncHostedService.cs index ec5158a4f9..5de102ee6a 100644 --- a/src/ServiceControl/Monitoring/HeartbeatEndpointSettingsSyncHostedService.cs +++ b/src/ServiceControl/Monitoring/HeartbeatEndpointSettingsSyncHostedService.cs @@ -90,7 +90,7 @@ async Task PurgeMonitoringDataThatDoesNotNeedToBeTracked(CancellationToken cance async Task InitialiseSettings(HashSet monitorEndpoints, CancellationToken cancellationToken) { bool hasDefault = false; - bool userSetTrackInstances = settings.TrackInstancesInitialValue; + bool userSetTrackInstances = settings.ServiceControl.TrackInstancesInitialValue; HashSet settingsNames = []; // Delete any endpoints data that no longer exists diff --git a/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs b/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs index 93f6818d74..448e3573dd 100644 --- a/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs +++ b/src/ServiceControl/Monitoring/HeartbeatMonitoringHostedService.cs @@ -17,7 +17,7 @@ public HeartbeatMonitoringHostedService(IEndpointInstanceMonitoring monitor, IMo this.persistence = persistence; this.scheduler = scheduler; this.logger = logger; - gracePeriod = settings.HeartbeatGracePeriod; + gracePeriod = settings.ServiceControl.HeartbeatGracePeriod; } public async Task StartAsync(CancellationToken cancellationToken) { diff --git a/src/ServiceControl/Monitoring/HeartbeatsPlatformConnectionDetailsProvider.cs b/src/ServiceControl/Monitoring/HeartbeatsPlatformConnectionDetailsProvider.cs index 57603cdce4..9d270a89b9 100644 --- a/src/ServiceControl/Monitoring/HeartbeatsPlatformConnectionDetailsProvider.cs +++ b/src/ServiceControl/Monitoring/HeartbeatsPlatformConnectionDetailsProvider.cs @@ -21,7 +21,7 @@ public Task ProvideConnectionDetails(PlatformConnectionDetails connection) { // NOTE: The default grace period is 40s and the default frequency is 10s. // In a low-latency environment, an endpoint would need to miss more than 4 heartbeats to be considered down - var frequency = TimeSpan.FromTicks(settings.HeartbeatGracePeriod.Ticks / 4); + var frequency = TimeSpan.FromTicks(settings.ServiceControl.HeartbeatGracePeriod.Ticks / 4); var timeToLive = TimeSpan.FromTicks(frequency.Ticks * 4); connection.Add( "Heartbeats", diff --git a/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs b/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs index be1807e54c..9a0ebc33bb 100644 --- a/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs +++ b/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs @@ -45,7 +45,7 @@ public async IAsyncEnumerable Endpoints([EnumeratorCancellation] C if (noResults) { - yield return new SettingsData { Name = string.Empty, TrackInstances = settings.TrackInstancesInitialValue }; + yield return new SettingsData { Name = string.Empty, TrackInstances = settings.ServiceControl.TrackInstancesInitialValue }; } } diff --git a/src/ServiceControl/Notifications/Api/NotificationsController.cs b/src/ServiceControl/Notifications/Api/NotificationsController.cs index af5f90cfe0..c3af6fd2b6 100644 --- a/src/ServiceControl/Notifications/Api/NotificationsController.cs +++ b/src/ServiceControl/Notifications/Api/NotificationsController.cs @@ -71,8 +71,8 @@ public async Task SendTestEmail() { await emailSender.Send( notificationsSettings.Email, - $"[{settings.InstanceName}] health check notification check successful", - $"[{settings.InstanceName}] health check notification check successful."); + $"[{settings.ServiceControl.InstanceName}] health check notification check successful", + $"[{settings.ServiceControl.InstanceName}] health check notification check successful."); } catch (Exception e) { diff --git a/src/ServiceControl/Notifications/Email/CustomChecksMailNotification.cs b/src/ServiceControl/Notifications/Email/CustomChecksMailNotification.cs index bbdaff35db..ccef09954e 100644 --- a/src/ServiceControl/Notifications/Email/CustomChecksMailNotification.cs +++ b/src/ServiceControl/Notifications/Email/CustomChecksMailNotification.cs @@ -7,12 +7,14 @@ using Contracts.CustomChecks; using Infrastructure.DomainEvents; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus; using ServiceBus.Management.Infrastructure.Settings; class CustomChecksMailNotification : IDomainHandler, IDomainHandler { readonly IMessageSession messageSession; + readonly Settings settings; readonly EmailThrottlingState throttlingState; readonly string instanceName; string instanceAddress; @@ -29,17 +31,18 @@ class CustomChecksMailNotification : IDomainHandler, IDomainH "Error Message Ingestion" }; - public CustomChecksMailNotification(IMessageSession messageSession, Settings settings, EmailThrottlingState throttlingState, ILogger logger) + public CustomChecksMailNotification(IMessageSession messageSession, IOptions settingsOptions, EmailThrottlingState throttlingState, ILogger logger) { this.messageSession = messageSession; this.throttlingState = throttlingState; this.logger = logger; - instanceName = settings.InstanceName; - instanceAddress = settings.ApiUrl; + settings = settingsOptions.Value; + instanceName = settings.ServiceControl.InstanceName; + instanceAddress = settings.ServiceControl.ApiUrl; - if (string.IsNullOrWhiteSpace(settings.NotificationsFilter) == false) + if (string.IsNullOrWhiteSpace(settings.ServiceControl.NotificationsFilter) == false) { - serviceControlHealthCustomCheckIds = NotificationsFilterParser.Parse(settings.NotificationsFilter); + serviceControlHealthCustomCheckIds = NotificationsFilterParser.Parse(settings.ServiceControl.NotificationsFilter); } } diff --git a/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs b/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs index 26f4ac0036..0f5f2ed5bc 100644 --- a/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs +++ b/src/ServiceControl/Notifications/Email/SendEmailNotificationHandler.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus; using NServiceBus.Transport; using Persistence; @@ -14,13 +15,13 @@ class SendEmailNotificationHandler : IHandleMessages readonly EmailThrottlingState throttlingState; readonly EmailSender emailSender; - public SendEmailNotificationHandler(IErrorMessageDataStore store, Settings settings, EmailThrottlingState throttlingState, EmailSender emailSender, ILogger logger) + public SendEmailNotificationHandler(IErrorMessageDataStore store, IOptions settings, EmailThrottlingState throttlingState, EmailSender emailSender, ILogger logger) { this.store = store; this.throttlingState = throttlingState; this.emailSender = emailSender; this.logger = logger; - emailDropFolder = settings.EmailDropFolder; + emailDropFolder = settings.Value.EmailDropFolder; } public async Task Handle(SendEmailNotification message, IMessageHandlerContext context) diff --git a/src/ServiceControl/Operations/CheckRemotes.cs b/src/ServiceControl/Operations/CheckRemotes.cs index 1aad310ba5..2ca36057d6 100644 --- a/src/ServiceControl/Operations/CheckRemotes.cs +++ b/src/ServiceControl/Operations/CheckRemotes.cs @@ -6,15 +6,16 @@ using System.Text; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Options; using NServiceBus.CustomChecks; using ServiceBus.Management.Infrastructure.Settings; class CheckRemotes : CustomCheck { - public CheckRemotes(Settings settings, IHttpClientFactory httpClientFactory) : base("ServiceControl Remotes", "Health", TimeSpan.FromSeconds(30)) + public CheckRemotes(IOptions options, IHttpClientFactory httpClientFactory) : base("ServiceControl Remotes", "Health", TimeSpan.FromSeconds(30)) { this.httpClientFactory = httpClientFactory; - remoteInstanceSetting = settings.RemoteInstances; + remoteInstanceSetting = options.Value.RemoteInstanceSettings; remoteQueryTasks = new List(remoteInstanceSetting.Length); } diff --git a/src/ServiceControl/Operations/ErrorIngestion.cs b/src/ServiceControl/Operations/ErrorIngestion.cs index ec04de2e72..cb100020ea 100644 --- a/src/ServiceControl/Operations/ErrorIngestion.cs +++ b/src/ServiceControl/Operations/ErrorIngestion.cs @@ -10,6 +10,7 @@ using Infrastructure.Metrics; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus; using NServiceBus.Transport; using Persistence; @@ -22,7 +23,7 @@ class ErrorIngestion : BackgroundService static readonly long FrequencyInMilliseconds = Stopwatch.Frequency / 1000; public ErrorIngestion( - Settings settings, + IOptions settingsOptions, ITransportCustomization transportCustomization, TransportSettings transportSettings, Metrics metrics, @@ -31,12 +32,14 @@ public ErrorIngestion( ErrorIngestor ingestor, IIngestionUnitOfWorkFactory unitOfWorkFactory, IHostApplicationLifetime applicationLifetime, - ILogger logger) + ILogger logger, + IOptions loggingOptions + ) { - this.settings = settings; + settings = settingsOptions.Value; this.transportCustomization = transportCustomization; this.transportSettings = transportSettings; - errorQueue = settings.ErrorQueue; + errorQueue = settings.ServiceBus.ErrorQueue; this.ingestor = ingestor; this.unitOfWorkFactory = unitOfWorkFactory; this.applicationLifetime = applicationLifetime; @@ -58,7 +61,7 @@ public ErrorIngestion( FullMode = BoundedChannelFullMode.Wait }); - errorHandlingPolicy = new ErrorIngestionFaultPolicy(dataStore, settings.LoggingSettings, OnCriticalError, logger); + errorHandlingPolicy = new ErrorIngestionFaultPolicy(dataStore, loggingOptions, OnCriticalError, logger); watchdog = new Watchdog( "failed message ingestion", @@ -66,7 +69,7 @@ public ErrorIngestion( EnsureStopped, ingestionState.ReportError, ingestionState.Clear, - settings.TimeToRestartErrorIngestionAfterFailure, + settings.ServiceControl.TimeToRestartErrorIngestionAfterFailure, logger ); } @@ -213,7 +216,7 @@ async Task SetUpAndStartInfrastructure(CancellationToken cancellationToken) messageReceiver = transportInfrastructure.Receivers[errorQueue]; - if (settings.ForwardErrorMessages) + if (settings.ServiceControl.ForwardErrorMessages == true) { await ingestor.VerifyCanReachForwardingAddress(cancellationToken); } @@ -264,7 +267,7 @@ async Task StopAndTeardownInfrastructure(CancellationToken cancellationToken) async Task OnMessage(MessageContext messageContext, CancellationToken cancellationToken) { - if (settings.MessageFilter != null && settings.MessageFilter(messageContext)) + if (settings.ServiceControl.MessageFilter != null && settings.ServiceControl.MessageFilter(messageContext)) { return; } diff --git a/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs b/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs index 6562e2e22f..20686a86ba 100644 --- a/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs +++ b/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs @@ -10,6 +10,7 @@ using Configuration; using Infrastructure; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus.Transport; using Persistence; using ServiceBus.Management.Infrastructure.Installers; @@ -21,7 +22,7 @@ class ErrorIngestionFaultPolicy ImportFailureCircuitBreaker failureCircuitBreaker; - public ErrorIngestionFaultPolicy(IErrorMessageDataStore store, LoggingSettings loggingSettings, Func onCriticalError, ILogger logger) + public ErrorIngestionFaultPolicy(IErrorMessageDataStore store, IOptions loggingOptions, Func onCriticalError, ILogger logger) { this.store = store; this.logger = logger; @@ -29,7 +30,7 @@ public ErrorIngestionFaultPolicy(IErrorMessageDataStore store, LoggingSettings l if (!AppEnvironment.RunningInContainer) { - logPath = Path.Combine(loggingSettings.LogPath, "FailedImports", "Error"); + logPath = Path.Combine(loggingOptions.Value.LogPath, "FailedImports", "Error"); Directory.CreateDirectory(logPath); } } diff --git a/src/ServiceControl/Operations/ErrorIngestor.cs b/src/ServiceControl/Operations/ErrorIngestor.cs index 39f4f264f5..4345e470e6 100644 --- a/src/ServiceControl/Operations/ErrorIngestor.cs +++ b/src/ServiceControl/Operations/ErrorIngestor.cs @@ -10,6 +10,7 @@ using Infrastructure.DomainEvents; using Infrastructure.Metrics; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus.Routing; using NServiceBus.Transport; using Recoverability; @@ -28,12 +29,12 @@ public ErrorIngestor(Metrics metrics, IIngestionUnitOfWorkFactory unitOfWorkFactory, Lazy messageDispatcher, ITransportCustomization transportCustomization, - Settings settings, + IOptions settingsOptions, ILogger logger) { this.unitOfWorkFactory = unitOfWorkFactory; this.messageDispatcher = messageDispatcher; - this.settings = settings; + settings = settingsOptions.Value; this.logger = logger; bulkInsertDurationMeter = metrics.GetMeter("Error ingestion - bulk insert duration", FrequencyInMilliseconds); var ingestedMeter = metrics.GetCounter("Error ingestion - ingested"); @@ -48,7 +49,7 @@ public ErrorIngestor(Metrics metrics, errorProcessor = new ErrorProcessor(enrichers, failedMessageEnrichers.ToArray(), domainEvents, ingestedMeter, logger); retryConfirmationProcessor = new RetryConfirmationProcessor(domainEvents); - logQueueAddress = new UnicastAddressTag(transportCustomization.ToTransportQualifiedQueueName(this.settings.ErrorLogQueue)); + logQueueAddress = new UnicastAddressTag(transportCustomization.ToTransportQualifiedQueueName(settings.ServiceBus.ErrorLogQueue)); } public async Task Ingest(List contexts, CancellationToken cancellationToken) @@ -85,7 +86,7 @@ public async Task Ingest(List contexts, CancellationToken cancel await Task.WhenAll(announcerTasks); - if (settings.ForwardErrorMessages) + if (settings.ServiceControl.ForwardErrorMessages == true) { logger.LogDebug("Forwarding {FailedMessageCount} messages", storedFailed.Count); @@ -184,7 +185,7 @@ public async Task VerifyCanReachForwardingAddress(CancellationToken cancellation } catch (Exception e) { - throw new Exception($"Unable to write to forwarding queue {settings.ErrorLogQueue}", e); + throw new Exception($"Unable to write to forwarding queue {settings.ServiceBus.ErrorLogQueue}", e); } } diff --git a/src/ServiceControl/Operations/ImportFailedErrors.cs b/src/ServiceControl/Operations/ImportFailedErrors.cs index b834248927..93adb6469e 100644 --- a/src/ServiceControl/Operations/ImportFailedErrors.cs +++ b/src/ServiceControl/Operations/ImportFailedErrors.cs @@ -2,6 +2,7 @@ { using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Options; using NServiceBus.Extensibility; using NServiceBus.Transport; using Persistence; @@ -10,11 +11,13 @@ public class ImportFailedErrors( IFailedErrorImportDataStore store, ErrorIngestor errorIngestor, - Settings settings) + IOptions settingsOptions + ) { public async Task Run(CancellationToken cancellationToken = default) { - if (settings.ForwardErrorMessages) + var settings = settingsOptions.Value; + if (settings.ServiceControl.ForwardErrorMessages == true) { await errorIngestor.VerifyCanReachForwardingAddress(cancellationToken); } @@ -26,7 +29,7 @@ await store.ProcessFailedErrorImports(async transportMessage => transportMessage.Headers, transportMessage.Body, EmptyTransaction, - settings.ErrorQueue, + settings.ServiceBus.ErrorQueue, EmptyContextBag ); var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/ServiceControl/Persistence/PersistenceFactory.cs b/src/ServiceControl/Persistence/PersistenceFactory.cs index 745faa5ef3..d6d548f8f2 100644 --- a/src/ServiceControl/Persistence/PersistenceFactory.cs +++ b/src/ServiceControl/Persistence/PersistenceFactory.cs @@ -2,36 +2,39 @@ namespace ServiceControl.Persistence { using System; using System.IO; + using System.Runtime.Loader; + using Microsoft.Extensions.Configuration; using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Infrastructure; static class PersistenceFactory { - public static IPersistence Create(Settings settings, bool maintenanceMode = false) - { - var persistenceConfiguration = CreatePersistenceConfiguration(settings); - - //HINT: This is false when executed from acceptance tests - settings.PersisterSpecificSettings ??= persistenceConfiguration.CreateSettings(Settings.SettingsRootNamespace); - settings.PersisterSpecificSettings.MaintenanceMode = maintenanceMode; + internal static Func AssemblyLoadContextResolver { get; set; } = static (assemblyPath) => new PluginAssemblyLoadContext(assemblyPath); - var persistence = persistenceConfiguration.Create(settings.PersisterSpecificSettings); + public static IPersistence Create(IConfiguration configuration) + { + var persistenceConfiguration = CreatePersistenceConfiguration(configuration); + var section = configuration.GetSection(PrimaryOptions.SectionName); + var persistence = persistenceConfiguration.Create(section); return persistence; } - static IPersistenceConfiguration CreatePersistenceConfiguration(Settings settings) + static IPersistenceConfiguration CreatePersistenceConfiguration(IConfiguration configuration) { + var persistenceType = configuration.GetValue("PersistenceType"); try { - var persistenceManifest = PersistenceManifestLibrary.Find(settings.PersistenceType); + var persistenceManifest = PersistenceManifestLibrary.Find(persistenceType); var assemblyPath = Path.Combine(persistenceManifest.Location, $"{persistenceManifest.AssemblyName}.dll"); - var loadContext = settings.AssemblyLoadContextResolver(assemblyPath); + + var loadContext = AssemblyLoadContextResolver(assemblyPath); var customizationType = Type.GetType(persistenceManifest.TypeName, loadContext.LoadFromAssemblyName, null, true); return (IPersistenceConfiguration)Activator.CreateInstance(customizationType); } catch (Exception e) { - throw new Exception($"Could not load persistence customization type {settings.PersistenceType}.", e); + throw new Exception($"Could not load persistence customization type {persistenceType}.", e); } } } diff --git a/src/ServiceControl/Persistence/PersistenceServiceCollectionExtensions.cs b/src/ServiceControl/Persistence/PersistenceServiceCollectionExtensions.cs index 2447bfe400..04cfdff605 100644 --- a/src/ServiceControl/Persistence/PersistenceServiceCollectionExtensions.cs +++ b/src/ServiceControl/Persistence/PersistenceServiceCollectionExtensions.cs @@ -1,15 +1,15 @@ namespace ServiceControl.Persistence { - using Microsoft.Extensions.DependencyInjection; - using ServiceBus.Management.Infrastructure.Settings; + using Microsoft.Extensions.Hosting; static class PersistenceServiceCollectionExtensions { - public static void AddPersistence(this IServiceCollection services, Settings settings, - bool maintenanceMode = false) + public static void AddPersistence( + this IHostApplicationBuilder hostBuilder + ) { - var persistence = PersistenceFactory.Create(settings, maintenanceMode); - persistence.AddPersistence(services); + var persistence = PersistenceFactory.Create(hostBuilder.Configuration); + persistence.AddPersistence(hostBuilder.Services, hostBuilder.Configuration); } } } diff --git a/src/ServiceControl/Program.cs b/src/ServiceControl/Program.cs index ec4a0bc70d..ebdb6fb8a4 100644 --- a/src/ServiceControl/Program.cs +++ b/src/ServiceControl/Program.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Particular.ServiceControl.Hosting; using ServiceBus.Management.Infrastructure.Settings; @@ -11,8 +12,17 @@ try { - var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); - LoggingConfigurator.ConfigureLogging(loggingSettings); + var bootstrapConfig = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddLegacyAppSettings() + .AddEnvironmentVariables() + .Build(); + + var section = bootstrapConfig.GetSection(PrimaryOptions.SectionName); + var loggingOptions = section.Get(); + + LoggingConfigurator.ConfigureLogging(loggingOptions); + logger = LoggerUtil.CreateStaticLogger(typeof(Program)); AppDomain.CurrentDomain.UnhandledException += (s, e) => logger.LogError(e.ExceptionObject as Exception, "Unhandled exception was caught"); @@ -25,9 +35,10 @@ return exitCode; } + ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - var arguments = new HostArguments(args); + var arguments = new HostArguments(args, bootstrapConfig.GetValue("ServiceControl:MaintenanceMode")); if (arguments.Help) { @@ -35,9 +46,7 @@ return 0; } - var settings = new Settings(loggingSettings: loggingSettings); - - await new CommandRunner(arguments.Command).Execute(arguments, settings); + await new CommandRunner(arguments.Command).Execute(arguments); return 0; } @@ -52,6 +61,7 @@ LoggingConfigurator.ConfigureNLog("bootstrap.${shortdate}.txt", "./", NLog.LogLevel.Fatal); NLog.LogManager.GetCurrentClassLogger().Fatal(ex, "Unrecoverable error"); } + throw; } finally diff --git a/src/ServiceControl/Recoverability/RecoverabilityComponent.cs b/src/ServiceControl/Recoverability/RecoverabilityComponent.cs index 8c52e4577b..4022d25531 100644 --- a/src/ServiceControl/Recoverability/RecoverabilityComponent.cs +++ b/src/ServiceControl/Recoverability/RecoverabilityComponent.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using Operations; using Particular.ServiceControl; using Persistence; @@ -23,7 +24,11 @@ class RecoverabilityComponent : ServiceControlComponent { - public override void Configure(Settings settings, ITransportCustomization transportCustomization, IHostApplicationBuilder hostBuilder) + public override void Configure( + Settings settings, + ITransportCustomization transportCustomization, + IHostApplicationBuilder hostBuilder + ) { var services = hostBuilder.Services; services.AddPlatformConnectionProvider(); @@ -55,7 +60,8 @@ public override void Configure(Settings settings, ITransportCustomization transp services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - if (settings.IngestErrorMessages) + + if (settings.ServiceControl.IngestErrorMessages) { services.AddHostedService(); } @@ -64,7 +70,7 @@ public override void Configure(Settings settings, ITransportCustomization transp services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - if (settings.RunRetryProcessor) + if (settings.ServiceControl.RunRetryProcessor) { services.AddHostedService(); services.AddHostedService(); @@ -107,16 +113,16 @@ public override void Configure(Settings settings, ITransportCustomization transp public override void Setup(Settings settings, IComponentInstallationContext context, IHostApplicationBuilder hostBuilder) { - context.CreateQueue(settings.StagingQueue); + context.CreateQueue(settings.ServiceControl.StagingQueue); - if (settings.IngestErrorMessages) + if (settings.ServiceControl.IngestErrorMessages) { - context.CreateQueue(settings.ErrorQueue); + context.CreateQueue(settings.ServiceBus.ErrorQueue); } - if (settings.ForwardErrorMessages && settings.ErrorLogQueue != null) + if (settings.ServiceControl.ForwardErrorMessages == true && settings.ServiceBus.ErrorLogQueue != null) { - context.CreateQueue(settings.ErrorLogQueue); + context.CreateQueue(settings.ServiceBus.ErrorLogQueue); } } @@ -125,7 +131,7 @@ class FailedMessageNotificationsHostedService : IHostedService public FailedMessageNotificationsHostedService( IDomainEvents domainEvents, IFailedMessageViewIndexNotifications store - ) + ) { this.domainEvents = domainEvents; this.store = store; @@ -249,19 +255,19 @@ class ProcessRetryBatchesHostedService : IHostedService { public ProcessRetryBatchesHostedService( RetryProcessor processor, - Settings settings, + IOptions settings, IAsyncTimer scheduler, ILogger logger) { this.processor = processor; - this.settings = settings; + this.settings = settings.Value; this.scheduler = scheduler; this.logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { - timer = scheduler.Schedule(Process, TimeSpan.Zero, settings.ProcessRetryBatchesFrequency, e => logger.LogError(e, "Unhandled exception while processing retry batches")); + timer = scheduler.Schedule(Process, TimeSpan.Zero, settings.ServiceControl.ProcessRetryBatchesFrequency, e => logger.LogError(e, "Unhandled exception while processing retry batches")); return Task.CompletedTask; } diff --git a/src/ServiceControl/Recoverability/RecoverabilityPlatformConnectionDetailsProvider.cs b/src/ServiceControl/Recoverability/RecoverabilityPlatformConnectionDetailsProvider.cs index 386396da15..23570b6098 100644 --- a/src/ServiceControl/Recoverability/RecoverabilityPlatformConnectionDetailsProvider.cs +++ b/src/ServiceControl/Recoverability/RecoverabilityPlatformConnectionDetailsProvider.cs @@ -2,17 +2,14 @@ { using System.Threading.Tasks; using Connection; + using Microsoft.Extensions.Options; using ServiceBus.Management.Infrastructure.Settings; - class RecoverabilityPlatformConnectionDetailsProvider : IProvidePlatformConnectionDetails + class RecoverabilityPlatformConnectionDetailsProvider(IOptions options) : IProvidePlatformConnectionDetails { - readonly Settings settings; - - public RecoverabilityPlatformConnectionDetailsProvider(Settings settings) => this.settings = settings; - public Task ProvideConnectionDetails(PlatformConnectionDetails connection) { - connection.Add("ErrorQueue", settings.ErrorQueue); + connection.Add("ErrorQueue", options.Value.ErrorQueue); return Task.CompletedTask; } } diff --git a/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs b/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs index 707fc1d340..a1b350bcfd 100644 --- a/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs +++ b/src/ServiceControl/Recoverability/Retrying/History/StoreHistoryHandler.cs @@ -3,17 +3,13 @@ using System.Threading; using System.Threading.Tasks; using Infrastructure.DomainEvents; + using Microsoft.Extensions.Options; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Persistence; - public class StoreHistoryHandler : IDomainHandler + public class StoreHistoryHandler(IRetryHistoryDataStore store, IOptions options) + : IDomainHandler { - public StoreHistoryHandler(IRetryHistoryDataStore store, Settings settings) - { - this.store = store; - this.settings = settings; - } - public Task Handle(RetryOperationCompleted message, CancellationToken cancellationToken) { return store.RecordRetryOperationCompleted( @@ -26,10 +22,8 @@ public Task Handle(RetryOperationCompleted message, CancellationToken cancellati message.Failed, message.NumberOfMessagesProcessed, message.Last, - settings.RetryHistoryDepth); + options.Value.RetryHistoryDepth + ); } - - readonly IRetryHistoryDataStore store; - readonly Settings settings; } } \ No newline at end of file diff --git a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs index 7bade70a97..485aebd5d5 100644 --- a/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs +++ b/src/ServiceControl/Recoverability/Retrying/Infrastructure/ReturnToSenderDequeuer.cs @@ -6,6 +6,7 @@ namespace ServiceControl.Recoverability; using Infrastructure.DomainEvents; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NServiceBus; using NServiceBus.Transport; using Persistence; @@ -20,14 +21,15 @@ public ReturnToSenderDequeuer( IDomainEvents domainEvents, ITransportCustomization transportCustomization, TransportSettings transportSettings, - Settings settings, + IOptions settingsOptions, ErrorQueueNameCache errorQueueNameCache, ILogger logger ) { - InputAddress = transportCustomization.ToTransportQualifiedQueueName(settings.StagingQueue); + var settings = settingsOptions.Value; + InputAddress = transportCustomization.ToTransportQualifiedQueueName(settings.ServiceControl.StagingQueue); this.returnToSender = returnToSender; - errorQueue = settings.ErrorQueue; + errorQueue = settings.ServiceBus.ErrorQueue; this.transportCustomization = transportCustomization; this.transportSettings = transportSettings; this.errorQueueNameCache = errorQueueNameCache;