diff --git a/README.md b/README.md index a6cda029c9..a3e032b4ef 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ to capture telemetry from a number of popular libraries and frameworks. | [JMS](https://javaee.github.io/javaee-spec/javadocs/javax/jms/package-summary.html) | 1.1+ | | [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | | [Kafka](https://kafka.apache.org/20/javadoc/overview-summary.html) | 0.11+ | +| [khttp](https://khttp.readthedocs.io) | 0.1.0+ | | [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | | [Log4j](https://logging.apache.org/log4j/2.x/) | 1.1+ | | [Logback](https://github.com/qos-ch/logback) | 1.0+ | diff --git a/instrumentation/khttp-0.1/khttp-0.1.gradle b/instrumentation/khttp-0.1/khttp-0.1.gradle new file mode 100644 index 0000000000..55e7267919 --- /dev/null +++ b/instrumentation/khttp-0.1/khttp-0.1.gradle @@ -0,0 +1,28 @@ +ext { + minJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +apply from: "${rootDir}/gradle/instrumentation.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +muzzle { + pass { + group = 'khttp' + module = 'khttp' + versions = "(,)" + assertInverse = true + } +} + + +testSets { + latestDepTest +} + +dependencies { + compileOnly group: 'khttp', name: 'khttp', version: '0.1.0' + + testCompile group: 'khttp', name: 'khttp', version: '0.1.0' + + latestDepTestCompile group: 'khttp', name: 'khttp', version: '+' +} \ No newline at end of file diff --git a/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpAdvice.java b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpAdvice.java new file mode 100644 index 0000000000..d04bc8d65f --- /dev/null +++ b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpAdvice.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.opentelemetry.auto.instrumentation.khttp; + +import static io.opentelemetry.auto.instrumentation.khttp.KHttpDecorator.DECORATE; +import static io.opentelemetry.auto.instrumentation.khttp.KHttpDecorator.TRACER; +import static io.opentelemetry.auto.instrumentation.khttp.KHttpHeadersInjectAdapter.SETTER; +import static io.opentelemetry.auto.instrumentation.khttp.KHttpHeadersInjectAdapter.asWritable; +import static io.opentelemetry.context.ContextUtils.withScopedContext; +import static io.opentelemetry.trace.Span.Kind.CLIENT; +import static io.opentelemetry.trace.TracingContextUtils.withSpan; + +import io.grpc.Context; +import io.opentelemetry.OpenTelemetry; +import io.opentelemetry.auto.bootstrap.CallDepthThreadLocalMap; +import io.opentelemetry.auto.instrumentation.api.SpanWithScope; +import io.opentelemetry.trace.Span; +import java.util.Map; +import khttp.KHttp; +import khttp.responses.Response; +import net.bytebuddy.asm.Advice; + +public class KHttpAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static SpanWithScope methodEnter( + @Advice.Argument(value = 0) String method, + @Advice.Argument(value = 1) String uri, + @Advice.Argument(value = 2) Map headers) { + + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(KHttp.class); + if (callDepth > 0) { + return null; + } + + final Span span = TRACER.spanBuilder("HTTP " + method).setSpanKind(CLIENT).startSpan(); + + DECORATE.afterStart(span); + DECORATE.onRequest(span, new RequestWrapper(method, uri)); + + final Context context = withSpan(span, Context.current()); + + OpenTelemetry.getPropagators().getHttpTextFormat().inject(context, asWritable(headers), SETTER); + return new SpanWithScope(span, withScopedContext(context)); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Enter final SpanWithScope spanWithScope, + @Advice.Return final Response result, + @Advice.Thrown final Throwable throwable) { + if (spanWithScope == null) { + return; + } + CallDepthThreadLocalMap.reset(KHttp.class); + + try { + final Span span = spanWithScope.getSpan(); + + DECORATE.onResponse(span, result); + DECORATE.onError(span, throwable); + DECORATE.beforeFinish(span); + span.end(); + } finally { + spanWithScope.closeScope(); + } + } +} diff --git a/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpDecorator.java b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpDecorator.java new file mode 100644 index 0000000000..9032cd9bce --- /dev/null +++ b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpDecorator.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.opentelemetry.auto.instrumentation.khttp; + +import io.opentelemetry.OpenTelemetry; +import io.opentelemetry.auto.bootstrap.instrumentation.decorator.HttpClientDecorator; +import io.opentelemetry.trace.Tracer; +import java.net.URI; +import java.net.URISyntaxException; +import khttp.responses.Response; + +public class KHttpDecorator extends HttpClientDecorator { + public static final KHttpDecorator DECORATE = new KHttpDecorator(); + + public static final Tracer TRACER = + OpenTelemetry.getTracerProvider().get("io.opentelemetry.auto.khttp-0.1"); + + @Override + protected String method(RequestWrapper requestWrapper) { + return requestWrapper.method; + } + + @Override + protected URI url(RequestWrapper requestWrapper) throws URISyntaxException { + return new URI(requestWrapper.uri); + } + + @Override + protected Integer status(Response response) { + return response.getStatusCode(); + } +} diff --git a/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpHeadersInjectAdapter.java b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpHeadersInjectAdapter.java new file mode 100644 index 0000000000..1809f7127c --- /dev/null +++ b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpHeadersInjectAdapter.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.opentelemetry.auto.instrumentation.khttp; + +import io.opentelemetry.context.propagation.HttpTextFormat; +import java.util.HashMap; +import java.util.Map; + +public class KHttpHeadersInjectAdapter implements HttpTextFormat.Setter> { + + private static Class emptyMap; + + static { + try { + emptyMap = Class.forName("kotlin.collections.EmptyMap"); + } catch (ClassNotFoundException e) { + } + } + + public static Map asWritable(Map headers) { + // EmptyMap is read-only so we have to substitute it with writable instance to be able to inject + // headers + if (emptyMap != null && emptyMap.isInstance(headers)) { + return new HashMap<>(); + } else { + return headers; + } + } + + public static final KHttpHeadersInjectAdapter SETTER = new KHttpHeadersInjectAdapter(); + + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } +} diff --git a/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpInstrumentation.java b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpInstrumentation.java new file mode 100644 index 0000000000..20457d9922 --- /dev/null +++ b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/KHttpInstrumentation.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.opentelemetry.auto.instrumentation.khttp; + +import static io.opentelemetry.auto.tooling.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.auto.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import io.opentelemetry.auto.tooling.Instrumenter; +import java.util.Map; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public class KHttpInstrumentation extends Instrumenter.Default { + + public KHttpInstrumentation() { + super("khttp"); + } + + @Override + public ElementMatcher classLoaderMatcher() { + return hasClassesNamed("khttp.KHttp"); + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(named("khttp.KHttp")); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".KHttpHeadersInjectAdapter", + packageName + ".KHttpDecorator", + packageName + ".RequestWrapper", + }; + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(not(isAbstract())) + .and(named("request")) + .and(takesArgument(0, named("java.lang.String"))) + .and(takesArgument(1, named("java.lang.String"))) + .and(takesArgument(2, named("java.util.Map"))) + .and(returns(named("khttp.responses.Response"))), + packageName + ".KHttpAdvice"); + } +} diff --git a/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/RequestWrapper.java b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/RequestWrapper.java new file mode 100644 index 0000000000..d8221e5df6 --- /dev/null +++ b/instrumentation/khttp-0.1/src/main/java/io/opentelemetry/auto/instrumentation/khttp/RequestWrapper.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.opentelemetry.auto.instrumentation.khttp; + +public class RequestWrapper { + String method; + String uri; + + public RequestWrapper(String method, String uri) { + this.method = method; + this.uri = uri; + } +} diff --git a/instrumentation/khttp-0.1/src/test/groovy/KHttpClientTest.groovy b/instrumentation/khttp-0.1/src/test/groovy/KHttpClientTest.groovy new file mode 100644 index 0000000000..a1012aa0a6 --- /dev/null +++ b/instrumentation/khttp-0.1/src/test/groovy/KHttpClientTest.groovy @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import io.opentelemetry.auto.test.base.HttpClientTest +import khttp.KHttp + +class KHttpClientTest extends HttpClientTest { + + @Override + int doRequest(String method, URI uri, Map headers, Closure callback) { + def response = KHttp.request(method, uri.toString(), headers, Collections.emptyMap(), null, null, null, null, 1) + if (callback != null) { + callback.call() + } + return response.statusCode + } + + @Override + boolean testCircularRedirects() { + return false + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a104a78c79..436d846419 100644 --- a/settings.gradle +++ b/settings.gradle @@ -99,6 +99,7 @@ include ':instrumentation:jms-1.1' include ':instrumentation:jsp-2.3' include ':instrumentation:kafka-clients-0.11' include ':instrumentation:kafka-streams-0.11' +include ':instrumentation:khttp-0.1' include ':instrumentation:lettuce:lettuce-4.0' include ':instrumentation:lettuce:lettuce-5.0' include ':instrumentation:log4j:log4j-1.1'