diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 01d42a1fe2..c8e20956ce 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -63,6 +63,7 @@ + @@ -92,4 +93,4 @@ - + \ No newline at end of file diff --git a/src/Particular.LicensingComponent.Contracts/IEnvironmentDataProvider.cs b/src/Particular.LicensingComponent.Contracts/IEnvironmentDataProvider.cs new file mode 100644 index 0000000000..e7bb79f9c7 --- /dev/null +++ b/src/Particular.LicensingComponent.Contracts/IEnvironmentDataProvider.cs @@ -0,0 +1,9 @@ +namespace Particular.LicensingComponent.Contracts; + +/// +/// Provides environment data that is included in usage reports +/// +public interface IEnvironmentDataProvider +{ + IEnumerable<(string key, string value)> GetData(); +} diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_AdditionalEnvironmentDataProvider_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_AdditionalEnvironmentDataProvider_Tests.cs new file mode 100644 index 0000000000..8d42beea1d --- /dev/null +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_AdditionalEnvironmentDataProvider_Tests.cs @@ -0,0 +1,42 @@ +namespace Particular.LicensingComponent.UnitTests; + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Particular.LicensingComponent.Contracts; +using Particular.LicensingComponent.UnitTests.Infrastructure; + +[TestFixture] +class ThroughputCollector_AdditionalEnvironmentDataProvider_Tests : ThroughputCollectorTestFixture +{ + public override Task Setup() + { + SetExtraDependencies = services => services.AddSingleton(); + + return base.Setup(); + } + + [Test] + public async Task Should_include_additional_environment_data_in_throughput_report() + { + // Arrange + // Act + var report = await ThroughputCollector.GenerateThroughputReport(null, null, default); + // Assert + Assert.That(report, Is.Not.Null); + Assert.That(report.ReportData, Is.Not.Null); + Assert.That(report.ReportData.EnvironmentInformation, Is.Not.Null); + Assert.That(report.ReportData.EnvironmentInformation.EnvironmentData, Is.Not.Null); + Assert.That(report.ReportData.EnvironmentInformation.EnvironmentData.ContainsKey("TestKey")); + Assert.That(report.ReportData.EnvironmentInformation.EnvironmentData["TestKey"], Is.EqualTo("TestValue")); + } + + class TestAdditionalEnvironmentDataProvider : IEnvironmentDataProvider + { + public IEnumerable<(string key, string value)> GetData() + { + yield return ("TestKey", "TestValue"); + } + } +} diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_SanitizedNameGrouping_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_SanitizedNameGrouping_Tests.cs index 1b1d258b57..703e842cea 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_SanitizedNameGrouping_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_SanitizedNameGrouping_Tests.cs @@ -35,7 +35,7 @@ await DataStore.CreateBuilder() .WithThroughput(data: [60]) .Build(); - var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, new BrokerThroughputQuery_WithLowerCaseSanitizedNameCleanse()); + var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, [], new BrokerThroughputQuery_WithLowerCaseSanitizedNameCleanse()); // Act var summary = await throughputCollector.GetThroughputSummary(default); @@ -61,7 +61,7 @@ await DataStore.CreateBuilder() .WithThroughput(data: [60]) .Build(); - var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, new BrokerThroughputQuery_WithLowerCaseSanitizedNameCleanse()); + var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, [], new BrokerThroughputQuery_WithLowerCaseSanitizedNameCleanse()); // Act var report = await throughputCollector.GenerateThroughputReport(null, null, default); @@ -88,7 +88,7 @@ await DataStore.CreateBuilder() .WithThroughput(data: [60]) .Build(); - var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, new BrokerThroughputQuery_WithNoSanitizedNameCleanse()); + var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, [], new BrokerThroughputQuery_WithNoSanitizedNameCleanse()); // Act var summary = await throughputCollector.GetThroughputSummary(default); @@ -114,7 +114,7 @@ await DataStore.CreateBuilder() .WithThroughput(data: [60]) .Build(); - var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, new BrokerThroughputQuery_WithNoSanitizedNameCleanse()); + var throughputCollector = new ThroughputCollector(DataStore, configuration.ThroughputSettings, configuration.AuditQuery, configuration.MonitoringService, [], new BrokerThroughputQuery_WithNoSanitizedNameCleanse()); // Act var report = await throughputCollector.GenerateThroughputReport(null, null, default); diff --git a/src/Particular.LicensingComponent/LicensingComponentServiceCollectionExtensions.cs b/src/Particular.LicensingComponent/LicensingComponentServiceCollectionExtensions.cs new file mode 100644 index 0000000000..34d26ad4eb --- /dev/null +++ b/src/Particular.LicensingComponent/LicensingComponentServiceCollectionExtensions.cs @@ -0,0 +1,11 @@ +namespace Particular.LicensingComponent; + +using Contracts; +using Microsoft.Extensions.DependencyInjection; + +public static class LicensingComponentServiceCollectionExtensions +{ + public static IServiceCollection AddEnvironmentDataProvider(this IServiceCollection services) + where T : class, IEnvironmentDataProvider + => services.AddSingleton(); +} diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs index f7cc882b28..958de9a6c1 100644 --- a/src/Particular.LicensingComponent/ThroughputCollector.cs +++ b/src/Particular.LicensingComponent/ThroughputCollector.cs @@ -12,7 +12,7 @@ using Shared; using QueueThroughput = Report.QueueThroughput; -public class ThroughputCollector(ILicensingDataStore dataStore, ThroughputSettings throughputSettings, IAuditQuery auditQuery, MonitoringService monitoringService, IBrokerThroughputQuery? throughputQuery = null) +public class ThroughputCollector(ILicensingDataStore dataStore, ThroughputSettings throughputSettings, IAuditQuery auditQuery, MonitoringService monitoringService, IEnumerable environmentDataProviders, IBrokerThroughputQuery? throughputQuery = null) : IThroughputCollector { public async Task GetThroughputConnectionSettingsInformation(CancellationToken cancellationToken) @@ -179,6 +179,14 @@ public async Task GenerateThroughputReport(string spVersion, DateT report.EnvironmentInformation.EnvironmentData[EnvironmentDataType.AuditEnabled.ToString()] = systemHasAuditEnabled.ToString(); report.EnvironmentInformation.EnvironmentData[EnvironmentDataType.MonitoringEnabled.ToString()] = systemHasMonitoringEnabled.ToString(); + foreach (var environmentDataProvider in environmentDataProviders) + { + foreach (var (key, value) in environmentDataProvider.GetData()) + { + report.EnvironmentInformation.EnvironmentData[key] = value; + } + } + var throughputReport = new SignedReport { ReportData = report, Signature = Signature.SignReport(report) }; return throughputReport; diff --git a/src/ServiceControl.Config/Commands/UpgradeServiceControlInstanceCommand.cs b/src/ServiceControl.Config/Commands/UpgradeServiceControlInstanceCommand.cs index 452e955f6d..de12a681b9 100644 --- a/src/ServiceControl.Config/Commands/UpgradeServiceControlInstanceCommand.cs +++ b/src/ServiceControl.Config/Commands/UpgradeServiceControlInstanceCommand.cs @@ -119,6 +119,24 @@ public override async Task ExecuteAsync(InstanceDetailsViewModel model) } } + if (!instance.AppConfig.AppSettingExists(ServiceControlSettings.EnableIntegratedServicePulse.Name)) + { + var result = await windowManager.ShowYesNoCancelDialog("INPUT REQUIRED - INTEGRATED SERVICEPULSE", + "ServiceControl can host an integrated version of ServicePulse which allows you to monitor your ServiceControl instance without needing to install ServicePulse separately.", + "Should an integrated ServicePulse be enabled for this ServiceControl instance?", + "Enable integrated ServicePulse", + "Do NOT enable integrated ServicePulse"); + + if (!result.HasValue) + { + //Dialog was cancelled + await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances()); + return; + } + + upgradeOptions.EnableIntegratedServicePulse = result.Value; + } + if (await commandChecks.StopBecauseInstanceIsRunning(instance)) { return; diff --git a/src/ServiceControl.Config/UI/InstanceAdd/EnableIntegratedServicePulseOption.cs b/src/ServiceControl.Config/UI/InstanceAdd/EnableIntegratedServicePulseOption.cs new file mode 100644 index 0000000000..9d16dc9942 --- /dev/null +++ b/src/ServiceControl.Config/UI/InstanceAdd/EnableIntegratedServicePulseOption.cs @@ -0,0 +1,8 @@ +namespace ServiceControl.Config.UI.InstanceAdd +{ + public class EnableIntegratedServicePulseOption + { + public string Name { get; set; } + public bool Value { get; set; } + } +} \ No newline at end of file diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs index 5e8eb5d97a..1d3c08009d 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs @@ -76,6 +76,7 @@ async Task Add() serviceControlNewInstance.ServiceAccount = viewModel.ServiceControl.ServiceAccount; serviceControlNewInstance.ServiceAccountPwd = viewModel.ServiceControl.Password; serviceControlNewInstance.EnableFullTextSearchOnBodies = viewModel.ServiceControl.EnableFullTextSearchOnBodies.Value; + serviceControlNewInstance.EnableIntegratedServicePulse = viewModel.ServiceControl.EnableIntegratedServicePulse.Value; } var auditNewInstance = viewModel.InstallAuditInstance ? ServiceControlAuditNewInstance.CreateWithDefaultPersistence() : null; diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml index 4b09b4623c..9ace3fbd7a 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml @@ -254,6 +254,12 @@ Header="FULL TEXT SEARCH ON MESSAGE BODIES" ItemsSource="{Binding ErrorEnableFullTextSearchOnBodiesOptions}" SelectedValue="{Binding ErrorEnableFullTextSearchOnBodies}" /> + diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs index f514290399..cc8bdb3d2a 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs @@ -220,6 +220,15 @@ public EnableFullTextSearchOnBodiesOption ErrorEnableFullTextSearchOnBodies set => ServiceControl.EnableFullTextSearchOnBodies = value; } + public IEnumerable ErrorEnableIntegratedServicePulseOptions => + ServiceControl.EnableIntegratedServicePulseOptions; + + public EnableIntegratedServicePulseOption ErrorEnableIntegratedServicePulse + { + get => ServiceControl.EnableIntegratedServicePulse; + set => ServiceControl.EnableIntegratedServicePulse = value; + } + /* Add Audit Instance */ public string AuditInstanceName diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlInformation.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlInformation.cs index 843dc4bbd6..13e6d1289c 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlInformation.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlInformation.cs @@ -38,6 +38,19 @@ public ServiceControlInformation(ServiceControlEditorViewModel viewModelParent) Value = false } }; + EnableIntegratedServicePulseOptions = new[] + { + new EnableIntegratedServicePulseOption + { + Name = "On", + Value = true + }, + new EnableIntegratedServicePulseOption + { + Name = "Off", + Value = false + } + }; ErrorRetention = SettingConstants.ErrorRetentionPeriodDefaultInDaysForUI; Description = "ServiceControl Service"; HostName = "localhost"; @@ -48,6 +61,7 @@ public ServiceControlInformation(ServiceControlEditorViewModel viewModelParent) PortNumber = "33333"; DatabaseMaintenancePortNumber = "33334"; EnableFullTextSearchOnBodies = EnableFullTextSearchOnBodiesOptions.First(p => p.Value); //Default to On. + EnableIntegratedServicePulse = EnableIntegratedServicePulseOptions.First(p => p.Value); //Default to On. ViewModelParent = viewModelParent; } @@ -92,6 +106,10 @@ public ForwardingOption ErrorForwarding public EnableFullTextSearchOnBodiesOption EnableFullTextSearchOnBodies { get; set; } + public IEnumerable EnableIntegratedServicePulseOptions { get; } + + public EnableIntegratedServicePulseOption EnableIntegratedServicePulse { get; set; } + protected void UpdateErrorRetention(TimeSpan value) { ErrorRetention = ErrorRetentionUnits == TimeSpanUnits.Days ? value.TotalDays : value.TotalHours; @@ -122,6 +140,7 @@ public void UpdateFromInstance(ServiceControlInstance instance) ErrorForwardingQueueName = instance.ErrorLogQueue; UpdateErrorRetention(instance.ErrorRetentionPeriod); EnableFullTextSearchOnBodies = EnableFullTextSearchOnBodiesOptions.FirstOrDefault(p => p.Value == instance.EnableFullTextSearchOnBodies); + EnableIntegratedServicePulse = EnableIntegratedServicePulseOptions.FirstOrDefault(p => p.Value == instance.EnableIntegratedServicePulse); } ForwardingOption errorForwarding; diff --git a/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsView.xaml b/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsView.xaml index 46485b62c3..dcddb94c1a 100644 --- a/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsView.xaml +++ b/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsView.xaml @@ -137,7 +137,7 @@ diff --git a/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsViewModel.cs b/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsViewModel.cs index af944eec87..57ad612610 100644 --- a/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsViewModel.cs @@ -114,6 +114,22 @@ public string BrowsableUrl public bool HasBrowsableUrl => ServiceInstance is IURLInfo; + public string UrlHeading + { + get + { + if (IsServiceControlInstance) + { + if (ServiceControlInstance.EnableIntegratedServicePulse) + { + return "SERVICEPULSE"; + } + } + + return "URL"; + } + } + public string InstallPath => ((IServicePaths)ServiceInstance).InstallPath; public string DBPath => GetDBPathIfAvailable(); @@ -291,6 +307,7 @@ public Task HandleAsync(PostRefreshInstances message, CancellationToken cancella NotifyOfPropertyChange("HasNewVersion"); NotifyOfPropertyChange("Transport"); NotifyOfPropertyChange("BrowsableUrl"); + NotifyOfPropertyChange("UrlHeading"); return Task.CompletedTask; } diff --git a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditAttachment.cs b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditAttachment.cs index 86ccfe100a..f978f2f82c 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditAttachment.cs @@ -70,6 +70,7 @@ async Task Save() instance.DatabaseMaintenancePort = !string.IsNullOrWhiteSpace(viewModel.ServiceControl.DatabaseMaintenancePortNumber) ? Convert.ToInt32(viewModel.ServiceControl.DatabaseMaintenancePortNumber) : null; instance.VirtualDirectory = null; instance.ForwardErrorMessages = viewModel.ServiceControl.ErrorForwarding.Value; + instance.EnableIntegratedServicePulse = viewModel.ServiceControl.EnableIntegratedServicePulse.Value; instance.ErrorQueue = viewModel.ServiceControl.ErrorQueueName; instance.ErrorLogQueue = viewModel.ServiceControl.ErrorForwardingQueueName; instance.ErrorRetentionPeriod = viewModel.ServiceControl.ErrorRetentionPeriod; diff --git a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditView.xaml b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditView.xaml index a81b7420e9..7565b9c214 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditView.xaml +++ b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditView.xaml @@ -231,6 +231,12 @@ Header="FULL TEXT SEARCH ON MESSAGE BODIES" ItemsSource="{Binding EnableFullTextSearchOnBodiesOptions}" SelectedValue="{Binding EnableFullTextSearchOnBodies}" /> + diff --git a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditViewModel.cs b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditViewModel.cs index 8ca48e5864..dcbeb0fd46 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditViewModel.cs @@ -42,6 +42,7 @@ public void UpdateInstanceFromViewModel(ServiceControlInstance instance) instance.ConnectionString = ConnectionString; instance.DatabaseMaintenancePort = Convert.ToInt32(ServiceControl.DatabaseMaintenancePortNumber); instance.EnableFullTextSearchOnBodies = ServiceControl.EnableFullTextSearchOnBodies.Value; + instance.EnableIntegratedServicePulse = ServiceControl.EnableIntegratedServicePulse.Value; } public string InstanceName => ServiceControl.InstanceName; @@ -189,6 +190,15 @@ public EnableFullTextSearchOnBodiesOption EnableFullTextSearchOnBodies set => ServiceControl.EnableFullTextSearchOnBodies = value; } + public IEnumerable EnableIntegratedServicePulseOptions => + ServiceControl.EnableIntegratedServicePulseOptions; + + public EnableIntegratedServicePulseOption EnableIntegratedServicePulse + { + get => ServiceControl.EnableIntegratedServicePulse; + set => ServiceControl.EnableIntegratedServicePulse = value; + } + public bool SubmitAttempted { get; set; } } } \ No newline at end of file diff --git a/src/ServiceControl.Management.PowerShell/Cmdlets/ServiceControlInstances/NewServiceControlInstance.cs b/src/ServiceControl.Management.PowerShell/Cmdlets/ServiceControlInstances/NewServiceControlInstance.cs index c7e2d6cfee..66bd8e8730 100644 --- a/src/ServiceControl.Management.PowerShell/Cmdlets/ServiceControlInstances/NewServiceControlInstance.cs +++ b/src/ServiceControl.Management.PowerShell/Cmdlets/ServiceControlInstances/NewServiceControlInstance.cs @@ -94,6 +94,9 @@ public class NewServiceControlInstance : PSCmdlet [Parameter(Mandatory = false, HelpMessage = "Specify whether to enable full text search on error messages.")] public SwitchParameter EnableFullTextSearchOnBodies { get; set; } = true; + [Parameter(Mandatory = false, HelpMessage = "Specify whether to enable integrated ServicePulse instance.")] + public SwitchParameter EnableIntegratedServicePulse { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Reuse the specified log, db, and install paths even if they are not empty")] public SwitchParameter Force { get; set; } @@ -172,6 +175,7 @@ protected override void ProcessRecord() details.TransportPackage = ServiceControlCoreTransports.Find(Transport); details.SkipQueueCreation = SkipQueueCreation; details.EnableFullTextSearchOnBodies = EnableFullTextSearchOnBodies; + details.EnableIntegratedServicePulse = EnableIntegratedServicePulse; var modulePath = Path.GetDirectoryName(MyInvocation.MyCommand.Module.Path); diff --git a/src/ServiceControl.Management.PowerShell/ServiceControl.Management.PowerShell.dll-help.xml b/src/ServiceControl.Management.PowerShell/ServiceControl.Management.PowerShell.dll-help.xml index 586dc08540..42ad697714 100644 --- a/src/ServiceControl.Management.PowerShell/ServiceControl.Management.PowerShell.dll-help.xml +++ b/src/ServiceControl.Management.PowerShell/ServiceControl.Management.PowerShell.dll-help.xml @@ -2655,6 +2655,13 @@ SwitchParameter + + EnableIntegratedServicePulse + + Enable the integrated version of ServicePulse that ships with ServiceControl. + + SwitchParameter + Force @@ -2942,6 +2949,18 @@ + + EnableIntegratedServicePulse + + Enable the integrated version of ServicePulse that ships with ServiceControl + + SwitchParameter + + SwitchParameter + + + + @@ -2998,7 +3017,8 @@ -DisplayName 'ServiceControl Test' ` -AuditRetentionPeriod $AuditRetention ` -ErrorRetentionPeriod $ErrorRetention ` - -ForwardErrorMessages:$false + -ForwardErrorMessages:$false ` + -EnableIntegratedServicePulse Add a servicecontrol instance diff --git a/src/ServiceControl/HostApplicationBuilderExtensions.cs b/src/ServiceControl/HostApplicationBuilderExtensions.cs index fbe99dada5..23987c6543 100644 --- a/src/ServiceControl/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl/HostApplicationBuilderExtensions.cs @@ -24,6 +24,7 @@ namespace Particular.ServiceControl using NServiceBus; using NServiceBus.Configuration.AdvancedExtensibility; using NServiceBus.Transport; + using Particular.LicensingComponent; using ServiceBus.Management.Infrastructure; using ServiceBus.Management.Infrastructure.Installers; using ServiceBus.Management.Infrastructure.Settings; @@ -54,6 +55,7 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S services.AddSingleton(); services.AddSingleton(settings); + services.AddEnvironmentDataProvider(); services.AddHttpLogging(options => { diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index ac5cd439b4..ebc08958cf 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -10,6 +10,7 @@ using ServiceControl; using ServiceControl.Hosting.Auth; using ServiceControl.Hosting.Https; + using ServicePulse; class RunCommand : AbstractCommand { @@ -30,6 +31,10 @@ public override async Task Execute(HostArguments args, Settings settings) var app = hostBuilder.Build(); app.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings); + if (settings.EnableIntegratedServicePulse) + { + app.UseServicePulse(settings.ServicePulseSettings); + } app.UseServiceControlAuthentication(settings.OpenIdConnectSettings.Enabled); await app.RunAsync(settings.RootUrl); diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index 939494975e..1fcdee1245 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -15,6 +15,7 @@ namespace ServiceBus.Management.Infrastructure.Settings using ServiceControl.Infrastructure.WebApi; using ServiceControl.Persistence; using ServiceControl.Transports; + using ServicePulse; using JsonSerializer = System.Text.Json.JsonSerializer; public class Settings @@ -65,6 +66,12 @@ public Settings( MaximumConcurrencyLevel = SettingsReader.Read(SettingsRootNamespace, "MaximumConcurrencyLevel"); RetryHistoryDepth = SettingsReader.Read(SettingsRootNamespace, "RetryHistoryDepth", 10); AllowMessageEditing = SettingsReader.Read(SettingsRootNamespace, "AllowMessageEditing"); + EnableIntegratedServicePulse = SettingsReader.Read(SettingsRootNamespace, "EnableIntegratedServicePulse", false); + ServicePulseSettings = ServicePulseSettings.GetFromEnvironmentVariables() with + { + ServiceControlUrl = $"{ApiUrl}/", + IsIntegrated = true + }; NotificationsFilter = SettingsReader.Read(SettingsRootNamespace, "NotificationsFilter"); RemoteInstances = GetRemoteInstances().ToArray(); TimeToRestartErrorIngestionAfterFailure = GetTimeToRestartErrorIngestionAfterFailure(); @@ -103,6 +110,9 @@ public Settings( public bool AllowMessageEditing { get; set; } + public bool EnableIntegratedServicePulse { get; set; } + public ServicePulseSettings ServicePulseSettings { get; set; } + //HINT: acceptance tests only public Func MessageFilter { get; set; } diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index f4d7a787bb..d931751d34 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -36,6 +36,7 @@ + diff --git a/src/ServiceControl/ServiceControlErrorInstanceEnvironmentDataProvider.cs b/src/ServiceControl/ServiceControlErrorInstanceEnvironmentDataProvider.cs new file mode 100644 index 0000000000..b3828e8b43 --- /dev/null +++ b/src/ServiceControl/ServiceControlErrorInstanceEnvironmentDataProvider.cs @@ -0,0 +1,13 @@ +namespace Particular.ServiceControl; + +using System.Collections.Generic; +using Particular.LicensingComponent.Contracts; +using ServiceBus.Management.Infrastructure.Settings; + +class ServiceControlErrorInstanceEnvironmentDataProvider(Settings settings) : IEnvironmentDataProvider +{ + public IEnumerable<(string key, string value)> GetData() + { + yield return ("Features.IntegratedServicePulse", settings.EnableIntegratedServicePulse ? "Enabled" : "Disabled"); + } +} \ No newline at end of file diff --git a/src/ServiceControlInstaller.Engine.UnitTests/Validation/QueueValidationTests.cs b/src/ServiceControlInstaller.Engine.UnitTests/Validation/QueueValidationTests.cs index 578ef22468..aaf6fdf138 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/Validation/QueueValidationTests.cs +++ b/src/ServiceControlInstaller.Engine.UnitTests/Validation/QueueValidationTests.cs @@ -21,6 +21,8 @@ class FakeServiceControlInstance : IServiceControlInstance public bool ForwardErrorMessages { get; set; } + public bool EnableIntegratedServicePulse { get; set; } + public TimeSpan ErrorRetentionPeriod { get; set; } public TimeSpan? AuditRetentionPeriod { get; set; } diff --git a/src/ServiceControlInstaller.Engine/Configuration/Monitoring/SettingsList.cs b/src/ServiceControlInstaller.Engine/Configuration/Monitoring/SettingsList.cs index e7651cd20e..d651da5e90 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/Monitoring/SettingsList.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/Monitoring/SettingsList.cs @@ -26,5 +26,12 @@ public static class SettingsList Name = "Monitoring/ShutdownTimeout", SupportedFrom = new SemanticVersion(6, 4, 1) }; + + public static readonly SettingInfo HttpsEnabled = new() + { + Name = "Monitoring/Https.Enabled", + SupportedFrom = new SemanticVersion(6, 11, 0) + }; + } } \ No newline at end of file diff --git a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAppConfig.cs b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAppConfig.cs index c3c79743bf..3f2ef78627 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAppConfig.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/ServiceControlAppConfig.cs @@ -38,6 +38,7 @@ protected override void UpdateSettings() settings.Set(ServiceControlSettings.ErrorRetentionPeriod, details.ErrorRetentionPeriod.ToString(), version); settings.Set(ServiceControlSettings.EnableFullTextSearchOnBodies, details.EnableFullTextSearchOnBodies.ToString(), version); settings.Set(ServiceControlSettings.RemoteInstances, RemoteInstanceConverter.ToJson(details.RemoteInstances), version); + settings.Set(ServiceControlSettings.EnableIntegratedServicePulse, details.EnableIntegratedServicePulse.ToString(), version); // Windows services allow a maximum of 125 seconds when stopping a service. // When shutting down or restarting the OS we have no control over the diff --git a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/SettingsList.cs b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/SettingsList.cs index 9dc2166000..841f850f71 100644 --- a/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/SettingsList.cs +++ b/src/ServiceControlInstaller.Engine/Configuration/ServiceControl/SettingsList.cs @@ -113,5 +113,17 @@ public static class ServiceControlSettings Name = "LicensingComponent/RabbitMQ/Password", RemovedFrom = new SemanticVersion(6, 5, 0) }; + + public static readonly SettingInfo EnableIntegratedServicePulse = new() + { + Name = "ServiceControl/EnableIntegratedServicePulse", + SupportedFrom = new SemanticVersion(6, 12, 0) + }; + + public static readonly SettingInfo HttpsEnabled = new() + { + Name = "ServiceControl/Https.Enabled", + SupportedFrom = new SemanticVersion(6, 11, 0) + }; } } diff --git a/src/ServiceControlInstaller.Engine/Instances/MonitoringInstance.cs b/src/ServiceControlInstaller.Engine/Instances/MonitoringInstance.cs index 7770296479..2b3f93c4ca 100644 --- a/src/ServiceControlInstaller.Engine/Instances/MonitoringInstance.cs +++ b/src/ServiceControlInstaller.Engine/Instances/MonitoringInstance.cs @@ -40,7 +40,10 @@ public MonitoringInstance(WindowsServiceController service) public bool SkipQueueCreation { get; set; } - public string Url => $"http://{HostName}:{Port}/"; + public string Url => $"{UrlScheme}://{HostName}:{Port}/"; + + public bool HttpsEnabled { get; set; } + string UrlScheme => HttpsEnabled ? "https" : "http"; public string BrowsableUrl { @@ -52,7 +55,7 @@ public string BrowsableUrl "+" => Environment.MachineName.ToLower(), _ => HostName, }; - return $"http://{host}:{Port}/"; + return $"{UrlScheme}://{host}:{Port}/"; } } @@ -71,6 +74,8 @@ public override void Reload() ConnectionString = ReadConnectionString(); Description = GetDescription(); ServiceAccount = Service.Account; + HttpsEnabled = AppConfig.Read(SettingsList.HttpsEnabled, false); + } string DefaultLogPath() diff --git a/src/ServiceControlInstaller.Engine/Instances/ServiceControlBaseService.cs b/src/ServiceControlInstaller.Engine/Instances/ServiceControlBaseService.cs index 5e2a3a762f..a5e2294538 100644 --- a/src/ServiceControlInstaller.Engine/Instances/ServiceControlBaseService.cs +++ b/src/ServiceControlInstaller.Engine/Instances/ServiceControlBaseService.cs @@ -85,19 +85,24 @@ public string AclMaintenanceUrl public TimeSpan ErrorRetentionPeriod { get; set; } public bool SkipQueueCreation { get; set; } public bool EnableFullTextSearchOnBodies { get; set; } + public bool EnableIntegratedServicePulse { get; set; } + public bool HttpsEnabled { get; set; } protected abstract string BaseServiceName { get; } + public string UrlScheme => HttpsEnabled ? "https" : "http"; + public string Url { get { + var suffix = EnableIntegratedServicePulse ? "" : "api/"; if (string.IsNullOrWhiteSpace(VirtualDirectory)) { - return $"http://{HostName}:{Port}/api/"; + return $"{UrlScheme}://{HostName}:{Port}/{suffix}"; } - return $"http://{HostName}:{Port}/{VirtualDirectory}{(VirtualDirectory.EndsWith("/") ? string.Empty : "/")}api/"; + return $"{UrlScheme}://{HostName}:{Port}/{VirtualDirectory}{(VirtualDirectory.EndsWith("/") ? string.Empty : "/")}{suffix}"; } } @@ -105,6 +110,7 @@ public string BrowsableUrl { get { + var suffix = EnableIntegratedServicePulse ? "" : "api/"; string host = HostName switch { "*" => "localhost", @@ -113,10 +119,10 @@ public string BrowsableUrl }; if (string.IsNullOrWhiteSpace(VirtualDirectory)) { - return $"http://{host}:{Port}/api/"; + return $"{UrlScheme}://{host}:{Port}/{suffix}"; } - return $"http://{host}:{Port}/{VirtualDirectory}{(VirtualDirectory.EndsWith("/") ? string.Empty : "/")}api/"; + return $"{UrlScheme}://{host}:{Port}/{VirtualDirectory}{(VirtualDirectory.EndsWith("/") ? string.Empty : "/")}{suffix}/"; } } diff --git a/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstallableBase.cs b/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstallableBase.cs index 6f237b1803..49d7b73ca9 100644 --- a/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstallableBase.cs +++ b/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstallableBase.cs @@ -91,6 +91,8 @@ string[] FlagFiles public bool ForwardErrorMessages { get; set; } + public bool EnableIntegratedServicePulse { get; set; } + public TransportInfo TransportPackage { get; set; } public string ConnectionString { get; set; } diff --git a/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstance.cs b/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstance.cs index f500a82545..d010a64a20 100644 --- a/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstance.cs +++ b/src/ServiceControlInstaller.Engine/Instances/ServiceControlInstance.cs @@ -135,6 +135,9 @@ public override void Reload() AuditLogQueue = AppConfig.Read(ServiceControlSettings.AuditLogQueue, string.IsNullOrEmpty(AuditQueue) ? null : $"{AuditQueue}.log"); } + EnableIntegratedServicePulse = AppConfig.Read(ServiceControlSettings.EnableIntegratedServicePulse, false); + HttpsEnabled = AppConfig.Read(ServiceControlSettings.HttpsEnabled, false); + if (TimeSpan.TryParse(AppConfig.Read(ServiceControlSettings.ErrorRetentionPeriod, (string)null), out var errorRetentionPeriod)) { ErrorRetentionPeriod = errorRetentionPeriod; @@ -181,6 +184,7 @@ protected override void ApplySettingsChanges(KeyValueConfigurationCollection set settings.Set(ServiceControlSettings.ErrorLogQueue, ErrorLogQueue, Version); settings.Set(ServiceControlSettings.EnableFullTextSearchOnBodies, EnableFullTextSearchOnBodies.ToString(), Version); settings.Set(ServiceControlSettings.PersistenceType, PersistenceManifest.Name); + settings.Set(ServiceControlSettings.EnableIntegratedServicePulse, EnableIntegratedServicePulse.ToString(), Version); if (RemoteInstances != null) { diff --git a/src/ServiceControlInstaller.Engine/Instances/ServiceControlUpgradeOptions.cs b/src/ServiceControlInstaller.Engine/Instances/ServiceControlUpgradeOptions.cs index d670ec50c7..5e74501c4a 100644 --- a/src/ServiceControlInstaller.Engine/Instances/ServiceControlUpgradeOptions.cs +++ b/src/ServiceControlInstaller.Engine/Instances/ServiceControlUpgradeOptions.cs @@ -10,6 +10,7 @@ public class ServiceControlUpgradeOptions public int? MaintenancePort { get; set; } public bool SkipQueueCreation { get; set; } public string RemoteUrl { get; set; } + public bool EnableIntegratedServicePulse { get; set; } public bool Force { get; set; } public void ApplyChangesToInstance(ServiceControlBaseService instance) @@ -53,6 +54,7 @@ void ApplyChangesTo(ServiceControlInstance instance) } instance.SkipQueueCreation = SkipQueueCreation; + instance.EnableIntegratedServicePulse = EnableIntegratedServicePulse; } void ApplyChangesTo(ServiceControlAuditInstance instance) diff --git a/src/ServiceControlInstaller.Engine/Interfaces.cs b/src/ServiceControlInstaller.Engine/Interfaces.cs index 914be2cc43..5d5968b7a5 100644 --- a/src/ServiceControlInstaller.Engine/Interfaces.cs +++ b/src/ServiceControlInstaller.Engine/Interfaces.cs @@ -109,6 +109,7 @@ public interface IServiceControlInstance : IServiceControlBaseInstance, IURLInfo string ErrorLogQueue { get; } string VirtualDirectory { get; } bool ForwardErrorMessages { get; } + bool EnableIntegratedServicePulse { get; } TimeSpan ErrorRetentionPeriod { get; } TimeSpan? AuditRetentionPeriod { get; set; } List RemoteInstances { get; }