From 60d0781cac9545291b0929acaa1b0b009ec052b0 Mon Sep 17 00:00:00 2001 From: John Simons Date: Fri, 7 Mar 2025 12:18:58 +1000 Subject: [PATCH 1/2] Add monthly highest usage --- .../EndpointThroughputSummary.cs | 1 + .../ThroughputSettings.cs | 7 ++++++- .../ThroughputCollector.cs | 5 +++-- .../ThroughputDataExtensions.cs | 18 +++++++++++++++++- .../LicenseDetails.cs | 1 - .../Throughput/LicensingDataStore.cs | 4 ++-- .../Licensing/LicensingComponent.cs | 7 +++++-- 7 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Particular.LicensingComponent.Contracts/EndpointThroughputSummary.cs b/src/Particular.LicensingComponent.Contracts/EndpointThroughputSummary.cs index 8a40330e27..5c6cd7392c 100644 --- a/src/Particular.LicensingComponent.Contracts/EndpointThroughputSummary.cs +++ b/src/Particular.LicensingComponent.Contracts/EndpointThroughputSummary.cs @@ -7,6 +7,7 @@ public class EndpointThroughputSummary public bool IsKnownEndpoint { get; set; } public string UserIndicator { get; set; } public long MaxDailyThroughput { get; set; } + public long MaxMonthlyThroughput { get; set; } } public class UpdateUserIndicator diff --git a/src/Particular.LicensingComponent.Contracts/ThroughputSettings.cs b/src/Particular.LicensingComponent.Contracts/ThroughputSettings.cs index e2e9e9fd78..972f630810 100644 --- a/src/Particular.LicensingComponent.Contracts/ThroughputSettings.cs +++ b/src/Particular.LicensingComponent.Contracts/ThroughputSettings.cs @@ -2,7 +2,12 @@ using ServiceControl.Configuration; -public class ThroughputSettings(string serviceControlQueue, string errorQueue, string transportType, string customerName, string serviceControlVersion) +public class ThroughputSettings( + string serviceControlQueue, + string errorQueue, + string transportType, + string customerName, + string serviceControlVersion) { public static readonly SettingsRootNamespace SettingsNamespace = new("LicensingComponent"); diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs index 5856bc6d96..45a2ecbdb4 100644 --- a/src/Particular.LicensingComponent/ThroughputCollector.cs +++ b/src/Particular.LicensingComponent/ThroughputCollector.cs @@ -71,7 +71,8 @@ public async Task> GetThroughputSummary(Cancella Name = endpointData.Name, UserIndicator = endpointData.UserIndicator ?? (endpointData.IsKnownEndpoint ? Contracts.UserIndicator.NServiceBusEndpoint.ToString() : string.Empty), IsKnownEndpoint = endpointData.IsKnownEndpoint, - MaxDailyThroughput = endpointData.ThroughputData.Max() + MaxDailyThroughput = endpointData.ThroughputData.MaxDailyThroughput(), + MaxMonthlyThroughput = endpointData.ThroughputData.MaxMonthlyThroughput() }; endpointSummaries.Add(endpointSummary); @@ -130,7 +131,7 @@ public async Task GenerateThroughputReport(string spVersion, DateT EndpointIndicators = endpointData.EndpointIndicators ?? [], NoDataOrSendOnly = endpointData.ThroughputData.Sum() == 0, Scope = endpointData.Scope ?? "", - Throughput = endpointData.ThroughputData.Max(), + Throughput = endpointData.ThroughputData.MaxDailyThroughput(), DailyThroughputFromAudit = endpointData.ThroughputData.FromSource(ThroughputSource.Audit).Select(s => new DailyThroughput { DateUTC = s.DateUTC, MessageCount = s.MessageCount }).ToArray(), DailyThroughputFromMonitoring = endpointData.ThroughputData.FromSource(ThroughputSource.Monitoring).Select(s => new DailyThroughput { DateUTC = s.DateUTC, MessageCount = s.MessageCount }).ToArray(), DailyThroughputFromBroker = notAnNsbEndpoint ? [] : endpointData.ThroughputData.FromSource(ThroughputSource.Broker).Select(s => new DailyThroughput { DateUTC = s.DateUTC, MessageCount = s.MessageCount }).ToArray() diff --git a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs index c794ae7274..79dedaf705 100644 --- a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs +++ b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs @@ -10,7 +10,7 @@ public static IEnumerable FromSource(this List throughputs) => throughputs.SelectMany(t => t).Sum(kvp => kvp.Value); - public static long Max(this List throughputs) + public static long MaxDailyThroughput(this List throughputs) { var items = throughputs.SelectMany(t => t).ToArray(); @@ -22,6 +22,22 @@ public static long Max(this List throughputs) return 0; } + public static long MaxMonthlyThroughput(this List throughputs) + { + var monthlySums = throughputs + .SelectMany(data => data) + .GroupBy(kvp => $"{kvp.Key.Year}-{kvp.Key.Month}") + .Select(group => group.Sum(kvp => kvp.Value)) + .ToArray(); + + if (monthlySums.Any()) + { + return monthlySums.Max(); + } + + return 0; + } + public static bool HasDataFromSource(this IDictionary> throughputPerQueue, ThroughputSource source) => throughputPerQueue.Any(queueName => queueName.Value.Any(data => data.ThroughputSource == source && data.Count > 0)); } diff --git a/src/ServiceControl.LicenseManagement/LicenseDetails.cs b/src/ServiceControl.LicenseManagement/LicenseDetails.cs index ef4f2accb7..64c68832ed 100644 --- a/src/ServiceControl.LicenseManagement/LicenseDetails.cs +++ b/src/ServiceControl.LicenseManagement/LicenseDetails.cs @@ -1,7 +1,6 @@ namespace ServiceControl.LicenseManagement { using System; - using System.Collections.Generic; using Particular.Licensing; public class LicenseDetails diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs index 005c9f1c09..8b2183cd66 100644 --- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs @@ -122,14 +122,14 @@ public async Task SaveEndpoint(Endpoint endpoint, CancellationToken cancellation public async Task>> GetEndpointThroughputByQueueName(IList queueNames, CancellationToken cancellationToken) { - var results = queueNames.ToDictionary(queueName => queueName, queueNames => new List() as IEnumerable); + var results = queueNames.ToDictionary(queueName => queueName, _ => new List() as IEnumerable); var store = await storeProvider.GetDocumentStore(cancellationToken); using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); var query = session.Query() .Where(document => document.SanitizedName.In(queueNames)) - .Include(builder => builder.IncludeTimeSeries(ThroughputTimeSeriesName)); + .Include(builder => builder.IncludeTimeSeries(ThroughputTimeSeriesName, DateTime.UtcNow.AddMonths(-14))); var documents = await query.ToListAsync(cancellationToken); diff --git a/src/ServiceControl/Licensing/LicensingComponent.cs b/src/ServiceControl/Licensing/LicensingComponent.cs index 77f4044da7..736bf79779 100644 --- a/src/ServiceControl/Licensing/LicensingComponent.cs +++ b/src/ServiceControl/Licensing/LicensingComponent.cs @@ -11,13 +11,16 @@ namespace Particular.ServiceControl; class LicensingComponent : ServiceControlComponent { public override void Configure(Settings settings, ITransportCustomization transportCustomization, - IHostApplicationBuilder hostBuilder) => + IHostApplicationBuilder hostBuilder) + { + var licenseDetails = LicenseManager.FindLicense().Details; hostBuilder.AddLicensingComponent( TransportManifestLibrary.Find(settings.TransportType)?.Name ?? settings.TransportType, settings.ErrorQueue, settings.InstanceName, - LicenseManager.FindLicense().Details.RegisteredTo, + licenseDetails.RegisteredTo, ServiceControlVersion.GetFileVersion()); + } public override void Setup(Settings settings, IComponentInstallationContext context, IHostApplicationBuilder hostBuilder) => From a6786e0ec55666dfc3da298428ee2a9f86bf771a Mon Sep 17 00:00:00 2001 From: John Simons Date: Fri, 7 Mar 2025 13:02:25 +1000 Subject: [PATCH 2/2] Adding test --- .../Infrastructure/DataStoreBuilder.cs | 27 +++++++++++ ...ughputCollector_ThroughputSummary_Tests.cs | 47 +++++++++++++++++-- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/Particular.LicensingComponent.UnitTests/Infrastructure/DataStoreBuilder.cs b/src/Particular.LicensingComponent.UnitTests/Infrastructure/DataStoreBuilder.cs index 5fff1f48e1..a6b54c9e84 100644 --- a/src/Particular.LicensingComponent.UnitTests/Infrastructure/DataStoreBuilder.cs +++ b/src/Particular.LicensingComponent.UnitTests/Infrastructure/DataStoreBuilder.cs @@ -91,6 +91,33 @@ public DataStoreBuilder WithThroughput(ThroughputSource? source = null, DateOnly return this; } + public DataStoreBuilder WithThroughput(ThroughputData throughput) + { + Endpoint endpoint = endpoints.LastOrDefault() ?? + throw new InvalidOperationException( + $"Need to add an endpoint before calling {nameof(WithThroughput)}"); + + var source = endpoint.Id.ThroughputSource; + if (endpoints.SingleOrDefault(e => e.Id.Name == endpoint.Id.Name && e.Id.ThroughputSource == source) == null) + { + throw new InvalidOperationException( + $"Need to add endpoint {endpoint.Id.Name}:{source} before calling {nameof(WithThroughput)}"); + } + + var idForThroughput = new EndpointIdentifier(endpoint.Id.Name, source); + + if (endpointThroughput.TryGetValue(idForThroughput, out List throughputList)) + { + throughputList.Add(throughput); + } + else + { + endpointThroughput.Add(idForThroughput, [throughput]); + } + + return this; + } + public async Task Build() { foreach (Endpoint endpoint in endpoints) diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs index c8351260ee..f30b9a24bf 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs @@ -1,6 +1,8 @@ namespace Particular.LicensingComponent.UnitTests; +using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -81,7 +83,7 @@ await DataStore.CreateBuilder() [TestCase(ThroughputSource.Audit)] [TestCase(ThroughputSource.Broker)] [TestCase(ThroughputSource.Monitoring)] - public async Task Should_return_correct_max_throughput_in_summary_when_data_only_from_one_source(ThroughputSource source) + public async Task Should_return_correct_max_daily_throughput_in_summary_when_data_only_from_one_source(ThroughputSource source) { // Arrange await DataStore.CreateBuilder() @@ -106,7 +108,7 @@ await DataStore.CreateBuilder() } [Test] - public async Task Should_return_correct_max_throughput_in_summary_when_multiple_sources() + public async Task Should_return_correct_max_daily_throughput_in_summary_when_multiple_sources() { // Arrange await DataStore.CreateBuilder() @@ -138,7 +140,44 @@ await DataStore.CreateBuilder() } [Test] - public async Task Should_return_correct_max_throughput_in_summary_when_endpoint_has_no_throughput() + public async Task Should_return_correct_max_monthly_throughput_in_summary_when_multiple_sources() + { + // Arrange + await DataStore.CreateBuilder() + .AddEndpoint("Endpoint1", sources: [ThroughputSource.Broker]) + .WithThroughput(new ThroughputData([ + new EndpointDailyThroughput(new DateOnly(2025, 1, 10), 50), + new EndpointDailyThroughput(new DateOnly(2025, 1, 15), 50), + new EndpointDailyThroughput(new DateOnly(2025, 1, 16), 150), + new EndpointDailyThroughput(new DateOnly(2025, 2, 20), 160), + new EndpointDailyThroughput(new DateOnly(2025, 3, 25), 65), + new EndpointDailyThroughput(new DateOnly(2025, 4, 30), 70), + new EndpointDailyThroughput(new DateOnly(2025, 5, 1), 75)])) + .AddEndpoint("Endpoint2", sources: [ThroughputSource.Broker]) + .WithThroughput(new ThroughputData([ + new EndpointDailyThroughput(new DateOnly(2025, 1, 10), 60), + new EndpointDailyThroughput(new DateOnly(2025, 1, 15), 65), + new EndpointDailyThroughput(new DateOnly(2025, 5, 20), 165), + new EndpointDailyThroughput(new DateOnly(2025, 3, 25), 65), + new EndpointDailyThroughput(new DateOnly(2025, 9, 30), 70)])) + .Build(); + + // Act + var summary = await ThroughputCollector.GetThroughputSummary(CancellationToken.None); + + // Assert + Assert.That(summary, Is.Not.Null); + Assert.That(summary, Has.Count.EqualTo(2)); + + Assert.Multiple(() => + { + Assert.That(summary.First(w => w.Name == "Endpoint1").MaxMonthlyThroughput, Is.EqualTo(250), $"Incorrect MaxDailyThroughput recorded for Endpoint1"); + Assert.That(summary.First(w => w.Name == "Endpoint2").MaxMonthlyThroughput, Is.EqualTo(165), $"Incorrect MaxDailyThroughput recorded for Endpoint2"); + }); + } + + [Test] + public async Task Should_return_correct_max_daily_throughput_in_summary_when_endpoint_has_no_throughput() { // Arrange await DataStore.CreateBuilder().AddEndpoint().Build(); @@ -153,7 +192,7 @@ public async Task Should_return_correct_max_throughput_in_summary_when_endpoint_ } [Test] - public async Task Should_return_correct_max_throughput_in_summary_when_data_from_multiple_sources_and_name_is_different() + public async Task Should_return_correct_max_daily_throughput_in_summary_when_data_from_multiple_sources_and_name_is_different() { // Arrange await DataStore.CreateBuilder()