aws-sdk-1.1: Copy SQS plugin/NoMuzzle approach from 2.2 (#8866)

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>
This commit is contained in:
Christian Neumüller 2023-07-06 13:17:13 +02:00 committed by GitHub
parent 40938cf9e2
commit ba2f8d209f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 311 additions and 266 deletions

View File

@ -15,6 +15,22 @@ muzzle {
module.set("aws-java-sdk-core")
versions.set("[1.10.33,)")
assertInverse.set(true)
excludeInstrumentationName("aws-sdk-1.11-sqs")
}
fail {
group.set("com.amazonaws")
module.set("aws-java-sdk-core")
versions.set("[1.10.33,)")
excludeInstrumentationName("aws-sdk-1.11-core")
}
pass {
group.set("com.amazonaws")
module.set("aws-java-sdk-sqs")
versions.set("[1.10.33,)")
}
}

View File

@ -0,0 +1,15 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v1_11;
public final class SqsAdviceBridge {
private SqsAdviceBridge() {}
public static void referenceForMuzzleOnly() {
throw new UnsupportedOperationException(
SqsImpl.class.getName() + " referencing for muzzle, should never be actually called");
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Collections.singletonList;
import static net.bytebuddy.matcher.ElementMatchers.named;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import java.util.List;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
// TODO: Copy & paste with only trivial adaptions from v2
abstract class AbstractAwsSdkInstrumentationModule extends InstrumentationModule {
protected AbstractAwsSdkInstrumentationModule(String additionalInstrumentationName) {
super("aws-sdk", "aws-sdk-1.11", additionalInstrumentationName);
}
@Override
public boolean isHelperClass(String className) {
return className.startsWith("io.opentelemetry.contrib.awsxray.");
}
@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
// We don't actually transform it but want to make sure we only apply the instrumentation when
// our key dependency is present.
return hasClassesNamed("com.amazonaws.AmazonWebServiceClient");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new ResourceInjectingTypeInstrumentation());
}
abstract void doTransform(TypeTransformer transformer);
// A type instrumentation is needed to trigger resource injection.
public class ResourceInjectingTypeInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
// This is essentially the entry point of the AWS SDK, all clients implement it. We can ensure
// our interceptor service definition is injected as early as possible if we typematch against
// it.
return named("com.amazonaws.AmazonWebServiceClient");
}
@Override
public void transform(TypeTransformer transformer) {
doTransform(transformer);
}
}
}

View File

@ -15,7 +15,7 @@ import java.util.List;
@AutoService(InstrumentationModule.class)
public class AwsSdkInstrumentationModule extends InstrumentationModule {
public AwsSdkInstrumentationModule() {
super("aws-sdk", "aws-sdk-1.11");
super("aws-sdk", "aws-sdk-1.11", "aws-sdk-1.11-core");
}
@Override

View File

@ -0,0 +1,38 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import static net.bytebuddy.matcher.ElementMatchers.none;
import com.google.auto.service.AutoService;
import io.opentelemetry.instrumentation.awssdk.v1_11.SqsAdviceBridge;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
@AutoService(InstrumentationModule.class)
public class SqsInstrumentationModule extends AbstractAwsSdkInstrumentationModule {
public SqsInstrumentationModule() {
super("aws-sdk-1.11-sqs");
}
@Override
public void doTransform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
none(), SqsInstrumentationModule.class.getName() + "$RegisterAdvice");
}
@SuppressWarnings("unused")
public static class RegisterAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit() {
// (indirectly) using SqsImpl class here to make sure it is available from SqsAccess
// (injected into app classloader) and checked by Muzzle
SqsAdviceBridge.referenceForMuzzleOnly();
}
}
}

View File

