Skip to content
Open
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 @@ -179,6 +179,15 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella
return Task.CompletedTask;
}

public Task RemoveEndpoints(EndpointIdentifier[] endpointIds, CancellationToken cancellationToken)
{
foreach (var id in endpointIds)
{
endpoints.Remove(id);
}
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 @@ -15,6 +15,8 @@

Task SaveEndpoint(Endpoint endpoint, CancellationToken cancellationToken = default);

Task RemoveEndpoints(EndpointIdentifier[] endpointIds, CancellationToken cancellationToken);

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PrimaryRavenPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PrimaryRavenPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PrimaryRavenAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PrimaryRavenAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PostgreSqlPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PostgreSqlPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PrimaryPostgreSqlAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PrimaryPostgreSqlAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-DefaultCore

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-DefaultCore

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-Compile

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-Compile

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PrimaryRavenPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PrimaryRavenPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PrimarySqlServerAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-PrimarySqlServerAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-SqlServerPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Linux-SqlServerPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PrimaryRavenAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PrimaryRavenAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PrimaryPostgreSqlAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PrimaryPostgreSqlAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PostgreSqlPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PostgreSqlPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-DefaultCore

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-DefaultCore

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-Compile

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-Compile

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / windows-installers / build

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / windows-installers / build

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-SqlServerPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-SqlServerPersistence

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PrimarySqlServerAcceptance

Make the CancellationToken parameter optional

Check failure on line 18 in src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs

View workflow job for this annotation

GitHub Actions / Windows-PrimarySqlServerAcceptance

Make the CancellationToken parameter optional

Task<IDictionary<string, IEnumerable<ThroughputData>>> GetEndpointThroughputByQueueName(IList<string> queueNames, CancellationToken cancellationToken = default);

Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSource, DateOnly date, long messageCount, CancellationToken cancellationToken = default) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,48 @@ await Task.Run(async () =>
}
}

[Test]
public async Task Should_only_create_new_endpoint_when_audit_counts_exist()
{
// Arrange
using var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3));
var token = tokenSource.Token;
var fakeTimeProvider = new FakeTimeProvider();

var date = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1));
var auditQuery = new AuditQuery_WithTwoEndpointsAndSelectiveCounts(
endpointWithoutCounts: "EndpointNoData",
endpointWithCounts: "EndpointWithData",
throughputDate: date,
throughputCount: 5);

using var auditThroughputCollectorHostedService = new AuditThroughputCollectorHostedService(
NullLogger<AuditThroughputCollectorHostedService>.Instance, configuration.ThroughputSettings, DataStore,
auditQuery, fakeTimeProvider)
{ DelayStart = TimeSpan.Zero };

// Act
await auditThroughputCollectorHostedService.StartAsync(token);
await Task.Run(async () =>
{
do
{
await Task.Delay(TimeSpan.FromMilliseconds(50));
} while (!token.IsCancellationRequested);
});
await auditThroughputCollectorHostedService.StopAsync(token);

var endpointWithoutCounts = await DataStore.GetEndpoint("EndpointNoData", ThroughputSource.Audit, default);
var endpointWithCounts = await DataStore.GetEndpoint("EndpointWithData", ThroughputSource.Audit, default);

// Assert
using (Assert.EnterMultipleScope())
{
Assert.That(endpointWithoutCounts, Is.Null, "Endpoint with empty auditCounts should not be created");
Assert.That(endpointWithCounts, Is.Not.Null, "Endpoint with auditCounts should be created");
}
}

class AuditQuery_NoAuditRemotes : IAuditQuery
{
public SemanticVersion MinAuditCountsVersion => new(4, 29, 0);
Expand Down Expand Up @@ -335,4 +377,50 @@ public string SanitizeEndpointName(string endpointName)

public string SanitizedEndpointNameCleanser(string endpointName) => endpointName;
}

