Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
public class EndpointThroughputSummary
{
public string Name { get; set; }
public string NameHash { get; set; }
public string? Scope { get; set; }
public string? ScopeHash { get; set; }
public bool IsKnownEndpoint { get; set; }
public string UserIndicator { get; set; }
public long MaxDailyThroughput { get; set; }
public MonthlyThroughput[] MonthlyThroughput { get; set; }
public long MaxMonthlyThroughput { get; set; }
public long AverageMonthlyThroughput { get; set; }
}

public class UpdateUserIndicator
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Particular.LicensingComponent.Contracts
{
public class LicensedEndpointDetails
{
public string? LicenseId { get; set; }
public LicensedEndpoint[] Endpoints { get; set; } = [];
public QueueIdentity[] InfrastructureQueues { get; set; } = [];
public QueueIdentity[] ExcludedQueues { get; set; } = [];
public string? ServiceEndDate { get; set; }
public Product[] Products { get; set; } = [];
public bool ValidId { get; set; }
}

public class Product
{
public string? ProductCode { get; set; }
public int? MonthlyThroughput { get; set; }
}

public class QueueIdentity
{
public string? NameHash { get; set; }
public string? ScopeHash { get; set; }
}

public class LicensedEndpoint
{
public string? Name { get; set; }
public int? Classification { get; set; }
public string? EndpointSize { get; set; }
public QueueIdentity[] Queues { get; set; } = [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class InMemoryLicensingDataStore : ILicensingDataStore
BrokerMetadata brokerMetadata = new(null, []);
AuditServiceMetadata auditServiceMetadata = new([], []);
List<string> reportMasks = [];
LicensedEndpointDetails? endpointDetails = null;

public Task<IEnumerable<Endpoint>> GetAllEndpoints(bool includePlatformEndpoints, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -170,6 +171,14 @@ public Task SaveReportMasks(List<string> reportMasks, CancellationToken cancella
return Task.CompletedTask;
}

public Task<LicensedEndpointDetails?> GetLicensedEndpointDetails(CancellationToken cancellationToken) => Task.FromResult(endpointDetails);

public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken)
{
endpointDetails = result;
return Task.CompletedTask;
}

class EndpointCollection : KeyedCollection<EndpointIdentifier, Endpoint>
{
protected override EndpointIdentifier GetKeyForItem(Endpoint item) => item.Id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSo
Task SaveAuditServiceMetadata(AuditServiceMetadata auditServiceMetadata, CancellationToken cancellationToken);
Task<List<string>> GetReportMasks(CancellationToken cancellationToken);
Task SaveReportMasks(List<string> reportMasks, CancellationToken cancellationToken);

Task<LicensedEndpointDetails?> GetLicensedEndpointDetails(CancellationToken cancellationToken);
Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ await DataStore.CreateBuilder()

using (Assert.EnterMultipleScope())
{
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");
Assert.That(summary.First(w => w.Name == "Endpoint1").AverageMonthlyThroughput, Is.EqualTo(2694), $"Incorrect AverageMonthlyThroughput recorded for Endpoint1");
Assert.That(summary.First(w => w.Name == "Endpoint2").AverageMonthlyThroughput, Is.EqualTo(2585), $"Incorrect AverageMonthlyThroughput recorded for Endpoint2");
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/Particular.LicensingComponent/ThroughputCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ public async Task<List<EndpointThroughputSummary>> GetThroughputSummary(Cancella
var endpointSummary = new EndpointThroughputSummary
{
Name = endpointData.Name,
NameHash = OneWayHasher.CalculateOneWayHash(endpointData.Name),
Scope = endpointData.Scope,
ScopeHash = string.IsNullOrEmpty(endpointData.Scope) ? null : OneWayHasher.CalculateOneWayHash(endpointData.Scope),
UserIndicator = endpointData.UserIndicator ?? (endpointData.IsKnownEndpoint ? Contracts.UserIndicator.NServiceBusEndpoint.ToString() : string.Empty),
IsKnownEndpoint = endpointData.IsKnownEndpoint,
MaxDailyThroughput = endpointData.ThroughputData.MaxDailyThroughput(),
MonthlyThroughput = endpointData.ThroughputData.MonthlyThroughput(),
MaxMonthlyThroughput = endpointData.ThroughputData.MaxMonthlyThroughput()
AverageMonthlyThroughput = endpointData.ThroughputData.AverageMonthlyThroughput()
};

endpointSummaries.Add(endpointSummary);
Expand Down
32 changes: 19 additions & 13 deletions src/Particular.LicensingComponent/ThroughputDataExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,29 @@ public static long MaxDailyThroughput(this List<ThroughputData> throughputs)

public static MonthlyThroughput[] MonthlyThroughput(this List<ThroughputData> throughputs) => [.. throughputs
.SelectMany(data => data)
.GroupBy(kvp => $"{kvp.Key:yyyy-MM}")
.Select(group => new MonthlyThroughput(group.Key, group.Sum(kvp => kvp.Value)))];

public static long MaxMonthlyThroughput(this List<ThroughputData> throughputs)
// Older SQL Reports could return a negative value for daily throughput. These are not valid. See https://github.com/Particular/ServiceControl/pull/5404
.Where(x => x.Value >= 0)
.GroupBy(x => x.Key, x => x.Value)
.ToLookup(x => x.Key, x => x.Max())
.GroupBy(kvp => $"{kvp.Key:yyyy-MM}", x => x.Sum())
.Select(group => new MonthlyThroughput(group.Key, group.Sum()))];

public static long AverageMonthlyThroughput(this List<ThroughputData> 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())
if (!throughputs.Any(x => x.Any()))
{
return monthlySums.Max();
return 0;
}

return 0;
// keep this in sync with the internal licensing calculation
var maxDailyThroughput = throughputs
.SelectMany(x => x)
.Where(x => x.Value >= 0)
.GroupBy(x => x.Key, x => x.Value)
.ToLookup(x => x.Key, x => x.Max())
.ToDictionary(x => x.Key, x => x.Sum());

return (long)Math.Truncate(maxDailyThroughput.Sum(x => x.Value) / (decimal)maxDailyThroughput.Count * 365 / 12);
}

public static bool HasDataFromSource(this IDictionary<string, IEnumerable<ThroughputData>> throughputPerQueue, ThroughputSource source) =>
Expand Down
2 changes: 2 additions & 0 deletions src/ServiceControl.LicenseManagement/LicenseDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

public class LicenseDetails
{
public string Id { get; private init; }
public DateTime? ExpirationDate { get; private init; }

public DateTime? UpgradeProtectionExpiration { get; private init; }
Expand Down Expand Up @@ -67,6 +68,7 @@ internal static LicenseDetails FromLicense(License license)

var details = new LicenseDetails
{
Id = license.LicenseId,
UpgradeProtectionExpiration = license.UpgradeProtectionExpiration,
//If expiration date is greater that 50 years treat is as no expiration date
ExpirationDate = license.ExpirationDate.HasValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ class LicensingDataStore : ILicensingDataStore
public Task<Particular.LicensingComponent.Contracts.Endpoint?> GetEndpoint(EndpointIdentifier id, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<IEnumerable<(EndpointIdentifier Id, Particular.LicensingComponent.Contracts.Endpoint? Endpoint)>> GetEndpoints(IList<EndpointIdentifier> endpointIds, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task<IDictionary<string, IEnumerable<ThroughputData>>> GetEndpointThroughputByQueueName(IList<string> queueNames, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task<LicensedEndpointDetails?> GetLicensedEndpointDetails(CancellationToken cancellationToken) => throw new NotImplementedException();
public Task<List<string>> GetReportMasks(CancellationToken cancellationToken) => throw new NotImplementedException();
public Task<bool> IsThereThroughputForLastXDays(int days, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task<bool> IsThereThroughputForLastXDaysForSource(int days, ThroughputSource throughputSource, bool includeToday, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSource, IList<EndpointDailyThroughput> throughput, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task SaveAuditServiceMetadata(AuditServiceMetadata auditServiceMetadata, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task SaveBrokerMetadata(BrokerMetadata brokerMetadata, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task SaveEndpoint(Particular.LicensingComponent.Contracts.Endpoint endpoint, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task SaveReportMasks(List<string> reportMasks, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task UpdateUserIndicatorOnEndpoints(List<UpdateUserIndicator> userIndicatorUpdates, CancellationToken cancellationToken) => throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class LicensingDataStore(
const string AuditServiceMetadataDocumentId = "AuditServiceMetadata";
const string BrokerMetadataDocumentId = "BrokerMetadata";
const string ReportMasksDocumentId = "ReportMasks";
const string LicencedEndpointDetailsDocumentId = "LicensedEndpointDetails";

static readonly AuditServiceMetadata DefaultAuditServiceMetadata = new([], []);
static readonly BrokerMetadata DefaultBrokerMetadata = new(null, []);
Expand Down Expand Up @@ -316,4 +317,21 @@ public async Task SaveReportMasks(List<string> reportMasks, CancellationToken ca
await session.StoreAsync(new ReportConfigurationDocument { MaskedStrings = reportMasks }, ReportMasksDocumentId, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
}

public async Task<LicensedEndpointDetails?> GetLicensedEndpointDetails(CancellationToken cancellationToken)
{
var store = await storeProvider.GetDocumentStore(cancellationToken);
using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name);

return await session.LoadAsync<LicensedEndpointDetails>(LicencedEndpointDetailsDocumentId, cancellationToken);
}

public async Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken)
{
var store = await storeProvider.GetDocumentStore(cancellationToken);
using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name);

await session.StoreAsync(result, LicencedEndpointDetailsDocumentId, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ GET /eventlogitems => ServiceControl.EventLog.EventLogApiController:Items(Paging
GET /heartbeats/stats => ServiceControl.Monitoring.EndpointsMonitoringController:HeartbeatStats()
GET /instance-info => ServiceControl.Infrastructure.WebApi.RootController:Config()
GET /license => ServiceControl.Licensing.LicenseController:License(Boolean refresh, String clientName, CancellationToken cancellationToken)
GET /license/details => ServiceControl.Licensing.LicenseController:LicenseDetails(CancellationToken cancellationToken)
POST /license/detailsUpload => ServiceControl.Licensing.LicenseController:UploadLicenseDetails(IFormFile file, CancellationToken cancellationToken)
GET /messages => ServiceControl.CompositeViews.Messages.GetMessagesController:Messages(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages)
GET /messages/{id}/body => ServiceControl.CompositeViews.Messages.GetMessagesController:Get(String id, String instanceId)
GET /messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String q)
Expand Down
56 changes: 34 additions & 22 deletions src/ServiceControl/Licensing/LicenseController.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
namespace ServiceControl.Licensing
#nullable enable
namespace ServiceControl.Licensing
{
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Infrastructure.Auth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Monitoring.HeartbeatMonitoring;
using Particular.LicensingComponent.Contracts;
using Particular.LicensingComponent.Persistence;
using Particular.ServiceControl.Licensing;
using ServiceBus.Management.Infrastructure.Settings;
using ServiceControl.LicenseManagement;

[ApiController]
[Route("api")]
public class LicenseController(ActiveLicense activeLicense, Settings settings, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : ControllerBase
public class LicenseController(ActiveLicense activeLicense, Settings settings, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus, ILicensingDataStore dataStore) : ControllerBase
{
[Authorize(Policy = Permissions.ErrorLicensingView)]
[HttpGet]
Expand Down Expand Up @@ -44,29 +52,33 @@ public async Task<ActionResult<LicenseInfo>> License(bool refresh, string client
return licenseInfo;
}

public class LicenseInfo
[Authorize(Policy = Permissions.ErrorLicensingView)]
[HttpGet]
[Route("license/details")]
public async Task<ActionResult<LicensedEndpointDetails?>> LicenseDetails(CancellationToken cancellationToken)
{
public bool TrialLicense { get; set; }

public string Edition { get; set; }

public string RegisteredTo { get; set; }

public string UpgradeProtectionExpiration { get; set; }

public string ExpirationDate { get; set; }

public string Status { get; set; }

public LicensedProduct[] Products { get; set; }

public string LicenseType { get; set; }
var licenseDetails = await dataStore.GetLicensedEndpointDetails(cancellationToken);
if (licenseDetails is null)
{
return (LicensedEndpointDetails?)null;
}

public string InstanceName { get; set; }
licenseDetails.ValidId = licenseDetails.LicenseId == activeLicense.Details.Id;
return licenseDetails;
}

public string LicenseStatus { get; set; }
[Authorize(Policy = Permissions.ErrorLicensingManage)]
[HttpPost]
[Route("license/detailsUpload")]
public async Task UploadLicenseDetails([FromForm] IFormFile file, CancellationToken cancellationToken)
{
//perform date and license id checks
using var brotliStream = new BrotliStream(file.OpenReadStream(), CompressionMode.Decompress);

public string LicenseExtensionUrl { get; set; }
var result = JsonSerializer.Deserialize<LicensedEndpointDetails>(brotliStream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidDataException("File contents cannot be deserialized");
//persist
await dataStore.SaveLicensedEndpointDetails(result, cancellationToken);
}
}
}
29 changes: 29 additions & 0 deletions src/ServiceControl/Licensing/LicenseControllerTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace ServiceControl.Licensing
{
using ServiceControl.LicenseManagement;

public class LicenseInfo
{
public bool TrialLicense { get; set; }

public string Edition { get; set; }

public string RegisteredTo { get; set; }

public string UpgradeProtectionExpiration { get; set; }

public string ExpirationDate { get; set; }

public string Status { get; set; }

public LicensedProduct[] Products { get; set; }

public string LicenseType { get; set; }

public string InstanceName { get; set; }

public string LicenseStatus { get; set; }

public string LicenseExtensionUrl { get; set; }
}
}
Loading