diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d1f6f0b2..5835090e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ## Features * Wildcard patterns are now not limited to only one wildcard in the middle and can be arbitrarily complex now. Example: `*foo*bar*baz`. + * Support for JAX-RS annotations. + Transactions are named based on your resources (`ResourceClass#resourceMethod`). ## Bug Fixes diff --git a/apm-agent-core/src/main/java/co/elastic/apm/bci/ElasticApmAgent.java b/apm-agent-core/src/main/java/co/elastic/apm/bci/ElasticApmAgent.java index a39ff94915..f873e739b7 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/bci/ElasticApmAgent.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/bci/ElasticApmAgent.java @@ -21,6 +21,7 @@ import co.elastic.apm.bci.bytebuddy.ErrorLoggingListener; import co.elastic.apm.bci.bytebuddy.MatcherTimer; +import co.elastic.apm.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory; import co.elastic.apm.bci.bytebuddy.SoftlyReferencingTypePoolCache; import co.elastic.apm.configuration.CoreConfiguration; import co.elastic.apm.impl.ElasticApmTracer; @@ -29,6 +30,7 @@ import net.bytebuddy.ByteBuddy; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.agent.builder.ResettableClassFileTransformer; +import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.scaffold.MethodGraph; @@ -146,6 +148,9 @@ private static AgentBuilder applyAdvice(final ElasticApmTracer tracer, final Age public boolean matches(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, Class classBeingRedefined, ProtectionDomain protectionDomain) { long start = System.nanoTime(); try { + if (!advice.getClassLoaderMatcher().matches(classLoader)) { + return false; + } if (typeMatchingWithNamePreFilter && !advice.getTypeMatcherPreFilter().matches(typeDescription)) { return false; } @@ -169,7 +174,9 @@ public boolean matches(TypeDescription typeDescription, ClassLoader classLoader, } } }) - .transform(new AgentBuilder.Transformer.ForAdvice() + .transform(new AgentBuilder.Transformer.ForAdvice(Advice + .withCustomMapping() + .bind(new SimpleMethodSignatureOffsetMappingFactory())) .advice(new ElementMatcher() { @Override public boolean matches(MethodDescription target) { diff --git a/apm-agent-core/src/main/java/co/elastic/apm/bci/ElasticApmInstrumentation.java b/apm-agent-core/src/main/java/co/elastic/apm/bci/ElasticApmInstrumentation.java index c814747ef5..552a6f8bab 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/bci/ElasticApmInstrumentation.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/bci/ElasticApmInstrumentation.java @@ -91,6 +91,10 @@ public ElementMatcher getTypeMatcherPreFilter() { */ public abstract ElementMatcher getTypeMatcher(); + public ElementMatcher.Junction getClassLoaderMatcher() { + return any(); + } + /** * The method matcher selects methods of types matching {@link #getTypeMatcher()}, * which should be instrumented diff --git a/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/ClassLoaderNameMatcher.java b/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/ClassLoaderNameMatcher.java index d8d67a2909..662c972464 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/ClassLoaderNameMatcher.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/ClassLoaderNameMatcher.java @@ -7,9 +7,9 @@ * 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. @@ -23,24 +23,23 @@ public class ClassLoaderNameMatcher extends ElementMatcher.Junction.AbstractBase { - private final String name; + private final String name; - private ClassLoaderNameMatcher(String name) { - this.name = name; - } + private ClassLoaderNameMatcher(String name) { + this.name = name; + } - public static ElementMatcher.Junction.AbstractBase classLoaderWithName(String name) { - return new ClassLoaderNameMatcher(name); - - } + public static ElementMatcher.Junction classLoaderWithName(String name) { + return new ClassLoaderNameMatcher(name); + } public static ElementMatcher.Junction isReflectionClassLoader() { return classLoaderWithName("sun.reflect.DelegatingClassLoader") .or(classLoaderWithName("jdk.internal.reflect.DelegatingClassLoader")); - } + } - @Override - public boolean matches(ClassLoader target) { - return target != null && name.equals(target.getClass().getName()); - } + @Override + public boolean matches(ClassLoader target) { + return target != null && name.equals(target.getClass().getName()); + } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/CustomElementMatchers.java b/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/CustomElementMatchers.java new file mode 100644 index 0000000000..a1468102a0 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/CustomElementMatchers.java @@ -0,0 +1,102 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * 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. + * #L% + */ +package co.elastic.apm.bci.bytebuddy; + +import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; +import net.bytebuddy.description.NamedElement; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import javax.annotation.Nullable; +import java.util.Collection; + +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.none; + +public class CustomElementMatchers { + + public static ElementMatcher.Junction isInAnyPackage(Collection includedPackages, + ElementMatcher.Junction defaultIfEmpty) { + if (includedPackages.isEmpty()) { + return defaultIfEmpty; + } + ElementMatcher.Junction matcher = none(); + for (String applicationPackage : includedPackages) { + matcher = matcher.or(nameStartsWith(applicationPackage)); + } + return matcher; + } + + /** + * Matches only class loaders which can load a certain class. + *