class AuditQuery_WithTwoEndpointsAndSelectiveCounts : IAuditQuery
{
public AuditQuery_WithTwoEndpointsAndSelectiveCounts(
string endpointWithoutCounts,
string endpointWithCounts,
DateOnly throughputDate,
long throughputCount)
{
this.endpointWithoutCounts = endpointWithoutCounts;
this.endpointWithCounts = endpointWithCounts;
this.throughputDate = throughputDate;
this.throughputCount = throughputCount;
}

public SemanticVersion MinAuditCountsVersion => new(4, 29, 0);
public Func<RemoteInstanceInformation, bool> ValidRemoteInstances => _ => true;

public Task<IEnumerable<ServiceControlEndpoint>> GetKnownEndpoints(CancellationToken cancellationToken) =>
Task.FromResult<IEnumerable<ServiceControlEndpoint>>(
[
new ServiceControlEndpoint { Name = endpointWithoutCounts, HeartbeatsEnabled = true },
new ServiceControlEndpoint { Name = endpointWithCounts, HeartbeatsEnabled = true }
]);

public Task<IEnumerable<AuditCount>> GetAuditCountForEndpoint(string endpointUrlName, CancellationToken cancellationToken)
{
if (endpointUrlName == endpointWithCounts)
{
return Task.FromResult<IEnumerable<AuditCount>>([new AuditCount { UtcDate = throughputDate, Count = throughputCount }]);
}

return Task.FromResult<IEnumerable<AuditCount>>([]);
}

public Task<List<RemoteInstanceInformation>> GetAuditRemotes(CancellationToken cancellationToken) =>
Task.FromResult<List<RemoteInstanceInformation>>([]);

public Task<ConnectionSettingsTestResult> TestAuditConnection(CancellationToken cancellationToken) =>
Task.FromResult(new ConnectionSettingsTestResult { ConnectionSuccessful = true, ConnectionErrorMessages = [] });

readonly string endpointWithoutCounts;
readonly string endpointWithCounts;
readonly DateOnly throughputDate;
readonly long throughputCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ await DataStore.CreateBuilder()
public async Task Should_return_correct_throughput_in_report_when_endpoint_has_no_throughput()
{
// Arrange
await DataStore.CreateBuilder().AddEndpoint().Build();
await DataStore.CreateBuilder().AddEndpoint().WithThroughput(ThroughputSource.Broker, data: [0]).Build();

// Act
var report = await ThroughputCollector.GenerateThroughputReport("", null);
Expand All @@ -212,6 +212,20 @@ public async Task Should_return_correct_throughput_in_report_when_endpoint_has_n
}
}

[Test]
public async Task Should_not_return_endpoint_in_report_when_endpoint_has_no_throughput()
{
// Arrange
await DataStore.CreateBuilder().AddEndpoint().Build();

// Act
var report = await ThroughputCollector.GenerateThroughputReport("", null, default);

// Assert
Assert.That(report, Is.Not.Null);
Assert.That(report.ReportData.Queues.Count, Is.Zero, "Invalid number of endpoints in throughput report");
}

[Test]
public async Task Should_return_correct_throughput_in_report_when_data_from_multiple_sources_and_name_is_different()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,20 +177,35 @@ await DataStore.CreateBuilder()
}

[Test]
public async Task Should_return_correct_max_daily_throughput_in_summary_when_endpoint_has_no_throughput()
public async Task Should_return_correct_max_daily_throughput_in_summary_when_endpoint_has_zero_throughput()
{
// Arrange
await DataStore.CreateBuilder().AddEndpoint().Build();
await DataStore.CreateBuilder().AddEndpoint().WithThroughput(new ThroughputData([
new EndpointDailyThroughput(new DateOnly(2025, 1, 10), 0)])).Build();

// Act
var summary = await ThroughputCollector.GetThroughputSummary();
var summary = await ThroughputCollector.GetThroughputSummary(default);

// Assert
Assert.That(summary, Is.Not.Null);
Assert.That(summary, Has.Count.EqualTo(1), "Invalid number of endpoints in throughput summary");
Assert.That(summary[0].MaxDailyThroughput, Is.EqualTo(0), $"Incorrect MaxDailyThroughput recorded for {summary[0].Name}");
}

[Test]
public async Task Should_not_return_endpoint_in_summary_when_endpoint_has_no_throughput()
{
// Arrange
await DataStore.CreateBuilder().AddEndpoint().Build();

// Act
var summary = await ThroughputCollector.GetThroughputSummary();

// Assert
Assert.That(summary, Is.Not.Null);
Assert.That(summary, Is.Empty, "Invalid number of endpoints in throughput summary");
}

[Test]
public async Task Should_return_correct_max_daily_throughput_in_summary_when_data_from_multiple_sources_and_name_is_different()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,15 @@ async Task GatherThroughput(CancellationToken cancellationToken)

var auditCounts = (await auditQuery.GetAuditCountForEndpoint(knownEndpointsLookup[endpointId].UrlName, cancellationToken)).ToList();

if (endpoint == null)
if (endpoint == null && auditCounts.Count > 0)
{
endpoint = ConvertToEndpoint(knownEndpointsLookup[endpointId]);
await dataStore.SaveEndpoint(endpoint, cancellationToken);
}
else if (endpoint is null)
{
continue;
}

