diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 736499ad24..9c4fa7a64e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -19,6 +19,7 @@ + diff --git a/src/ServiceControl.Configuration/AppConfigConfigurationProvider.cs b/src/ServiceControl.Configuration/AppConfigConfigurationProvider.cs new file mode 100644 index 0000000000..03e11611a6 --- /dev/null +++ b/src/ServiceControl.Configuration/AppConfigConfigurationProvider.cs @@ -0,0 +1,25 @@ +#nullable enable + +namespace ServiceControl.Configuration; + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +public class AppConfigConfigurationProvider : ConfigurationProvider +{ + public AppConfigConfigurationProvider(Dictionary mappings) + { + foreach (var (msConfigurationExtensionKey, appConfigKeys) in mappings) + { + foreach (var appConfigKey in appConfigKeys) + { + var appConfigValue = System.Configuration.ConfigurationManager.AppSettings[appConfigKey]; + + if (appConfigValue is not null) + { + Data[msConfigurationExtensionKey] = appConfigValue; + } + } + } + } +} diff --git a/src/ServiceControl.Configuration/AppConfigConfigurationSource.cs b/src/ServiceControl.Configuration/AppConfigConfigurationSource.cs new file mode 100644 index 0000000000..8321b521a0 --- /dev/null +++ b/src/ServiceControl.Configuration/AppConfigConfigurationSource.cs @@ -0,0 +1,32 @@ +#nullable enable + +namespace ServiceControl.Configuration; + +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; + +public class AppConfigConfigurationSource : IConfigurationSource +{ + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + var propertiesWithAttribute = from a in AppDomain.CurrentDomain.GetAssemblies() + from t in a.GetTypes() + from p in t.GetProperties() + let attributes = p.GetCustomAttributes(typeof(AppConfigSettingAttribute), true) + where attributes != null && attributes.Length > 0 + select new { Type = p, Attribute = attributes.Cast().Single() }; + + var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var property in propertiesWithAttribute) + { + var section = property.Type.DeclaringType!.Name.Replace("Options", ""); + var name = property.Type.Name; + mappings[$"{section}:{name}"] = property.Attribute.Keys; + } + + return new AppConfigConfigurationProvider(mappings); + } +} diff --git a/src/ServiceControl.Configuration/AppConfigSettingAttribute.cs b/src/ServiceControl.Configuration/AppConfigSettingAttribute.cs new file mode 100644 index 0000000000..794da79c7f --- /dev/null +++ b/src/ServiceControl.Configuration/AppConfigSettingAttribute.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace ServiceControl.Configuration; + +using System; + +[AttributeUsage(AttributeTargets.All)] +public class AppConfigSettingAttribute(params string[] keys) : Attribute +{ + public string[] Keys { get; } = keys; +} \ No newline at end of file diff --git a/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj b/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj index 302ef7cbea..08dd5000ad 100644 --- a/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj +++ b/src/ServiceControl.Configuration/ServiceControl.Configuration.csproj @@ -7,6 +7,7 @@ + diff --git a/src/ServiceControl.UnitTests/API/APIApprovals.cs b/src/ServiceControl.UnitTests/API/APIApprovals.cs index 1a75ce026f..706bec93b9 100644 --- a/src/ServiceControl.UnitTests/API/APIApprovals.cs +++ b/src/ServiceControl.UnitTests/API/APIApprovals.cs @@ -13,12 +13,14 @@ using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging.Abstractions; + using Microsoft.Extensions.Options; using NServiceBus.CustomChecks; using NUnit.Framework; using Particular.Approvals; using Particular.ServiceControl.Licensing; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Infrastructure.Api; + using ServiceControl.Infrastructure.Settings; using ServiceControl.Infrastructure.WebApi; using ServiceControl.Monitoring.HeartbeatMonitoring; @@ -31,9 +33,11 @@ public async Task 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 controllerContext = new ControllerContext(actionContext); + var configurationApi = new ConfigurationApi( new ActiveLicense(null, NullLogger.Instance) { IsValid = true }, new Settings(), + Options.Create(new ServiceControlOptions()), null, new MassTransitConnectorHeartbeatStatus()); diff --git a/src/ServiceControl/HostApplicationBuilderExtensions.cs b/src/ServiceControl/HostApplicationBuilderExtensions.cs index fbe99dada5..d34eebde08 100644 --- a/src/ServiceControl/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl/HostApplicationBuilderExtensions.cs @@ -10,6 +10,7 @@ namespace Particular.ServiceControl using global::ServiceControl.Infrastructure.BackgroundTasks; using global::ServiceControl.Infrastructure.DomainEvents; using global::ServiceControl.Infrastructure.Metrics; + using global::ServiceControl.Infrastructure.Settings; using global::ServiceControl.Infrastructure.SignalR; using global::ServiceControl.Infrastructure.WebApi; using global::ServiceControl.Notifications.Email; @@ -17,6 +18,7 @@ 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; @@ -76,7 +78,8 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S services.AddPersistence(settings); services.AddMetrics(settings.PrintMetrics); - NServiceBusFactory.Configure(settings, transportCustomization, transportSettings, configuration); + var scOptions = hostBuilder.Configuration.GetSection("ServiceControl").Get(); + NServiceBusFactory.Configure(scOptions, transportCustomization, transportSettings, configuration); hostBuilder.UseNServiceBus(configuration); if (!settings.DisableExternalIntegrationsPublishing) diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index db658857de..19a6912c36 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -3,11 +3,15 @@ using System.Threading.Tasks; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; using NServiceBus; using Particular.ServiceControl; using Particular.ServiceControl.Hosting; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl; + using ServiceControl.Configuration; + using ServiceControl.Infrastructure.Settings; class RunCommand : AbstractCommand { @@ -20,6 +24,10 @@ public override async Task Execute(HostArguments args, Settings settings) settings.RunCleanupBundle = true; var hostBuilder = WebApplication.CreateBuilder(); + + hostBuilder.Configuration.Add(source => { }); + hostBuilder.Services.Configure(hostBuilder.Configuration.GetSection("ServiceControl")); + hostBuilder.AddServiceControl(settings, endpointConfiguration); hostBuilder.AddServiceControlApi(); diff --git a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs index f8af272eb4..82c8b6056f 100644 --- a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs +++ b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs @@ -8,16 +8,23 @@ using System.Threading; using System.Threading.Tasks; using Configuration; +using Microsoft.Extensions.Options; using Monitoring.HeartbeatMonitoring; using Particular.ServiceControl.Licensing; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Api; using ServiceControl.Api.Contracts; +using ServiceControl.Infrastructure.Settings; -class ConfigurationApi(ActiveLicense license, +class ConfigurationApi( + ActiveLicense license, Settings settings, - IHttpClientFactory httpClientFactory, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : IConfigurationApi + IOptions scOptions, + IHttpClientFactory httpClientFactory, + MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : IConfigurationApi { + readonly ServiceControlOptions scOptions = scOptions.Value; + public Task GetUrls(string baseUrl, CancellationToken cancellationToken) { var model = new RootUrls @@ -56,33 +63,33 @@ public Task GetConfig(CancellationToken cancellationToken) { Host = new { - settings.InstanceName, + scOptions.InstanceName, Logging = new { - settings.LoggingSettings.LogPath, - LoggingLevel = settings.LoggingSettings.LogLevel + scOptions.LogPath, + LoggingLevel = scOptions.LogLevel } }, DataRetention = new { - settings.AuditRetentionPeriod, - settings.ErrorRetentionPeriod + scOptions.AuditRetentionPeriod, + scOptions.ErrorRetentionPeriod }, PerformanceTunning = new { - settings.ExternalIntegrationsDispatchingBatchSize + scOptions.ExternalIntegrationsDispatchingBatchSize }, PersistenceSettings = settings.PersisterSpecificSettings, Transport = new { - settings.TransportType, - settings.ErrorLogQueue, - settings.ErrorQueue, - settings.ForwardErrorMessages + scOptions.TransportType, + scOptions.ErrorLogQueue, + scOptions.ErrorQueue, + scOptions.ForwardErrorMessages }, Plugins = new { - settings.HeartbeatGracePeriod + scOptions.HeartbeatGracePeriod }, MassTransitConnector = connectorHeartbeatStatus.LastHeartbeat }; diff --git a/src/ServiceControl/Infrastructure/NServiceBusFactory.cs b/src/ServiceControl/Infrastructure/NServiceBusFactory.cs index 72116beaae..c4f6b759ac 100644 --- a/src/ServiceControl/Infrastructure/NServiceBusFactory.cs +++ b/src/ServiceControl/Infrastructure/NServiceBusFactory.cs @@ -9,6 +9,7 @@ namespace ServiceBus.Management.Infrastructure using ServiceControl.Configuration; using ServiceControl.ExternalIntegrations; using ServiceControl.Infrastructure; + using ServiceControl.Infrastructure.Settings; using ServiceControl.Infrastructure.Subscriptions; using ServiceControl.Monitoring.HeartbeatMonitoring; using ServiceControl.Notifications.Email; @@ -18,8 +19,11 @@ namespace ServiceBus.Management.Infrastructure static class NServiceBusFactory { - public static void Configure(Settings.Settings settings, ITransportCustomization transportCustomization, - TransportSettings transportSettings, EndpointConfiguration configuration) + public static void Configure( + ServiceControlOptions scOptions, + ITransportCustomization transportCustomization, + TransportSettings transportSettings, + EndpointConfiguration configuration) { if (configuration == null) { @@ -28,14 +32,12 @@ public static void Configure(Settings.Settings settings, ITransportCustomization assemblyScanner.ExcludeAssemblies("ServiceControl.Plugin"); } - configuration.GetSettings().Set("ServiceControl.Settings", settings); - transportCustomization.CustomizePrimaryEndpoint(configuration, transportSettings); - configuration.GetSettings().Set(settings.LoggingSettings); - configuration.SetDiagnosticsPath(settings.LoggingSettings.LogPath); + configuration.GetSettings().Set(scOptions); + configuration.SetDiagnosticsPath(scOptions.LogPath); - if (settings.DisableExternalIntegrationsPublishing) + if (scOptions.DisableExternalIntegrationsPublishing) { configuration.DisableFeature(); } diff --git a/src/ServiceControl/Infrastructure/Settings/ServiceControlOptions.cs b/src/ServiceControl/Infrastructure/Settings/ServiceControlOptions.cs new file mode 100644 index 0000000000..9af585641a --- /dev/null +++ b/src/ServiceControl/Infrastructure/Settings/ServiceControlOptions.cs @@ -0,0 +1,53 @@ +namespace ServiceControl.Infrastructure.Settings; + +using System; +using System.IO; +using ServiceControl.Configuration; + +public class ServiceControlOptions +{ + const string DefaultInstanceName = "Particular.ServiceControl"; + const int DefaultExternalIntegrationsDispatchingBatchSize = 100; + static readonly string DefaultLogPath = Path.Combine(AppContext.BaseDirectory, ".logs"); + + [AppConfigSetting( + "ServiceControl/InternalQueueName", // LEGACY SETTING NAME + "ServiceControl/InstanceName")] + public string InstanceName { get; set; } = DefaultInstanceName; + + [AppConfigSetting("ServiceControl/LogLevel")] + public string LogLevel { get; set; } + + // 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 + [AppConfigSetting("ServiceControl/LogPath")] + public string LogPath { get; set; } = DefaultLogPath; + + [AppConfigSetting("ServiceControl/TransportType")] + public string TransportType { get; set; } + + [AppConfigSetting("ServiceBus/ErrorQueue")] + public string ErrorQueue { get; set; } + + [AppConfigSetting("ServiceBus/ErrorLogQueue")] + public string ErrorLogQueue { get; set; } + + public bool ForwardErrorMessages { get; set; } + + [AppConfigSetting("ServiceControl/ErrorRetentionPeriod")] + public TimeSpan ErrorRetentionPeriod { get; set; } + + [AppConfigSetting("ServiceControl/AuditRetentionPeriod")] + public TimeSpan? AuditRetentionPeriod { get; set; } + + [AppConfigSetting("ServiceControl/HeartbeatGracePeriod")] + public TimeSpan HeartbeatGracePeriod { get; set; } = TimeSpan.FromSeconds(40); + + public string StagingQueue => $"{InstanceName}.staging"; + + [AppConfigSetting("ServiceControl/DisableExternalIntegrationsPublishing")] + public bool DisableExternalIntegrationsPublishing { get; set; } = false; + + [AppConfigSetting("ServiceControl/ExternalIntegrationsDispatchingBatchSize")] + public int ExternalIntegrationsDispatchingBatchSize { get; set; } = DefaultExternalIntegrationsDispatchingBatchSize; +} diff --git a/src/ServiceControl/Operations/ErrorIngestion.cs b/src/ServiceControl/Operations/ErrorIngestion.cs index ec04de2e72..1daf2c2ac6 100644 --- a/src/ServiceControl/Operations/ErrorIngestion.cs +++ b/src/ServiceControl/Operations/ErrorIngestion.cs @@ -10,11 +10,13 @@ using Infrastructure.Metrics; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using NServiceBus; using NServiceBus.Transport; using Persistence; using Persistence.UnitOfWork; using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Infrastructure.Settings; using Transports; class ErrorIngestion : BackgroundService @@ -23,6 +25,7 @@ class ErrorIngestion : BackgroundService public ErrorIngestion( Settings settings, + IOptions scOptions, ITransportCustomization transportCustomization, TransportSettings transportSettings, Metrics metrics, @@ -34,9 +37,10 @@ public ErrorIngestion( ILogger logger) { this.settings = settings; + this.scOptions = scOptions.Value; this.transportCustomization = transportCustomization; this.transportSettings = transportSettings; - errorQueue = settings.ErrorQueue; + errorQueue = this.scOptions.ErrorQueue; this.ingestor = ingestor; this.unitOfWorkFactory = unitOfWorkFactory; this.applicationLifetime = applicationLifetime; @@ -58,7 +62,7 @@ public ErrorIngestion( FullMode = BoundedChannelFullMode.Wait }); - errorHandlingPolicy = new ErrorIngestionFaultPolicy(dataStore, settings.LoggingSettings, OnCriticalError, logger); + errorHandlingPolicy = new ErrorIngestionFaultPolicy(dataStore, this.scOptions, OnCriticalError, logger); watchdog = new Watchdog( "failed message ingestion", @@ -213,7 +217,7 @@ async Task SetUpAndStartInfrastructure(CancellationToken cancellationToken) messageReceiver = transportInfrastructure.Receivers[errorQueue]; - if (settings.ForwardErrorMessages) + if (scOptions.ForwardErrorMessages) { await ingestor.VerifyCanReachForwardingAddress(cancellationToken); } @@ -312,6 +316,7 @@ async Task EnsureStopped(CancellationToken cancellationToken = default) IMessageReceiver messageReceiver; readonly Settings settings; + readonly ServiceControlOptions scOptions; readonly ITransportCustomization transportCustomization; readonly TransportSettings transportSettings; readonly Watchdog watchdog; diff --git a/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs b/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs index 6562e2e22f..d6cd582a7d 100644 --- a/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs +++ b/src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs @@ -8,11 +8,11 @@ using System.Threading; using System.Threading.Tasks; using Configuration; - using Infrastructure; using Microsoft.Extensions.Logging; using NServiceBus.Transport; using Persistence; using ServiceBus.Management.Infrastructure.Installers; + using ServiceControl.Infrastructure.Settings; class ErrorIngestionFaultPolicy { @@ -21,7 +21,7 @@ class ErrorIngestionFaultPolicy ImportFailureCircuitBreaker failureCircuitBreaker; - public ErrorIngestionFaultPolicy(IErrorMessageDataStore store, LoggingSettings loggingSettings, Func onCriticalError, ILogger logger) + public ErrorIngestionFaultPolicy(IErrorMessageDataStore store, ServiceControlOptions scOptions, Func onCriticalError, ILogger logger) { this.store = store; this.logger = logger; @@ -29,7 +29,7 @@ public ErrorIngestionFaultPolicy(IErrorMessageDataStore store, LoggingSettings l if (!AppEnvironment.RunningInContainer) { - logPath = Path.Combine(loggingSettings.LogPath, "FailedImports", "Error"); + logPath = Path.Combine(scOptions.LogPath, "FailedImports", "Error"); Directory.CreateDirectory(logPath); } } diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 04f5956ccf..046e7c82f2 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -28,6 +28,7 @@ + diff --git a/src/ServiceControl/appsettings.json b/src/ServiceControl/appsettings.json new file mode 100644 index 0000000000..3a0938814b --- /dev/null +++ b/src/ServiceControl/appsettings.json @@ -0,0 +1,25 @@ +{ + "ServiceControl": { + //"InstanceName": "Particular.ServiceControl", + //"LogLevel": "Information", + //"LogPath": "C:\\MyLogs" + + // DEVS - Pick a transport to run Primary instance on + "TransportType": "LearningTransport", + //"TransportType": "AmazonSQS", + //"TransportType": "MSMQ", + //"TransportType": "NetStandardAzureServiceBus", + //"TransportType": "PostgreSQL", + //"TransportType": "RabbitMQ.QuorumConventionalRouting", + //"TransportType": "SQLServer", + + "ErrorQueue": "", + "ErrorLogQueue": "", + //"ForwardErrorMessages": "false", + "ErrorRetentionPeriod": "10.00:00:00", + "AuditRetentionPeriod": "" + //"HeartbeatGracePeriod": "00:00:40", + //"DisableExternalIntegrationsPublishing": "false", + //"ExternalIntegrationsDispatchingBatchSize": "100" + } +} \ No newline at end of file