-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
feat: client metrics #3988
base: main
Are you sure you want to change the base?
feat: client metrics #3988
Conversation
f3ea68a
to
5b1c35c
Compare
5b1c35c
to
2e9cab1
Compare
Hey @monosoul, thanks for the PR! @marychatte, could you please check? |
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.
Thanks a lot for the PR! And sorry for the big delay in the review
Good work, I like the idea of plugin. And I added some comments
* @see pathParameter | ||
*/ | ||
public fun pathParameters(parameters: Iterable<Pair<String, Any>>) { | ||
val currentUrl = url.build() |
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.
Can we remove url.build()
here and make it call only once? Maybe we can use HttpRequestBuilder.attributes
to save parameters and build later
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 guess one option here would be to just update URLBuilder
to support path parameters in addition to query parameters
public fun pathParameters(parameters: Iterable<Pair<String, Any>>) { | ||
val currentUrl = url.build() | ||
// save original URI pattern | ||
attributes.computeIfAbsent(UriPatternAttributeKey) { "$currentUrl" } |
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.
Why does it need to be saved as a String
? Later, it's used to parse, but maybe we can use functions URL.encodedQuery
or URL.encodedPath
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 we use that value as a metric tag, we don't really want to have an encoded url there.
builder.host = host | ||
builder.protocol = protocol | ||
builder.port = port | ||
builder.pathSegments = |
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.
What about pathSegment
= /{year}_{month}/
for example?
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.
Should we care about cases like that? Could be managed like that:
client.get("https://blog.jetbrains.com/kotlin/{year_month}/{name}/") {
pathParameter("year_month", "2023_11")
pathParameter("name", "kotlin-1-9-20-released")
}.bodyAsText()
pathSegments.map { pathSegment -> | ||
substitutionsMap[pathSegment] ?: pathSegment | ||
} | ||
parameters.forEach { parameterName, parameterValues -> |
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 also replace query parameters here, so the names pathParameters(...)
and pathParameter(...)
can be confusing. And it's also better to add in example query parameter values for clarity
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.
The thing is, nothing prevents you from specifying the URL for the client like that: https://www.google.com/search?client=firefox-b-d&q={query}
, in which case to me it feels like it makes sense to substitute query parameters while doing so for the path parameters.
} | ||
} | ||
builder.user = user | ||
builder.password = password |
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 need to copy fragment
and trailingQuery
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.
} | ||
|
||
/** | ||
* A client's plugin that provides the capability to meter HTTP calls with micrometer |
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.
Can you please add in Kdoc example of plugin usage?
Tag.of( | ||
TAG_URI, | ||
request.attributes.getOrNull(UriPatternAttributeKey) | ||
.let { it ?: request.url.takeIf { useExpandedUrlWhenPatternUnavailable }?.toString() } |
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.
Can we extract it so it does not have copypaste?
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.
Something like this: f802824 ?
try { | ||
proceed() | ||
} catch (cause: Throwable) { | ||
timer.stop( |
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.
Can we add FailureHook
so there is no need to write try-catch
?
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 part was inspired by the logging plugin. From what I see, ResponseHook
attaches to HttpReceivePipeline
, SendHook
attaches to HttpSendPipeline
and ReceiveHook
attaches to HttpResponsePipeline
. If I get that right, I'd have to introduce 3 different failure hooks then to handle this, which doesn't seem to be very different from just handling the exception 3 times 😅
cause?.javaClass?.simpleName ?: javaClass.simpleName, | ||
) | ||
|
||
private fun String.removeHostPart(host: String) = replace("^.*$host[^/]*".toRegex(), "") |
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.
Can we extract regex here so as not to compile it every time?
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.
Unless you know a way to parameterize the regex, no, we can't
|
||
private fun String.removeQueryPart() = replace(QUERY_PART_REGEX, "") | ||
|
||
private object ResponseHook : ClientHook<suspend ResponseHook.Context.(response: HttpResponse) -> Unit> { |
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.
Can we, instead of creating new Hooks, use functions onRequest {... }
, onResponse {... }
etc?
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.
Only if we wouldn't need to explicitly call proceed()
to handle exceptions
a5de960
to
f802824
Compare
Quick question here : why use directly MeterRegistry, instead of using ObservationRegistry ? |
Subsystem
Client
Motivation
Provide a plugin to instrument Ktor client similarly to how the server lib is instrumented with Micrometer.
Solution
This is basically just a PoC to discuss if the approach is okay or not. If it's okay, then I'll proceed with adding tests etc. If not, then we can discuss it here.
The idea here is to provide a client instrumentation similar to how it's done in OkHttpMetricsEventListener - introduce a timer for client requests.
The implementation is heavily inspired by the Logging plugin.
Since using an expanded URI as the value for the URI tag can lead to cardinality blow up, I had to introduce support for URI templates. This is done in 0e542bd. While those methods could potentially be added as extension functions in the plugin module, it feels like they belong to the core lib more.
( Probably a better option there would be to have a set of explicit builder functions like:
Then it wouldn't be necessary to rebuild the URL. This is the path Spring followed with their client for example: https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java#L400 )
The URI template is stored as a call attribute with the first call to
HttpRequestBuilder.pathParameters
and is later used inMicrometerMetrics
plugin.Here's how using the new methods would look like:
The plugin is implemented in 2e9cab1 and provides configuration to make it flexible enough to cover most of the use cases.
The timer will be started when request has been sent and will be stopped either when an exception has occurred, or when the response has been received.