var missingAuditThroughput = auditCounts
.Where(auditCount => auditCount.UtcDate > endpoint.LastCollectedDate &&
Expand Down
9 changes: 8 additions & 1 deletion src/Particular.LicensingComponent/ThroughputCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,14 @@ async IAsyncEnumerable<EndpointData> GetDistinctEndpointData([EnumeratorCancella

var userIndicator = UserIndicator(endpointGroupPerQueue) ?? null;

yield return new EndpointData(endpointName, throughputData, userIndicator, EndpointScope(endpointGroupPerQueue), EndpointIndicators(endpointGroupPerQueue), IsKnownEndpoint(endpointGroupPerQueue));
if (throughputData.Any(x => x.Any()))
{
yield return new EndpointData(endpointName, throughputData, userIndicator, EndpointScope(endpointGroupPerQueue), EndpointIndicators(endpointGroupPerQueue), IsKnownEndpoint(endpointGroupPerQueue));
}
else
{
await dataStore.RemoveEndpoints([.. endpointGroupPerQueue.Select(endpoint => endpoint.Id)], cancellationToken);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ public static long AverageMonthlyThroughput(this List<ThroughputData> throughput
}

public static bool HasDataFromSource(this IDictionary<string, IEnumerable<ThroughputData>> throughputPerQueue, ThroughputSource source) =>
throughputPerQueue.Any(queueName => queueName.Value.Any(data => data.ThroughputSource == source && data.Count > 0));
throughputPerQueue.Any(queueThroughput => queueThroughput.Value.Any(data => data.ThroughputSource == source && data.Count > 0));
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenTelemetry.Trace;
using ServiceControl.Audit.Auditing;
using ServiceControl.Audit.Auditing.BodyStorage;
using ServiceControl.Audit.Auditing.MessagesView;
using ServiceControl.Audit.Infrastructure;
using ServiceControl.SagaAudit;
using static System.Runtime.InteropServices.JavaScript.JSType;

class InMemoryAuditDataStore : IAuditDataStore
{
Expand Down Expand Up @@ -173,6 +175,7 @@ Task<MessageBodyView> GetMessageBodyFromMetadata(string messageId, CancellationT

public Task<QueryResult<IList<AuditCount>>> QueryAuditCounts(string endpointName, CancellationToken cancellationToken = default)
{
var hasSent = messageViews.Any(m => m.SendingEndpoint?.Name == endpointName);
var results = messageViews
.Where(m => m.ReceivingEndpoint.Name == endpointName && !m.IsSystemMessage)
.GroupBy(m => m.ProcessedAt.ToUniversalTime().Date)
Expand All @@ -184,7 +187,13 @@ public Task<QueryResult<IList<AuditCount>>> QueryAuditCounts(string endpointName
.OrderBy(r => r.UtcDate)
.ToList();

return Task.FromResult(new QueryResult<IList<AuditCount>>(results, QueryStatsInfo.Zero));
return results.Count == 0 && hasSent
? Task.FromResult(new QueryResult<IList<AuditCount>>([new AuditCount
{
UtcDate = messageViews.First().ProcessedAt.ToUniversalTime().Date,
Count = 0
}], QueryStatsInfo.Zero))
: Task.FromResult(new QueryResult<IList<AuditCount>>(results, QueryStatsInfo.Zero));
}

public Task SaveProcessedMessage(ProcessedMessage processedMessage, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ from message in messages
TimeSent = (DateTime)message.MessageMetadata["TimeSent"],
ProcessedAt = message.ProcessedAt,
ReceivingEndpointName = ((EndpointDetails)message.MessageMetadata["ReceivingEndpoint"]).Name,
SendingEndpointName = ((EndpointDetails)message.MessageMetadata["SendingEndpoint"]).Name,
CriticalTime = (TimeSpan?)message.MessageMetadata["CriticalTime"],
ProcessingTime = (TimeSpan?)message.MessageMetadata["ProcessingTime"],
DeliveryTime = (TimeSpan?)message.MessageMetadata["DeliveryTime"],
Query = message.MessageMetadata.Select(_ => _.Value.ToString()).Union(new[]
{
string.Join(" ", message.Headers.Select(x => x.Value))
}).ToArray(),
string.Join(" ", message.Headers.Select(x => x.Value))
}).ToArray(),
ConversationId = (string)message.MessageMetadata["ConversationId"]
};

Expand All @@ -48,6 +49,7 @@ public class SortAndFilterOptions
public MessageStatus Status { get; set; }
public DateTime ProcessedAt { get; set; }
public string ReceivingEndpointName { get; set; }
public string SendingEndpointName { get; set; }
public TimeSpan? CriticalTime { get; set; }
public TimeSpan? ProcessingTime { get; set; }
public TimeSpan? DeliveryTime { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ from message in messages
TimeSent = (DateTime)message.MessageMetadata["TimeSent"],
ProcessedAt = message.ProcessedAt,
ReceivingEndpointName = ((EndpointDetails)message.MessageMetadata["ReceivingEndpoint"]).Name,
SendingEndpointName = ((EndpointDetails)message.MessageMetadata["SendingEndpoint"]).Name,
CriticalTime = (TimeSpan?)message.MessageMetadata["CriticalTime"],
ProcessingTime = (TimeSpan?)message.MessageMetadata["ProcessingTime"],
DeliveryTime = (TimeSpan?)message.MessageMetadata["DeliveryTime"],
Query = message.MessageMetadata.Select(_ => _.Value.ToString()).Union(new[]
{
string.Join(" ", message.Headers.Select(x => x.Value)),
LoadAttachment(message, "body").GetContentAsString()
}).ToArray(),
string.Join(" ", message.Headers.Select(x => x.Value)),
LoadAttachment(message, "body").GetContentAsString()
}).ToArray(),
ConversationId = (string)message.MessageMetadata["ConversationId"]
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ public async Task<QueryResult<IList<AuditCount>>> QueryAuditCounts(string endpoi
.OrderBy(m => m.ProcessedAt)
.FirstOrDefaultAsync(token: cancellationToken);

var hasSent = await session.Query<MessagesViewIndex.SortAndFilterOptions>(indexName)
.AnyAsync(m => m.SendingEndpointName == endpointName, token: cancellationToken);

if (oldestMsg != null)
{
var endDate = DateTime.UtcNow.Date.AddDays(1);
Expand Down Expand Up @@ -166,6 +169,14 @@ public async Task<QueryResult<IList<AuditCount>>> QueryAuditCounts(string endpoi
}
}
}
else if (hasSent)
{
results.Add(new AuditCount
{
UtcDate = DateTime.UtcNow.Date,
Count = 0
});
}

return new QueryResult<IList<AuditCount>>(results, QueryStatsInfo.Zero);
}
Expand Down
37 changes: 35 additions & 2 deletions src/ServiceControl.Audit.Persistence.Tests/AuditCountingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,36 @@ string ScrubDates(string input)
}, ScrubDates);
}