+ * Warning: the class will be tried to load by each class loader. + * You should choose a class which does not have optional dependencies (imports classes which are not on the class path). + * Ideally, choose an interface or annotation without dependencies. + *

+ * + * @param className the name of the class to check + * @return a matcher which only matches class loaders which can load a certain class. + */ + public static ElementMatcher.Junction classLoaderCanLoadClass(final String className) { + return new ElementMatcher.Junction.AbstractBase() { + + private final boolean loadableByBootstrapClassLoader = canLoadClass(null, className); + private WeakConcurrentMap cache = new WeakConcurrentMap.WithInlinedExpunction<>(); + + @Override + public boolean matches(@Nullable ClassLoader target) { + if (target == null) { + return loadableByBootstrapClassLoader; + } + + Boolean result = cache.get(target); + if (result == null) { + result = canLoadClass(target, className); + cache.put(target, result); + } + return result; + } + }; + } + + private static boolean canLoadClass(@Nullable ClassLoader target, String className) { + boolean result; + try { + Class.forName(className, false, target); + result = true; + } catch (Exception ignore) { + result = false; + } + return result; + } + + /** + * Matches overridden methods of a super class or implemented methods of an interface. + * Recursively traverses the superclasses and interfaces. + * The the superclasses and interfaces to examine can be limited via {@link MethodHierarchyMatcher#onSuperClassesThat(ElementMatcher)}. + * + * @param methodElementMatcher The matcher which is applied on the method hierarchy + * @return a matcher which is applied on the method hierarchy + */ + public static MethodHierarchyMatcher overridesOrImplementsMethodThat(ElementMatcher methodElementMatcher) { + return new MethodHierarchyMatcher(methodElementMatcher); + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/MethodHierarchyMatcher.java b/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/MethodHierarchyMatcher.java new file mode 100644 index 0000000000..b52629e5bf --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/MethodHierarchyMatcher.java @@ -0,0 +1,84 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * 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. + * #L% + */ +package co.elastic.apm.bci.bytebuddy; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +/** + * This implementation is based on org.stagemonitor.core.instrument.OverridesMethodElementMatcher, + * under Apache License 2.0 + * + * @see CustomElementMatchers#overridesOrImplementsMethodThat(ElementMatcher) + */ +public class MethodHierarchyMatcher extends ElementMatcher.Junction.AbstractBase { + + private final ElementMatcher extraMethodMatcher; + private final ElementMatcher superClassMatcher; + + MethodHierarchyMatcher(ElementMatcher extraMethodMatcher) { + this(extraMethodMatcher, not(is(TypeDescription.ForLoadedType.OBJECT))); + } + + private MethodHierarchyMatcher(ElementMatcher extraMethodMatcher, ElementMatcher superClassMatcher) { + this.extraMethodMatcher = extraMethodMatcher; + this.superClassMatcher = superClassMatcher; + } + + public ElementMatcher onSuperClassesThat(ElementMatcher superClassMatcher) { + return new MethodHierarchyMatcher(extraMethodMatcher, superClassMatcher); + } + + @Override + public boolean matches(MethodDescription targetMethod) { + return declaresInHierarchy(targetMethod, targetMethod.getDeclaringType().asErasure()); + } + + private boolean declaresInHierarchy(MethodDescription targetMethod, TypeDescription type) { + if (declaresMethod(named(targetMethod.getName()) + .and(returns(targetMethod.getReturnType().asErasure())) + .and(takesArguments(targetMethod.getParameters().asTypeList().asErasures())) + .and(extraMethodMatcher)) + .matches(type)) { + return true; + } + for (TypeDescription interfaze : type.getInterfaces().asErasures()) { + if (superClassMatcher.matches(interfaze)) { + if (declaresInHierarchy(targetMethod, interfaze)) { + return true; + } + } + } + final TypeDescription.Generic superClass = type.getSuperClass(); + if (superClass != null && superClassMatcher.matches(superClass.asErasure())) { + return declaresInHierarchy(targetMethod, superClass.asErasure()); + } + return false; + } + +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/SimpleMethodSignatureOffsetMappingFactory.java b/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/SimpleMethodSignatureOffsetMappingFactory.java new file mode 100644 index 0000000000..d867201675 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/bci/bytebuddy/SimpleMethodSignatureOffsetMappingFactory.java @@ -0,0 +1,72 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * 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. + * #L% + */ +package co.elastic.apm.bci.bytebuddy; + +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.ParameterDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; + +/** + * Enables using {@link SimpleMethodSignature} in {@link net.bytebuddy.asm.Advice.OnMethodEnter} and + * {@link net.bytebuddy.asm.Advice.OnMethodExit} methods. + */ +public class SimpleMethodSignatureOffsetMappingFactory implements Advice.OffsetMapping.Factory { + + @Override + public Class getAnnotationType() { + return SimpleMethodSignature.class; + } + + @Override + public Advice.OffsetMapping make(ParameterDescription.InDefinedShape target, + AnnotationDescription.Loadable annotation, + AdviceType adviceType) { + return new Advice.OffsetMapping() { + @Override + public Target resolve(TypeDescription instrumentedType, MethodDescription instrumentedMethod, Assigner assigner, + Advice.ArgumentHandler argumentHandler, Sort sort) { + final String className = instrumentedMethod.getDeclaringType().getTypeName(); + final String simpleClassName = className.substring(className.lastIndexOf('.') + 1); + final String signature = String.format("%s#%s", simpleClassName, instrumentedMethod.getName()); + return Target.ForStackManipulation.of(signature); + } + }; + } + + /** + * Indicates that the annotated parameter should be mapped to a string representation of the instrumented method, + * a constant representing {@link Class#getSimpleName()}{@code #}{@link Method#getName()}, + * for example {@code FooClass#barMethod} + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface SimpleMethodSignature { + } + +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/configuration/CoreConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/configuration/CoreConfiguration.java index 97aa60ac16..56b263d9c5 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/configuration/CoreConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/configuration/CoreConfiguration.java @@ -19,6 +19,7 @@ */ package co.elastic.apm.configuration; +import co.elastic.apm.bci.ElasticApmInstrumentation; import co.elastic.apm.configuration.validation.RegexValidator; import co.elastic.apm.matcher.WildcardMatcher; import co.elastic.apm.matcher.WildcardMatcherValueConverter; @@ -29,7 +30,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.TreeSet; public class CoreConfiguration extends ConfigurationOptionProvider { @@ -175,8 +180,7 @@ public void assertValid(Double value) { .aliasKeys("disabled_instrumentations") .configurationCategory(CORE_CATEGORY) .description("A list of instrumentations which should be disabled.\n" + - "Valid options are `jdbc`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `http-client`, `apache-httpclient`," + - "`spring-resttemplate` and `incubating`.\n" + + "Valid options are " + getAllInstrumentationGroupNames() + ".\n" + "If you want to try out incubating features,\n" + "set the value to an empty string.") .buildWithDefault(Collections.singleton("incubating")); @@ -194,6 +198,22 @@ public void assertValid(Double value) { .dynamic(true) .buildWithDefault(Collections.singletonList(WildcardMatcher.valueOf("(?-i)*Nested*Exception"))); + public static String getAllInstrumentationGroupNames() { + Set instrumentationGroupNames = new TreeSet<>(); + for (ElasticApmInstrumentation instrumentation : ServiceLoader.load(ElasticApmInstrumentation.class)) { + instrumentationGroupNames.addAll(instrumentation.getInstrumentationGroupNames()); + } + + StringBuilder allGroups = new StringBuilder(); + for (Iterator iterator = instrumentationGroupNames.iterator(); iterator.hasNext(); ) { + allGroups.append('`').append(iterator.next()).append('`'); + if (iterator.hasNext()) { + allGroups.append(", "); + } + } + return allGroups.toString(); + } + private final ConfigurationOption typePoolCache = ConfigurationOption.booleanOption() .key("enable_type_pool_cache") .configurationCategory(CORE_CATEGORY) @@ -231,7 +251,6 @@ public void assertValid(Double value) { WildcardMatcher.valueOf("(?-i)org.wildfly.security*") )); - public boolean isActive() { return active.get(); } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/configuration/StartupInfo.java b/apm-agent-core/src/main/java/co/elastic/apm/configuration/StartupInfo.java index eae4c1dcb5..97a399abb4 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/configuration/StartupInfo.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/configuration/StartupInfo.java @@ -22,6 +22,7 @@ import co.elastic.apm.configuration.converter.TimeDuration; import co.elastic.apm.context.LifecycleListener; import co.elastic.apm.impl.ElasticApmTracer; +import co.elastic.apm.impl.stacktrace.StacktraceConfiguration; import co.elastic.apm.util.VersionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,6 +73,10 @@ void logConfiguration(ConfigurationRegistry configurationRegistry, Logger logger } } } + if (configurationRegistry.getConfig(StacktraceConfiguration.class).getApplicationPackages().isEmpty()) { + logger.warn("To enable all features and to increase startup times, please configure {}", + StacktraceConfiguration.APPLICATION_PACKAGES); + } } private void logConfigWithNonDefaultValue(Logger logger, ConfigurationOption option) { diff --git a/apm-agent-core/src/main/java/co/elastic/apm/impl/stacktrace/StacktraceConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/impl/stacktrace/StacktraceConfiguration.java index 674fbf2a89..4b18a84b63 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/impl/stacktrace/StacktraceConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/impl/stacktrace/StacktraceConfiguration.java @@ -30,10 +30,12 @@ public class StacktraceConfiguration extends ConfigurationOptionProvider { private static final String STACKTRACE_CATEGORY = "Stacktrace"; + public static final String APPLICATION_PACKAGES = "application_packages"; private final ConfigurationOption> applicationPackages = ConfigurationOption.stringsOption() - .key("application_packages") + .key(APPLICATION_PACKAGES) .configurationCategory(STACKTRACE_CATEGORY) - .description("Used to determine whether a stack trace frame is an 'in-app frame' or a 'library frame'.") + .description("Used to determine whether a stack trace frame is an 'in-app frame' or a 'library frame'.\n" + + "Setting this option can also improve the startup time.") .dynamic(true) .buildWithDefault(Collections.emptyList()); diff --git a/apm-agent-core/src/main/resources/META-INF/NOTICE b/apm-agent-core/src/main/resources/META-INF/NOTICE index dfc13b833e..6d56cbaed8 100644 --- a/apm-agent-core/src/main/resources/META-INF/NOTICE +++ b/apm-agent-core/src/main/resources/META-INF/NOTICE @@ -3,3 +3,4 @@ under the Apache License 2.0. See: - co.elastic.apm.configuration.source.PropertyFileConfigurationSource - co.elastic.apm.configuration.source.SystemPropertyConfigurationSource - co.elastic.apm.configuration.StartupInfo + - co.elastic.apm.bci.bytebuddy.MethodHierarchyMatcher diff --git a/apm-agent-core/src/test/java/co/elastic/apm/AbstractInstrumentationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/AbstractInstrumentationTest.java index 97168ab354..be8e133ab3 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/AbstractInstrumentationTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/AbstractInstrumentationTest.java @@ -55,10 +55,26 @@ public static void afterAll() { ElasticApmAgent.reset(); } + public static void reset() { + SpyConfiguration.reset(config); + reporter.reset(); + } + + public static ElasticApmTracer getTracer() { + return tracer; + } + + public static MockReporter getReporter() { + return reporter; + } + + public static ConfigurationRegistry getConfig() { + return config; + } + @Before @BeforeEach public final void resetReporter() { - SpyConfiguration.reset(config); - reporter.reset(); + reset(); } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/CustomElementMatchersTest.java b/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/CustomElementMatchersTest.java new file mode 100644 index 0000000000..e25e3be206 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/CustomElementMatchersTest.java @@ -0,0 +1,48 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * 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. + * #L% + */ +package co.elastic.apm.bci.bytebuddy; + +import net.bytebuddy.description.type.TypeDescription; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static co.elastic.apm.bci.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass; +import static co.elastic.apm.bci.bytebuddy.CustomElementMatchers.isInAnyPackage; +import static net.bytebuddy.matcher.ElementMatchers.none; +import static org.assertj.core.api.Assertions.assertThat; + +class CustomElementMatchersTest { + + @Test + void testIncludedPackages() { + final TypeDescription thisClass = TypeDescription.ForLoadedType.of(getClass()); + assertThat(isInAnyPackage(List.of(), none()).matches(thisClass)).isFalse(); + assertThat(isInAnyPackage(List.of(thisClass.getPackage().getName()), none()).matches(thisClass)).isTrue(); + assertThat(isInAnyPackage(List.of(thisClass.getPackage().getName()), none()).matches(TypeDescription.ForLoadedType.of(Object.class))).isFalse(); + } + + @Test + void testClassLoaderCanLoadClass() { + assertThat(classLoaderCanLoadClass(Object.class.getName()).matches(ClassLoader.getSystemClassLoader())).isTrue(); + assertThat(classLoaderCanLoadClass(Object.class.getName()).matches(null)).isTrue(); + assertThat(classLoaderCanLoadClass("not.Here").matches(ClassLoader.getSystemClassLoader())).isFalse(); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/MethodHierarchyMatcherTest.java b/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/MethodHierarchyMatcherTest.java new file mode 100644 index 0000000000..dae48e500a --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/MethodHierarchyMatcherTest.java @@ -0,0 +1,131 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * 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. + * #L% + */ +package co.elastic.apm.bci.bytebuddy; + +import net.bytebuddy.description.method.MethodDescription; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static org.assertj.core.api.Assertions.assertThat; + +class MethodHierarchyMatcherTest { + + @Test + void testMatchInSameClass() throws Exception { + assertThat(CustomElementMatchers.overridesOrImplementsMethodThat(isAnnotatedWith(FindMe.class)) + .matches(new MethodDescription.ForLoadedMethod(TestClass.class.getDeclaredMethod("findInSameClass")))) + .isTrue(); + } + + @Test + void testMatchInSuperClass() throws Exception { + assertThat(CustomElementMatchers + .overridesOrImplementsMethodThat(isAnnotatedWith(FindMe.class)) + .onSuperClassesThat(nameContains("Super")) + .matches(new MethodDescription.ForLoadedMethod(TestClass.class.getDeclaredMethod("findInSuperClass")))) + .isTrue(); + } + + @Test + void testMatchInSuperClass_NotMatchedBySuperClassMatcher() throws Exception { + assertThat(CustomElementMatchers + .overridesOrImplementsMethodThat(isAnnotatedWith(FindMe.class)) + .onSuperClassesThat(not(nameContains("Super"))) + .matches(new MethodDescription.ForLoadedMethod(TestClass.class.getDeclaredMethod("findInSuperClass")))) + .isFalse(); + } + + @Test + void testMatchInInterfaceOfSuperClass() throws Exception { + assertThat(CustomElementMatchers.overridesOrImplementsMethodThat(isAnnotatedWith(FindMe.class)) + .matches(new MethodDescription.ForLoadedMethod(TestClass.class.getDeclaredMethod("findInInterfaceOfSuperClass")))) + .isTrue(); + } + + @Test + void testMatchInSuperInterface() throws Exception { + assertThat(CustomElementMatchers.overridesOrImplementsMethodThat(isAnnotatedWith(FindMe.class)) + .matches(new MethodDescription.ForLoadedMethod(TestClass.class.getDeclaredMethod("findInSuperInterface")))) + .isTrue(); + } + + @Test + void testMatchInInterface() throws Exception { + assertThat(CustomElementMatchers.overridesOrImplementsMethodThat(isAnnotatedWith(FindMe.class)) + .matches(new MethodDescription.ForLoadedMethod(TestClass.class.getDeclaredMethod("findInInterface")))) + .isTrue(); + } + + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + public @interface FindMe { + } + + public interface SuperInterface { + @FindMe + void findInSuperInterface(); + } + + public interface Interfaze extends SuperInterface { + @FindMe + void findInInterface(); + } + + public interface InterfaceOfSuperClass { + @FindMe + void findInInterfaceOfSuperClass(); + } + + public abstract static class SuperClass implements InterfaceOfSuperClass { + @FindMe + public void findInSuperClass() { + } + } + + public static class TestClass extends SuperClass implements Interfaze { + @FindMe + public void findInSameClass() { + } + + @Override + public void findInSuperClass() { + } + + @Override + public void findInInterfaceOfSuperClass() { + } + + @Override + public void findInSuperInterface() { + } + + @Override + public void findInInterface() { + + } + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/SoftlyReferencingTypePoolCacheTest.java b/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/SoftlyReferencingTypePoolCacheTest.java index 803b675967..2e05530b69 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/SoftlyReferencingTypePoolCacheTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/bci/bytebuddy/SoftlyReferencingTypePoolCacheTest.java @@ -7,9 +7,9 @@ * 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. diff --git a/apm-agent-core/src/test/java/co/elastic/apm/configuration/StartupInfoTest.java b/apm-agent-core/src/test/java/co/elastic/apm/configuration/StartupInfoTest.java index d996710769..f71a422578 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/configuration/StartupInfoTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/configuration/StartupInfoTest.java @@ -21,6 +21,7 @@ import co.elastic.apm.configuration.converter.TimeDuration; import co.elastic.apm.configuration.converter.TimeDurationValueConverter; +import co.elastic.apm.impl.stacktrace.StacktraceConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -46,6 +47,7 @@ void setUp() { this.configurationRegistry = ConfigurationRegistry.builder() .addOptionProvider(config) .addOptionProvider(new CoreConfiguration()) + .addOptionProvider(new StacktraceConfiguration()) .addConfigSource(new SimpleSource().add("duration", "1")) .build(); startupInfo = new StartupInfo(); diff --git a/apm-agent-plugins/apm-jaxrs-plugin/pom.xml b/apm-agent-plugins/apm-jaxrs-plugin/pom.xml new file mode 100644 index 0000000000..7b6c84e192 --- /dev/null +++ b/apm-agent-plugins/apm-jaxrs-plugin/pom.xml @@ -0,0 +1,52 @@ + + + + apm-agent-plugins + co.elastic.apm + 0.8.0-SNAPSHOT + + 4.0.0 + + apm-jaxrs-plugin + ${project.groupId}:${project.artifactId} + + 2.27 + + + + + javax.ws.rs + javax.ws.rs-api + 2.1 + provided + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + ${version.jersey} + test + + + org.glassfish.jersey.inject + jersey-hk2 + ${version.jersey} + test + + + javax.xml.bind + jaxb-api + 2.3.0 + test + + + javax.activation + activation + 1.1.1 + test + + + + diff --git a/apm-agent-plugins/apm-jaxrs-plugin/src/main/java/co/elastic/apm/jaxrs/JaxRsTransactionNameInstrumentation.java b/apm-agent-plugins/apm-jaxrs-plugin/src/main/java/co/elastic/apm/jaxrs/JaxRsTransactionNameInstrumentation.java new file mode 100644 index 0000000000..d04448901d --- /dev/null +++ b/apm-agent-plugins/apm-jaxrs-plugin/src/main/java/co/elastic/apm/jaxrs/JaxRsTransactionNameInstrumentation.java @@ -0,0 +1,108 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * 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. + * #L% + */ +package co.elastic.apm.jaxrs; + +import co.elastic.apm.bci.ElasticApmInstrumentation; +import co.elastic.apm.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature; +import co.elastic.apm.impl.ElasticApmTracer; +import co.elastic.apm.impl.stacktrace.StacktraceConfiguration; +import co.elastic.apm.impl.transaction.Transaction; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.NamedElement; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static co.elastic.apm.bci.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass; +import static co.elastic.apm.bci.bytebuddy.CustomElementMatchers.isInAnyPackage; +import static co.elastic.apm.bci.bytebuddy.CustomElementMatchers.overridesOrImplementsMethodThat; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +public class JaxRsTransactionNameInstrumentation extends ElasticApmInstrumentation { + + private Collection applicationPackages = Collections.emptyList(); + + @Advice.OnMethodEnter + private static void setTransactionName(@SimpleMethodSignature String signature) { + if (tracer != null) { + final Transaction transaction = tracer.currentTransaction(); + if (transaction != null) { + transaction.withName(signature); + } + } + } + + @Override + public void init(ElasticApmTracer tracer) { + applicationPackages = tracer.getConfig(StacktraceConfiguration.class).getApplicationPackages(); + } + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + // setting application_packages makes this matcher more performant but is not required + // could lead to false negative matches when importing a 3rd party library whose JAX-RS resources are exposed + return isInAnyPackage(applicationPackages, ElementMatchers.any()); + } + + @Override + public ElementMatcher getTypeMatcher() { + // quote from JAX-RS 2.0 spec (section 3.6 Annotation Inheritance) + // "Note that inheritance of class or interface annotations is not supported." + // However, at least Jersey also supports the @Path to be at a parent class/interface + // we don't to support that at the moment because of performance concerns + // (matching on the class hierarchy vs matching one class) + return isAnnotatedWith(named("javax.ws.rs.Path")); + } + + @Override + public ElementMatcher.Junction getClassLoaderMatcher() { + return not(isBootstrapClassLoader()) + .and(classLoaderCanLoadClass("javax.ws.rs.Path")); + } + + @Override + public ElementMatcher getMethodMatcher() { + // quote from JAX-RS 2.0 spec (section 3.6 Annotation Inheritance) + // "JAX-RS annotations may be used on the methods and method parameters of a super-class or an implemented interface." + return overridesOrImplementsMethodThat( + isAnnotatedWith( + named("javax.ws.rs.GET") + .or(named("javax.ws.rs.POST")) + .or(named("javax.ws.rs.PUT")) + .or(named("javax.ws.rs.DELETE")) + .or(named("javax.ws.rs.HEAD")) + .or(named("javax.ws.rs.OPTIONS")) + .or(named("javax.ws.rs.HttpMethod")))) + .onSuperClassesThat(isInAnyPackage(applicationPackages, ElementMatchers.any())); + } + + @Override + public Collection getInstrumentationGroupNames() { + return Arrays.asList("jax-rs", "jax-rs-annotations"); + } +} diff --git a/apm-agent-plugins/apm-jaxrs-plugin/src/main/java/co/elastic/apm/jaxrs/package-info.java b/apm-agent-plugins/apm-jaxrs-plugin/src/main/java/co/elastic/apm/jaxrs/package-info.java new file mode 100644 index 0000000000..dc1b898a4b --- /dev/null +++ b/apm-agent-plugins/apm-jaxrs-plugin/src/main/java/co/elastic/apm/jaxrs/package-info.java @@ -0,0 +1,23 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * 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. + * #L% + */ +@NonnullApi +package co.elastic.apm.jaxrs; + +import co.elastic.apm.annotation.NonnullApi; diff --git a/apm-agent-plugins/apm-jaxrs-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation b/apm-agent-plugins/apm-jaxrs-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation new file mode 100644 index 0000000000..4536a909ed --- /dev/null +++ b/apm-agent-plugins/apm-jaxrs-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation @@ -0,0 +1 @@ +co.elastic.apm.jaxrs.JaxRsTransactionNameInstrumentation diff --git a/apm-agent-plugins/apm-jaxrs-plugin/src/test/java/co/elastic/apm/jaxrs/JaxRsTransactionNameInstrumentationTest.java b/apm-agent-plugins/apm-jaxrs-plugin/src/test/java/co/elastic/apm/jaxrs/JaxRsTransactionNameInstrumentationTest.java new file mode 100644 index 0000000000..239f9bb330 --- /dev/null +++ b/apm-agent-plugins/apm-jaxrs-plugin/src/test/java/co/elastic/apm/jaxrs/JaxRsTransactionNameInstrumentationTest.java @@ -0,0 +1,88 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * 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. + * #L% + */ +package co.elastic.apm.jaxrs; + +import co.elastic.apm.AbstractInstrumentationTest; +import co.elastic.apm.impl.transaction.Transaction; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JaxRsTransactionNameInstrumentationTest extends JerseyTest { + + @BeforeClass + public static void beforeClass() { + AbstractInstrumentationTest.beforeAll(); + } + + @AfterClass + public static void afterClass() { + AbstractInstrumentationTest.afterAll(); + } + + public Application configure() { + return new ResourceConfig(TestResource.class); + } + + @Before + public void before() { + AbstractInstrumentationTest.reset(); + } + + @Test + public void testJaxRsTransactionName() { + final Transaction request = AbstractInstrumentationTest.getTracer().startTransaction().withType("request").activate(); + try { + assertThat(getClient().target(getBaseUri()).path("test").request().buildGet().invoke(String.class)).isEqualTo("ok"); + } finally { + request.deactivate().end(); + } + assertThat(AbstractInstrumentationTest.getReporter().getFirstTransaction().getName().toString()) + .isEqualTo("JaxRsTransactionNameInstrumentationTest$TestResource#testMethod"); + } + + public interface SuperResourceInterface { + @GET + String testMethod(); + } + + public interface TestResourceInterface extends SuperResourceInterface { + String testMethod(); + } + + static abstract class AbstractResourceClass implements TestResourceInterface { + } + + @Path("test") + public static class TestResource extends AbstractResourceClass { + public String testMethod() { + return "ok"; + } + } +} diff --git a/apm-agent-plugins/pom.xml b/apm-agent-plugins/pom.xml index 8eae601be6..14aa2b87cc 100644 --- a/apm-agent-plugins/pom.xml +++ b/apm-agent-plugins/pom.xml @@ -8,6 +8,7 @@ 4.0.0 pom + apm-jaxrs-plugin apm-jdbc-plugin apm-opentracing-plugin apm-servlet-plugin diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index e4f6b70cae..3d69321c9b 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -250,7 +250,7 @@ you should add an additional entry to this list (make sure to also include the d ==== `disable_instrumentations` A list of instrumentations which should be disabled. -Valid options are `jdbc`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `http-client`, `apache-httpclient`,`spring-resttemplate` and `incubating`. +Valid options are `apache-httpclient`, `http-client`, `incubating`, `jax-rs`, `jax-rs-annotations`, `jdbc`, `opentracing`, `public-api`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `spring-resttemplate`. If you want to try out incubating features, set the value to an empty string. @@ -702,6 +702,7 @@ Disabled by default to save disk space. ==== `application_packages` Used to determine whether a stack trace frame is an 'in-app frame' or a 'library frame'. +Setting this option can also improve the startup time. [options="header"] @@ -874,7 +875,7 @@ The default unit for this option is `ms` # sanitize_field_names=password,passwd,pwd,secret,*key,*token*,*session*,*credit*,*card*,authorization,set-cookie # A list of instrumentations which should be disabled. -# Valid options are `jdbc`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `http-client`, `apache-httpclient`,`spring-resttemplate` and `incubating`. +# Valid options are `apache-httpclient`, `http-client`, `incubating`, `jax-rs`, `jax-rs-annotations`, `jdbc`, `opentracing`, `public-api`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `spring-resttemplate`. # If you want to try out incubating features, # set the value to an empty string. # @@ -1132,6 +1133,7 @@ The default unit for this option is `ms` ############################################ # Used to determine whether a stack trace frame is an 'in-app frame' or a 'library frame'. +# Setting this option can also improve the startup time. # # This setting can be changed at runtime # Type: comma separated list diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index 238f217470..2a6bd8f7fc 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -59,6 +59,16 @@ the agent does not capture transactions. |1.5+, 2.x |Supports embedded Tomcat, Jetty and Undertow +|JAX-RS +|2.x +|The transactions are named based on your resources (`ResourceClass#resourceMethod`). + Note that only the packages configured in <> are scanned for JAX-RS resources. + If you don't set this option, + all classes are scanned. + This comes at the cost of increased startup times, however. + + Note: JAX-RS is only supported when running on a supported <>. + |=== diff --git a/elastic-apm-agent/pom.xml b/elastic-apm-agent/pom.xml index e4c7ce5a21..2d4017e4a0 100644 --- a/elastic-apm-agent/pom.xml +++ b/elastic-apm-agent/pom.xml @@ -152,6 +152,11 @@ apm-httpclient-core ${project.version} + + ${project.groupId} + apm-jaxrs-plugin + ${project.version} + ${project.groupId} apm-jdbc-plugin