Skip to content

Commit 76bdb03

Browse files
author
Doug Hilpipre
committed
changes to make Http clients work across various implementations
1 parent 1665dbb commit 76bdb03

24 files changed

Lines changed: 549 additions & 238 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Provides instrumentation of both the client and server sides of Ktor. This incl
2323

2424
## Installation
2525

26-
It is recommended to also use the instrumentation for Kotlin Coroutines: https://github.com/newrelic/newrelic-java-kotlin-coroutines
26+
It is recommended to use version 8.25.1 or later of the Java Agent as those versions contain instrumentation for Kotlin Coroutines. If you use an earlier version of the Java Agent then use the last set of instrumentation in the archived repo for Kotlin Coroutines (https://github.com/newrelic/newrelic-java-kotlin-coroutines/releases/tag/v1.0.8).
27+
2728
Additionally it recommended to version 8.20.0 of the New Relic Java Agent in order to avoid problems with the retransformation of Kotlin classes. This requires you to include a system property in the startup arguments of your application. Include -Dnewrelic.config.class_transformer.clear_return_stacks=true
2829
See 8.20.0 Release Notes for more details (https://docs.newrelic.com/docs/release-notes/agent-release-notes/java-release-notes/java-agent-8200/)
2930

@@ -40,9 +41,12 @@ After deployment, you should get route names for your web transactions instead o
4041
You should also see deeper visibiity into what is happening in your application via the enhanced transaction traces/distributed traces provided by this instrumentation.
4142

4243
### Recommended Kotlin Coroutine Configuration
43-
Ktor intiates a long running lazy Coroutine that will dominate the transaction times for your application unless it is ignored. Please use this version or later (https://github.com/newrelic/newrelic-java-kotlin-coroutines/releases/tag/v1.0.4) to enable this feature and add io.ktor.server.netty.cio.RequestBodyHandler to the scopes elemeent in the Coroutines stanza in newrelic.yml as shown here: https://github.com/newrelic/newrelic-java-kotlin-coroutines?tab=readme-ov-file#configuring-scopes-to-ignore
44+
Ktor initiates a long running lazy Coroutine that will dominate the transaction times for your application unless it is ignored. Please use this version or later (https://github.com/newrelic/newrelic-java-kotlin-coroutines/releases/tag/v1.0.4) to enable this feature and add io.ktor.server.netty.cio.RequestBodyHandler to the scopes elemeent in the Coroutines stanza in newrelic.yml as shown here: https://github.com/newrelic/newrelic-java-kotlin-coroutines?tab=readme-ov-file#configuring-scopes-to-ignore
4445
The Kotlin Coroutines instrumentation also will trace suspend functions so it recommended that you ignore internal Ktor suspend functions by adding the regular expression ".\*io\\.ktor\\..\*" to the list of suspends to ignore.
4546

47+
## Notes
48+
If using the Ktor CIO Http Client, it currently splits into several different transactions. Two of these are the major ones, the first will track the actual external call response time and the second will track the request timeout. The others are typically of short duration.
49+
This problem requires a change to the Kotlin Coroutine instrumentation that will be fixed in an upcoming Java Agent release. After the release there will be a single transaction.
4650
## Support
4751

4852
New Relic has open-sourced this project. This project is provided AS-IS WITHOUT WARRANTY OR DEDICATED SUPPORT. Issues and contributions should be reported to the project here on GitHub.
@@ -63,4 +67,3 @@ If you believe you have found a security vulnerability in this project or any of
6367

6468
New Relic Java Instrumentation for Ktor is licensed under the [Apache 2.0](http://apache.org/licenses/LICENSE-2.0.txt) License.
6569

66-
>[If applicable: [Project Name] also uses source code from third-party libraries. You can find full details on which libraries are used and the terms under which they are licensed in the third-party notices document.]

ktor-client-core-2.0/build.gradle.kts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
apply(plugin = "java")
44

55
dependencies {
6-
implementation("io.ktor:ktor-client-core-jvm:2.0.0")
6+
implementation("io.ktor:ktor-client-core-jvm:2.0.0")
77

8-
// New Relic Java Agent dependencies
8+
// New Relic Java Agent dependencies
99
implementation("com.newrelic.agent.java:newrelic-api:9.1.0")
1010
implementation("com.newrelic.agent.java:newrelic-agent:9.1.0")
1111
implementation("com.newrelic.agent.java:newrelic-weaver-api:9.1.0")
1212
implementation("com.newrelic.agent.java:agent-bridge:9.1.0")
13-
implementation(fileTree("../test-lib"){
13+
implementation(fileTree("../test-lib") {
1414
include("*.jar")
1515
})
1616
}
@@ -28,7 +28,7 @@ tasks.jar {
2828
}
2929

3030
verifyInstrumentation {
31-
passesOnly("io.ktor:ktor-client-core-jvm:[2.0.0,)")
32-
excludeRegex(".*beta.*")
33-
excludeRegex(".*rc.*")
31+
passesOnly("io.ktor:ktor-client-core-jvm:[2.0.0,3.0.0)")
32+
excludeRegex(".*beta.*")
33+
excludeRegex(".*rc.*")
3434
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.newrelic.instrumentation.labs.ktor.client;
2+
3+
import com.newrelic.api.agent.HttpParameters;
4+
import kotlin.coroutines.Continuation;
5+
6+
public class KtorClientUtils {
7+
8+
public static <T> NRContinuationWrapper<T> getContinuationWrapper(Continuation<T> continuation, HttpParameters httpParameters) {
9+
if(continuation instanceof NRContinuationWrapper) {
10+
return null;
11+
}
12+
return new NRContinuationWrapper<>(continuation, httpParameters);
13+
}
14+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.newrelic.instrumentation.labs.ktor.client;
2+
3+
import com.newrelic.agent.bridge.AgentBridge;
4+
import com.newrelic.api.agent.HttpParameters;
5+
import com.newrelic.api.agent.NewRelic;
6+
import com.newrelic.api.agent.Segment;
7+
import com.newrelic.api.agent.Trace;
8+
import kotlin.coroutines.Continuation;
9+
import kotlin.coroutines.CoroutineContext;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
public class NRContinuationWrapper<T> implements Continuation<T> {
13+
14+
private final Continuation<T> delegate;
15+
private static boolean isTransformed = false;
16+
private final Segment segment;
17+
18+
protected NRContinuationWrapper(Continuation<T> delegate, HttpParameters httpParameters) {
19+
this.delegate = delegate;
20+
segment = NewRelic.getAgent().getTransaction().startSegment("Ktor-HttpRequest");
21+
if(httpParameters != null) {
22+
segment.reportAsExternal(httpParameters);
23+
}
24+
if(!isTransformed) {
25+
isTransformed = true;
26+
AgentBridge.instrumentation.retransformUninstrumentedClass(getClass());
27+
}
28+
}
29+
30+
@Override
31+
@Trace(dispatcher = true)
32+
public void resumeWith(@NotNull Object o) {
33+
if(segment != null) {
34+
segment.end();
35+
}
36+
delegate.resumeWith(o);
37+
}
38+
39+
@Override
40+
public @NotNull CoroutineContext getContext() {
41+
return delegate.getContext();
42+
}
43+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.newrelic.instrumentation.labs.ktor.client;
2+
3+
import com.newrelic.agent.bridge.AgentBridge;
4+
import com.newrelic.api.agent.Trace;
5+
import kotlin.jvm.functions.Function2;
6+
7+
public class NRFunction2Wrapper<R,S,T> implements Function2<R,S,T> {
8+
9+
private final Function2<R,S,T> delegate;
10+
private static boolean isTransformed = false;
11+
12+
public NRFunction2Wrapper(Function2<R,S,T> delegate) {
13+
this.delegate = delegate;
14+
if (!isTransformed) {
15+
isTransformed = true;
16+
AgentBridge.instrumentation.retransformUninstrumentedClass(getClass());
17+
}
18+
}
19+
20+
@Override
21+
@Trace(dispatcher = true)
22+
public T invoke(R r, S s) {
23+
return delegate != null ? delegate.invoke(r, s) : null;
24+
}
25+
}

ktor-client-core-2.0/src/main/java/io/ktor/client/HttpClient_Instrumentation.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package io.ktor.client;
22

3+
import com.newrelic.api.agent.NewRelic;
34
import com.newrelic.api.agent.Trace;
45
import com.newrelic.api.agent.weaver.Weave;
56
import com.newrelic.api.agent.weaver.Weaver;
7+
import com.newrelic.instrumentation.labs.ktor.client.KtorHeaderWrapper;
68
import io.ktor.client.call.HttpClientCall;
79
import io.ktor.client.request.HttpRequestBuilder;
10+
import io.ktor.http.HeadersBuilder;
11+
import io.ktor.http.HttpHeaders;
812
import kotlin.coroutines.Continuation;
913

1014
/*
@@ -16,6 +20,12 @@ public class HttpClient_Instrumentation {
1620

1721
@Trace(dispatcher = true)
1822
public Object execute$ktor_client_core(HttpRequestBuilder requestBuilder, Continuation<? super HttpClientCall> continuation) {
23+
if(requestBuilder != null) {
24+
HeadersBuilder headers = requestBuilder.getHeaders();
25+
if(headers != null) {
26+
NewRelic.getAgent().getTransaction().insertDistributedTraceHeaders(new KtorHeaderWrapper(headers));
27+
}
28+
}
1929
return Weaver.callOriginal();
2030
}
2131
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.ktor.client.statement;
2+
3+
import com.newrelic.api.agent.HttpParameters;
4+
import com.newrelic.api.agent.NewRelic;
5+
import com.newrelic.api.agent.Trace;
6+
import com.newrelic.api.agent.weaver.NewField;
7+
import com.newrelic.api.agent.weaver.Weave;
8+
import com.newrelic.api.agent.weaver.Weaver;
9+
import com.newrelic.instrumentation.labs.ktor.client.KtorClientUtils;
10+
import com.newrelic.instrumentation.labs.ktor.client.KtorHeaderWrapper;
11+
import com.newrelic.instrumentation.labs.ktor.client.NRContinuationWrapper;
12+
import com.newrelic.instrumentation.labs.ktor.client.NRFunction2Wrapper;
13+
import io.ktor.client.HttpClient;
14+
import io.ktor.client.request.HttpRequestBuilder;
15+
import io.ktor.http.HeadersBuilder;
16+
import io.ktor.http.HttpMethod;
17+
import io.ktor.http.URLBuilder;
18+
import io.ktor.http.Url;
19+
20+
import java.net.URI;
21+
22+
@Weave(originalName = "io.ktor.client.statement.HttpStatement")
23+
public class HttpStatement_Instrumentation {
24+
25+
@NewField
26+
private HttpParameters httpParameters = null;
27+
28+
public HttpStatement_Instrumentation(HttpRequestBuilder builder, HttpClient client) {
29+
HeadersBuilder headersBuilder = builder.getHeaders();
30+
if(headersBuilder != null) {
31+
KtorHeaderWrapper headerWrapper = new KtorHeaderWrapper(headersBuilder);
32+
NewRelic.getAgent().getTransaction().insertDistributedTraceHeaders(headerWrapper);
33+
}
34+
URLBuilder urlBuilder = builder.getUrl();
35+
if(urlBuilder != null) {
36+
Url url = urlBuilder.build();
37+
String urlString = url.toString();
38+
if(urlString != null) {
39+
HttpMethod method = builder.getMethod();
40+
URI uri = URI.create(urlString);
41+
httpParameters = HttpParameters.library("Ktor-Client").uri(uri).procedure(method.getValue()).noInboundHeaders().build();
42+
}
43+
}
44+
}
45+
46+
@Trace(dispatcher = true)
47+
public <T> java.lang.Object execute(kotlin.jvm.functions.Function2<? super io.ktor.client.statement.HttpResponse, ? super kotlin.coroutines.Continuation<? super T>, ? extends java.lang.Object> function2, kotlin.coroutines.Continuation<? super T> continuation) {
48+
if(!(function2 instanceof NRFunction2Wrapper)) {
49+
function2 = new NRFunction2Wrapper<>(function2);
50+
}
51+
return Weaver.callOriginal();
52+
}
53+
54+
@Trace(dispatcher = true)
55+
public java.lang.Object execute(kotlin.coroutines.Continuation<? super io.ktor.client.statement.HttpResponse> continuation) {
56+
NRContinuationWrapper<? super HttpResponse> wrapper = KtorClientUtils.getContinuationWrapper(continuation, httpParameters);
57+
if(wrapper != null) {
58+
continuation = wrapper;
59+
}
60+
return Weaver.callOriginal();
61+
}
62+
63+
@Trace(dispatcher = true)
64+
public java.lang.Object executeUnsafe(kotlin.coroutines.Continuation<? super io.ktor.client.statement.HttpResponse> continuation) {
65+
return Weaver.callOriginal();
66+
}
67+
68+
}

ktor-client-core-2.0/src/main/kotlin/io/ktor/client/statement/HttpStatement_Instrumentation.kt

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Build.gradle generated for instrumentation module ktor-client-core
2+
3+
apply(plugin = "java")
4+
5+
dependencies {
6+
implementation("io.ktor:ktor-client-core-jvm:3.0.0")
7+
8+
// New Relic Java Agent dependencies
9+
implementation("com.newrelic.agent.java:newrelic-api:9.1.0")
10+
implementation("com.newrelic.agent.java:newrelic-agent:9.1.0")
11+
implementation("com.newrelic.agent.java:newrelic-weaver-api:9.1.0")
12+
implementation("com.newrelic.agent.java:agent-bridge:9.1.0")
13+
implementation(fileTree("../test-lib") {
14+
include("*.jar")
15+
})
16+
}
17+
18+
19+
tasks.jar {
20+
manifest {
21+
attributes(
22+
"Implementation-Title" to "com.newrelic.instrumentation.labs.ktor-client-core-3.0",
23+
"Implementation-Vendor" to "New Relic Labs",
24+
"Implementation-Vendor-Id" to "com.newrelic.labs",
25+
"Implementation-Version" to 1.0
26+
)
27+
}
28+
}
29+
30+
verifyInstrumentation {
31+
passesOnly("io.ktor:ktor-client-core-jvm:[3.0.0,)")
32+
excludeRegex(".*beta.*")
33+
excludeRegex(".*rc.*")
34+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.newrelic.instrumentation.labs.ktor.client;
2+
3+
import com.newrelic.api.agent.HttpParameters;
4+
import kotlin.coroutines.Continuation;
5+
6+
public class KtorClientUtils {
7+
8+
public static <T> NRContinuationWrapper<T> getContinuationWrapper(Continuation<T> continuation, HttpParameters httpParameters) {
9+
if(continuation instanceof NRContinuationWrapper) {
10+
return null;
11+
}
12+
return new NRContinuationWrapper<>(continuation, httpParameters);
13+
}
14+
}

0 commit comments

Comments
 (0)