-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Add Counters for Endpoints #21675
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
Add Counters for Endpoints #21675
Changes from all commits
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 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,101 @@ | ||||||||||||||
// Copyright (c) .NET Foundation. All rights reserved. | ||||||||||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||||||||||||||
|
||||||||||||||
using System; | ||||||||||||||
using System.Collections.Concurrent; | ||||||||||||||
using System.Diagnostics.Tracing; | ||||||||||||||
using System.Threading; | ||||||||||||||
|
||||||||||||||
namespace Microsoft.AspNetCore.Routing | ||||||||||||||
{ | ||||||||||||||
internal class EndpointMiddlewareEventSource : EventSource | ||||||||||||||
{ | ||||||||||||||
public static readonly EndpointMiddlewareEventSource Log = new EndpointMiddlewareEventSource(); | ||||||||||||||
|
||||||||||||||
private readonly ConcurrentDictionary<string, EndpointCounter> _endpointCounters = new ConcurrentDictionary<string, EndpointCounter>(); | ||||||||||||||
|
||||||||||||||
internal EndpointMiddlewareEventSource() | ||||||||||||||
: base("Microsoft.AspNetCore.Routing") | ||||||||||||||
{ | ||||||||||||||
|
||||||||||||||
} | ||||||||||||||
|
||||||||||||||
[Event(1, Level = EventLevel.Informational)] | ||||||||||||||
public void ExecutingEndpoint(string endpoint) | ||||||||||||||
{ | ||||||||||||||
var endpointCounter = _endpointCounters.GetOrAdd(endpoint, key => new EndpointCounter(key, this)); | ||||||||||||||
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. Looking up a dictionary multiple times (executing, executed, failed) for every call would have a performance impact. This code is on a very hot path. I don't know the best way to mitigate but EventSource.OnEventCommand provides a hook to decide to enable counters. We should only be initializing counters when needed. 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. aspnetcore/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs Lines 314 to 319 in 5a0c097
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. The problem is that even if the event source is disabled, we would still need to do the dictionary lookup to ensure We can still override OnEventCommand to lazily initialize the PollingCounters, but we'd have to iterate 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. Ah, right. We could add counters as metadata or a property on the endpoint. However endpoints are intended to be immutable and Ryan would murder us all 😄 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. I was thinking the same thing. Problem with a property is that Endpoint is defined in another assembly and InternalsVisibleTo is bad. Putting it in Metadata doesn't work because it is immutable and Ryan would murder us. Retrieving the EndpointCounter from Metadata would involve a dictionary lookup anyway. 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. Yes. There would be some other challenges, like an application's endpoints can change. For example, dropping a new Razor file into a directory would change that endpoint data source to include the new endpoint and the matcher would recompute the DFA graph. When that happens we'd need to keep the endpoint+counter instances together. Not difficult, but something to take into account. 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. What should happen if someone wants to Correct me if I'm wrong, but Routing matcher only gives 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. Routing matching usual matches That's a good point about SetEndpoint. Matching is the primary way of setting endpoints, but not the only way. Matching providing the counter type wouldn't work. 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. @davidfowl Thoughts? 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. @noahfalk This is an example of where we want dimensions for metrics to avoid the dictionary workaround. |
||||||||||||||
endpointCounter.Executing(); | ||||||||||||||
WriteEvent(3, endpoint); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
[Event(2, Level = EventLevel.Informational)] | ||||||||||||||
public void ExecutedEndpoint(string endpoint) | ||||||||||||||
{ | ||||||||||||||
var endpointCounter = _endpointCounters.GetOrAdd(endpoint, key => new EndpointCounter(key, this)); | ||||||||||||||
endpointCounter.Executed(); | ||||||||||||||
WriteEvent(2, endpoint); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
[Event(3, Level = EventLevel.Error)] | ||||||||||||||
public void FailedEndpoint(string endpoint) | ||||||||||||||
{ | ||||||||||||||
var endpointCounter = _endpointCounters.GetOrAdd(endpoint, key => new EndpointCounter(key, this)); | ||||||||||||||
endpointCounter.Failed(); | ||||||||||||||
WriteEvent(3, endpoint); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
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. Nit: one empty line too much. |
||||||||||||||
|
||||||||||||||
private class EndpointCounter | ||||||||||||||
{ | ||||||||||||||
private readonly IncrementingPollingCounter _requestsPerSecondCounter; | ||||||||||||||
private readonly PollingCounter _totalRequestsCounter; | ||||||||||||||
private readonly PollingCounter _failedRequestsCounter; | ||||||||||||||
private readonly PollingCounter _currentRequestsCounter; | ||||||||||||||
|
||||||||||||||
private long _totalRequests; | ||||||||||||||
private long _currentRequests; | ||||||||||||||
private long _failedRequests; | ||||||||||||||
|
||||||||||||||
public EndpointCounter(string endpoint, EventSource eventSource) | ||||||||||||||
{ | ||||||||||||||
_requestsPerSecondCounter = new IncrementingPollingCounter($"{endpoint}:requests-per-second", eventSource, () => _totalRequests) | ||||||||||||||
{ | ||||||||||||||
DisplayName = $"{endpoint}:Request Rate", | ||||||||||||||
DisplayRateTimeScale = TimeSpan.FromSeconds(1) | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
_totalRequestsCounter = new PollingCounter($"{endpoint}:total-requests", eventSource, () => _totalRequests) | ||||||||||||||
{ | ||||||||||||||
DisplayName = $"{endpoint}:Total Requests", | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
_currentRequestsCounter = new PollingCounter($"{endpoint}:current-requests", eventSource, () => _currentRequests) | ||||||||||||||
{ | ||||||||||||||
DisplayName = $"{endpoint}:Current Requests" | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
_failedRequestsCounter = new PollingCounter($"{endpoint}:failed-requests", eventSource, () => _failedRequests) | ||||||||||||||
{ | ||||||||||||||
DisplayName = $"{endpoint}:Failed Requests" | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
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. Nit: remove empty line |
||||||||||||||
} | ||||||||||||||
|
||||||||||||||
public void Executing() | ||||||||||||||
{ | ||||||||||||||
Interlocked.Increment(ref _totalRequests); | ||||||||||||||
Interlocked.Increment(ref _currentRequests); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
public void Executed() | ||||||||||||||
{ | ||||||||||||||
Interlocked.Decrement(ref _currentRequests); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
public void Failed() | ||||||||||||||
{ | ||||||||||||||
Interlocked.Increment(ref _failedRequests); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
} |
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.
May it be "better" to handle the executed within the failed?
So that here is just one method call, and in the internals of
Log.FailedEndpoint
this case gets handled and has the potential to adapt it later if needed.