Add Jodd-Http instrumentation (#7868)

This PR resolves #7629 

This adds javaagent instrumentation for the
[jodd-http](https://http.jodd.org/) `HttpRequest`.
It creates `Http Client Spans` and `Http Client Metrics`, the lowest
supported version is `org.jodd:jodd-http:4.2.0` (most recent: `6.3.0`),
since this is the first version of the library supporting java 8, having
follow-redirect capability and `HttpRequest#overwriteHeader()` method.
The instrumented method's signature and return type `HttpRequest#send()`
has not been modified since, and therefore the instrumentation works for
all `jodd-http` versions above `4.2.0`.

Since this is my first contribution/instrumentation, I orientated myself
on the `apache-httpclient-5.0` instrumentation, but obviously I would be
glad to get some feedback on this

---------

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>
This commit is contained in:
Phil 2023-02-23 16:54:13 +01:00 committed by GitHub
parent 6cb00d3de0
commit fad7b24253
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 467 additions and 1 deletions

View File

@ -77,6 +77,7 @@ These are the supported libraries and frameworks:
| [JDBC](https://docs.oracle.com/javase/8/docs/api/java/sql/package-summary.html) | Java 8+ | [opentelemetry-jdbc](../instrumentation/jdbc/library) | [Database Client Spans] |
| [Jedis](https://github.com/xetorthio/jedis) | 1.4+ | N/A | [Database Client Spans] |
| [JMS](https://javaee.github.io/javaee-spec/javadocs/javax/jms/package-summary.html) | 1.1+ | N/A | [Messaging Spans] |
| [Jodd Http](https://javadoc.io/doc/org.jodd/jodd-http/latest/index.html) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | none |
| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation |
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Server Spans], [HTTP Server Metrics] |

View File

@ -0,0 +1,19 @@
plugins {
id("otel.javaagent-instrumentation")
}
muzzle {
pass {
group.set("org.jodd")
module.set("jodd-http")
versions.set("[4.2.0,)")
}
}
dependencies {
// 4.2 is the first version with java 8, follow-redirects and HttpRequest#headerOverwrite method
library("org.jodd:jodd-http:4.2.0")
testImplementation(project(":instrumentation:jodd-http-4.2:javaagent"))
testImplementation(project(":instrumentation-api-semconv"))
}

View File

@ -0,0 +1,22 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2;
import io.opentelemetry.context.propagation.TextMapSetter;
import javax.annotation.Nullable;
import jodd.http.HttpRequest;
enum HttpHeaderSetter implements TextMapSetter<HttpRequest> {
INSTANCE;
@Override
public void set(@Nullable HttpRequest carrier, String key, String value) {
if (carrier == null) {
return;
}
carrier.headerOverwrite(key, value);
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_1_0;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_1_1;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_2_0;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_3_0;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
final class JoddHttpHttpAttributesGetter
implements HttpClientAttributesGetter<HttpRequest, HttpResponse> {
private static final Logger logger =
Logger.getLogger(JoddHttpHttpAttributesGetter.class.getName());
private static final Set<String> ALLOWED_HTTP_FLAVORS =
new HashSet<>(Arrays.asList(HTTP_1_0, HTTP_1_1, HTTP_2_0, HTTP_3_0));
@Override
public String getMethod(HttpRequest request) {
return request.method();
}
@Override
public String getUrl(HttpRequest request) {
return request.url();
}
@Override
public List<String> getRequestHeader(HttpRequest request, String name) {
return request.headers(name);
}
@Override
public Integer getStatusCode(
HttpRequest request, HttpResponse response, @Nullable Throwable error) {
return response.statusCode();
}
@Override
@Nullable
public String getFlavor(HttpRequest request, @Nullable HttpResponse response) {
String httpVersion = request.httpVersion();
if (httpVersion == null && response != null) {
httpVersion = response.httpVersion();
}
if (httpVersion != null) {
if (httpVersion.contains("/")) {
httpVersion = httpVersion.substring(httpVersion.lastIndexOf("/") + 1);
}
if (ALLOWED_HTTP_FLAVORS.contains(httpVersion)) {
return httpVersion;
}
}
logger.log(Level.FINE, "unexpected http protocol version: {0}", httpVersion);
return null;
}
@Override
public List<String> getResponseHeader(HttpRequest request, HttpResponse response, String name) {
return response.headers(name);
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2;
import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
import static io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2.JoddHttpSingletons.instrumenter;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
class JoddHttpInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("jodd.http.HttpRequest");
}
@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isMethod().and(named("send")).and(takesArguments(0)),
this.getClass().getName() + "$RequestAdvice");
}
@SuppressWarnings("unused")
public static class RequestAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.This HttpRequest request,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
Context parentContext = currentContext();
if (!instrumenter().shouldStart(parentContext, request)) {
return;
}
context = instrumenter().start(parentContext, request);
scope = context.makeCurrent();
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.This HttpRequest request,
@Advice.Return HttpResponse response,
@Advice.Thrown Throwable throwable,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
if (scope == null) {
return;
}
scope.close();
instrumenter().end(context, request, response, throwable);
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.Collections;
import java.util.List;
@AutoService(InstrumentationModule.class)
public class JoddHttpInstrumentationModule extends InstrumentationModule {
public JoddHttpInstrumentationModule() {
super("jodd-http", "jodd-http-4.2");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(new JoddHttpInstrumentation());
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2;
import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import javax.annotation.Nullable;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
final class JoddHttpNetAttributesGetter
implements NetClientAttributesGetter<HttpRequest, HttpResponse> {
@Override
public String getTransport(HttpRequest request, @Nullable HttpResponse response) {
return SemanticAttributes.NetTransportValues.IP_TCP;
}
@Override
@Nullable
public String getPeerName(HttpRequest request) {
return request.host();
}
@Override
public Integer getPeerPort(HttpRequest request) {
return request.port();
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor;
import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
public final class JoddHttpSingletons {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jodd-http-4.2";
private static final Instrumenter<HttpRequest, HttpResponse> INSTRUMENTER;
static {
JoddHttpHttpAttributesGetter httpAttributesGetter = new JoddHttpHttpAttributesGetter();
JoddHttpNetAttributesGetter netAttributesGetter = new JoddHttpNetAttributesGetter();
INSTRUMENTER =
Instrumenter.<HttpRequest, HttpResponse>builder(
GlobalOpenTelemetry.get(),
INSTRUMENTATION_NAME,
HttpSpanNameExtractor.create(httpAttributesGetter))
.setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter))
.addAttributesExtractor(
HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter)
.setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders())
.setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders())
.build())
.addAttributesExtractor(
PeerServiceAttributesExtractor.create(
netAttributesGetter, CommonConfig.get().getPeerServiceMapping()))
.addOperationMetrics(HttpClientMetrics.get())
.buildClientInstrumenter(HttpHeaderSetter.INSTANCE);
}
public static Instrumenter<HttpRequest, HttpResponse> instrumenter() {
return INSTRUMENTER;
}
private JoddHttpSingletons() {}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2;
import static jodd.http.HttpStatus.HTTP_FORBIDDEN;
import static jodd.http.HttpStatus.HTTP_INTERNAL_ERROR;
import static jodd.http.HttpStatus.HTTP_NOT_FOUND;
import static jodd.http.HttpStatus.HTTP_OK;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues;
import java.util.Arrays;
import java.util.List;
import jodd.http.HttpBase;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import org.junit.jupiter.api.Test;
class JoddHttpHttpAttributesGetterTest {
private static final JoddHttpHttpAttributesGetter attributesGetter =
new JoddHttpHttpAttributesGetter();
@Test
void getMethod() {
for (String method : Arrays.asList("GET", "PUT", "POST", "PATCH")) {
assertEquals(method, attributesGetter.getMethod(new HttpRequest().method(method)));
}
}
@Test
void getUrl() {
HttpRequest request =
HttpRequest.get("/test/subpath")
.host("test.com")
.query("param1", "val1")
.query("param2", "val1")
.query("param2", "val2");
assertEquals(
"http://test.com/test/subpath?param1=val1&param2=val1&param2=val2",
attributesGetter.getUrl(request));
}
@Test
void getRequestHeader() {
HttpRequest request =
HttpRequest.get("/test")
.header("single", "val1")
.header("multiple", "val1")
.header("multiple", "val2");
List<String> headerVals = attributesGetter.getRequestHeader(request, "single");
assertEquals(1, headerVals.size());
assertEquals("val1", headerVals.get(0));
headerVals = attributesGetter.getRequestHeader(request, "multiple");
assertEquals(2, headerVals.size());
assertEquals("val1", headerVals.get(0));
assertEquals("val2", headerVals.get(1));
headerVals = attributesGetter.getRequestHeader(request, "not-existing");
assertEquals(0, headerVals.size());
}
@Test
void getStatusCode() {
for (Integer code :
Arrays.asList(HTTP_OK, HTTP_FORBIDDEN, HTTP_INTERNAL_ERROR, HTTP_NOT_FOUND)) {
assertEquals(
code, attributesGetter.getStatusCode(null, new HttpResponse().statusCode(code), null));
}
}
@Test
void getFlavor() {
HttpRequest request = HttpRequest.get("/test").httpVersion(HttpBase.HTTP_1_1);
assertEquals(HttpFlavorValues.HTTP_1_1, attributesGetter.getFlavor(request, null));
request.httpVersion(null);
assertNull(attributesGetter.getFlavor(request, null));
request.httpVersion("INVALID-HTTP-Version");
assertNull(attributesGetter.getFlavor(request, null));
request.httpVersion(null);
HttpResponse response = new HttpResponse().httpVersion(HttpBase.HTTP_1_0);
assertEquals(HttpFlavorValues.HTTP_1_0, attributesGetter.getFlavor(request, response));
response.httpVersion(null);
assertNull(attributesGetter.getFlavor(request, response));
}
@Test
void getResponseHeader() {
HttpResponse response =
new HttpResponse()
.header("single", "val1")
.header("multiple", "val1")
.header("multiple", "val2");
List<String> headerVals = attributesGetter.getResponseHeader(null, response, "single");
assertEquals(1, headerVals.size());
assertEquals("val1", headerVals.get(0));
headerVals = attributesGetter.getResponseHeader(null, response, "multiple");
assertEquals(2, headerVals.size());
assertEquals("val1", headerVals.get(0));
assertEquals("val2", headerVals.get(1));
headerVals = attributesGetter.getResponseHeader(null, response, "not-existing");
assertEquals(0, headerVals.size());
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
import java.net.URI;
import java.util.Map;
import jodd.http.HttpRequest;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.extension.RegisterExtension;
public class JoddHttpTest extends AbstractHttpClientTest<HttpRequest> {
@RegisterExtension
static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent();
@Nullable
@Override
protected String userAgent() {
return "Jodd HTTP";
}
@Override
public HttpRequest buildRequest(String method, URI uri, Map<String, String> headers) {
HttpRequest request =
new HttpRequest()
.method(method)
.set(uri.toString())
.followRedirects(true)
.connectionKeepAlive(true)
.header("user-agent", userAgent());
for (Map.Entry<String, String> header : headers.entrySet()) {
request.headerOverwrite(header.getKey(), header.getValue());
}
if (uri.toString().contains("/read-timeout")) {
request.timeout((int) READ_TIMEOUT.toMillis());
}
return request;
}
@Override
public int sendRequest(HttpRequest request, String method, URI uri, Map<String, String> headers)
throws Exception {
request.method(method).set(uri.toString());
for (Map.Entry<String, String> header : headers.entrySet()) {
request.headerOverwrite(header.getKey(), header.getValue());
}
return request.send().statusCode();
}
@Override
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.enableTestReadTimeout();
optionsBuilder.disableTestCallback();
// Circular Redirects are not explicitly handled by jodd-http
optionsBuilder.disableTestCircularRedirects();
}
}

View File

@ -300,6 +300,7 @@ hideFromDependabot(":instrumentation:jms:jms-common:javaagent")
hideFromDependabot(":instrumentation:jms:jms-common:javaagent-unit-tests")
hideFromDependabot(":instrumentation:jmx-metrics:javaagent")
hideFromDependabot(":instrumentation:jmx-metrics:library")
hideFromDependabot(":instrumentation:jodd-http-4.2:javaagent")
hideFromDependabot(":instrumentation:jsf:jsf-javax-common:javaagent")
hideFromDependabot(":instrumentation:jsf:jsf-javax-common:testing")
hideFromDependabot(":instrumentation:jsf:jsf-jakarta-common:javaagent")