-
Notifications
You must be signed in to change notification settings - Fork 50
Include endpoint-provided usage information in the usage report #5905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
1246c4a
eff0ba3
45b0148
8404500
58dee44
254918f
664e27b
936eeb8
a40a413
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,5 +4,6 @@ public enum ThroughputSource | |
| { | ||
| Broker, | ||
| Monitoring, | ||
| Audit | ||
| Audit, | ||
| Endpoint | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| namespace NServiceBus; | ||
|
|
||
| using System.Text.Json.Serialization; | ||
| using Particular.LicensingComponent.Contracts; | ||
| using Particular.LicensingComponent.Persistence; | ||
| using ServiceControl.Transports.BrokerThroughput; | ||
|
|
||
| public class EndpointUsageReport : IMessage | ||
| { | ||
| public required string EndpointName { get; set; } | ||
| public DateTimeOffset TimeStamp { get; set; } | ||
| public long MessagesSuccessfullyProcessed { get; set; } | ||
| } | ||
|
|
||
| [Handler] | ||
| class EndpointUsageReportHandler( | ||
| ILicensingDataStore licensingDataStore, | ||
| IBrokerThroughputQuery? brokerThroughputQuery = null | ||
| ) : IHandleMessages<EndpointUsageReport> | ||
| { | ||
| public async Task Handle(EndpointUsageReport message, IMessageHandlerContext context) | ||
| { | ||
| var endpointId = new EndpointIdentifier(message.EndpointName, ThroughputSource.Endpoint); | ||
|
|
||
| var endpoint = await licensingDataStore.GetEndpoint(endpointId, context.CancellationToken); | ||
|
|
||
| if (endpoint is null) | ||
| { | ||
| // TODO: Fill in more of the endpoint details if needed | ||
|
|
||
| endpoint = new Particular.LicensingComponent.Contracts.Endpoint(endpointId) | ||
| { | ||
| EndpointIndicators = [EndpointIndicator.KnownEndpoint.ToString()], | ||
| SanitizedName = brokerThroughputQuery?.SanitizeEndpointName(endpointId.Name) ?? endpointId.Name | ||
| }; | ||
|
|
||
| await licensingDataStore.SaveEndpoint(endpoint, context.CancellationToken); | ||
| } | ||
|
|
||
| await licensingDataStore.RecordEndpointThroughput( | ||
| message.EndpointName, | ||
| ThroughputSource.Endpoint, | ||
| [new EndpointDailyThroughput(DateOnly.FromDateTime(message.TimeStamp.Date), message.MessagesSuccessfullyProcessed)], | ||
| context.CancellationToken); | ||
| } | ||
| } | ||
|
|
||
| [JsonSerializable(typeof(EndpointUsageReport))] | ||
| public partial class UsageReportingSerializationContext : JsonSerializerContext; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,44 +12,53 @@ public static IEnumerable<EndpointDailyThroughput> FromSource(this List<Throughp | |
|
|
||
| public static long Sum(this List<ThroughputData> throughputs) => throughputs.SelectMany(t => t).Sum(kvp => kvp.Value); | ||
|
|
||
| public static long MaxDailyThroughput(this List<ThroughputData> throughputs) | ||
| { | ||
| var items = throughputs.SelectMany(t => t).ToArray(); | ||
|
|
||
| if (items.Any()) | ||
| public static long MaxDailyThroughput(this Dictionary<DateOnly, long> dailyThroughput) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. alias |
||
| => dailyThroughput switch | ||
| { | ||
| return items.Max(kvp => kvp.Value); | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| public static MonthlyThroughput[] MonthlyThroughput(this List<ThroughputData> throughputs) => [.. throughputs | ||
| .SelectMany(data => data) | ||
| // 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.ToString("yyyy-MM", CultureInfo.InvariantCulture), x => x.Sum()) | ||
| .Select(group => new MonthlyThroughput(group.Key, group.Sum()))]; | ||
|
|
||
| public static long AverageMonthlyThroughput(this List<ThroughputData> throughputs) | ||
| { | ||
| if (!throughputs.Any(x => x.Any())) | ||
| { Count: 0 } => 0, | ||
| var x => x.Values.Max() | ||
| }; | ||
|
|
||
| public static Dictionary<DateOnly, long> DailyThroughput(this List<ThroughputData> throughputs) => | ||
| throughputs.SelectMany( | ||
| throughput => throughput.Select( | ||
| daily => ( | ||
| Source: throughput.ThroughputSource, | ||
| Date: daily.Key, | ||
| Throughput: daily.Value | ||
| ) | ||
| ) | ||
| ) | ||
| // Older SQL Reports could return a negative value for daily throughput. These are not valid. See https://github.com/Particular/ServiceControl/pull/5404 | ||
| .Where(entry => entry.Throughput >= 0) | ||
| .GroupBy(entry => entry.Date) | ||
| .ToDictionary( | ||
| entries => entries.Key, | ||
| entries => entries.OrderBy(entry => entry.Source switch | ||
| { | ||
| ThroughputSource.Endpoint => 0, | ||
| ThroughputSource.Audit or ThroughputSource.Monitoring => 1, | ||
| ThroughputSource.Broker => 2, | ||
| _ => int.MaxValue | ||
| }) | ||
| .ThenByDescending(entry => entry.Throughput) | ||
| .Select(entry => entry.Throughput) | ||
| .First() | ||
| ); | ||
|
|
||
| public static MonthlyThroughput[] MonthlyThroughput(this Dictionary<DateOnly, long> dailyThroughput) => [ | ||
| ..dailyThroughput | ||
| .GroupBy(kvp => kvp.Key.ToString("yyyy-MM", CultureInfo.InvariantCulture), kvp => kvp.Value) | ||
| .Select(group => new MonthlyThroughput(group.Key, group.Sum())) | ||
| ]; | ||
|
|
||
|
|
||
| public static long AverageMonthlyThroughput(this Dictionary<DateOnly, long> dailyThroughput) | ||
| => dailyThroughput switch | ||
| { | ||
| return 0; | ||
| } | ||
|
|
||
| // keep this in sync with the internal licensing calculation | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this comment was important, but the calculation is spread out now. I think it still needs to be captured somewhere that we have an internal calculation that this class needs to match behaviour-wise |
||
| 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); | ||
| } | ||
| { Count: 0 } => 0, | ||
| var throughput => (long)Math.Truncate(throughput.Sum(x => x.Value) / (decimal)throughput.Count * 365 / 12) | ||
| }; | ||
|
|
||
| public static bool HasDataFromSource(this IDictionary<string, IEnumerable<ThroughputData>> throughputPerQueue, ThroughputSource source) => | ||
| throughputPerQueue.Any(queueThroughput => queueThroughput.Value.Any(data => data.ThroughputSource == source && data.Count > 0)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.