Convert undertow instrumentation to instrumenter api
MODIFY
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/EndSpanListener.java
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/EndSpanListener.java
package io.opentelemetry.javaagent.instrumentation.undertow;
-import static io.opentelemetry.javaagent.instrumentation.undertow.UndertowHttpServerTracer.tracer;
+import static io.opentelemetry.javaagent.instrumentation.undertow.UndertowSingletons.helper;
import io.opentelemetry.context.Context;
import io.undertow.server.ExchangeCompletionListener;
@@ -20,7 +20,7 @@ public class EndSpanListener implements ExchangeCompletionListener {
@Override
public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) {
- tracer().exchangeCompleted(context, exchange);
+ helper().exchangeCompleted(context, exchange);
nextListener.proceed();
}
}

MODIFY
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java
@@ -7,7 +7,7 @@ package io.opentelemetry.javaagent.instrumentation.undertow;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
-import static io.opentelemetry.javaagent.instrumentation.undertow.UndertowHttpServerTracer.tracer;
+import static io.opentelemetry.javaagent.instrumentation.undertow.UndertowSingletons.helper;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
@@ -51,18 +51,23 @@ public class HandlerInstrumentation implements TypeInstrumentation {
@Advice.Argument(value = 0, readOnly = false) HttpServerExchange exchange,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
- Context attachedContext = tracer().getServerContext(exchange);
+ Context attachedContext = helper().getServerContext(exchange);
if (attachedContext != null) {
if (!Java8BytecodeBridge.currentContext().equals(attachedContext)) {
// request processing is dispatched to another thread
scope = attachedContext.makeCurrent();
context = attachedContext;
- tracer().handlerStarted(attachedContext);
+ helper().handlerStarted(attachedContext);
}
return;
}
- context = tracer().startServerSpan(exchange);
+ Context parentContext = Java8BytecodeBridge.currentContext();
+ if (!helper().shouldStart(parentContext, exchange)) {
+ return;
+ }
+
+ context = helper().start(parentContext, exchange);
scope = context.makeCurrent();
exchange.addExchangeCompleteListener(new EndSpanListener(context));
@@ -79,7 +84,7 @@ public class HandlerInstrumentation implements TypeInstrumentation {
}
scope.close();
- tracer().handlerCompleted(context, throwable, exchange);
+ helper().handlerCompleted(context, throwable, exchange);
}
}
}

