Skip to content

Commit d7b35c1

Browse files
Introduce processing time metric. (#7097)
* Introduce processing time metric. * Move the record of the metrics for a message successfully processed after the outbox transaction commit. * Remove unnecessary comment. * Move the critical time --------- Co-authored-by: SzymonPobiega <[email protected]>
1 parent 1a053ae commit d7b35c1

File tree

8 files changed

+114
-19
lines changed

8 files changed

+114
-19
lines changed

src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Metrics/When_message_is_processed_successfully.cs

+37-4
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,50 @@ public async Task Should_report_successful_message_metric()
3030
metricsListener.AssertMetric("nservicebus.messaging.successes", 5);
3131
metricsListener.AssertMetric("nservicebus.messaging.fetches", 5);
3232
metricsListener.AssertMetric("nservicebus.messaging.failures", 0);
33+
metricsListener.AssertMetric("nservicebus.messaging.critical_time", 5);
34+
metricsListener.AssertMetric("nservicebus.messaging.processing_time", 5);
35+
metricsListener.AssertMetric("nservicebus.messaging.handler_time", 5);
3336

3437
metricsListener.AssertTags("nservicebus.messaging.fetches",
3538
new Dictionary<string, object>
3639
{
3740
["nservicebus.queue"] = Conventions.EndpointNamingConvention(typeof(EndpointWithMetrics)),
3841
["nservicebus.discriminator"] = "disc",
42+
["nservicebus.message_type"] = typeof(OutgoingMessage).FullName
3943
});
4044

4145
metricsListener.AssertTags("nservicebus.messaging.successes",
46+
new Dictionary<string, object>
47+
{
48+
["nservicebus.queue"] = Conventions.EndpointNamingConvention(typeof(EndpointWithMetrics)),
49+
["nservicebus.discriminator"] = "disc",
50+
["nservicebus.message_type"] = typeof(OutgoingMessage).FullName
51+
});
52+
53+
metricsListener.AssertTags("nservicebus.messaging.critical_time",
54+
new Dictionary<string, object>
55+
{
56+
["nservicebus.queue"] = Conventions.EndpointNamingConvention(typeof(EndpointWithMetrics)),
57+
["nservicebus.discriminator"] = "disc",
58+
["nservicebus.message_type"] = typeof(OutgoingMessage).FullName
59+
});
60+
61+
metricsListener.AssertTags("nservicebus.messaging.processing_time",
62+
new Dictionary<string, object>
63+
{
64+
["nservicebus.queue"] = Conventions.EndpointNamingConvention(typeof(EndpointWithMetrics)),
65+
["nservicebus.discriminator"] = "disc",
66+
["nservicebus.message_type"] = typeof(OutgoingMessage).FullName
67+
});
68+
69+
metricsListener.AssertTags("nservicebus.messaging.handler_time",
4270
new Dictionary<string, object>
4371
{
4472
["nservicebus.queue"] = Conventions.EndpointNamingConvention(typeof(EndpointWithMetrics)),
4573
["nservicebus.discriminator"] = "disc",
4674
["nservicebus.message_type"] = typeof(OutgoingMessage).FullName,
75+
["nservicebus.message_handler_type"] = typeof(EndpointWithMetrics.MessageHandler).FullName,
76+
["execution.result"] = "success"
4777
});
4878
}
4979

@@ -68,9 +98,12 @@ public async Task Should_only_tag_most_concrete_type_on_metric()
6898
metricsListener.AssertMetric("nservicebus.messaging.fetches", 5);
6999
metricsListener.AssertMetric("nservicebus.messaging.failures", 0);
70100

71-
var successEndpoint = metricsListener.AssertTagKeyExists("nservicebus.messaging.successes", "nservicebus.queue");
72-
var successType = metricsListener.AssertTagKeyExists("nservicebus.messaging.successes", "nservicebus.message_type");
73-
var successHandlerType = metricsListener.AssertTagKeyExists("nservicebus.messaging.successes", "nservicebus.message_handler_types");
101+
var successEndpoint =
102+
metricsListener.AssertTagKeyExists("nservicebus.messaging.successes", "nservicebus.queue");
103+
var successType =
104+
metricsListener.AssertTagKeyExists("nservicebus.messaging.successes", "nservicebus.message_type");
105+
var successHandlerType =
106+
metricsListener.AssertTagKeyExists("nservicebus.messaging.successes", "nservicebus.message_handler_types");
74107

75108
var fetchedEndpoint = metricsListener.AssertTagKeyExists("nservicebus.messaging.fetches", "nservicebus.queue");
76109