@ -6,6 +6,8 @@ dependencies {
implementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator")
library("com.amazonaws:aws-java-sdk-core:1.11.0")
library("com.amazonaws:aws-java-sdk-sqs:1.11.106")
compileOnly(project(":muzzle"))
testImplementation(project(":instrumentation:aws-sdk:aws-sdk-1.11:testing"))
@ -15,5 +17,4 @@ dependencies {
testLibrary("com.amazonaws:aws-java-sdk-kinesis:1.11.106")
testLibrary("com.amazonaws:aws-java-sdk-dynamodb:1.11.106")
testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106")
testLibrary("com.amazonaws:aws-java-sdk-sqs:1.11.106")
}

View File

@ -0,0 +1,56 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v1_11;
import java.util.logging.Level;
import java.util.logging.Logger;
final class PluginImplUtil { // TODO: Copy & paste from v2
private PluginImplUtil() {}
private static final Logger logger = Logger.getLogger(PluginImplUtil.class.getName());
/**
* Check if the given {@code moduleNameImpl} is present.
*
* <p>For library instrumentations, the Impls will always be available but might fail to
* load/link/initialize if the corresponding SDK classes are not on the class path. For javaagent,
* the Impl is available only when the corresponding InstrumentationModule was successfully
* applied (muzzle passed).
*
* <p>Note that an present-but-incompatible library can only be reliably detected by Muzzle. In
* library-mode, users need to ensure they are using a compatible SDK (component) versions
* themselves.
*
* @param implSimpleClassName The simple name of the impl class, e.g. {@code "SqsImpl"}. *
*/
static boolean isImplPresent(String implSimpleClassName) {
// Computing the full name dynamically name here because library instrumentation classes are
// relocated when embedded in the agent.
// We use getName().replace() instead of getPackage() because the latter is not guaranteed to
// work in all cases (e.g., we might be loaded into a custom classloader that doesn't handle it)
String implFullClassName =
PluginImplUtil.class.getName().replace(".PluginImplUtil", "." + implSimpleClassName);
try {
Class.forName(implFullClassName);
return true;
} catch (ClassNotFoundException | LinkageError e) {
// ClassNotFoundException will happen when muzzle disabled us in javaagent mode; LinkageError
// (most likely a NoClassDefFoundError, potentially wrapped in an ExceptionInInitializerError)
// should be thrown when the class is loaded in library mode (where the Impl class itself can
// always be found) but a dependency failed to load (most likely because the corresponding SDK
// dependency is not on the class path).
logger.log(
Level.FINE,
e,
() ->
implFullClassName
+ " not present. "
+ "Most likely, corresponding SDK component is either not on classpath or incompatible.");
return false;
}
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v1_11;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.Request;
import com.amazonaws.Response;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle;
final class SqsAccess {
private SqsAccess() {}
private static final boolean enabled = PluginImplUtil.isImplPresent("SqsImpl");
@NoMuzzle
static boolean afterResponse(
Request<?> request,
Response<?> response,
Instrumenter<Request<?>, Response<?>> consumerInstrumenter) {
return enabled && SqsImpl.afterResponse(request, response, consumerInstrumenter);
}
@NoMuzzle
static boolean beforeMarshalling(AmazonWebServiceRequest request) {
return enabled && SqsImpl.beforeMarshalling(request);
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v1_11;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.Request;
import com.amazonaws.Response;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.ReceiveMessageResult;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
final class SqsImpl {
static {
// Force loading of SQS class; this ensures that an exception is thrown at this point when the
// SQS library is not present, which will cause SqsAccess to have enabled=false in library mode.
@SuppressWarnings("unused")
String ensureLoadedDummy = AmazonSQS.class.getName();
}
private SqsImpl() {}
static boolean afterResponse(
Request<?> request,
Response<?> response,
Instrumenter<Request<?>, Response<?>> consumerInstrumenter) {
if (response.getAwsResponse() instanceof ReceiveMessageResult) {
afterConsumerResponse(request, response, consumerInstrumenter);
return true;
}
return false;
}
/** Create and close CONSUMER span for each message consumed. */
private static void afterConsumerResponse(
Request<?> request,
Response<?> response,
Instrumenter<Request<?>, Response<?>> consumerInstrumenter) {
ReceiveMessageResult receiveMessageResult = (ReceiveMessageResult) response.getAwsResponse();
for (Message message : receiveMessageResult.getMessages()) {
createConsumerSpan(message, request, response, consumerInstrumenter);
}
}
private static void createConsumerSpan(
Message message,
Request<?> request,
Response<?> response,
Instrumenter<Request<?>, Response<?>> consumerInstrumenter) {
Context parentContext = SqsParentContext.ofSystemAttributes(message.getAttributes());
Context context = consumerInstrumenter.start(parentContext, request);
consumerInstrumenter.end(context, request, response, null);
}
static boolean beforeMarshalling(AmazonWebServiceRequest rawRequest) {
if (rawRequest instanceof ReceiveMessageRequest) {
ReceiveMessageRequest request = (ReceiveMessageRequest) rawRequest;
if (!request.getAttributeNames().contains(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE)) {
request.withAttributeNames(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE);
}
return true;
}
return false;
}
}

View File

@ -1,63 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v1_11;
import static java.lang.invoke.MethodType.methodType;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Reflective access to aws-sdk-java-sqs class Message.
*
* <p>We currently don't have a good pattern of instrumenting a core library with various plugins
* that need plugin-specific instrumentation - if we accessed the class directly, Muzzle would
* prevent the entire instrumentation from loading when the plugin isn't available. We need to
* carefully check this class has all reflection errors result in no-op, and in the future we will
* hopefully come up with a better pattern.
*/
final class SqsMessageAccess {
@Nullable private static final MethodHandle GET_ATTRIBUTES;
static {
Class<?> messageClass = null;
try {
messageClass = Class.forName("com.amazonaws.services.sqs.model.Message");
} catch (Throwable t) {
// Ignore.
}
if (messageClass != null) {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle getAttributes = null;
try {
getAttributes = lookup.findVirtual(messageClass, "getAttributes", methodType(Map.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
GET_ATTRIBUTES = getAttributes;
} else {
GET_ATTRIBUTES = null;
}
}
@SuppressWarnings("unchecked")
static Map<String, String> getAttributes(Object message) {
if (GET_ATTRIBUTES == null) {
return Collections.emptyMap();
}
try {
return (Map<String, String>) GET_ATTRIBUTES.invoke(message);
} catch (Throwable t) {
return Collections.emptyMap();
}
}
private SqsMessageAccess() {}
}

View File

@ -1,99 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v1_11;
import static java.lang.invoke.MethodType.methodType;
import com.amazonaws.AmazonWebServiceRequest;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
/**
* Reflective access to aws-sdk-java-sqs class ReceiveMessageRequest.
*
* <p>We currently don't have a good pattern of instrumenting a core library with various plugins
* that need plugin-specific instrumentation - if we accessed the class directly, Muzzle would
* prevent the entire instrumentation from loading when the plugin isn't available. We need to
* carefully check this class has all reflection errors result in no-op, and in the future we will
* hopefully come up with a better pattern.
*/
final class SqsReceiveMessageRequestAccess {
@Nullable private static final MethodHandle WITH_ATTRIBUTE_NAMES;
@Nullable private static final MethodHandle GET_ATTRIBUTE_NAMES;
static {
Class<?> receiveMessageRequestClass = null;
try {
receiveMessageRequestClass =
Class.forName("com.amazonaws.services.sqs.model.ReceiveMessageRequest");
} catch (Throwable t) {
// Ignore.
}
if (receiveMessageRequestClass != null) {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle withAttributeNames = null;
try {
withAttributeNames =
lookup.findVirtual(
receiveMessageRequestClass,
"withAttributeNames",
methodType(receiveMessageRequestClass, String[].class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
WITH_ATTRIBUTE_NAMES = withAttributeNames;
MethodHandle getAttributeNames = null;
try {
getAttributeNames =
lookup.findVirtual(
receiveMessageRequestClass, "getAttributeNames", methodType(List.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
GET_ATTRIBUTE_NAMES = getAttributeNames;
} else {
WITH_ATTRIBUTE_NAMES = null;
GET_ATTRIBUTE_NAMES = null;
}
}
static boolean isInstance(AmazonWebServiceRequest request) {
return request
.getClass()
.getName()
.equals("com.amazonaws.services.sqs.model.ReceiveMessageRequest");
}
static void withAttributeNames(AmazonWebServiceRequest request, String name) {
if (WITH_ATTRIBUTE_NAMES == null) {
return;
}
try {
WITH_ATTRIBUTE_NAMES.invoke(request, name);
} catch (Throwable throwable) {
// Ignore
}
}
@SuppressWarnings("unchecked")
static List<String> getAttributeNames(AmazonWebServiceRequest request) {
if (GET_ATTRIBUTE_NAMES == null) {
return Collections.emptyList();
}
try {
return (List<String>) GET_ATTRIBUTE_NAMES.invoke(request);
} catch (Throwable t) {
return Collections.emptyList();
}
}
private SqsReceiveMessageRequestAccess() {}
}

View File

@ -1,64 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v1_11;
import static java.lang.invoke.MethodType.methodType;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
/**
* Reflective access to aws-sdk-java-sqs class ReceiveMessageResult.
*
* <p>We currently don't have a good pattern of instrumenting a core library with various plugins
* that need plugin-specific instrumentation - if we accessed the class directly, Muzzle would
* prevent the entire instrumentation from loading when the plugin isn't available. We need to
* carefully check this class has all reflection errors result in no-op, and in the future we will
* hopefully come up with a better pattern.
*/
final class SqsReceiveMessageResultAccess {
@Nullable private static final MethodHandle GET_MESSAGES;
static {
Class<?> receiveMessageResultClass = null;
try {
receiveMessageResultClass =
Class.forName("com.amazonaws.services.sqs.model.ReceiveMessageResult");
} catch (Throwable t) {
// Ignore.
}
if (receiveMessageResultClass != null) {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle getMessages = null;
try {
getMessages =
lookup.findVirtual(receiveMessageResultClass, "getMessages", methodType(List.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
GET_MESSAGES = getMessages;
} else {
GET_MESSAGES = null;
}
}
static List<?> getMessages(Object result) {
if (GET_MESSAGES == null) {
return Collections.emptyList();
}
try {
return (List<?>) GET_MESSAGES.invoke(result);
} catch (Throwable t) {
return Collections.emptyList();
}
}
private SqsReceiveMessageResultAccess() {}
}

View File

@ -14,7 +14,6 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.context.Context;
import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import java.util.List;
import javax.annotation.Nullable;
/** Tracing Request Handler. */
@ -58,40 +57,18 @@ final class TracingRequestHandler extends RequestHandler2 {
@Override
@CanIgnoreReturnValue
public AmazonWebServiceRequest beforeMarshalling(AmazonWebServiceRequest request) {
if (SqsReceiveMessageRequestAccess.isInstance(request)) {
if (!SqsReceiveMessageRequestAccess.getAttributeNames(request)
.contains(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE)) {
SqsReceiveMessageRequestAccess.withAttributeNames(
request, SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE);
}
}
// TODO: We are modifying the request in-place instead of using clone() as recommended
// by the Javadoc in the interface.
SqsAccess.beforeMarshalling(request);
return request;
}
@Override
public void afterResponse(Request<?> request, Response<?> response) {
if (SqsReceiveMessageRequestAccess.isInstance(request.getOriginalRequest())) {
afterConsumerResponse(request, response);
}
SqsAccess.afterResponse(request, response, consumerInstrumenter);
finish(request, response, null);
}
/** Create and close CONSUMER span for each message consumed. */
private void afterConsumerResponse(Request<?> request, Response<?> response) {
Object receiveMessageResult = response.getAwsResponse();
List<?> messages = SqsReceiveMessageResultAccess.getMessages(receiveMessageResult);
for (Object message : messages) {
createConsumerSpan(message, request, response);
}
}
private void createConsumerSpan(Object message, Request<?> request, Response<?> response) {
Context parentContext =
SqsParentContext.ofSystemAttributes(SqsMessageAccess.getAttributes(message));
Context context = consumerInstrumenter.start(parentContext, request);
consumerInstrumenter.end(context, request, response, null);
}
@Override
public void afterError(Request<?> request, Response<?> response, Exception e) {
finish(request, response, e);

View File

@ -8,7 +8,7 @@ package io.opentelemetry.instrumentation.awssdk.v2_2;
import java.util.logging.Level;
import java.util.logging.Logger;
final class PluginImplUtil {
final class PluginImplUtil { // TODO: Copy & pasted to v1
private PluginImplUtil() {}
private static final Logger logger = Logger.getLogger(PluginImplUtil.class.getName());
@ -16,33 +16,40 @@ final class PluginImplUtil {
/**
* Check if the given {@code moduleNameImpl} is present.
*
* <p>For library instrumentations, the Impls will always be available but might fail to load if
* the corresponding SDK classes are not on the class path. For javaagent, the Impl is available
* only when the corresponding InstrumentationModule was successfully applied (muzzle passed).
* <p>For library instrumentations, the Impls will always be available but might fail to
* load/link/initialize if the corresponding SDK classes are not on the class path. For javaagent,
* the Impl is available only when the corresponding InstrumentationModule was successfully
* applied (muzzle passed).
*
* <p>Note that an present-but-incompatible library can only be detected by Muzzle. In
* <p>Note that an present-but-incompatible library can only be reliably detected by Muzzle. In
* library-mode, users need to ensure they are using a compatible SDK (component) versions
* themselves.
*
* @param implSimpleClassName The simple name of the impl class, e.g. {@code "SqsImpl"}. *
*/
static boolean isImplPresent(String implSimpleClassName) {
// Computing the full name dynamically name here because library instrumentation classes are
// relocated when embedded in the agent.
// We use getName().replace() instead of getPackage() because the latter is not guaranteed to
// work in all cases (e.g., we might be loaded into a custom classloader that doesn't handle it)
String implFullClassName =
PluginImplUtil.class.getName().replace(".PluginImplUtil", "." + implSimpleClassName);
try {
// using package name here because library instrumentation classes are relocated when embedded
// in the agent
Class.forName(implFullClassName);
return true;
} catch (ClassNotFoundException | LinkageError e) {
// ClassNotFoundException will happen when muzzle disabled us in javaagent mode; LinkageError
// (most likely NoClassDefFoundError) should happen when the class is loaded in library mode
// (where it is always available) but a dependency failed to load (most likely because the
// corresponding SDK dependency is not on the class path).
// (most likely a NoClassDefFoundError, potentially wrapped in an ExceptionInInitializerError)
// should be thrown when the class is loaded in library mode (where the Impl class itself can
// always be found) but a dependency failed to load (most likely because the corresponding SDK
// dependency is not on the class path).
logger.log(
Level.FINE, e, () -> implFullClassName + " not present, probably incompatible version");
Level.FINE,
e,
() ->
implFullClassName
+ " not present. "
+ "Most likely, corresponding SDK component is either not on classpath or incompatible.");
return false;
}
}