ADD
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHelper.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.undertow;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.servlet.AppServerBridge;
+import io.opentelemetry.javaagent.bootstrap.undertow.KeyHolder;
+import io.opentelemetry.javaagent.bootstrap.undertow.UndertowActiveHandlers;
+import io.undertow.server.DefaultResponseListener;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.AttachmentKey;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public class UndertowHelper {
+ private final Instrumenter<HttpServerExchange, HttpServerExchange> instrumenter;
+
+ UndertowHelper(Instrumenter<HttpServerExchange, HttpServerExchange> instrumenter) {
+ this.instrumenter = instrumenter;
+ }
+
+ public boolean shouldStart(Context parentContext, HttpServerExchange exchange) {
+ return instrumenter.shouldStart(parentContext, exchange);
+ }
+
+ public Context start(Context parentContext, HttpServerExchange exchange) {
+ Context context = instrumenter.start(parentContext, exchange);
+ attachServerContext(context, exchange);
+
+ return context;
+ }
+
+ public void end(Context context, HttpServerExchange exchange, @Nullable Throwable error) {
+ if (error == null) {
+ error = AppServerBridge.getException(context);
+ }
+
+ instrumenter.end(context, exchange, exchange, error);
+ }
+
+ public void handlerStarted(Context context) {
+ // request was dispatched to a new thread, handler on the original thread
+ // may exit before this one so we need to wait for this handler to complete
+ // before ending span
+ UndertowActiveHandlers.increment(context);
+ }
+
+ public void handlerCompleted(Context context, Throwable throwable, HttpServerExchange exchange) {
+ // end the span when this is the last handler to complete and exchange has
+ // been completed
+ if (UndertowActiveHandlers.decrementAndGet(context) == 0) {
+ end(context, exchange, throwable);
+ }
+ }
+
+ public void exchangeCompleted(Context context, HttpServerExchange exchange) {
+ // after exchange is completed we can read response status
+ // if all handlers have completed we can end the span, if there are running
+ // handlers we'll end the span when last handler exits
+ if (UndertowActiveHandlers.decrementAndGet(context) == 0) {
+ Throwable throwable = exchange.getAttachment(DefaultResponseListener.EXCEPTION);
+ end(context, exchange, throwable);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public Context getServerContext(HttpServerExchange exchange) {
+ AttachmentKey<Context> contextKey =
+ (AttachmentKey<Context>) KeyHolder.contextKeys.get(AttachmentKey.class);
+ if (contextKey == null) {
+ return null;
+ }
+ return exchange.getAttachment(contextKey);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void attachServerContext(Context context, HttpServerExchange exchange) {
+ AttachmentKey<Context> contextKey =
+ (AttachmentKey<Context>)
+ KeyHolder.contextKeys.computeIfAbsent(
+ AttachmentKey.class, key -> AttachmentKey.create(Context.class));
+ exchange.putAttachment(contextKey, context);
+ }
+}

ADD
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.undertow;
+
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor;
+import io.undertow.server.HttpServerExchange;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public class UndertowHttpAttributesExtractor
+ extends HttpServerAttributesExtractor<HttpServerExchange, HttpServerExchange> {
+
+ @Override
+ protected String method(HttpServerExchange exchange) {
+ return exchange.getRequestMethod().toString();
+ }
+
+ @Override
+ protected @Nullable String userAgent(HttpServerExchange exchange) {
+ return exchange.getRequestHeaders().getFirst("User-Agent");
+ }
+
+ @Override
+ protected @Nullable Long requestContentLength(
+ HttpServerExchange exchange, @Nullable HttpServerExchange unused) {
+ long requestContentLength = exchange.getRequestContentLength();
+ return requestContentLength != -1 ? requestContentLength : null;
+ }
+
+ @Override
+ protected @Nullable Long requestContentLengthUncompressed(
+ HttpServerExchange exchange, @Nullable HttpServerExchange unused) {
+ return null;
+ }
+
+ @Override
+ protected String flavor(HttpServerExchange exchange, @Nullable HttpServerExchange unused) {
+ String flavor = exchange.getProtocol().toString();
+ // remove HTTP/ prefix to comply with semantic conventions
+ if (flavor.startsWith("HTTP/")) {
+ flavor = flavor.substring("HTTP/".length());
+ }
+ return flavor;
+ }
+
+ @Override
+ protected Integer statusCode(HttpServerExchange exchange, HttpServerExchange unused) {
+ return exchange.getStatusCode();
+ }
+
+ @Override
+ protected @Nullable Long responseContentLength(
+ HttpServerExchange exchange, HttpServerExchange unused) {
+ long responseContentLength = exchange.getResponseContentLength();
+ return responseContentLength != -1 ? responseContentLength : null;
+ }
+
+ @Override
+ protected @Nullable Long responseContentLengthUncompressed(
+ HttpServerExchange exchange, HttpServerExchange unused) {
+ return null;
+ }
+
+ @Override
+ protected String url(HttpServerExchange exchange) {
+ String result = exchange.getRequestURL();
+ if (exchange.getQueryString() == null || exchange.getQueryString().isEmpty()) {
+ return result;
+ } else {
+ return result + "?" + exchange.getQueryString();
+ }
+ }
+
+ @Override
+ protected @Nullable String target(HttpServerExchange exchange) {
+ String requestPath = exchange.getRequestPath();
+ String queryString = exchange.getQueryString();
+ if (requestPath != null && queryString != null && !queryString.isEmpty()) {
+ return requestPath + "?" + queryString;
+ }
+ return requestPath;
+ }
+
+ @Override
+ protected @Nullable String host(HttpServerExchange exchange) {
+ return exchange.getHostAndPort();
+ }
+
+ @Override
+ protected @Nullable String scheme(HttpServerExchange exchange) {
+ return exchange.getRequestScheme();
+ }
+
+ @Override
+ protected @Nullable String route(HttpServerExchange exchange) {
+ return null;
+ }
+
+ @Override
+ protected @Nullable String serverName(
+ HttpServerExchange exchange, @Nullable HttpServerExchange unused) {
+ return null;
+ }
+}

DELETE
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpServerTracer.java
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.undertow;
-
-import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTAINER;
-
-import io.opentelemetry.context.Context;
-import io.opentelemetry.context.propagation.TextMapGetter;
-import io.opentelemetry.instrumentation.api.servlet.AppServerBridge;
-import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming;
-import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer;
-import io.opentelemetry.javaagent.bootstrap.undertow.KeyHolder;
-import io.opentelemetry.javaagent.bootstrap.undertow.UndertowActiveHandlers;
-import io.undertow.server.DefaultResponseListener;
-import io.undertow.server.HttpServerExchange;
-import io.undertow.util.AttachmentKey;
-import java.net.InetSocketAddress;
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-public class UndertowHttpServerTracer
- extends HttpServerTracer<
- HttpServerExchange, HttpServerExchange, HttpServerExchange, HttpServerExchange> {
- private static final UndertowHttpServerTracer TRACER = new UndertowHttpServerTracer();
-
- public static UndertowHttpServerTracer tracer() {
- return TRACER;
- }
-
- @Override
- protected String getInstrumentationName() {
- return "io.opentelemetry.undertow-1.4";
- }
-
- public Context startServerSpan(HttpServerExchange exchange) {
- return startSpan(
- exchange, exchange, exchange, "HTTP " + exchange.getRequestMethod().toString());
- }
-
- @Override
- protected Context customizeContext(Context context, HttpServerExchange exchange) {
- context = ServerSpanNaming.init(context, CONTAINER);
- // span is ended when counter reaches 0, we start from 2 which accounts for the
- // handler that started the span and exchange completion listener
- context = UndertowActiveHandlers.init(context, 2);
- return AppServerBridge.init(context);
- }
-
- public void handlerStarted(Context context) {
- // request was dispatched to a new thread, handler on the original thread
- // may exit before this one so we need to wait for this handler to complete
- // before ending span
- UndertowActiveHandlers.increment(context);
- }
-
- public void handlerCompleted(Context context, Throwable throwable, HttpServerExchange exchange) {
- // end the span when this is the last handler to complete and exchange has
- // been completed
- if (UndertowActiveHandlers.decrementAndGet(context) == 0) {
- endSpan(context, throwable, exchange);
- }
- }
-
- public void exchangeCompleted(Context context, HttpServerExchange exchange) {
- // after exchange is completed we can read response status
- // if all handlers have completed we can end the span, if there are running
- // handlers we'll end the span when last handler exits
- if (UndertowActiveHandlers.decrementAndGet(context) == 0) {
- Throwable throwable = exchange.getAttachment(DefaultResponseListener.EXCEPTION);
- endSpan(context, throwable, exchange);
- }
- }
-
- private static void endSpan(Context context, Throwable throwable, HttpServerExchange exchange) {
- if (throwable == null) {
- throwable = AppServerBridge.getException(context);
- }
-
- if (throwable != null) {
- tracer().endExceptionally(context, throwable, exchange);
- } else {
- tracer().end(context, exchange);
- }
- }
-
- @SuppressWarnings("unchecked")
- @Override
- @Nullable
- public Context getServerContext(HttpServerExchange exchange) {
- AttachmentKey<Context> contextKey =
- (AttachmentKey<Context>) KeyHolder.contextKeys.get(AttachmentKey.class);
- if (contextKey == null) {
- return null;
- }
- return exchange.getAttachment(contextKey);
- }
-
- @Override
- @Nullable
- protected Integer peerPort(HttpServerExchange exchange) {
- InetSocketAddress peerAddress =
- exchange.getConnection().getPeerAddress(InetSocketAddress.class);
- return peerAddress.getPort();
- }
-
- @Override
- @Nullable
- protected String peerHostIp(HttpServerExchange exchange) {
- InetSocketAddress peerAddress =
- exchange.getConnection().getPeerAddress(InetSocketAddress.class);
- return peerAddress.getHostString();
- }
-
- @Override
- protected String flavor(HttpServerExchange exchange, HttpServerExchange exchange2) {
- return exchange.getProtocol().toString();
- }
-
- @Override
- protected TextMapGetter<HttpServerExchange> getGetter() {
- return UndertowExchangeGetter.GETTER;
- }
-
- @Override
- protected String url(HttpServerExchange exchange) {
- String result = exchange.getRequestURL();
- if (exchange.getQueryString() == null || exchange.getQueryString().isEmpty()) {
- return result;
- } else {
- return result + "?" + exchange.getQueryString();
- }
- }
-
- @Override
- protected String method(HttpServerExchange exchange) {
- return exchange.getRequestMethod().toString();
- }
-
- @Override
- @Nullable
- protected String requestHeader(HttpServerExchange exchange, String name) {
- return exchange.getRequestHeaders().getFirst(name);
- }
-
- @Override
- protected int responseStatus(HttpServerExchange exchange) {
- return exchange.getStatusCode();
- }
-
- @SuppressWarnings("unchecked")
- @Override
- protected void attachServerContext(Context context, HttpServerExchange exchange) {
- AttachmentKey<Context> contextKey =
- (AttachmentKey<Context>)
- KeyHolder.contextKeys.computeIfAbsent(
- AttachmentKey.class, key -> AttachmentKey.create(Context.class));
- exchange.putAttachment(contextKey, context);
- }
-}

ADD
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowNetAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.undertow;
+
+import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import io.undertow.server.HttpServerExchange;
+import java.net.InetSocketAddress;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public class UndertowNetAttributesExtractor
+ extends InetSocketAddressNetAttributesExtractor<HttpServerExchange, HttpServerExchange> {
+
+ @Override
+ public @Nullable InetSocketAddress getAddress(
+ HttpServerExchange exchange, @Nullable HttpServerExchange unused) {
+ return exchange.getConnection().getPeerAddress(InetSocketAddress.class);
+ }
+
+ @Override
+ public String transport(HttpServerExchange exchange) {
+ return SemanticAttributes.NetTransportValues.IP_TCP;
+ }
+}

ADD
instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowSingletons.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.undertow;
+
+import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTAINER;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor;
+import io.opentelemetry.instrumentation.api.servlet.AppServerBridge;
+import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming;
+import io.opentelemetry.javaagent.bootstrap.undertow.UndertowActiveHandlers;
+import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor;
+import io.undertow.server.HttpServerExchange;
+
+public final class UndertowSingletons {
+ private static final String INSTRUMENTATION_NAME = "io.opentelemetry.undertow-1.4";
+
+ private static final Instrumenter<HttpServerExchange, HttpServerExchange> INSTRUMENTER;
+
+ static {
+ HttpServerAttributesExtractor<HttpServerExchange, HttpServerExchange> httpAttributesExtractor =
+ new UndertowHttpAttributesExtractor();
+ SpanNameExtractor<HttpServerExchange> spanNameExtractor =
+ HttpSpanNameExtractor.create(httpAttributesExtractor);
+ SpanStatusExtractor<HttpServerExchange, HttpServerExchange> spanStatusExtractor =
+ HttpSpanStatusExtractor.create(httpAttributesExtractor);
+ NetAttributesExtractor<HttpServerExchange, HttpServerExchange> netAttributesExtractor =
+ new UndertowNetAttributesExtractor();
+
+ INSTRUMENTER =
+ Instrumenter.<HttpServerExchange, HttpServerExchange>newBuilder(
+ GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor)
+ .setSpanStatusExtractor(spanStatusExtractor)
+ .addAttributesExtractor(httpAttributesExtractor)
+ .addAttributesExtractor(netAttributesExtractor)
+ .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor))
+ .addContextCustomizer(
+ (context, request, attributes) -> {
+ context = ServerSpanNaming.init(context, CONTAINER);
+ // span is ended when counter reaches 0, we start from 2 which accounts for the
+ // handler that started the span and exchange completion listener
+ context = UndertowActiveHandlers.init(context, 2);
+ return AppServerBridge.init(context);
+ })
+ .addRequestMetrics(HttpServerMetrics.get())
+ .newServerInstrumenter(UndertowExchangeGetter.GETTER);
+ }
+
+ private static final UndertowHelper HELPER = new UndertowHelper(INSTRUMENTER);
+
+ public static UndertowHelper helper() {
+ return HELPER;
+ }
+
+ private UndertowSingletons() {}
+}

MODIFY
instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy
instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy
* SPDX-License-Identifier: Apache-2.0
*/
+import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.api.trace.StatusCode
@@ -93,6 +94,18 @@ class UndertowServerTest extends HttpServerTest<Undertow> implements AgentTestTr
return "HTTP GET"
}
+ @Override
+ List<AttributeKey<?>> extraAttributes() {
+ [
+ SemanticAttributes.HTTP_HOST,
+ SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
+ SemanticAttributes.HTTP_SCHEME,
+ SemanticAttributes.HTTP_TARGET,
+ SemanticAttributes.NET_PEER_NAME,
+ SemanticAttributes.NET_TRANSPORT
+ ]
+ }
+
def "test send response"() {
setup:
def uri = address.resolve("sendResponse")
@@ -126,6 +139,13 @@ class UndertowServerTest extends HttpServerTest<Undertow> implements AgentTestTr
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
"${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT
+ "${SemanticAttributes.HTTP_HOST}" "localhost:${port}"
+ "${SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH}" Long
+ "${SemanticAttributes.HTTP_SCHEME}" "http"
+ "${SemanticAttributes.HTTP_TARGET}" "/sendResponse"
+ // "localhost" on linux, "127.0.0.1" on windows
+ "${SemanticAttributes.NET_PEER_NAME.key}" { it == "localhost" || it == "127.0.0.1" }
+ "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP
}
}
span(1) {
@@ -172,6 +192,13 @@ class UndertowServerTest extends HttpServerTest<Undertow> implements AgentTestTr
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
"${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT
+ "${SemanticAttributes.HTTP_HOST}" "localhost:${port}"
+ "${SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH}" Long
+ "${SemanticAttributes.HTTP_SCHEME}" "http"
+ "${SemanticAttributes.HTTP_TARGET}" "/sendResponseWithException"
+ // "localhost" on linux, "127.0.0.1" on windows
+ "${SemanticAttributes.NET_PEER_NAME.key}" { it == "localhost" || it == "127.0.0.1" }
+ "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP
}
}
span(1) {

Convert undertow instrumentation to instrumenter api (#4239)