@@ -90,7 +123,7 @@ class EndpointWithMetrics : EndpointConfigurationBuilder
90123
{
91124
public EndpointWithMetrics() => EndpointSetup<OpenTelemetryEnabledEndpoint>();
92125

93-
class MessageHandler : IHandleMessages<OutgoingMessage>
126+
public class MessageHandler : IHandleMessages<OutgoingMessage>
94127
{
95128
readonly Context testContext;
96129

src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Metrics/When_message_processing_fails.cs

+22-1
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,34 @@ public async Task Should_report_failing_message_metrics()
2323
metricsListener.AssertMetric("nservicebus.messaging.fetches", 1);
2424
metricsListener.AssertMetric("nservicebus.messaging.failures", 1);
2525
metricsListener.AssertMetric("nservicebus.messaging.successes", 0);
26+
metricsListener.AssertMetric("nservicebus.messaging.critical_time", 0);
27+
metricsListener.AssertMetric("nservicebus.messaging.processing_time", 0);
28+
metricsListener.AssertMetric("nservicebus.messaging.handler_time", 1);
29+
30+
metricsListener.AssertTags("nservicebus.messaging.fetches",
31+
new Dictionary<string, object>
32+
{
33+
["nservicebus.queue"] = Conventions.EndpointNamingConvention(typeof(FailingEndpoint)),
34+
["nservicebus.discriminator"] = "disc",
35+
["nservicebus.message_type"] = typeof(FailingMessage).FullName
36+
});
2637

2738
metricsListener.AssertTags("nservicebus.messaging.failures",
2839
new Dictionary<string, object>
2940
{
3041
["nservicebus.queue"] = Conventions.EndpointNamingConvention(typeof(FailingEndpoint)),
3142
["nservicebus.discriminator"] = "disc",
43+
["nservicebus.message_type"] = typeof(FailingMessage).FullName,
44+
["error.type"] = typeof(SimulatedException).FullName,
45+
});
46+
47+
metricsListener.AssertTags("nservicebus.messaging.handler_time",
48+
new Dictionary<string, object>
49+
{
50+
["nservicebus.queue"] = Conventions.EndpointNamingConvention(typeof(FailingEndpoint)),
51+
["nservicebus.discriminator"] = "disc",
52+
["nservicebus.message_type"] = typeof(FailingMessage).FullName,
53+
["execution.result"] = "failure",
3254
["error.type"] = typeof(SimulatedException).FullName,
3355
});
3456
}
@@ -55,7 +77,6 @@ public Task Handle(FailingMessage message, IMessageHandlerContext context)
5577
}
5678

5779
const string ErrorMessage = "oh no!";
58-
5980
}
6081
}
6182

src/NServiceBus.Core.Tests/ApprovalFiles/MeterTests.Verify_MeterAPI.approved.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"Note": "Changes to metrics API should result in an update to NServiceBusMeter version.",
3-
"ActivitySourceVersion": "0.2.0",
3+
"MetricsSourceName": "NServiceBus.Core.Pipeline.Incoming",
4+
"MetricsSourceVersion": "0.2.0",
45
"Tags": [
56
"error.type",
67
"execution.result",
@@ -15,6 +16,7 @@
1516
"nservicebus.messaging.failures => Counter",
1617
"nservicebus.messaging.fetches => Counter",
1718
"nservicebus.messaging.handler_time => Histogram, Unit: s",
19+
"nservicebus.messaging.processing_time => Histogram, Unit: s",
1820
"nservicebus.messaging.successes => Counter",
1921
"nservicebus.recoverability.delayed => Counter",
2022
"nservicebus.recoverability.error => Counter",

src/NServiceBus.Core.Tests/OpenTelemetry/Helpers/TestingMetricListener.cs

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class TestingMetricListener : IDisposable
1212
readonly MeterListener meterListener;
1313
public List<Instrument> metrics = [];
1414
public string version = "";
15+
public string metricsSourceName = "";
1516

1617
public TestingMetricListener(string sourceName)
1718
{
@@ -25,6 +26,7 @@ public TestingMetricListener(string sourceName)
2526
listener.EnableMeasurementEvents(instrument);
2627
metrics.Add(instrument);
2728
version = instrument.Meter.Version;
29+
metricsSourceName = instrument.Meter.Name;
2830
}
2931
}
3032
};

src/NServiceBus.Core.Tests/OpenTelemetry/MeterTests.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public void Verify_MeterAPI()
3030
Approver.Verify(new
3131
{
3232
Note = "Changes to metrics API should result in an update to NServiceBusMeter version.",
33-
ActivitySourceVersion = metricsListener.version,
33+
MetricsSourceName = metricsListener.metricsSourceName,
34+
MetricsSourceVersion = metricsListener.version,
3435
Tags = meterTags,
3536
Metrics = metrics
3637
});

src/NServiceBus.Core/Pipeline/Incoming/IncomingPipelineMetrics.cs

