Skip to content

Commit

Permalink
Merge pull request #100 from signalfx/netty-3.8-server-timing
Browse files Browse the repository at this point in the history
Netty 3.8 server timing
  • Loading branch information
Mateusz Rzeszutek authored Feb 5, 2021
2 parents f93c904 + 2f6663e commit 2089415
Show file tree
Hide file tree
Showing 6 changed files with 433 additions and 0 deletions.
16 changes: 16 additions & 0 deletions instrumentation/netty-3.8/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apply from: "$rootDir/gradle/instrumentation.gradle"

dependencies {
compileOnly group: 'io.netty', name: 'netty', version: '3.8.0.Final'
compileOnly group: 'io.opentelemetry.javaagent.instrumentation', name: 'opentelemetry-javaagent-netty-3.8', version: versions.opentelemetryJavaagent

implementation project(':instrumentation:common')

testInstrumentation group: 'io.opentelemetry.javaagent.instrumentation', name: 'opentelemetry-javaagent-netty-3.8', version: versions.opentelemetryJavaagent

testImplementation group: 'io.netty', name: 'netty', version: '3.8.0.Final'
}

tasks.withType(Test) {
jvmArgs '-Dsplunk.context.server-timing.enabled=true'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.opentelemetry.netty.v3_8;

import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;

import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandler;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.jboss.netty.handler.codec.http.HttpServerCodec;

public class NettyChannelPipelineInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("org.jboss.netty.channel.ChannelPipeline");
}

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("org.jboss.netty.channel.ChannelPipeline"));
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
transformers.put(
isMethod()
.and(nameStartsWith("add"))
.and(takesArgument(1, named("org.jboss.netty.channel.ChannelHandler"))),
this.getClass().getName() + "$ChannelPipelineAdd2ArgsAdvice");
transformers.put(
isMethod()
.and(nameStartsWith("add"))
.and(takesArgument(2, named("org.jboss.netty.channel.ChannelHandler"))),
this.getClass().getName() + "$ChannelPipelineAdd3ArgsAdvice");
return transformers;
}

public static class ChannelPipelineAdd2ArgsAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static int checkDepth(
@Advice.This ChannelPipeline pipeline, @Advice.Argument(1) ChannelHandler handler) {
return ChannelPipelineUtil.removeDuplicatesAndIncrementDepth(pipeline, handler);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void addHandler(
@Advice.Enter int depth,
@Advice.This ChannelPipeline pipeline,
@Advice.Argument(1) ChannelHandler handler) {
if (depth > 0) {
return;
}

ContextStore<Channel, ChannelTraceContext> contextStore =
InstrumentationContext.get(Channel.class, ChannelTraceContext.class);

ChannelPipelineUtil.addServerTimingHandler(pipeline, handler, contextStore);
}
}

public static class ChannelPipelineAdd3ArgsAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static int checkDepth(
@Advice.This ChannelPipeline pipeline, @Advice.Argument(2) ChannelHandler handler) {
return ChannelPipelineUtil.removeDuplicatesAndIncrementDepth(pipeline, handler);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void addHandler(
@Advice.Enter int depth,
@Advice.This ChannelPipeline pipeline,
@Advice.Argument(2) ChannelHandler handler) {
if (depth > 0) {
return;
}

ContextStore<Channel, ChannelTraceContext> contextStore =
InstrumentationContext.get(Channel.class, ChannelTraceContext.class);

ChannelPipelineUtil.addServerTimingHandler(pipeline, handler, contextStore);
}
}

public static final class ChannelPipelineUtil {
public static int removeDuplicatesAndIncrementDepth(
ChannelPipeline pipeline, ChannelHandler handler) {
// Pipelines are created once as a factory and then copied multiple times using the same add
// methods as we are hooking. If our handler has already been added we need to remove it so we
// don't end up with duplicates (this throws an exception)
if (pipeline.get(handler.getClass().getName()) != null) {
pipeline.remove(handler.getClass().getName());
}
// CallDepth does not allow just getting the depth value, so to avoid interfering with the
// upstream netty implementation we do the same count but with our class
return CallDepthThreadLocalMap.incrementCallDepth(ServerTimingHandler.class);
}

public static void addServerTimingHandler(
ChannelPipeline pipeline,
ChannelHandler handler,
ContextStore<Channel, ChannelTraceContext> contextStore) {
try {
if (handler instanceof HttpServerCodec || handler instanceof HttpResponseEncoder) {
pipeline.addLast(
ServerTimingHandler.class.getName(), new ServerTimingHandler(contextStore));
}
} finally {
CallDepthThreadLocalMap.reset(ServerTimingHandler.class);
}
}

private ChannelPipelineUtil() {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.opentelemetry.netty.v3_8;

import com.google.auto.service.AutoService;
import com.splunk.opentelemetry.servertiming.ServerTimingHeader;
import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@AutoService(InstrumentationModule.class)
public class NettyInstrumentationModule extends InstrumentationModule {
public NettyInstrumentationModule() {
super("netty", "netty-3.8");
}

@Override
protected String[] additionalHelperClassNames() {
return new String[] {
ServerTimingHeader.class.getName(),
getClass().getPackage().getName() + ".ServerTimingHandler",
getClass().getPackage().getName() + ".ServerTimingHandler$HeadersSetter",
getClass().getPackage().getName()
+ ".NettyChannelPipelineInstrumentation$ChannelPipelineUtil",
};
}

// run after the upstream netty instrumentation
@Override
public int getOrder() {
return 1;
}

// enable the instrumentation only if the server-timing header flag is on
@Override
protected boolean defaultEnabled() {
return super.defaultEnabled() && ServerTimingHeader.shouldEmitServerTimingHeader();
}

@Override
public Map<String, String> contextStore() {
return Collections.singletonMap(
"org.jboss.netty.channel.Channel", ChannelTraceContext.class.getName());
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(new NettyChannelPipelineInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.opentelemetry.netty.v3_8;

import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.server.NettyHttpServerTracer.tracer;

import com.splunk.opentelemetry.servertiming.ServerTimingHeader;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelDownstreamHandler;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpResponse;

public class ServerTimingHandler extends SimpleChannelDownstreamHandler {
private final ContextStore<Channel, ChannelTraceContext> contextStore;

public ServerTimingHandler(ContextStore<Channel, ChannelTraceContext> contextStore) {
this.contextStore = contextStore;
}

@Override
public void writeRequested(ChannelHandlerContext ctx, MessageEvent msg) {
ChannelTraceContext channelTraceContext =
contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE);

Context context = tracer().getServerContext(channelTraceContext);
if (context == null || !(msg.getMessage() instanceof HttpResponse)) {
ctx.sendDownstream(msg);
return;
}

HttpResponse response = (HttpResponse) msg.getMessage();
ServerTimingHeader.setHeaders(context, response.headers(), HeadersSetter.INSTANCE);
ctx.sendDownstream(msg);
}

public static final class HeadersSetter implements Setter<HttpHeaders> {
private static final HeadersSetter INSTANCE = new HeadersSetter();

@Override
public void set(HttpHeaders carrier, String key, String value) {
carrier.add(key, value);
}
}
}
Loading

0 comments on commit 2089415

Please sign in to comment.