-
Notifications
You must be signed in to change notification settings - Fork 17
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
Added endpoint-level metrics keeping track of the request rate for each path. #104
Conversation
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.
let me know if you have questions
|
||
protected static String getMeterNameForRoute(Route route, String httpMethod) { | ||
String result = ""; | ||
if (httpMethod != null) { |
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.
we should default this to 'ANY'
result += httpMethod.toLowerCase(); | ||
} | ||
|
||
if (route != null && route.toString() != null) { |
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.
no route should cause us to throw a new ArgumentNullException
return getMeterNameForRoute(route, httpMethod.name()); | ||
} | ||
|
||
protected static String getMeterNameForRoute(Route route, String httpMethod) { |
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.
since httpMethod can be null we should use Optional<String> httpMethod
here
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.
Optional
is much better for readability, but it adds extra garbage for each request. I'm planning on running a benchmarking series at some point; it would be good to see how this impacts things.
@@ -46,4 +46,8 @@ | |||
routes = new AtomicReference<>(); | |||
|
|||
@Getter private final ObjectMapper mapper; | |||
|
|||
@Getter | |||
private final ConcurrentHashMap<String, Meter> rateMetersByRouteAndMethod = |
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.
this should be named something like metersByRoute
. rate doesn't apply and Method is unneeded. ideally, method is already part of the route (conceptually)
configEndpointLevelRateMeters(metrics, ctx); | ||
} | ||
|
||
private void configEndpointLevelRateMeters( |
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.
this doesn't belong in this class. it should be done just below the configResponseCodeMeters()
in Router
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.
I originally had it where you suggest, in the constructor for Router, but at that point the routes aren't configured yet, so I can't set up meters for each route.
Looking at it again, perhaps a more appropriate place would be to put it would be early in Router.listenAndServe?
ImmutableSortedMap<Route, List<ImmutableMap<XHttpMethod, Handler>>> routes = | ||
ctx.getRoutes().get(); | ||
|
||
final String NAME_PREFIX = "handler.rate."; |
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.
this should be changed to something like "routes.";
also the name should be changed to namePrefix
|
||
final String NAME_PREFIX = "handler.rate."; | ||
|
||
Set<String> routeAndMethodNames = new HashSet<>(); |
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.
no need to have this temp holding set. the routes can be added directly to the metric registry
|
||
for (String routeName : routeAndMethodNames) { | ||
ctx.getRateMetersByRouteAndMethod() | ||
.put(routeName, metricRegistry.meter(name(Router.class, NAME_PREFIX + routeName))); |
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.
change to .meter(namePrefix + routeName)...
. this includes my other comment about changing NAME_PREFIX
to namePrefix
I'd like to see the regressions numbers between this and Master to make sure we aren't adding additional latency with this. |
…e names for clarity, used Optional instead of null check.
Thanks for the comments, Andy. I moved the metrics config out of ServiceRateLimiter and into Router.listenAndServe, which is hopefully a better place for it. I also updated the variable names and metric names, and changed MetricsUtil to use Optional to handle possible null parameters. Let me know if any of this still has issues. I am working on doing benchmarking to check on the latency. |
} | ||
|
||
return result; | ||
return method + routeIdentifier; |
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.
this return can happen inside the if, eliminating the need for the routeIdentifier variable. in addition it is a good practice to use inverted if conditions to allow the happy path logic to happen outside of nested conditionals. for instance...
if (route == null || route.toString() == null) {
throw new IllegalArgumentException("Route cannot be null.");
}
return String.format("{}{}", method, route.toString().replace('/', '.');
is better than the alternative you've presented. it allows the happy path to be as unencumbered by levels of nesting while reducing the overall Cyclomatic Complexity of the method.
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.
Makes sense, I updated this.
However, I also added a route identifier map to cache the route identifiers per one of Jesse's comments, so hopefully the way I refactored it still makes sense and reduces complexity.
@@ -365,6 +369,26 @@ public void listenAndServe(boolean serveAdmin, boolean scheduleHealthChecks) thr | |||
channel = future.channel(); | |||
} | |||
|
|||
private void configEndpointLevelRateMeters( |
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.
I'm not sure that these are accurately Rate meters in the same sense as we use the word Rate in Rate Limiting
. Or at minimum they are using different Rates. That being said we should use a different word here to avoid confusion. Some suggestions ThroughPut
, RequestCount
, Requests
....
for (XHttpMethod httpMethod : map.keySet()) { | ||
String routeName = MetricsUtil.getMeterNameForRoute(route, httpMethod); | ||
ctx.getMetersByRoute() | ||
.put(routeName, metricRegistry.meter(name(Router.class, namePrefix + routeName))); |
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.
this should use the .meter method that uses only namePrefix + routeName
as the name without Router.class
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.
using the class makes the name overly verbose and noisy
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.
Sounds good, I'll remove it.
I originally included the class name because ServiceRateLimiter did it, here:
https://github.com/Nordstrom/xrpc/blob/master/src/main/java/com/nordstrom/xrpc/server/ServiceRateLimiter.java#L56
Does it also make sense to remove the class name there, too?
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.
yes i'd say making the names as short and sweet but unique is the way to go.
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.
great, removed them.
…d with Rate meters. Removed class name from metrics registry.
…o feat-endpoint-level-metrics
I used the run regression script to check out latency in master vs my branch. The max is definitely higher but the average is very close. (Note I had to increase the server and global rate limits for this to work): master:
my branch:
|
@@ -88,6 +89,7 @@ private void executeHandler(ChannelHandlerContext ctx, int streamId, Route route | |||
.findFirst(); | |||
|
|||
if (handlerMapOptional.isPresent()) { | |||
httpRequestMethodName = handlerMapOptional.get().keySet().asList().get(0).name(); |
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.
On line 81 above, the method name is calculated.
You should be able to speed things up by calculating this once before line 69, and saving the method name in the local.
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.
(you'll speed things up by avoiding the calls here, but also by avoiding re-calculating the method in the inner loop)
// in | ||
// Router.serveAdmin()); we do not track metrics for admin endpoints. | ||
String meterName = MetricsUtil.getMeterNameForRoute(route, httpRequestMethodName); | ||
if (xctx.getMetersByRoute().get(meterName) != null) { |
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.
Cache result of this expression:
// too lazy to look up actual type
MeterType meter = xctx.getMetersByRoute().get(meterName);
if (meter != null) {
meter.mark();
}
|
||
String routeIdentifier; | ||
if (route != null && route.toString() != null) { | ||
routeIdentifier = route.toString().replace('/', '.'); |
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.
This should probably be cached as a property of the route, or in a map somewhere.
// configured in | ||
// Router.serveAdmin()); we do not track metrics for admin endpoints. | ||
String meterName = MetricsUtil.getMeterNameForRoute(route, request.method().name()); | ||
if (xctx.getMetersByRoute().get(meterName) != null) { |
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.
Cache expression result as above.
…and meters when applicable, reduced redundant calls to calculate method name
…es as succinct and readable as possible.
@@ -108,6 +102,15 @@ private void executeHandler(ChannelHandlerContext ctx, int streamId, Route route | |||
.handle(ctx.channel().attr(XrpcConstants.XRPC_REQUEST).get()); | |||
} | |||
|
|||
// Check here for the case of an admin endpoint (eg /metrics, /health, and all others configured | |||
// in Router.serveAdmin()); we do not track metrics for admin endpoints. | |||
Meter meter = |
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.
I'd like to see the functional Optional pattern here like on line 113
Optional<Meter> routeMeter =
Optional.ofNullable(xctx.getMetersByRoute()
.get(MetricsUtil.getMeterNameForRoute(route, httpRequestMethod.name()));
routeMeter.ifPresent(Meter::mark);
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.
Sounds good, changed it in Http2Handler and UrlRouter.
Once you fix the merge conflicts, I am good to approve. |
Merge conflicts now fixed (and addressed one more comment that I missed before). |
👍 All of my remaining comments are unblocking... The one about cyclomatic complexity is worth noting... |
you still need to fix the build issues. CorsTest is failing... |
Build issues fixed! Thanks all for the help |
I added metrics for request rate at the endpoint level. In addition to having this:
we now also have metrics that keep track of the rate of requests for each endpoint (that is, combination of HTTP method and path):
A few things I'm unsure about and would especially appreciate feedback on:
Is ServiceRateLimiter the right place to configure these metrics? Since the overall server level rate is configured there, that's what I went with, but it seems a little off.
Re: issue 103, these metric names might be contributing to further inconsistency- any thoughts on naming these better?
Note we also may consider adding metrics per client. If we have metrics per client and per endpoint, this could end up being a large/unreadable number of metrics... how might we deal with this?