From f1611f92919e40369bac52389a699de31c6c30c7 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Mon, 7 Jan 2019 16:27:22 +0100 Subject: [PATCH] Add trace_methods option (#398) Defines a syntax for matching methods which should be traced closes #347 --- CHANGELOG.md | 4 + .../apm/agent/bci/ElasticApmAgent.java | 19 ++- .../bci/bytebuddy/CustomElementMatchers.java | 15 +++ .../bci/methodmatching/MethodMatcher.java | 123 ++++++++++++++++++ .../TraceMethodInstrumentation.java | 114 ++++++++++++++++ .../MethodMatcherValueConverter.java | 42 ++++++ .../configuration/package-info.java | 23 ++++ .../bci/methodmatching/package-info.java | 23 ++++ .../configuration/CoreConfiguration.java | 33 +++++ .../apm/agent/matcher/WildcardMatcher.java | 40 +++++- .../MethodMatcherInstrumentationTest.java | 82 ++++++++++++ .../bci/methodmatching/MethodMatcherTest.java | 82 ++++++++++++ .../TraceMethodInstrumentationTest.java | 96 ++++++++++++++ .../agent/matcher/WildcardMatcherTest.java | 3 +- docs/configuration.asciidoc | 68 ++++++++++ 15 files changed, 758 insertions(+), 9 deletions(-) create mode 100644 apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcher.java create mode 100644 apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java create mode 100644 apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/configuration/MethodMatcherValueConverter.java create mode 100644 apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/configuration/package-info.java create mode 100644 apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/package-info.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcherInstrumentationTest.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcherTest.java create mode 100644 apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentationTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ff74392785..0fc6fcdaeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # next ## Features + * Added `trace_methods` configuration option which lets you define which methods in your project or 3rd party libraries should be traced. + To create spans for all `public` methods of classes whose name ends in `Service` which are in a sub-package of `org.example.services` use this matcher: + `public org.example.services.*.*Service#*` + ## Bug Fixes # 1.2.0 diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java index 870935afc4..1782ae3075 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java @@ -25,6 +25,8 @@ import co.elastic.apm.agent.bci.bytebuddy.MinimumClassFileVersionValidator; import co.elastic.apm.agent.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory; import co.elastic.apm.agent.bci.bytebuddy.SoftlyReferencingTypePoolCache; +import co.elastic.apm.agent.bci.methodmatching.MethodMatcher; +import co.elastic.apm.agent.bci.methodmatching.TraceMethodInstrumentation; import co.elastic.apm.agent.configuration.CoreConfiguration; import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.ElasticApmTracerBuilder; @@ -44,10 +46,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.File; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.ServiceLoader; @@ -84,7 +88,20 @@ public static void initialize(Instrumentation instrumentation, File agentJarFile } public static void initInstrumentation(ElasticApmTracer tracer, Instrumentation instrumentation) { - initInstrumentation(tracer, instrumentation, ServiceLoader.load(ElasticApmInstrumentation.class, ElasticApmInstrumentation.class.getClassLoader())); + initInstrumentation(tracer, instrumentation, loadInstrumentations(tracer)); + } + + @Nonnull + private static Iterable loadInstrumentations(ElasticApmTracer tracer) { + final ArrayList instrumentations = new ArrayList(); + for (ElasticApmInstrumentation instrumentation : ServiceLoader.load(ElasticApmInstrumentation.class, ElasticApmInstrumentation.class.getClassLoader())) { + instrumentations.add(instrumentation); + } + for (MethodMatcher traceMethod : tracer.getConfig(CoreConfiguration.class).getTraceMethods()) { + instrumentations.add(new TraceMethodInstrumentation(traceMethod)); + } + + return instrumentations; } public static void initInstrumentation(final ElasticApmTracer tracer, Instrumentation instrumentation, diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/CustomElementMatchers.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/CustomElementMatchers.java index c8a6594bf6..ae8ccc3297 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/CustomElementMatchers.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/CustomElementMatchers.java @@ -19,6 +19,7 @@ */ package co.elastic.apm.agent.bci.bytebuddy; +import co.elastic.apm.agent.matcher.WildcardMatcher; import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; @@ -99,4 +100,18 @@ private static boolean canLoadClass(@Nullable ClassLoader target, String classNa public static MethodHierarchyMatcher overridesOrImplementsMethodThat(ElementMatcher methodElementMatcher) { return new MethodHierarchyMatcher(methodElementMatcher); } + + public static ElementMatcher.Junction matches(final WildcardMatcher matcher) { + return new ElementMatcher.Junction.AbstractBase() { + @Override + public boolean matches(NamedElement target) { + return matcher.matches(target.getActualName()); + } + + @Override + public String toString() { + return "matches(" + matcher + ")"; + } + }; + } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcher.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcher.java new file mode 100644 index 0000000000..fd932bc911 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcher.java @@ -0,0 +1,123 @@ +/*- + * #%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.agent.bci.methodmatching; + +import co.elastic.apm.agent.matcher.WildcardMatcher; +import org.stagemonitor.util.StringUtils; + +import javax.annotation.Nullable; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static co.elastic.apm.agent.matcher.WildcardMatcher.caseSensitiveMatcher; + +public class MethodMatcher { + + private static final String MODIFIER = "(public|private|protected|\\*)"; + private static final String CLASS_NAME = "([a-zA-Z\\d_$.\\*]+)"; + private static final String METHOD_NAME = "([a-zA-Z\\d_$\\*]+)"; + private static final String PARAM = "([a-zA-Z\\d_$.\\[\\]\\*]+)"; + private static final String PARAMS = PARAM + "(,\\s*" + PARAM + ")*"; + private static final Pattern METHOD_MATCHER_PATTERN = Pattern.compile("^(" + MODIFIER + "\\s+)?" + CLASS_NAME + "#" + METHOD_NAME + "(\\((" + PARAMS + ")*\\))?$"); + + private final String stringRepresentation; + @Nullable + private final Integer modifier; + private final WildcardMatcher classMatcher; + private final WildcardMatcher methodMatcher; + @Nullable + private final List argumentMatchers; + + private MethodMatcher(String stringRepresentation, @Nullable Integer modifier, WildcardMatcher classMatcher, WildcardMatcher methodMatcher, @Nullable List argumentMatchers) { + this.stringRepresentation = stringRepresentation; + this.modifier = modifier; + this.classMatcher = classMatcher; + this.methodMatcher = methodMatcher; + this.argumentMatchers = argumentMatchers; + } + + public static MethodMatcher of(String methodMatcher) { + final Matcher matcher = METHOD_MATCHER_PATTERN.matcher(methodMatcher); + if (!matcher.matches()) { + throw new IllegalArgumentException("'" + methodMatcher + "'" + " is not a valid method matcher"); + } + + return new MethodMatcher(methodMatcher, getModifier(matcher.group(2)), caseSensitiveMatcher(matcher.group(3)), caseSensitiveMatcher(matcher.group(4)), + getArgumentMatchers(matcher.group(5))); + } + + @Nullable + private static Integer getModifier(@Nullable String modifier) { + if (modifier == null) { + return null; + } + switch (modifier) { + case "public": + return Modifier.PUBLIC; + case "private": + return Modifier.PRIVATE; + case "protected": + return Modifier.PROTECTED; + default: + return null; + } + } + + @Nullable + private static List getArgumentMatchers(@Nullable String arguments) { + if (arguments == null) { + return null; + } + // remove parenthesis + arguments = arguments.substring(1, arguments.length() - 1); + final String[] splitArguments = StringUtils.split(arguments, ','); + List matchers = new ArrayList<>(splitArguments.length); + for (String argument : splitArguments) { + matchers.add(caseSensitiveMatcher(argument.trim())); + } + return matchers; + } + + public WildcardMatcher getClassMatcher() { + return classMatcher; + } + + @Nullable + public Integer getModifier() { + return modifier; + } + + public WildcardMatcher getMethodMatcher() { + return methodMatcher; + } + + @Nullable + public List getArgumentMatchers() { + return argumentMatchers; + } + + @Override + public String toString() { + return stringRepresentation; + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java new file mode 100644 index 0000000000..2fa04538d1 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java @@ -0,0 +1,114 @@ +/*- + * #%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.agent.bci.methodmatching; + +import co.elastic.apm.agent.bci.ElasticApmInstrumentation; +import co.elastic.apm.agent.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.matcher.WildcardMatcher; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import javax.annotation.Nullable; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static co.elastic.apm.agent.bci.bytebuddy.CustomElementMatchers.matches; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public class TraceMethodInstrumentation extends ElasticApmInstrumentation { + + protected final MethodMatcher methodMatcher; + + public TraceMethodInstrumentation(MethodMatcher methodMatcher) { + this.methodMatcher = methodMatcher; + } + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onMethodEnter(@SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature String signature, + @Advice.Local("span") AbstractSpan span) { + if (tracer != null) { + final AbstractSpan parent = tracer.activeSpan(); + if (parent == null) { + span = tracer.startTransaction() + .withName(signature) + .activate(); + } else { + span = parent.createSpan() + .withName(signature) + .activate(); + } + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onMethodExit(@Nullable @Advice.Local("span") AbstractSpan span, + @Advice.Thrown Throwable t) { + if (span != null) { + span.captureException(t) + .deactivate() + .end(); + } + } + + @Override + public ElementMatcher getTypeMatcher() { + return matches(methodMatcher.getClassMatcher()) + .and(declaresMethod(matches(methodMatcher.getMethodMatcher()))); + } + + @Override + public ElementMatcher getMethodMatcher() { + ElementMatcher.Junction matcher = matches(methodMatcher.getMethodMatcher()); + if (methodMatcher.getModifier() != null) { + switch (methodMatcher.getModifier()) { + case Modifier.PUBLIC: + matcher = matcher.and(ElementMatchers.isPublic()); + break; + case Modifier.PROTECTED: + matcher = matcher.and(ElementMatchers.isProtected()); + break; + case Modifier.PRIVATE: + matcher = matcher.and(ElementMatchers.isPrivate()); + break; + } + } + if (methodMatcher.getArgumentMatchers() != null) { + matcher = matcher.and(takesArguments(methodMatcher.getArgumentMatchers().size())); + List argumentMatchers = methodMatcher.getArgumentMatchers(); + for (int i = 0; i < argumentMatchers.size(); i++) { + matcher = matcher.and(takesArgument(i, matches(argumentMatchers.get(i)))); + } + } + return matcher; + } + + @Override + public Collection getInstrumentationGroupNames() { + return Collections.singletonList("method-matching"); + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/configuration/MethodMatcherValueConverter.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/configuration/MethodMatcherValueConverter.java new file mode 100644 index 0000000000..bfcbf2d1fb --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/configuration/MethodMatcherValueConverter.java @@ -0,0 +1,42 @@ +/*- + * #%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.agent.bci.methodmatching.configuration; + +import co.elastic.apm.agent.bci.methodmatching.MethodMatcher; +import org.stagemonitor.configuration.converter.ValueConverter; + +public enum MethodMatcherValueConverter implements ValueConverter { + INSTANCE; + + @Override + public MethodMatcher convert(String methodMatcher) throws IllegalArgumentException { + return MethodMatcher.of(methodMatcher); + } + + @Override + public String toString(MethodMatcher methodMatcher) { + return methodMatcher.toString(); + } + + @Override + public String toSafeString(MethodMatcher value) { + return toString(value); + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/configuration/package-info.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/configuration/package-info.java new file mode 100644 index 0000000000..be7cb84d15 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/configuration/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.agent.bci.methodmatching.configuration; + +import co.elastic.apm.agent.annotation.NonnullApi; diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/package-info.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/package-info.java new file mode 100644 index 0000000000..a383b16fa4 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/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.agent.bci.methodmatching; + +import co.elastic.apm.agent.annotation.NonnullApi; diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java index 033c33ddfe..91ea629517 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java @@ -20,6 +20,8 @@ package co.elastic.apm.agent.configuration; import co.elastic.apm.agent.bci.ElasticApmInstrumentation; +import co.elastic.apm.agent.bci.methodmatching.MethodMatcher; +import co.elastic.apm.agent.bci.methodmatching.configuration.MethodMatcherValueConverter; import co.elastic.apm.agent.configuration.validation.RegexValidator; import co.elastic.apm.agent.matcher.WildcardMatcher; import co.elastic.apm.agent.matcher.WildcardMatcherValueConverter; @@ -237,6 +239,33 @@ public static String getAllInstrumentationGroupNames() { WildcardMatcher.valueOf("(?-i)org.wildfly.security*") )); + private final ConfigurationOption> traceMethods = ConfigurationOption + .builder(new ListValueConverter<>(MethodMatcherValueConverter.INSTANCE), List.class) + .key("trace_methods") + .configurationCategory(CORE_CATEGORY) + .description("A list of methods for with to create a transaction or span.\n" + + "\n" + + "The syntax is `modifier fully.qualified.class.Name#methodName(fully.qualified.parameter.Type)`.\n" + + "You can use wildcards for the class name, the method name and the parameter types.\n" + + "The `*` wildcard matches zero or more characters.\n" + + "Specifying the parameter types is optional.\n" + + "The `modifier` can be omitted or one of `public`, `protected`, `private` or `*`.\n" + + "\n" + + "A few examples:\n" + + "\n" + + " - `org.example.MyClass#myMethod`\n" + + " - `org.example.MyClass#myMethod()`\n" + + " - `org.example.MyClass#myMethod(java.lang.String)`\n" + + " - `org.example.MyClass#myMe*od(java.lang.String, int)`\n" + + " - `private org.example.MyClass#myMe*od(java.lang.String, *)`\n" + + " - `* org.example.MyClas*#myMe*od(*.String, int[])`\n" + + " - `public org.example.services.*.*Service#*`\n" + + "\n" + + "NOTE: Only use wildcards if necessary.\n" + + "The more methods you match to more overhead will be caused by the agent.\n" + + "Also note that there is a maximum amount of spans per transaction (see <>).") + .buildWithDefault(Collections.emptyList()); + public boolean isActive() { return active.get(); } @@ -288,4 +317,8 @@ public boolean isTypeMatchingWithNamePreFilter() { public List getExcludedFromInstrumentation() { return excludedFromInstrumentation.get(); } + + public List getTraceMethods() { + return traceMethods.get(); + } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/matcher/WildcardMatcher.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/matcher/WildcardMatcher.java index 6c7f83f1c8..df89b15dfd 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/matcher/WildcardMatcher.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/matcher/WildcardMatcher.java @@ -75,6 +75,10 @@ public abstract class WildcardMatcher { private static final String CASE_SENSITIVE_PREFIX = "(?-i)"; private static final String WILDCARD = "*"; + public static WildcardMatcher caseSensitiveMatcher(String matcher) { + return valueOf(CASE_SENSITIVE_PREFIX + matcher); + } + /** * Constructs a new {@link WildcardMatcher} via a wildcard string. *

@@ -116,7 +120,7 @@ public static WildcardMatcher valueOf(final String wildcardString) { !isLast || matcher.endsWith(WILDCARD), ignoreCase)); } - return new CompoundWildcardMatcher(wildcardString, matchers); + return new CompoundWildcardMatcher(wildcardString, matcher, matchers); } /** @@ -204,7 +208,7 @@ static char charAt(int i, String firstPart, String secondPart, int firstPartLeng * @param s the String to match * @return whether the String matches the given pattern */ - abstract boolean matches(String s); + public abstract boolean matches(String s); /** * This is a different version of {@link #matches(String)} which has the same semantics as calling @@ -219,7 +223,17 @@ static char charAt(int i, String firstPart, String secondPart, int firstPartLeng * when the wildcard pattern matches the partitioned string, * {@code false} otherwise. */ - abstract boolean matches(String firstPart, @Nullable String secondPart); + public abstract boolean matches(String firstPart, @Nullable String secondPart); + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof WildcardMatcher)) { + return false; + } + return toString().equals(obj.toString()); + } + + public abstract String getMatcher(); /** * This {@link WildcardMatcher} supports wildcards in the middle of the matcher by decomposing the matcher into several @@ -227,15 +241,17 @@ static char charAt(int i, String firstPart, String secondPart, int firstPartLeng */ static class CompoundWildcardMatcher extends WildcardMatcher { private final String wildcardString; + private final String matcher; private final List wildcardMatchers; - CompoundWildcardMatcher(String wildcardString, List wildcardMatchers) { + CompoundWildcardMatcher(String wildcardString, String matcher, List wildcardMatchers) { this.wildcardString = wildcardString; + this.matcher = matcher; this.wildcardMatchers = wildcardMatchers; } @Override - boolean matches(String s) { + public boolean matches(String s) { int offset = 0; for (int i = 0; i < wildcardMatchers.size(); i++) { final SimpleWildcardMatcher matcher = wildcardMatchers.get(i); @@ -249,7 +265,7 @@ boolean matches(String s) { } @Override - boolean matches(String firstPart, @Nullable String secondPart) { + public boolean matches(String firstPart, @Nullable String secondPart) { int offset = 0; for (int i = 0; i < wildcardMatchers.size(); i++) { final SimpleWildcardMatcher matcher = wildcardMatchers.get(i); @@ -266,6 +282,11 @@ boolean matches(String firstPart, @Nullable String secondPart) { public String toString() { return wildcardString; } + + @Override + public String getMatcher() { + return matcher; + } } /** @@ -319,7 +340,7 @@ int indexOf(String firstPart, @Nullable String secondPart, int offset) { if (wildcardAtEnd && wildcardAtBeginning) { return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, offset, totalLength); } else if (wildcardAtEnd) { - return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, 0, matcher.length()); + return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, 0, 1); } else if (wildcardAtBeginning) { return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, totalLength - matcher.length(), totalLength); } else if (totalLength == matcher.length()) { @@ -328,5 +349,10 @@ int indexOf(String firstPart, @Nullable String secondPart, int offset) { return -1; } } + + @Override + public String getMatcher() { + return matcher; + } } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcherInstrumentationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcherInstrumentationTest.java new file mode 100644 index 0000000000..ae1e84b514 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcherInstrumentationTest.java @@ -0,0 +1,82 @@ +/*- + * #%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.agent.bci.methodmatching; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; + +class MethodMatcherInstrumentationTest { + + @Test + void testMethodMatching() throws Exception { + assertMatches(MethodMatcher.of(getClass().getName() + "#*"), getClass().getDeclaredMethod("testMethodMatching")); + assertMatches(MethodMatcher.of(getClass().getName() + "#testMethodMatching"), getClass().getDeclaredMethod("testMethodMatching")); + assertMatches(MethodMatcher.of(getClass().getName() + "#testMethodMatching()"), getClass().getDeclaredMethod("testMethodMatching")); + assertMatches(MethodMatcher.of(getClass().getName() + "#testIntParameter"), getClass().getDeclaredMethod("testIntParameter", int.class)); + assertMatches(MethodMatcher.of("private " + getClass().getName() + "#testIntParameter"), getClass().getDeclaredMethod("testIntParameter", int.class)); + assertMatches(MethodMatcher.of("* " + getClass().getName() + "#testIntParameter"), getClass().getDeclaredMethod("testIntParameter", int.class)); + assertMatches(MethodMatcher.of(getClass().getName() + "#testIntParameter(int)"), getClass().getDeclaredMethod("testIntParameter", int.class)); + assertMatches(MethodMatcher.of(getClass().getName() + "#testStringParameter(java.lang.String)"), getClass().getDeclaredMethod("testStringParameter", String.class)); + assertMatches(MethodMatcher.of("protected " + getClass().getName() + "#testStringParameter(java.lang.String)"), getClass().getDeclaredMethod("testStringParameter", String.class)); + assertMatches(MethodMatcher.of("* " + getClass().getName() + "#testStringParameter(java.lang.String)"), getClass().getDeclaredMethod("testStringParameter", String.class)); + assertMatches(MethodMatcher.of(getClass().getName() + "#testMultipleParameters(java.lang.String, int[], java.lang.Object[])"), getClass().getDeclaredMethod("testMultipleParameters", String.class, int[].class, Object[].class)); + assertMatches(MethodMatcher.of(getClass().getName() + "#testMultipleParameters(*.String, int[], java.lang.Object[])"), getClass().getDeclaredMethod("testMultipleParameters", String.class, int[].class, Object[].class)); + assertMatches(MethodMatcher.of(getClass().getName() + "#testMultipleParameters(*, *, *)"), getClass().getDeclaredMethod("testMultipleParameters", String.class, int[].class, Object[].class)); + } + + @Test + void testDoesNotMatch() throws Exception { + assertDoesNotMatch(MethodMatcher.of(getClass().getName().toLowerCase() + "#testDoesNotMatch"), getClass().getDeclaredMethod("testDoesNotMatch")); + assertDoesNotMatch(MethodMatcher.of(getClass().getName() + "#testdoesnotmatch"), getClass().getDeclaredMethod("testDoesNotMatch")); + assertDoesNotMatch(MethodMatcher.of(getClass().getName() + "#DoesNot*"), getClass().getDeclaredMethod("testDoesNotMatch")); + } + + private void testIntParameter(int i) { + } + + protected void testStringParameter(String s) { + } + + private void testMultipleParameters(String foo, int[] bar, Object... baz) { + } + + private void assertDoesNotMatch(MethodMatcher methodMatcher, Method method) { + assertThat(method).isNotNull(); + assertThat(methodMatcher).isNotNull(); + final TraceMethodInstrumentation methodMatcherInstrumentation = new TraceMethodInstrumentation(methodMatcher); + assertThat( + methodMatcherInstrumentation.getTypeMatcher().matches(TypeDescription.ForLoadedType.of(method.getDeclaringClass())) + && methodMatcherInstrumentation.getMethodMatcher().matches(new MethodDescription.ForLoadedMethod(method))) + .isFalse(); + } + + private void assertMatches(MethodMatcher methodMatcher, Method method) { + assertThat(method).isNotNull(); + assertThat(methodMatcher).isNotNull(); + final TraceMethodInstrumentation methodMatcherInstrumentation = new TraceMethodInstrumentation(methodMatcher); + assertThat(methodMatcherInstrumentation.getTypeMatcher().matches(TypeDescription.ForLoadedType.of(method.getDeclaringClass()))).isTrue(); + assertThat(methodMatcherInstrumentation.getMethodMatcher().matches(new MethodDescription.ForLoadedMethod(method))).isTrue(); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcherTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcherTest.java new file mode 100644 index 0000000000..01a91b6eb9 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/MethodMatcherTest.java @@ -0,0 +1,82 @@ +/*- + * #%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.agent.bci.methodmatching; + +import org.junit.jupiter.api.Test; + +import static co.elastic.apm.agent.matcher.WildcardMatcher.caseSensitiveMatcher; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class MethodMatcherTest { + + @Test + void testMethodMatcherWithoutMethod() { + assertThatThrownBy(() -> MethodMatcher.of("co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testMethodMatcherWithoutArguments() { + final MethodMatcher methodMatcher = MethodMatcher.of("co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest#testMethodMatcher"); + assertThat(methodMatcher).isNotNull(); + assertThat(methodMatcher.getClassMatcher().getMatcher()).isEqualTo("co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest"); + assertThat(methodMatcher.getMethodMatcher().getMatcher()).isEqualTo("testMethodMatcher"); + assertThat(methodMatcher.getArgumentMatchers()).isNull(); + } + + @Test + void testMethodMatcherNoArguments() { + final MethodMatcher methodMatcher = MethodMatcher.of("public co.elastic.apm.agent.bci.methodmatching.Method*Test#testMethodMatcher()"); + assertThat(methodMatcher).isNotNull(); + assertThat(methodMatcher.getClassMatcher().getMatcher()).isEqualTo("co.elastic.apm.agent.bci.methodmatching.Method*Test"); + assertThat(methodMatcher.getMethodMatcher().getMatcher()).isEqualTo("testMethodMatcher"); + assertThat(methodMatcher.getArgumentMatchers()).isEmpty(); + } + + @Test + void testMethodMatcherOneArg() { + final MethodMatcher methodMatcher = MethodMatcher.of("private co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest#test*Matcher(java.lang.String)"); + assertThat(methodMatcher).isNotNull(); + assertThat(methodMatcher.getClassMatcher().getMatcher()).isEqualTo("co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest"); + assertThat(methodMatcher.getMethodMatcher().getMatcher()).isEqualTo("test*Matcher"); + assertThat(methodMatcher.getArgumentMatchers()).hasSize(1); + assertThat(methodMatcher.getArgumentMatchers()).contains(caseSensitiveMatcher("java.lang.String")); + } + + @Test + void testMethodMatcherTwoArgs() { + final MethodMatcher methodMatcher = MethodMatcher.of("protected co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest#testMethodMatcher(*String, foo)"); + assertThat(methodMatcher).isNotNull(); + assertThat(methodMatcher.getClassMatcher().getMatcher()).isEqualTo("co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest"); + assertThat(methodMatcher.getMethodMatcher().getMatcher()).isEqualTo("testMethodMatcher"); + assertThat(methodMatcher.getArgumentMatchers()).hasSize(2); + assertThat(methodMatcher.getArgumentMatchers()).containsExactly(caseSensitiveMatcher("*String"), caseSensitiveMatcher("foo")); + } + + @Test + void testMethodMatcherThreeArgs() { + final MethodMatcher methodMatcher = MethodMatcher.of("* co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest#testMethodMatcher(java.lang.String, foo,bar)"); + assertThat(methodMatcher).isNotNull(); + assertThat(methodMatcher.getClassMatcher().getMatcher()).isEqualTo("co.elastic.apm.agent.bci.methodmatching.MethodMatcherTest"); + assertThat(methodMatcher.getMethodMatcher().getMatcher()).isEqualTo("testMethodMatcher"); + assertThat(methodMatcher.getArgumentMatchers()).hasSize(3); + assertThat(methodMatcher.getArgumentMatchers()).containsExactly(caseSensitiveMatcher("java.lang.String"), caseSensitiveMatcher("foo"), caseSensitiveMatcher("bar")); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentationTest.java new file mode 100644 index 0000000000..7d2643b5f3 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentationTest.java @@ -0,0 +1,96 @@ +/*- + * #%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.agent.bci.methodmatching; + +import co.elastic.apm.agent.MockReporter; +import co.elastic.apm.agent.bci.ElasticApmAgent; +import co.elastic.apm.agent.configuration.CoreConfiguration; +import co.elastic.apm.agent.configuration.SpyConfiguration; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.ElasticApmTracerBuilder; +import net.bytebuddy.agent.ByteBuddyAgent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.stagemonitor.configuration.ConfigurationRegistry; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +class TraceMethodInstrumentationTest { + + private MockReporter reporter; + private ElasticApmTracer tracer; + + @BeforeEach + void setUp() { + reporter = new MockReporter(); + ConfigurationRegistry config = SpyConfiguration.createSpyConfig(); + when(config.getConfig(CoreConfiguration.class).getTraceMethods()).thenReturn(Collections.singletonList( + MethodMatcher.of("private co.elastic.apm.agent.bci.methodmatching.TraceMethodInstrumentationTest#traceMe*()")) + ); + tracer = new ElasticApmTracerBuilder() + .configurationRegistry(config) + .reporter(reporter) + .build(); + ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install()); + } + + @AfterEach + void tearDown() { + ElasticApmAgent.reset(); + } + + @Test + void testTraceMethod() { + traceMe(); + assertThat(reporter.getTransactions()).hasSize(1); + assertThat(reporter.getSpans()).hasSize(1); + } + + @Test + void testNotMatched_VisibilityModifier() { + traceMeNot(); + assertThat(reporter.getTransactions()).isEmpty(); + } + + @Test + void testNotMatched_Parameters() { + traceMeNot(false); + assertThat(reporter.getTransactions()).isEmpty(); + } + + // not traced because visibility modifier does not match + public void traceMeNot() { + } + + private void traceMeNot(boolean doesNotMatchParameterMatcher) { + } + + private void traceMe() { + traceMeToo(); + } + + private void traceMeToo() { + + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/matcher/WildcardMatcherTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/matcher/WildcardMatcherTest.java index 8f0063def2..88dfd8c7a4 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/matcher/WildcardMatcherTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/matcher/WildcardMatcherTest.java @@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test; import java.util.Arrays; -import java.util.List; import static co.elastic.apm.agent.matcher.WildcardMatcher.indexOfIgnoreCase; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -38,6 +37,7 @@ void testMatchesStartsWith() { softly.assertThat(matcher.matches("foobar")).isTrue(); softly.assertThat(matcher.matches("bar")).isFalse(); softly.assertThat(matcher.matches("barfoo")).isFalse(); + softly.assertThat(matcher.matches("rfoo")).isFalse(); }); } @@ -200,6 +200,7 @@ void testMatchesEndsWith() { softly.assertThat(matcher.matches("foobar")).isFalse(); softly.assertThat(matcher.matches("bar")).isFalse(); softly.assertThat(matcher.matches("barfoo")).isTrue(); + softly.assertThat(matcher.matches("foor")).isFalse(); }); } diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index c5dca204f2..07b8e664d8 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -298,6 +298,46 @@ Prepending an element with `(?-i)` makes the matching case sensitive. | `elastic.apm.unnest_exceptions` | `unnest_exceptions` | `ELASTIC_APM_UNNEST_EXCEPTIONS` |============ +[float] +[[config-trace-methods]] +==== `trace_methods` + +A list of methods for with to create a transaction or span. + +The syntax is `modifier fully.qualified.class.Name#methodName(fully.qualified.parameter.Type)`. +You can use wildcards for the class name, the method name and the parameter types. +The `*` wildcard matches zero or more characters. +Specifying the parameter types is optional. +The `modifier` can be omitted or one of `public`, `protected`, `private` or `*`. + +A few examples: + + - `org.example.MyClass#myMethod` + - `org.example.MyClass#myMethod()` + - `org.example.MyClass#myMethod(java.lang.String)` + - `org.example.MyClass#myMe*od(java.lang.String, int)` + - `private org.example.MyClass#myMe*od(java.lang.String, *)` + - `* org.example.MyClas*#myMe*od(*.String, int[])` + - `public org.example.services.*.*Service#*` + +NOTE: Only use wildcards if necessary. +The more methods you match to more overhead will be caused by the agent. +Also note that there is a maximum amount of spans per transaction (see <>). + + +[options="header"] +|============ +| Default | Type | Dynamic +| `` | List | false +|============ + + +[options="header"] +|============ +| Java System Properties | Property file | Environment +| `elastic.apm.trace_methods` | `trace_methods` | `ELASTIC_APM_TRACE_METHODS` +|============ + [[config-http]] === HTTP configuration options [float] @@ -1005,6 +1045,34 @@ The default unit for this option is `ms` # # unnest_exceptions=(?-i)*Nested*Exception +# A list of methods for with to create a transaction or span. +# +# The syntax is `modifier fully.qualified.class.Name#methodName(fully.qualified.parameter.Type)`. +# You can use wildcards for the class name, the method name and the parameter types. +# The `*` wildcard matches zero or more characters. +# Specifying the parameter types is optional. +# The `modifier` can be omitted or one of `public`, `protected`, `private` or `*`. +# +# A few examples: +# +# - `org.example.MyClass#myMethod` +# - `org.example.MyClass#myMethod()` +# - `org.example.MyClass#myMethod(java.lang.String)` +# - `org.example.MyClass#myMe*od(java.lang.String, int)` +# - `private org.example.MyClass#myMe*od(java.lang.String, *)` +# - `* org.example.MyClas*#myMe*od(*.String, int[])` +# - `public org.example.services.*.*Service#*` +# +# NOTE: Only use wildcards if necessary. +# The more methods you match to more overhead will be caused by the agent. +# Also note that there is a maximum amount of spans per transaction (see <>). +# +# This setting can not be changed at runtime. Changes require a restart of the application. +# Type: comma separated list +# Default value: +# +# trace_methods= + ############################################ # HTTP # ############################################