Skip to content

Commit 8e09d59

Browse files
authored
feat: Add OpenTelemetry instrumentation for ActiveJ HTTP server (#13335)
1 parent 5f41b7f commit 8e09d59

File tree

11 files changed

+491
-0
lines changed

11 files changed

+491
-0
lines changed

.fossa.yml

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ targets:
4949
- type: gradle
5050
path: ./
5151
target: ':testing:agent-for-testing'
52+
- type: gradle
53+
path: ./
54+
target: ':instrumentation:activej-http-6.0:javaagent'
5255
- type: gradle
5356
path: ./
5457
target: ':instrumentation:alibaba-druid-1.0:javaagent'

docs/supported-libraries.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ These are the supported libraries and frameworks:
1919

2020
| Library/Framework | Auto-instrumented versions | Standalone Library Instrumentation [1] | Semantic Conventions |
2121
|---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
22+
| [ActiveJ](https://activej.io/) | 6.0+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] |
2223
| [Akka Actors](https://doc.akka.io/docs/akka/current/typed/index.html) | 2.3+ | N/A | Context propagation |
2324
| [Akka HTTP](https://doc.akka.io/docs/akka-http/current/index.html) | 10.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics], Provides `http.route` [2] |
2425
| [Alibaba Druid](https://github.com/alibaba/druid) | 1.0+ | [opentelemetry-alibaba-druid-1.0](../instrumentation/alibaba-druid-1.0/library) | [Database Pool Metrics] |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
id("otel.javaagent-instrumentation")
3+
}
4+
5+
muzzle {
6+
pass {
7+
group.set("io.activej")
8+
module.set("activej-http")
9+
versions.set("[6.0,)")
10+
assertInverse.set(true)
11+
}
12+
}
13+
14+
dependencies {
15+
library("io.activej:activej-http:6.0-rc2")
16+
latestDepTestLibrary("io.activej:activej-http:6.+") // documented limitation, can be removed when there is a non rc version in 6.x series
17+
}
18+
19+
otelJava {
20+
minJavaVersionSupported.set(JavaVersion.VERSION_17)
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
7+
8+
import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
9+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
10+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
11+
import static io.opentelemetry.javaagent.instrumentation.activejhttp.ActivejHttpServerConnectionSingletons.instrumenter;
12+
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
13+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
14+
import static net.bytebuddy.matcher.ElementMatchers.named;
15+
import static net.bytebuddy.matcher.ElementMatchers.not;
16+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
17+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
18+
19+
import io.activej.http.AsyncServlet;
20+
import io.activej.http.HttpRequest;
21+
import io.activej.http.HttpResponse;
22+
import io.activej.promise.Promise;
23+
import io.opentelemetry.context.Context;
24+
import io.opentelemetry.context.Scope;
25+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
26+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
27+
import net.bytebuddy.asm.Advice;
28+
import net.bytebuddy.description.type.TypeDescription;
29+
import net.bytebuddy.matcher.ElementMatcher;
30+
31+
public class ActivejHttpServerConnectionInstrumentation implements TypeInstrumentation {
32+
33+
@Override
34+
public ElementMatcher<TypeDescription> typeMatcher() {
35+
return hasSuperType(named("io.activej.http.AsyncServlet")).and(not(isInterface()));
36+
}
37+
38+
@Override
39+
public ElementMatcher<ClassLoader> classLoaderOptimization() {
40+
return hasClassesNamed("io.activej.http.AsyncServlet");
41+
}
42+
43+
@Override
44+
public void transform(TypeTransformer transformer) {
45+
transformer.applyAdviceToMethod(
46+
isMethod()
47+
.and(named("serve"))
48+
.and(takesArguments(1).and(takesArgument(0, named("io.activej.http.HttpRequest")))),
49+
this.getClass().getName() + "$ServeAdvice");
50+
}
51+
52+
@SuppressWarnings("unused")
53+
public static class ServeAdvice {
54+
55+
@Advice.OnMethodEnter(suppress = Throwable.class)
56+
public static void methodEnter(
57+
@Advice.This AsyncServlet asyncServlet,
58+
@Advice.Argument(0) HttpRequest request,
59+
@Advice.Local("otelContext") Context context,
60+
@Advice.Local("otelScope") Scope scope,
61+
@Advice.Local("httpRequest") HttpRequest httpRequest) {
62+
Context parentContext = currentContext();
63+
httpRequest = request;
64+
if (!instrumenter().shouldStart(parentContext, request)) {
65+
return;
66+
}
67+
context = instrumenter().start(parentContext, request);
68+
scope = context.makeCurrent();
69+
}
70+
71+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
72+
public static void methodExit(
73+
@Advice.This AsyncServlet asyncServlet,
74+
@Advice.Return(readOnly = false) Promise<HttpResponse> responsePromise,
75+
@Advice.Thrown Throwable throwable,
76+
@Advice.Local("otelContext") Context context,
77+
@Advice.Local("otelScope") Scope scope,
78+
@Advice.Local("httpRequest") HttpRequest httpRequest) {
79+
if (scope == null) {
80+
return;
81+
}
82+
scope.close();
83+
if (throwable != null) {
84+
instrumenter().end(context, httpRequest, null, throwable);
85+
} else {
86+
responsePromise = PromiseWrapper.wrap(responsePromise, httpRequest, context);
87+
}
88+
}
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
7+
8+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
9+
import static java.util.Collections.singletonList;
10+
11+
import com.google.auto.service.AutoService;
12+
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
14+
import java.util.List;
15+
import net.bytebuddy.matcher.ElementMatcher;
16+
17+
@AutoService(InstrumentationModule.class)
18+
public class ActivejHttpServerConnectionInstrumentationModule extends InstrumentationModule {
19+
20+
public ActivejHttpServerConnectionInstrumentationModule() {
21+
super("activej-http", "activej-http-6.0");
22+
}
23+
24+
@Override
25+
public List<TypeInstrumentation> typeInstrumentations() {
26+
return singletonList(new ActivejHttpServerConnectionInstrumentation());
27+
}
28+
29+
@Override
30+
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
31+
// class which was added in 6.0, the minimum version we support.
32+
return hasClassesNamed("io.activej.http.HttpServer");
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
7+
8+
import io.activej.http.HttpRequest;
9+
import io.activej.http.HttpResponse;
10+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
11+
import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpServerInstrumenters;
12+
13+
public final class ActivejHttpServerConnectionSingletons {
14+
15+
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.activej-http-6.0";
16+
17+
private static final Instrumenter<HttpRequest, HttpResponse> INSTRUMENTER;
18+
19+
static {
20+
INSTRUMENTER =
21+
JavaagentHttpServerInstrumenters.create(
22+
INSTRUMENTATION_NAME,
23+
new ActivejHttpServerHttpAttributesGetter(),
24+
ActivejHttpServerRequestGetter.INSTANCE);
25+
}
26+
27+
public static Instrumenter<HttpRequest, HttpResponse> instrumenter() {
28+
return INSTRUMENTER;
29+
}
30+
31+
private ActivejHttpServerConnectionSingletons() {}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
7+
8+
import io.activej.http.HttpHeader;
9+
import io.activej.http.HttpHeaderValue;
10+
import io.activej.http.HttpHeaders;
11+
import io.activej.http.HttpRequest;
12+
import io.activej.http.HttpResponse;
13+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter;
14+
import java.net.InetAddress;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.Map;
18+
import javax.annotation.Nullable;
19+
20+
final class ActivejHttpServerHttpAttributesGetter
21+
implements HttpServerAttributesGetter<HttpRequest, HttpResponse> {
22+
23+
@Override
24+
public String getHttpRequestMethod(HttpRequest request) {
25+
return request.getMethod().name();
26+
}
27+
28+
@Override
29+
public List<String> getHttpRequestHeader(HttpRequest request, String name) {
30+
HttpHeader httpHeader = HttpHeaders.of(name);
31+
List<String> values = new ArrayList<>();
32+
for (Map.Entry<HttpHeader, HttpHeaderValue> entry : request.getHeaders()) {
33+
if (httpHeader.equals(entry.getKey())) {
34+
values.add(entry.getValue().toString());
35+
}
36+
}
37+
38+
return values;
39+
}
40+
41+
@Override
42+
public Integer getHttpResponseStatusCode(
43+
HttpRequest request, HttpResponse httpResponse, @Nullable Throwable error) {
44+
return httpResponse.getCode();
45+
}
46+
47+
@Override
48+
public List<String> getHttpResponseHeader(
49+
HttpRequest request, HttpResponse httpResponse, String name) {
50+
HttpHeader httpHeader = HttpHeaders.of(name);
51+
List<String> values = new ArrayList<>();
52+
for (Map.Entry<HttpHeader, HttpHeaderValue> entry : httpResponse.getHeaders()) {
53+
if (httpHeader.equals(entry.getKey())) {
54+
values.add(entry.getValue().toString());
55+
}
56+
}
57+
58+
return values;
59+
}
60+
61+
@Override
62+
public String getUrlScheme(HttpRequest request) {
63+
return request.getProtocol().lowercase();
64+
}
65+
66+
@Override
67+
public String getUrlPath(HttpRequest request) {
68+
return request.getPath();
69+
}
70+
71+
@Override
72+
public String getUrlQuery(HttpRequest request) {
73+
return request.getQuery();
74+
}
75+
76+
@Override
77+
public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse httpResponse) {
78+
return switch (request.getVersion()) {
79+
case HTTP_0_9, HTTP_1_0, HTTP_1_1, HTTP_2_0 -> "http";
80+
default -> null;
81+
};
82+
}
83+
84+
@Override
85+
public String getNetworkProtocolVersion(
86+
HttpRequest request, @Nullable HttpResponse httpResponse) {
87+
return switch (request.getVersion()) {
88+
case HTTP_0_9 -> "0.9";
89+
case HTTP_1_0 -> "1.0";
90+
case HTTP_1_1 -> "1.1";
91+
case HTTP_2_0 -> "2";
92+
default -> null;
93+
};
94+
}
95+
96+
@Nullable
97+
@Override
98+
public String getNetworkPeerAddress(HttpRequest request, @Nullable HttpResponse httpResponse) {
99+
InetAddress remoteAddress = request.getConnection().getRemoteAddress();
100+
return remoteAddress != null ? remoteAddress.getHostAddress() : null;
101+
}
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
7+
8+
import io.activej.http.HttpHeader;
9+
import io.activej.http.HttpHeaderValue;
10+
import io.activej.http.HttpHeaders;
11+
import io.activej.http.HttpRequest;
12+
import io.opentelemetry.context.propagation.internal.ExtendedTextMapGetter;
13+
import java.util.ArrayList;
14+
import java.util.Collections;
15+
import java.util.Iterator;
16+
import java.util.List;
17+
import java.util.Map;
18+
19+
enum ActivejHttpServerRequestGetter implements ExtendedTextMapGetter<HttpRequest> {
20+
INSTANCE;
21+
22+
@Override
23+
public Iterable<String> keys(HttpRequest httpRequest) {
24+
return httpRequest.getHeaders().stream().map(h -> h.getKey().toString()).toList();
25+
}
26+
27+
@Override
28+
public String get(HttpRequest carrier, String key) {
29+
if (carrier == null) {
30+
return null;
31+
}
32+
33+
return carrier.getHeader(HttpHeaders.of(key));
34+
}
35+
36+
@Override
37+
public Iterator<String> getAll(HttpRequest carrier, String key) {
38+
if (carrier == null) {
39+
return Collections.emptyIterator();
40+
}
41+
42+
HttpHeader httpHeader = HttpHeaders.of(key);
43+
List<String> values = new ArrayList<>();
44+
for (Map.Entry<HttpHeader, HttpHeaderValue> entry : carrier.getHeaders()) {
45+
if (httpHeader.equals(entry.getKey())) {
46+
values.add(entry.getValue().toString());
47+
}
48+
}
49+
return values.iterator();
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
7+
8+
import static io.opentelemetry.javaagent.instrumentation.activejhttp.ActivejHttpServerConnectionSingletons.instrumenter;
9+
10+
import io.activej.http.HttpRequest;
11+
import io.activej.http.HttpResponse;
12+
import io.activej.promise.Promise;
13+
import io.opentelemetry.context.Context;
14+
15+
public final class PromiseWrapper {
16+
17+
public static Promise<HttpResponse> wrap(
18+
Promise<HttpResponse> promise, HttpRequest httpRequest, Context context) {
19+
return promise.whenComplete(
20+
(httpResponse, exception) ->
21+
instrumenter().end(context, httpRequest, httpResponse, exception));
22+
}
23+
24+
private PromiseWrapper() {}
25+
}

0 commit comments

Comments
 (0)