ProcessedMessage MakeMessage(string processingEndpoint, DateTime processedAt, bool systemMessage)
[Test]
public async Task Should_return_zero_throughput_entry_when_SendOnly()
{
// Arrange
var today = DateTime.UtcNow.Date;
const string sendOnlyEndpoint = "SendOnlyEndpoint";

var messages = new[]
{
// Endpoint sent a message, but did not receive any
MakeMessage("SomeOtherEndpoint", sendOnlyEndpoint, today, false)
};

await IngestProcessedMessagesAudits(messages);

// Act
var result = (await DataStore.QueryAuditCounts(sendOnlyEndpoint, TestContext.CurrentContext.CancellationToken)).Results;

// Assert
Assert.That(result, Is.Not.Empty, "Expected non-empty result for endpoint that only sent messages");
Assert.That(result, Has.Count.EqualTo(1), "Expected single audit count for send-only endpoint");
using (Assert.EnterMultipleScope())
{
Assert.That(result[0].UtcDate, Is.EqualTo(today), "Expected today's date placeholder");
Assert.That(result[0].Count, Is.Zero, "Expected zero throughput count for send-only endpoint");
}
}

static ProcessedMessage MakeMessage(string processingEndpoint, DateTime processedAt, bool systemMessage) => MakeMessage(processingEndpoint, null, processedAt, systemMessage);
static ProcessedMessage MakeMessage(string processingEndpoint, string sendingEndpoint, DateTime processedAt, bool systemMessage)
{
var messageId = Guid.NewGuid().ToString();
var messageType = "MyMessageType";
Expand All @@ -85,8 +114,12 @@ ProcessedMessage MakeMessage(string processingEndpoint, DateTime processedAt, bo
{ "MessageType", messageType },
{ "IsRetried", false },
{ "ConversationId", messageId },
{ "ReceivingEndpoint", new EndpointDetails { Name = processingEndpoint } }
{ "ReceivingEndpoint", new EndpointDetails { Name = processingEndpoint } },
};
if (!string.IsNullOrEmpty(sendingEndpoint))
{
metadata.Add("SendingEndpoint", new EndpointDetails { Name = sendingEndpoint });
}

var headers = new Dictionary<string, string>
{
Expand Down
Loading
Loading