Skip to content

Commit e51a735

Browse files
committed
Introduces OpenTelemetry instrumentation for the ActiveJ framework, enabling distributed tracing and context propagation for ActiveJ-based HTTP servers
1 parent d52a3b5 commit e51a735

12 files changed

+755
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# OpenTelemetry Java Agent for ActiveJ Framework
2+
3+
This repository provides an OpenTelemetry Java agent specifically designed to instrument applications built on the [ActiveJ framework](https://activej.io/). The agent enables distributed tracing, context propagation, and telemetry data collection for ActiveJ-based HTTP servers, making it easier to monitor and debug applications in distributed systems.
4+
5+
## Table of Contents
6+
7+
1. [Overview](#overview)
8+
2. [Features](#features)
9+
3. [Prerequisites](#prerequisites)
10+
4. [Installation & Usage](#installation)
11+
12+
---
13+
14+
## Overview
15+
16+
The OpenTelemetry Java agent for ActiveJ integrates with the OpenTelemetry API to provide automatic instrumentation for ActiveJ HTTP servers. It captures trace context from incoming HTTP requests, propagates it through responses, and enriches telemetry data with HTTP-specific attributes such as request methods, headers, status codes, and URL components.
17+
18+
This agent is particularly useful for applications that rely on ActiveJ's high-performance, event-driven architecture and need observability in distributed systems.
19+
20+
---
21+
22+
## Features
23+
24+
- **Distributed Tracing**: Automatically propagates trace context across service boundaries using the `traceparent` header.
25+
- **HTTP Attribute Extraction**: Captures detailed HTTP attributes (e.g., method, path, query, headers) for enriched telemetry data.
26+
- **Error Handling**: Handles exceptions and maps them to appropriate HTTP status codes for better error visibility.
27+
- **Compatibility**: Works seamlessly with OpenTelemetry's Java instrumentation framework and exporters (e.g., Jaeger, Zipkin, HyperDX).
28+
29+
---
30+
31+
## Prerequisites
32+
33+
Before using this agent, ensure you have the following:
34+
35+
- Java 8 or higher
36+
- ActiveJ framework
37+
- An OpenTelemetry collector or backend (e.g., Jaeger, Zipkin, HyperDX) for visualizing traces
38+
39+
---
40+
41+
## Installation
42+
43+
### Using the Java Agent JAR
44+
45+
1. Download the latest release of the OpenTelemetry Java agent JAR file.
46+
2. Add the agent to your application's JVM arguments:
47+
48+
```bash
49+
java -javaagent:/path/to/opentelemetry-java-agent.jar -jar your-application.jar
50+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
plugins {
2+
id("otel.javaagent-instrumentation")
3+
}
4+
5+
muzzle {
6+
pass {
7+
group.set("io.activej:activej-http")
8+
module.set("activej-http")
9+
10+
versions.set("[1.0,)")
11+
}
12+
}
13+
14+
dependencies {
15+
library("io.activej:activej-http:6.0-beta2")
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
2+
3+
import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
4+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
5+
import static io.opentelemetry.javaagent.instrumentation.activejhttp.ActiveJHttpServerConnectionSingletons.instrumenter;
6+
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
7+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
8+
import static net.bytebuddy.matcher.ElementMatchers.named;
9+
import static net.bytebuddy.matcher.ElementMatchers.not;
10+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
11+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
12+
13+
import io.activej.http.AsyncServlet;
14+
import io.activej.http.HttpHeaders;
15+
import io.activej.http.HttpRequest;
16+
import io.activej.http.HttpResponse;
17+
import io.activej.promise.Promise;
18+
import io.opentelemetry.api.trace.Span;
19+
import io.opentelemetry.context.Context;
20+
import io.opentelemetry.context.Scope;
21+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
22+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
23+
import net.bytebuddy.asm.Advice;
24+
import net.bytebuddy.description.type.TypeDescription;
25+
import net.bytebuddy.matcher.ElementMatcher;
26+
27+
/**
28+
* <p>This class provides instrumentation for ActiveJ HTTP server connections by applying advice to the
29+
* {@code serve} method of classes that extend {@code io.activej.http.AsyncServlet}. The instrumentation is
30+
* designed to integrate with OpenTelemetry for distributed tracing, capturing and propagating trace context
31+
* through HTTP requests and responses.</p>
32+
*
33+
* @author Krishna Chaitanya Surapaneni
34+
*/
35+
public class ActiveJHttpServerConnectionInstrumentation implements TypeInstrumentation {
36+
37+
/**
38+
* Matches classes that extend {@code io.activej.http.AsyncServlet} but are not interfaces.
39+
*
40+
* @return An {@code ElementMatcher} that identifies target classes for instrumentation.
41+
*/
42+
@Override
43+
public ElementMatcher<TypeDescription> typeMatcher() {
44+
return hasSuperType(named("io.activej.http.AsyncServlet"))
45+
.and(not(isInterface()));
46+
}
47+
48+
/**
49+
* Applies advice to the {@code serve} method of the matched classes. The advice captures trace context
50+
* at the start of the method and propagates it through the response.
51+
*
52+
* @param transformer The {@code TypeTransformer} used to apply the advice.
53+
*/
54+
@Override
55+
public void transform(TypeTransformer transformer) {
56+
transformer.applyAdviceToMethod(
57+
isMethod().and(named("serve")).and(takesArguments(1)
58+
.and(takesArgument(0, named("io.activej.http.HttpRequest")))),
59+
this.getClass().getName() + "$ServeAdvice");
60+
}
61+
62+
/**
63+
* <p>Inner class containing the advice logic for the {@code serve} method. This class defines two methods:</p>
64+
* <ul>
65+
* <li>{@code methodEnter}: Captures the trace context at the start of the method.</li>
66+
* <li>{@code methodExit}: Propagates the trace context to the response and ends the span.</li>
67+
* </ul>
68+
*/
69+
@SuppressWarnings("unused")
70+
public static class ServeAdvice {
71+
72+
/**
73+
* Advice executed at the start of the {@code serve} method. Captures the current trace context and
74+
* starts a new span if tracing is enabled for the request.
75+
*
76+
* @param asyncServlet The {@code AsyncServlet} instance handling the request.
77+
* @param request The incoming HTTP request.
78+
* @param context Local variable to store the OpenTelemetry context.
79+
* @param scope Local variable to store the OpenTelemetry scope.
80+
* @param httpRequest Local variable to store the HTTP request.
81+
*/
82+
@Advice.OnMethodEnter(suppress = Throwable.class)
83+
public static void methodEnter(
84+
@Advice.This AsyncServlet asyncServlet,
85+
@Advice.Argument(0) HttpRequest request,
86+
@Advice.Local("otelContext") Context context,
87+
@Advice.Local("otelScope") Scope scope,
88+
@Advice.Local("httpRequest") HttpRequest httpRequest) {
89+
Context parentContext = currentContext();
90+
httpRequest = request;
91+
if (!instrumenter().shouldStart(parentContext, request)) {
92+
return;
93+
}
94+
context = instrumenter().start(parentContext, request);
95+
scope = context.makeCurrent();
96+
}
97+
98+
/**
99+
* Advice executed at the end of the {@code serve} method. Propagates the trace context to the response,
100+
* handles exceptions, and ends the span.
101+
*
102+
* @param asyncServlet The {@code AsyncServlet} instance handling the request.
103+
* @param responsePromise The promise representing the HTTP response.
104+
* @param throwable Any exception thrown during the execution of the method.
105+
* @param context Local variable storing the OpenTelemetry context.
106+
* @param scope Local variable storing the OpenTelemetry scope.
107+
* @param httpRequest Local variable storing the HTTP request.
108+
*/
109+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
110+
public static void methodExit(
111+
@Advice.This AsyncServlet asyncServlet,
112+
@Advice.Return(readOnly = false) Promise<HttpResponse> responsePromise,
113+
@Advice.Thrown(readOnly = false) Throwable throwable,
114+
@Advice.Local("otelContext") Context context,
115+
@Advice.Local("otelScope") Scope scope,
116+
@Advice.Local("httpRequest") HttpRequest httpRequest) {
117+
if (context == null || scope == null || httpRequest == null) {
118+
return;
119+
}
120+
String traceId = Span.fromContext(context).getSpanContext().getTraceId();
121+
String spanId = Span.fromContext(context).getSpanContext().getSpanId();
122+
String traceFlags = Span.fromContext(context).getSpanContext().getTraceFlags().asHex()
123+
.substring(0, 2);
124+
String traceparent = String.format("00-%s-%s-%s", traceId, spanId, traceFlags);
125+
126+
scope.close();
127+
String traceparentHeader = "traceparent";
128+
if (responsePromise != null) {
129+
HttpResponse httpResponse = responsePromise.getResult();
130+
Throwable error = throwable;
131+
if (responsePromise.isException()) {
132+
error = responsePromise.getException();
133+
}
134+
httpResponse = ActiveJHttpServerHelper.createResponse(error, traceparent, httpResponse);
135+
instrumenter().end(context, httpRequest, httpResponse, error);
136+
responsePromise = Promise.of(httpResponse);
137+
} else if (throwable != null) {
138+
HttpResponse httpResponse = HttpResponse.builder()
139+
.withCode(500)
140+
.withPlainText(throwable.getMessage())
141+
.withHeader(HttpHeaders.of(traceparentHeader), traceparent)
142+
.build();
143+
instrumenter().end(context, httpRequest, httpResponse,
144+
throwable);
145+
responsePromise = Promise.of(httpResponse);
146+
throwable = null;
147+
} else {
148+
HttpResponse httpResponse = HttpResponse.notFound404()
149+
.withHeader(HttpHeaders.of(traceparentHeader), traceparent)
150+
.build();
151+
instrumenter().end(context, httpRequest, httpResponse,
152+
throwable);
153+
responsePromise = Promise.of(httpResponse);
154+
}
155+
}
156+
}
157+
158+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
2+
3+
import static java.util.Collections.singletonList;
4+
5+
import com.google.auto.service.AutoService;
6+
import io.opentelemetry.javaagent.extension.instrumentation.HelperResourceBuilder;
7+
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
8+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
9+
import java.util.List;
10+
11+
/**
12+
* <p>This class is an instrumentation module for ActiveJ HTTP server connections. It integrates with
13+
* OpenTelemetry to provide distributed tracing capabilities for applications using the ActiveJ HTTP server.</p>
14+
*
15+
* <p>The module is annotated with {@code @AutoService(InstrumentationModule.class)}, which automatically
16+
* registers it as a service provider for the OpenTelemetry instrumentation framework. This allows the module
17+
* to be discovered and loaded dynamically during runtime.</p>
18+
*
19+
* @author Krishna Chaitanya Surapaneni
20+
*/
21+
@AutoService(InstrumentationModule.class)
22+
public class ActiveJHttpServerConnectionInstrumentationModule extends InstrumentationModule {
23+
24+
/**
25+
* Constructs the instrumentation module with the specified instrumentation names. These names are used to
26+
* identify the instrumentation in the OpenTelemetry framework.
27+
*
28+
* <p>In this case, the module is identified by the names "activej-http" and "activej-http-server".</p>
29+
*/
30+
public ActiveJHttpServerConnectionInstrumentationModule() {
31+
super("activej-http", "activej-http-server");
32+
}
33+
34+
/**
35+
* Returns a list of type instrumentation's provided by this module. Each type instrumentation applies advice
36+
* to specific methods or classes to capture trace context and propagate it through HTTP requests and responses.
37+
*
38+
* @return A list containing the {@code ActiveJHttpServerConnectionInstrumentation} instance.
39+
*/
40+
@Override
41+
public List<TypeInstrumentation> typeInstrumentations() {
42+
return singletonList(new ActiveJHttpServerConnectionInstrumentation());
43+
}
44+
45+
/**
46+
* Registers helper resources required for the instrumentation. Helper resources are typically utility classes
47+
* or configurations that support the instrumentation logic.
48+
*
49+
* <p>In this case, the {@code ActiveJHttpServerHelper} class is registered as a helper resource. This class
50+
* provides utilities for creating HTTP responses with trace context.</p>
51+
*
52+
* @param helperResourceBuilder The builder used to register helper resources.
53+
*/
54+
@Override
55+
public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) {
56+
helperResourceBuilder.register(ActiveJHttpServerHelper.class.getName());
57+
}
58+
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
2+
3+
import io.activej.http.HttpRequest;
4+
import io.activej.http.HttpResponse;
5+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
6+
import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpServerInstrumenters;
7+
8+
/**
9+
* <p>This class provides singleton instances and utilities for ActiveJ HTTP server instrumentation. It is
10+
* designed to centralize the creation and access of OpenTelemetry-related components, such as the
11+
* {@code Instrumenter} used for tracing HTTP requests and responses.</p>
12+
*
13+
* @author Krishna Chaitanya Surapaneni
14+
*/
15+
public class ActiveJHttpServerConnectionSingletons {
16+
17+
/**
18+
* The name of the instrumentation, used to identify this module in the OpenTelemetry framework.
19+
*/
20+
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.activej-http";
21+
22+
private static final Instrumenter<HttpRequest, HttpResponse> INSTRUMENTER;
23+
24+
static {
25+
INSTRUMENTER =
26+
JavaagentHttpServerInstrumenters.create(
27+
INSTRUMENTATION_NAME,
28+
new ActiveJHttpServerHttpAttributesGetter(),
29+
ActiveJHttpServerHeaders.INSTANCE);
30+
}
31+
32+
public static Instrumenter<HttpRequest, HttpResponse> instrumenter() {
33+
return INSTRUMENTER;
34+
}
35+
36+
private ActiveJHttpServerConnectionSingletons() {}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.opentelemetry.javaagent.instrumentation.activejhttp;
2+
3+
import io.activej.http.HttpHeader;
4+
import io.activej.http.HttpHeaderValue;
5+
import io.activej.http.HttpHeaders;
6+
import io.activej.http.HttpRequest;
7+
import io.opentelemetry.context.propagation.internal.ExtendedTextMapGetter;
8+
import java.util.ArrayList;
9+
import java.util.Iterator;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.stream.Collectors;
13+
14+
/**
15+
* <p>This enum implements the {@code ExtendedTextMapGetter<HttpRequest>} interface and provides methods for
16+
* extracting HTTP headers from an ActiveJ {@code HttpRequest}. It is used in the context of OpenTelemetry
17+
* instrumentation to propagate trace context across service boundaries.</p>
18+
*
19+
* @author Krishna Chaitanya Surapaneni
20+
*/
21+
enum ActiveJHttpServerHeaders implements ExtendedTextMapGetter<HttpRequest> {
22+
INSTANCE;
23+
24+
@Override
25+
public Iterable<String> keys(HttpRequest httpRequest) {
26+
return httpRequest.getHeaders().stream()
27+
.map(h -> h.getKey().toString())
28+
.collect(Collectors.toList());
29+
}
30+
31+
@Override
32+
public String get(HttpRequest carrier, String key) {
33+
if (carrier == null) {
34+
return null;
35+
}
36+
return carrier.getHeader(HttpHeaders.of(key));
37+
}
38+
39+
@Override
40+
public Iterator<String> getAll(HttpRequest carrier, String key) {
41+
List<String> values = new ArrayList<>();
42+
if (carrier != null) {
43+
for (Map.Entry<HttpHeader, HttpHeaderValue> entry : carrier.getHeaders()) {
44+
if (entry.getKey().toString().equalsIgnoreCase(key)) {
45+
values.add(entry.getValue().toString());
46+
}
47+
}
48+
}
49+
return values.iterator();
50+
}
51+
52+
}

0 commit comments

Comments
 (0)