Skip to content

Commit

Permalink
Add agent instrumentation for Ratpack 1.7
Browse files Browse the repository at this point in the history
  • Loading branch information
johnrengelman authored and John Engelman committed Nov 5, 2024
1 parent 2b5c4f5 commit cdbef3f
Show file tree
Hide file tree
Showing 21 changed files with 715 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@

public abstract class AbstractRatpackHttpClientTest extends AbstractHttpClientTest<Void> {

private final ExecHarness exec = ExecHarness.harness();
protected final ExecHarness exec = ExecHarness.harness();

private HttpClient client;
private HttpClient singleConnectionClient;
protected HttpClient client;
protected HttpClient singleConnectionClient;

@BeforeAll
void setUpClient() throws Exception {
protected void setUpClient() throws Exception {
exec.run(
unused -> {
client = buildHttpClient();
Expand Down Expand Up @@ -66,7 +66,8 @@ public Void buildRequest(String method, URI uri, Map<String, String> headers) {
@Override
public int sendRequest(Void request, String method, URI uri, Map<String, String> headers)
throws Exception {
return exec.yield(unused -> internalSendRequest(client, method, uri, headers))
return exec.yield(
execution -> internalSendRequest(client, method, uri, headers))
.getValueOrThrow();
}

Expand All @@ -78,13 +79,17 @@ public final void sendRequestWithCallback(
Map<String, String> headers,
HttpClientResult httpClientResult)
throws Exception {
exec.execute(
Operation.of(
() ->
internalSendRequest(client, method, uri, headers)
.result(
result ->
httpClientResult.complete(result::getValue, result.getThrowable()))));
exec.yield(
(e) ->
Operation.of(
() ->
internalSendRequest(client, method, uri, headers)
.result(
result ->
httpClientResult.complete(
result::getValue, result.getThrowable())))
.promise())
.getValueOrThrow();
}

// overridden in RatpackForkedHttpClientTest
Expand Down
53 changes: 53 additions & 0 deletions instrumentation/ratpack/ratpack-1.7/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
plugins {
id("otel.javaagent-instrumentation")
}

muzzle {
pass {
group.set("io.ratpack")
module.set("ratpack-core")
versions.set("[1.7.0,)")
}
}

dependencies {
library("io.ratpack:ratpack-core:1.7.0")

implementation(project(":instrumentation:netty:netty-4.1:library"))
implementation(project(":instrumentation:ratpack:ratpack-1.4:javaagent"))
implementation(project(":instrumentation:ratpack:ratpack-1.7:library"))

testImplementation(project(":instrumentation:ratpack:ratpack-1.4:testing"))

testLibrary("io.ratpack:ratpack-test:1.7.0")

if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) {
testImplementation("com.sun.activation:jakarta.activation:1.2.2")
}
}

// to allow all tests to pass we need to choose a specific netty version
if (!(findProperty("testLatestDeps") as Boolean)) {
configurations.configureEach {
if (!name.contains("muzzle")) {
resolutionStrategy {
eachDependency {
// specifying a fixed version for all libraries with io.netty group
if (requested.group == "io.netty" && requested.name != "netty-tcnative") {
useVersion("4.1.37.Final")
}
}
}
}
}
}

tasks {
withType<Test>().configureEach {
systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean)
}
}

tasks.withType<Test>().configureEach {
jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.ratpack.v1_7;

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import ratpack.exec.Downstream;

public class DownstreamWrapper<T> implements Downstream<T> {

private final Downstream<T> delegate;
private final Context parentContext;

private DownstreamWrapper(Downstream<T> delegate, Context parentContext) {
assert parentContext != null;
this.delegate = delegate;
this.parentContext = parentContext;
}

@Override
public void success(T value) {
try (Scope ignored = parentContext.makeCurrent()) {
delegate.success(value);
}
}

@Override
public void error(Throwable throwable) {
try (Scope ignored = parentContext.makeCurrent()) {
delegate.error(throwable);
}
}

@Override
public void complete() {
try (Scope ignored = parentContext.makeCurrent()) {
delegate.complete();
}
}

public static <T> Downstream<T> wrapIfNeeded(Downstream<T> delegate) {
if (delegate instanceof DownstreamWrapper) {
return delegate;
}
Context context = Context.current();
if (context == Context.root()) {
// Skip wrapping, there is no need to propagate root context.
return delegate;
}
return new DownstreamWrapper<>(delegate, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.ratpack.v1_7;

import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isStatic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;

import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import ratpack.http.client.HttpClient;

public class HttpClientInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("ratpack.http.client.HttpClient");
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isMethod()
.and(isStatic())
.and(named("of"))
.and(takesArgument(0, named("ratpack.func.Action"))),
HttpClientInstrumentation.class.getName() + "$OfAdvice");
}

@SuppressWarnings("unused")
public static class OfAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void injectTracing(@Advice.Return(readOnly = false) HttpClient httpClient)
throws Exception {
httpClient = RatpackSingletons.telemetry().instrumentHttpClient(httpClient);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.ratpack.v1_7;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Arrays.asList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumentationModule.class)
public class RatpackInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {
public RatpackInstrumentationModule() {
super("ratpack", "ratpack-1.7");
}

@Override
public String getModuleGroup() {
// relies on netty
return "netty";
}

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
// Only activate when running ratpack 1.7 or later
return hasClassesNamed("ratpack.exec.util.retry.Delay");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new ServerRegistryInstrumentation(),
new HttpClientInstrumentation(),
new RequestActionSupportInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.ratpack.v1_7;

import io.netty.channel.Channel;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys;
import io.opentelemetry.instrumentation.ratpack.v1_7.RatpackTelemetry;
import io.opentelemetry.instrumentation.ratpack.v1_7.internal.ContextHolder;
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig;
import ratpack.exec.Execution;

public final class RatpackSingletons {

static {
TELEMETRY =
RatpackTelemetry.builder(GlobalOpenTelemetry.get())
.configure(AgentCommonConfig.get())
.build();
}

private static final Instrumenter<String, Void> INSTRUMENTER =
Instrumenter.<String, Void>builder(
GlobalOpenTelemetry.get(), "io.opentelemetry.ratpack-1.7", s -> s)
.setEnabled(ExperimentalConfig.get().controllerTelemetryEnabled())
.buildInstrumenter();

public static Instrumenter<String, Void> instrumenter() {
return INSTRUMENTER;
}

private static final RatpackTelemetry TELEMETRY;

public static RatpackTelemetry telemetry() {
return TELEMETRY;
}

public static void propagateContextToChannel(Execution execution, Channel channel) {
Context parentContext =
execution
.maybeGet(ContextHolder.class)
.map(ContextHolder::context)
.orElse(Context.current());
channel.attr(AttributeKeys.CLIENT_PARENT_CONTEXT).set(parentContext);
}

private RatpackSingletons() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.ratpack.v1_7;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPrivate;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;

import io.netty.channel.Channel;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.ratpack.v1_7.internal.ContextHolder;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import ratpack.exec.Downstream;
import ratpack.exec.Execution;

public class RequestActionSupportInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return extendsClass(named("ratpack.http.client.internal.RequestActionSupport"));
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isMethod()
.and(isPrivate())
.and(named("send"))
.and(takesArgument(0, named("ratpack.exec.Downstream")))
.and(takesArgument(1, named("io.netty.channel.Channel"))),
RequestActionSupportInstrumentation.class.getName() + "$SendAdvice");
transformer.applyAdviceToMethod(
isMethod().and(named("connect")).and(takesArgument(0, named("ratpack.exec.Downstream"))),
RequestActionSupportInstrumentation.class.getName() + "$ConnectAdvice");
}

@SuppressWarnings("unused")
public static class SendAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void injectChannelAttribute(
@Advice.FieldValue("execution") Execution execution,
@Advice.Argument(value = 0, readOnly = false) Downstream<?> downstream,
@Advice.Argument(value = 1, readOnly = false) Channel channel) {
RatpackSingletons.propagateContextToChannel(execution, channel);
}
}

public static class ConnectAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope injectChannelAttribute(
@Advice.FieldValue("execution") Execution execution,
@Advice.Argument(value = 0, readOnly = false) Downstream<?> downstream) {
// Propagate the current context to downstream
// since that the is the subsequent
downstream = DownstreamWrapper.wrapIfNeeded(downstream);

// Capture the CLIENT span and make it current before cally Netty layer
return execution
.maybeGet(ContextHolder.class)
.map(ContextHolder::context)
.map(Context::makeCurrent)
.orElse(null);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void exit(@Advice.Enter Scope scope) {
if (scope != null) {
scope.close();
}
}
}
}
Loading

0 comments on commit cdbef3f

Please sign in to comment.