diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 188cc534d5..be7144faf5 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -47,7 +47,7 @@ - + diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/When_editing_message_headers.cs b/src/ServiceControl.AcceptanceTests/Recoverability/When_editing_message_headers.cs index faa49666b8..113d45a820 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/When_editing_message_headers.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/When_editing_message_headers.cs @@ -66,8 +66,8 @@ public async Task A_new_message_with_edited_headers_is_sent() { Assert.That(context.EditedMessageId, Is.Not.EqualTo(context.OriginalMessageId)); Assert.That(context.OriginalMessageFailure.Status, Is.EqualTo(FailedMessageStatus.Resolved)); - Assert.That(context.EditedMessageHeaders["AcceptanceTest.NewHeader"], Is.EqualTo("42").AsCollection); - Assert.That(context.EditedMessageHeaders["ServiceControl.EditOf"], Is.EqualTo(context.UniqueMessageId).AsCollection); + Assert.That(context.EditedMessageHeaders["AcceptanceTest.NewHeader"], Is.EqualTo("42")); + Assert.That(context.EditedMessageHeaders["ServiceControl.EditOf"], Is.EqualTo(context.UniqueMessageId)); }); } diff --git a/src/ServiceControl.Persistence.RavenDB/ConnectedApplicationsDataStore.cs b/src/ServiceControl.Persistence.RavenDB/ConnectedApplicationsDataStore.cs deleted file mode 100644 index 96bc4c1476..0000000000 --- a/src/ServiceControl.Persistence.RavenDB/ConnectedApplicationsDataStore.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace ServiceControl.Persistence.RavenDB -{ - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - - class ConnectedApplicationsDataStore(IRavenSessionProvider sessionProvider) : IConnectedApplicationsDataStore - { - public async Task Add(string connectedApplication) - { - using var session = await sessionProvider.OpenSession(); - var connectedApplications = await session.LoadAsync(StorageKey) ?? new ConnectedApplications(); - - if (!connectedApplications.Applications.Any(application => string.Equals(application, connectedApplication, System.StringComparison.InvariantCultureIgnoreCase))) - { - connectedApplications.Applications.Add(connectedApplication); - } - - await session.StoreAsync(connectedApplications, StorageKey); - await session.SaveChangesAsync(); - } - - public async Task> GetConnectedApplications() - { - using var session = await sessionProvider.OpenSession(); - var applications = await session.LoadAsync(StorageKey); - - return applications?.Applications; - } - - class ConnectedApplications - { - public List Applications { get; set; } = []; - } - - const string StorageKey = "ConnectedApplications"; - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs b/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs index 335e8a8d4f..7b1e07ecbb 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenPersistence.cs @@ -66,7 +66,6 @@ public void AddPersistence(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); } public void AddInstaller(IServiceCollection services) diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/ConnectedApplications/ConnectedApplicationsTests.cs b/src/ServiceControl.Persistence.Tests.RavenDB/ConnectedApplications/ConnectedApplicationsTests.cs deleted file mode 100644 index 760279079f..0000000000 --- a/src/ServiceControl.Persistence.Tests.RavenDB/ConnectedApplications/ConnectedApplicationsTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace ServiceControl.Persistence.Tests.RavenDB.ConnectedApplications -{ - using System.Threading.Tasks; - using Microsoft.Extensions.DependencyInjection; - using NUnit.Framework; - using ServiceControl.Persistence.RavenDB; - - [TestFixture] - class ConnectedApplicationsTests : RavenPersistenceTestBase - { - public ConnectedApplicationsTests() => - RegisterServices = services => - { - services.AddSingleton(); - }; - - [Test] - public async Task ConnectedApplications_can_be_saved() - { - var connectedApplicationsDataStore = ServiceProvider.GetRequiredService(); - - var connectedApplication1 = "ServiceControl.Connector.MassTransit"; - var connectedApplication2 = "ServiceControl.Connector.Kafka"; - - await connectedApplicationsDataStore.Add(connectedApplication1).ConfigureAwait(false); - await connectedApplicationsDataStore.Add(connectedApplication2).ConfigureAwait(false); - - var result = await connectedApplicationsDataStore.GetConnectedApplications(); - - Assert.That(result, Is.EqualTo(new[] { connectedApplication1, connectedApplication2 }).AsCollection); - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Persistence/IConnectedApplicationsDataStore.cs b/src/ServiceControl.Persistence/IConnectedApplicationsDataStore.cs deleted file mode 100644 index dd45cc72ca..0000000000 --- a/src/ServiceControl.Persistence/IConnectedApplicationsDataStore.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ServiceControl.Persistence -{ - using System.Collections.Generic; - using System.Threading.Tasks; - - public interface IConnectedApplicationsDataStore - { - Task Add(string connectedApplication); - Task> GetConnectedApplications(); - } -} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/API/APIApprovals.cs b/src/ServiceControl.UnitTests/API/APIApprovals.cs index 4e8708febb..af4da85885 100644 --- a/src/ServiceControl.UnitTests/API/APIApprovals.cs +++ b/src/ServiceControl.UnitTests/API/APIApprovals.cs @@ -19,6 +19,7 @@ using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Infrastructure.Api; using ServiceControl.Infrastructure.WebApi; + using ServiceControl.Monitoring.HeartbeatMonitoring; [TestFixture] class APIApprovals @@ -34,7 +35,7 @@ public async Task RootPathValue() new ActiveLicense(null) { IsValid = true }, new Settings(), null, - null + new MassTransitConnectorHeartbeatStatus() ) ) { diff --git a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs index 4212fc51c5..fa06e80bc9 100644 --- a/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs +++ b/src/ServiceControl/Infrastructure/Api/ConfigurationApi.cs @@ -8,16 +8,15 @@ using System.Threading; using System.Threading.Tasks; using Configuration; +using Monitoring.HeartbeatMonitoring; using Particular.ServiceControl.Licensing; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Api; using ServiceControl.Api.Contracts; -using ServiceControl.Persistence; class ConfigurationApi(ActiveLicense license, Settings settings, - IConnectedApplicationsDataStore connectedApplicationsDataStore, - IHttpClientFactory httpClientFactory) : IConfigurationApi + IHttpClientFactory httpClientFactory, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : IConfigurationApi { public Task GetUrls(string baseUrl, CancellationToken cancellationToken) { @@ -51,7 +50,7 @@ public Task GetUrls(string baseUrl, CancellationToken cancellationToke } - public async Task GetConfig(CancellationToken cancellationToken) + public Task GetConfig(CancellationToken cancellationToken) { object content = new { @@ -85,13 +84,10 @@ public async Task GetConfig(CancellationToken cancellationToken) { settings.HeartbeatGracePeriod }, - ConnectedApplications = new - { - ConnectedApplications = await connectedApplicationsDataStore.GetConnectedApplications() - } + MassTransitConnector = connectorHeartbeatStatus.LastHeartbeat }; - return content; + return Task.FromResult(content); } public async Task GetRemoteConfigs(CancellationToken cancellationToken = default) diff --git a/src/ServiceControl/Licensing/ActiveLicense.cs b/src/ServiceControl/Licensing/ActiveLicense.cs index f72f4f72f0..d50a5bdb38 100644 --- a/src/ServiceControl/Licensing/ActiveLicense.cs +++ b/src/ServiceControl/Licensing/ActiveLicense.cs @@ -1,8 +1,8 @@ namespace Particular.ServiceControl.Licensing { - using System.Threading.Tasks; - using System.Threading; using System; + using System.Threading; + using System.Threading.Tasks; using global::ServiceControl.LicenseManagement; using global::ServiceControl.Persistence; using NServiceBus.Logging; @@ -10,6 +10,7 @@ public class ActiveLicense(ITrialLicenseDataProvider trialLicenseDataProvider) { public bool IsValid { get; set; } + public bool IsEvaluation { get; set; } public LicenseDetails Details { get; set; } @@ -22,6 +23,8 @@ public async Task Refresh(CancellationToken cancellationToken) Details = await ValidateTrialLicense(detectedLicense.Details, trialLicenseDataProvider, cancellationToken); IsValid = !Details.HasLicenseExpired(); + + IsEvaluation = detectedLicense.IsEvaluationLicense; } internal static async Task ValidateTrialLicense(LicenseDetails licenseDetails, ITrialLicenseDataProvider trialLicenseDataProvider, CancellationToken cancellationToken) diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index 75cd044f58..15539d9db0 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -3,13 +3,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; + using Monitoring.HeartbeatMonitoring; using Particular.ServiceControl.Licensing; - using Persistence; using ServiceBus.Management.Infrastructure.Settings; [ApiController] [Route("api")] - public class LicenseController(ActiveLicense activeLicense, Settings settings, IConnectedApplicationsDataStore connectedApplicationsStore) : ControllerBase + public class LicenseController(ActiveLicense activeLicense, Settings settings, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : ControllerBase { [HttpGet] [Route("license")] @@ -31,21 +31,14 @@ public async Task> License(bool refresh, string client LicenseType = activeLicense.Details.LicenseType ?? string.Empty, InstanceName = settings.InstanceName ?? string.Empty, LicenseStatus = activeLicense.Details.Status, - LicenseExtensionUrl = $"https://particular.net/extend-your-trial?p={clientName}{await BuildConnectedApplicationsListPart()}" + LicenseExtensionUrl = connectorHeartbeatStatus.LastHeartbeat == null + ? $"https://particular.net/extend-your-trial?p={clientName}" + : $"https://particular.net/license/mt?p={clientName}&t={(activeLicense.IsEvaluation ? 0 : 1)}" }; return licenseInfo; } - async Task BuildConnectedApplicationsListPart() - { - var connectedApplications = await connectedApplicationsStore.GetConnectedApplications(); - - return connectedApplications != null - ? $"&ca={string.Join(',', connectedApplications)}" - : string.Empty; - } - public class LicenseInfo { public bool TrialLicense { get; set; } diff --git a/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs b/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs index 355499d519..7a375746ed 100644 --- a/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs +++ b/src/ServiceControl/Monitoring/EndpointInstanceMonitor.cs @@ -5,7 +5,6 @@ namespace ServiceControl.Monitoring using Contracts.HeartbeatMonitoring; using EndpointControl.Contracts; using Infrastructure.DomainEvents; - using NLog.Fluent; using NServiceBus.Logging; using ServiceControl.Operations; using ServiceControl.Persistence; diff --git a/src/ServiceControl/Monitoring/HeartbeatMonitoring/InternalMessages/MassTransitConnectorHeartbeat.cs b/src/ServiceControl/Monitoring/HeartbeatMonitoring/InternalMessages/MassTransitConnectorHeartbeat.cs new file mode 100644 index 0000000000..a9a8c70f48 --- /dev/null +++ b/src/ServiceControl/Monitoring/HeartbeatMonitoring/InternalMessages/MassTransitConnectorHeartbeat.cs @@ -0,0 +1,27 @@ +namespace ServiceControl.Connector.MassTransit; + +using System; +using NServiceBus; + +public class MassTransitConnectorHeartbeat : IMessage +{ + public required string Version { get; set; } + public required ErrorQueue[] ErrorQueues { get; set; } + public required LogEntry[] Logs { get; set; } + public required DateTimeOffset SentDateTimeOffset { get; set; } +} + +#pragma warning disable CA1711 +public class ErrorQueue +#pragma warning restore CA1711 +{ + public required string Name { get; set; } + public required bool Ingesting { get; set; } +} + +public class LogEntry +{ + public string Message { get; set; } + public DateTimeOffset Date { get; set; } + public string Level { get; set; } +} \ No newline at end of file diff --git a/src/ServiceControl/Monitoring/HeartbeatMonitoring/MassTransitConnectorHeartbeatStatus.cs b/src/ServiceControl/Monitoring/HeartbeatMonitoring/MassTransitConnectorHeartbeatStatus.cs new file mode 100644 index 0000000000..f837d42ef5 --- /dev/null +++ b/src/ServiceControl/Monitoring/HeartbeatMonitoring/MassTransitConnectorHeartbeatStatus.cs @@ -0,0 +1,11 @@ +#nullable enable +namespace ServiceControl.Monitoring.HeartbeatMonitoring; + +using Connector.MassTransit; + +public class MassTransitConnectorHeartbeatStatus +{ + public MassTransitConnectorHeartbeat? LastHeartbeat { get; private set; } + + public void Update(MassTransitConnectorHeartbeat lastHeartbeat) => LastHeartbeat = lastHeartbeat; +} \ No newline at end of file diff --git a/src/ServiceControl/Monitoring/HeartbeatMonitoringComponent.cs b/src/ServiceControl/Monitoring/HeartbeatMonitoringComponent.cs index 3b519d0640..eb6ed312e3 100644 --- a/src/ServiceControl/Monitoring/HeartbeatMonitoringComponent.cs +++ b/src/ServiceControl/Monitoring/HeartbeatMonitoringComponent.cs @@ -4,6 +4,7 @@ using EndpointControl.Handlers; using EventLog; using ExternalIntegrations; + using HeartbeatMonitoring; using Infrastructure.DomainEvents; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -21,6 +22,7 @@ public override void Configure(Settings settings, ITransportCustomization transp hostBuilder.Services.AddHostedService(); hostBuilder.Services.AddSingleton(); + hostBuilder.Services.AddSingleton(); hostBuilder.Services.AddDomainEventHandler(); diff --git a/src/ServiceControl/Monitoring/Transport/MassTransitConnectorHeartbeatHandler.cs b/src/ServiceControl/Monitoring/Transport/MassTransitConnectorHeartbeatHandler.cs new file mode 100644 index 0000000000..68871aa92b --- /dev/null +++ b/src/ServiceControl/Monitoring/Transport/MassTransitConnectorHeartbeatHandler.cs @@ -0,0 +1,19 @@ +namespace ServiceControl.Monitoring; + +using System.Threading.Tasks; +using Connector.MassTransit; +using HeartbeatMonitoring; +using NServiceBus; + +class MassTransitConnectorHeartbeatHandler(MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : IHandleMessages +{ + public Task Handle(MassTransitConnectorHeartbeat message, IMessageHandlerContext context) + { + if (connectorHeartbeatStatus.LastHeartbeat == null || message.SentDateTimeOffset > connectorHeartbeatStatus.LastHeartbeat.SentDateTimeOffset) + { + connectorHeartbeatStatus.Update(message); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/ServiceControl/Operations/MassTransitDetector.cs b/src/ServiceControl/Operations/MassTransitDetector.cs deleted file mode 100644 index 7b75ea0616..0000000000 --- a/src/ServiceControl/Operations/MassTransitDetector.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace ServiceControl.Operations -{ - using System.Linq; - using Persistence; - - class MassTransitDetector(IConnectedApplicationsDataStore connectedApplicationsDataStore) : IEnrichImportedErrorMessages - { - public void Enrich(ErrorEnricherContext context) - { - if (alreadyDetected) - { - return; - } - - if (context.Headers.Any(h => IsMassTransitHeader(h.Key))) - { - alreadyDetected = true; - - _ = connectedApplicationsDataStore.Add("MassTransitConnector"); - } - } - - static bool IsMassTransitHeader(string headerName) => headerName.StartsWith("MT-", System.StringComparison.InvariantCultureIgnoreCase); - - static bool alreadyDetected; - } -}