Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trace SSL handshakes in netty 4.1 #4604

Merged
merged 3 commits into from
Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ public final class NettyClientInstrumenterFactory {

private final String instrumentationName;
private final boolean alwaysCreateConnectSpan;
private final boolean sslTelemetryEnabled;

public NettyClientInstrumenterFactory(
String instrumentationName, boolean alwaysCreateConnectSpan) {
String instrumentationName, boolean alwaysCreateConnectSpan, boolean sslTelemetryEnabled) {
this.instrumentationName = instrumentationName;
this.alwaysCreateConnectSpan = alwaysCreateConnectSpan;
this.sslTelemetryEnabled = sslTelemetryEnabled;
}

public Instrumenter<HttpRequestAndChannel, HttpResponse> createHttpInstrumenter() {
Expand Down Expand Up @@ -66,4 +68,24 @@ public NettyConnectionInstrumenter createConnectionInstrumenter() {
? new NettyConnectionInstrumenterImpl(instrumenter)
: new NettyErrorOnlyConnectionInstrumenter(instrumenter);
}

public NettySslInstrumenter createSslInstrumenter() {
NettySslNetAttributesExtractor netAttributesExtractor = new NettySslNetAttributesExtractor();
Instrumenter<NettySslRequest, Void> instrumenter =
Instrumenter.<NettySslRequest, Void>builder(
GlobalOpenTelemetry.get(), instrumentationName, NettySslRequest::spanName)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor))
.setTimeExtractors(
request -> request.timer().startTime(),
(request, channel, error) -> request.timer().now())
.newInstrumenter(
sslTelemetryEnabled
? SpanKindExtractor.alwaysInternal()
: SpanKindExtractor.alwaysClient());

return sslTelemetryEnabled
? new NettySslInstrumenterImpl(instrumenter)
: new NettySslErrorOnlyInstrumenter(instrumenter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.netty.common.client;

import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import javax.annotation.Nullable;

final class NettySslErrorOnlyInstrumenter implements NettySslInstrumenter {

private final Instrumenter<NettySslRequest, Void> instrumenter;

NettySslErrorOnlyInstrumenter(Instrumenter<NettySslRequest, Void> instrumenter) {
this.instrumenter = instrumenter;
}

@Override
public boolean shouldStart(Context parentContext, NettySslRequest request) {
// the "real" check is done on end() anyway
return true;
}

@Override
public Context start(Context parentContext, NettySslRequest request) {
return parentContext;
}

@Override
public void end(Context context, NettySslRequest request, @Nullable Throwable error) {
if (error != null && instrumenter.shouldStart(context, request)) {
Context connectContext = instrumenter.start(context, request);
instrumenter.end(connectContext, request, null, error);
}
}
}
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.netty.common.client;

import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.opentelemetry.context.Context;
import java.net.SocketAddress;

// inspired by reactor-netty SslProvider.SslReadHandler
public final class NettySslInstrumentationHandler extends ChannelDuplexHandler {

private final NettySslInstrumenter instrumenter;
private Context parentContext;
private NettySslRequest request;
private Context context;
private boolean handshakeDone = false;

public NettySslInstrumentationHandler(NettySslInstrumenter instrumenter) {
this.instrumenter = instrumenter;
}

@Override
public void channelRegistered(ChannelHandlerContext ctx) {
// remember the parent context from the time of channel registration;
// this happens inside Bootstrap#connect()
parentContext = Context.current();
ctx.fireChannelRegistered();
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
if (!this.handshakeDone) {
ctx.read();
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
}
ctx.fireChannelReadComplete();
}

@Override
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress,
SocketAddress localAddress,
ChannelPromise promise) {

// netty SslHandler starts the handshake after it receives the channelActive() signal; this
// happens just after the connection is established
// this makes connect() promise a good place to start the SSL handshake span
promise.addListener(
future -> {
// there won't be any SSL handshake if the channel fails to connect
if (!future.isSuccess()) {
return;
}
request = NettySslRequest.create(ctx.channel());
if (instrumenter.shouldStart(parentContext, request)) {
context = instrumenter.start(parentContext, request);
}
});
ctx.connect(remoteAddress, localAddress, promise);
}

@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof SslHandshakeCompletionEvent) {
this.handshakeDone = true;
if (ctx.pipeline().context(this) != null) {
ctx.pipeline().remove(this);
}

SslHandshakeCompletionEvent handshake = (SslHandshakeCompletionEvent) evt;
if (context != null) {
instrumenter.end(context, request, handshake.cause());
}
}

ctx.fireUserEventTriggered(evt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.netty.common.client;

import io.opentelemetry.context.Context;
import javax.annotation.Nullable;

public interface NettySslInstrumenter {

boolean shouldStart(Context parentContext, NettySslRequest request);

Context start(Context parentContext, NettySslRequest request);

void end(Context context, NettySslRequest request, @Nullable Throwable error);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.netty.common.client;

import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import javax.annotation.Nullable;

final class NettySslInstrumenterImpl implements NettySslInstrumenter {

private final Instrumenter<NettySslRequest, Void> instrumenter;

NettySslInstrumenterImpl(Instrumenter<NettySslRequest, Void> instrumenter) {
this.instrumenter = instrumenter;
}

@Override
public boolean shouldStart(Context parentContext, NettySslRequest request) {
return instrumenter.shouldStart(parentContext, request);
}

@Override
public Context start(Context parentContext, NettySslRequest request) {
return instrumenter.start(parentContext, request);
}

@Override
public void end(Context context, NettySslRequest request, @Nullable Throwable error) {
instrumenter.end(context, request, null, error);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.netty.common.client;

import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_UDP;

import io.netty.channel.socket.DatagramChannel;
import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetClientAttributesExtractor;
import java.net.InetSocketAddress;
import javax.annotation.Nullable;

final class NettySslNetAttributesExtractor
extends InetSocketAddressNetClientAttributesExtractor<NettySslRequest, Void> {

@Nullable
@Override
public InetSocketAddress getAddress(NettySslRequest request, @Nullable Void unused) {
if (request.remoteAddress() instanceof InetSocketAddress) {
return (InetSocketAddress) request.remoteAddress();
}
return null;
}

@Override
public String transport(NettySslRequest request, @Nullable Void unused) {
return request.channel() instanceof DatagramChannel ? IP_UDP : IP_TCP;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.netty.common.client;

import com.google.auto.value.AutoValue;
import io.netty.channel.Channel;
import io.opentelemetry.javaagent.instrumentation.netty.common.Timer;
import java.net.SocketAddress;
import javax.annotation.Nullable;

@AutoValue
public abstract class NettySslRequest {

static NettySslRequest create(Channel channel) {
return new AutoValue_NettySslRequest(Timer.start(), channel, channel.remoteAddress());
}

String spanName() {
return "SSL handshake";
}

abstract Timer timer();

abstract Channel channel();

@Nullable
abstract SocketAddress remoteAddress();
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public final class NettyClientSingletons {

static {
NettyClientInstrumenterFactory factory =
new NettyClientInstrumenterFactory("io.opentelemetry.netty-4.0", alwaysCreateConnectSpan);
new NettyClientInstrumenterFactory(
"io.opentelemetry.netty-4.0", alwaysCreateConnectSpan, false);
INSTRUMENTER = factory.createHttpInstrumenter();
CONNECTION_INSTRUMENTER = factory.createConnectionInstrumenter();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ tasks {
val testConnectionSpan by registering(Test::class) {
filter {
includeTestsMatching("Netty41ConnectionSpanTest")
includeTestsMatching("Netty41ClientSslTest")
isFailOnNoMatchingTests = false
}
include("**/Netty41ConnectionSpanTest.*")
include("**/Netty41ConnectionSpanTest.*", "**/Netty41ClientSslTest.*")
jvmArgs("-Dotel.instrumentation.netty.always-create-connect-span=true")
jvmArgs("-Dotel.instrumentation.netty.ssl-telemetry.enabled=true")
}

test {
Expand All @@ -58,6 +60,7 @@ tasks {
dependsOn(testConnectionSpan)
filter {
excludeTestsMatching("Netty41ConnectionSpanTest")
excludeTestsMatching("Netty41ClientSslTest")
isFailOnNoMatchingTests = false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.javaagent.instrumentation.netty.v4_1;

import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.NettyClientSingletons.sslInstrumenter;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
Expand All @@ -23,6 +24,7 @@
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.instrumentation.api.CallDepth;
import io.opentelemetry.javaagent.instrumentation.netty.common.AbstractNettyChannelPipelineInstrumentation;
import io.opentelemetry.javaagent.instrumentation.netty.common.client.NettySslInstrumentationHandler;
import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientRequestTracingHandler;
import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientResponseTracingHandler;
import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientTracingHandler;
Expand Down Expand Up @@ -115,6 +117,10 @@ public static void addHandler(
ourHandler = new HttpClientRequestTracingHandler();
} else if (handler instanceof HttpResponseDecoder) {
ourHandler = new HttpClientResponseTracingHandler();
// the SslHandler lives in the netty-handler module, using class name comparison to avoid
// adding a dependency
} else if (handler.getClass().getName().equals("io.netty.handler.ssl.SslHandler")) {
ourHandler = new NettySslInstrumentationHandler(sslInstrumenter());
}

if (ourHandler != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.opentelemetry.javaagent.instrumentation.netty.common.HttpRequestAndChannel;
import io.opentelemetry.javaagent.instrumentation.netty.common.client.NettyClientInstrumenterFactory;
import io.opentelemetry.javaagent.instrumentation.netty.common.client.NettyConnectionInstrumenter;
import io.opentelemetry.javaagent.instrumentation.netty.common.client.NettySslInstrumenter;

public final class NettyClientSingletons {

Expand All @@ -22,15 +23,20 @@ public final class NettyClientSingletons {

private static final boolean alwaysCreateConnectSpan =
Config.get().getBoolean("otel.instrumentation.netty.always-create-connect-span", false);
private static final boolean sslTelemetryEnabled =
Config.get().getBoolean("otel.instrumentation.netty.ssl-telemetry.enabled", false);

private static final Instrumenter<HttpRequestAndChannel, HttpResponse> INSTRUMENTER;
private static final NettyConnectionInstrumenter CONNECTION_INSTRUMENTER;
private static final NettySslInstrumenter SSL_INSTRUMENTER;

static {
NettyClientInstrumenterFactory factory =
new NettyClientInstrumenterFactory("io.opentelemetry.netty-4.1", alwaysCreateConnectSpan);
new NettyClientInstrumenterFactory(
"io.opentelemetry.netty-4.1", alwaysCreateConnectSpan, sslTelemetryEnabled);
INSTRUMENTER = factory.createHttpInstrumenter();
CONNECTION_INSTRUMENTER = factory.createConnectionInstrumenter();
SSL_INSTRUMENTER = factory.createSslInstrumenter();
}

public static Instrumenter<HttpRequestAndChannel, HttpResponse> instrumenter() {
Expand All @@ -41,5 +47,9 @@ public static NettyConnectionInstrumenter connectionInstrumenter() {
return CONNECTION_INSTRUMENTER;
}

public static NettySslInstrumenter sslInstrumenter() {
return SSL_INSTRUMENTER;
}

private NettyClientSingletons() {}
}
Loading