+31-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class IncomingPipelineMetrics
1313
const string TotalFailures = "nservicebus.messaging.failures";
1414
const string MessageHandlerTime = "nservicebus.messaging.handler_time";
1515
const string CriticalTime = "nservicebus.messaging.critical_time";
16+
const string ProcessingTime = "nservicebus.messaging.processing_time";
1617
const string RecoverabilityImmediate = "nservicebus.recoverability.immediate";
1718
const string RecoverabilityDelayed = "nservicebus.recoverability.delayed";
1819
const string RecoverabilityError = "nservicebus.recoverability.error";
@@ -30,6 +31,8 @@ public IncomingPipelineMetrics(IMeterFactory meterFactory, string queueName, str
3031
"The time in seconds for the execution of the business code.");
3132
criticalTime = meter.CreateHistogram<double>(CriticalTime, "s",
3233
"The time in seconds between when the message was sent until processed by the endpoint.");
34+
processingTime = meter.CreateHistogram<double>(ProcessingTime, "s",
35+
"The time in seconds between when the message was fetched from the input queue until successfully processed by the endpoint.");
3336
totalImmediateRetries = meter.CreateCounter<long>(RecoverabilityImmediate,
3437
description: "Total number of immediate retries requested.");
3538
totalDelayedRetries = meter.CreateCounter<long>(RecoverabilityDelayed,
@@ -47,13 +50,35 @@ public void AddDefaultIncomingPipelineMetricTags(IncomingPipelineMetricTags inco
4750
incomingPipelineMetricsTags.Add(MeterTags.EndpointDiscriminator, endpointDiscriminator ?? "");
4851
}
4952

50-
public void RecordMessageSuccessfullyProcessed(ITransportReceiveContext context, IncomingPipelineMetricTags incomingPipelineMetricTags)
53+
public void RecordProcessingTime(ITransportReceiveContext context, TimeSpan elapsed)
54+
{
55+
if (!processingTime.Enabled)
56+
{
57+
return;
58+
}
59+
60+
var incomingPipelineMetricTags = context.Extensions.Get<IncomingPipelineMetricTags>();
61+
62+
TagList tags;
63+
tags.Add(new(MeterTags.ExecutionResult, "success"));
64+
incomingPipelineMetricTags.ApplyTags(ref tags, [
65+
MeterTags.QueueName,
66+
MeterTags.EndpointDiscriminator,
67+
MeterTags.MessageType,
68+
MeterTags.MessageHandlerTypes]);
69+
70+
processingTime.Record(elapsed.TotalSeconds, tags);
71+
}
72+
73+
public void RecordCriticalTimeAndTotalProcessed(ITransportReceiveContext context)
5174
{
5275
if (!totalProcessedSuccessfully.Enabled && !criticalTime.Enabled)
5376
{
5477
return;
5578
}
5679

80+
var incomingPipelineMetricTags = context.Extensions.Get<IncomingPipelineMetricTags>();
81+
5782
TagList tags;
5883
tags.Add(new(MeterTags.ExecutionResult, "success"));
5984
incomingPipelineMetricTags.ApplyTags(ref tags, [
@@ -66,10 +91,9 @@ public void RecordMessageSuccessfullyProcessed(ITransportReceiveContext context,
6691
{
6792
totalProcessedSuccessfully.Add(1, tags);
6893
}
94+
var completedAt = DateTimeOffset.UtcNow;
6995
if (criticalTime.Enabled)
7096
{
71-
var completedAt = DateTimeOffset.UtcNow;
72-
7397
if (context.Message.Headers.TryGetDeliverAt(out var startTime)
7498
|| context.Message.Headers.TryGetTimeSent(out startTime))
7599
{
@@ -96,7 +120,7 @@ public void RecordMessageProcessingFailure(IncomingPipelineMetricTags incomingPi
96120
MeterTags.MessageHandlerTypes]);
97121
totalFailures.Add(1, tags);
98122

99-
// the critical time is intentionally not recorded in case of failure
123+
// the processing and critical time are intentionally not recorded in case of failure
100124
}
101125

102126
public void RecordFetchedMessage(IncomingPipelineMetricTags incomingPipelineMetricTags)
@@ -109,7 +133,8 @@ public void RecordFetchedMessage(IncomingPipelineMetricTags incomingPipelineMetr
109133
TagList tags;
110134
incomingPipelineMetricTags.ApplyTags(ref tags, [
111135
MeterTags.EndpointDiscriminator,
112-
MeterTags.QueueName]);
136+
MeterTags.QueueName,
137+
MeterTags.MessageType]);
113138

114139
totalFetched.Add(1, tags);
115140
}
@@ -217,6 +242,7 @@ public void RecordSendToErrorQueue(IRecoverabilityContext recoverabilityContext)
217242
readonly Counter<long> totalFailures;
218243
readonly Histogram<double> messageHandlerTime;
219244
readonly Histogram<double> criticalTime;
245+
readonly Histogram<double> processingTime;
220246
readonly Counter<long> totalImmediateRetries;
221247
readonly Counter<long> totalDelayedRetries;
222248
readonly Counter<long> totalSentToErrorQueue;

src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs

+9-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public TransportReceiveToPhysicalMessageConnector(IOutboxStorage outboxStorage,
2020

2121
public async Task Invoke(ITransportReceiveContext context, Func<IIncomingPhysicalMessageContext, Task> next)
2222
{
23+
var processingStartedAt = DateTimeOffset.UtcNow;
2324
var messageId = context.Message.MessageId;
2425
var physicalMessageContext = this.CreateIncomingPhysicalMessageContext(context.Message, context);
2526

@@ -34,14 +35,14 @@ public async Task Invoke(ITransportReceiveContext context, Func<IIncomingPhysica
3435
context.Extensions.Set(outboxTransaction);
3536
await next(physicalMessageContext).ConfigureAwait(false);
3637

37-
context.Extensions.TryGet<IncomingPipelineMetricTags>(out IncomingPipelineMetricTags incomingPipelineMetricsTags);
38-
incomingPipelineMetrics.RecordMessageSuccessfullyProcessed(context, incomingPipelineMetricsTags);
39-
4038
var outboxMessage = new OutboxMessage(messageId, ConvertToOutboxOperations(pendingTransportOperations.Operations));
4139
await outboxStorage.Store(outboxMessage, outboxTransaction, context.Extensions, context.CancellationToken).ConfigureAwait(false);
4240

4341
context.Extensions.Remove<IOutboxTransaction>();
4442
await outboxTransaction.Commit(context.CancellationToken).ConfigureAwait(false);
43+
44+
var processingCompletedAt = DateTimeOffset.UtcNow;
45+
incomingPipelineMetrics.RecordProcessingTime(context, processingCompletedAt - processingStartedAt);
4546
}
4647

4748
physicalMessageContext.Extensions.Remove<PendingTransportOperations>();
@@ -68,6 +69,11 @@ public async Task Invoke(ITransportReceiveContext context, Func<IIncomingPhysica
6869
}
6970

7071
await outboxStorage.SetAsDispatched(messageId, context.Extensions, context.CancellationToken).ConfigureAwait(false);
72+
73+
if (pendingTransportOperations.HasOperations || deduplicationEntry == null)
74+
{
75+
incomingPipelineMetrics.RecordCriticalTimeAndTotalProcessed(context);
76+
}
7177
}
7278

7379
static void ConvertToPendingOperations(OutboxMessage deduplicationEntry, PendingTransportOperations pendingTransportOperations)

src/NServiceBus.Core/Pipeline/MainPipelineExecutor.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@ class MainPipelineExecutor(
2020
public async Task Invoke(MessageContext messageContext, CancellationToken cancellationToken = default)
2121
{
2222
var pipelineStartedAt = DateTimeOffset.UtcNow;
23-
2423
using var activity = activityFactory.StartIncomingPipelineActivity(messageContext);
2524

2625
var incomingPipelineMetricsTags = messageContext.Extensions.Get<IncomingPipelineMetricTags>();
2726

2827
incomingPipelineMetrics.AddDefaultIncomingPipelineMetricTags(incomingPipelineMetricsTags);
29-
incomingPipelineMetrics.RecordFetchedMessage(incomingPipelineMetricsTags);
3028

3129
var childScope = rootBuilder.CreateAsyncScope();
3230
await using (childScope.ConfigureAwait(false))
@@ -64,10 +62,16 @@ public async Task Invoke(MessageContext messageContext, CancellationToken cancel
6462

6563
ex.Data["Pipeline canceled"] = transportReceiveContext.CancellationToken.IsCancellationRequested;
6664

67-
incomingPipelineMetrics.RecordMessageProcessingFailure(incomingPipelineMetricsTags, ex);
68-
65+
if (!ex.IsCausedBy(transportReceiveContext.CancellationToken))
66+
{
67+
incomingPipelineMetrics.RecordMessageProcessingFailure(incomingPipelineMetricsTags, ex);
68+
}
6969
throw;
7070
}
71+
finally
72+
{
73+
incomingPipelineMetrics.RecordFetchedMessage(incomingPipelineMetricsTags);
74+
}
7175

7276
var completedAt = DateTimeOffset.UtcNow;
7377
await receivePipelineNotification.Raise(new ReceivePipelineCompleted(message, pipelineStartedAt, completedAt), cancellationToken).ConfigureAwait(false);

0 commit comments

Comments
 (0)