Skip to content

Commit

Permalink
Add trace_methods option (#398)
Browse files Browse the repository at this point in the history
Defines a syntax for matching methods which should be traced

closes #347
  • Loading branch information
felixbarny authored Jan 7, 2019
1 parent 035ba3a commit f1611f9
Show file tree
Hide file tree
Showing 15 changed files with 758 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<ElasticApmInstrumentation> loadInstrumentations(ElasticApmTracer tracer) {
final ArrayList<ElasticApmInstrumentation> instrumentations = new ArrayList<ElasticApmInstrumentation>();
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,4 +100,18 @@ private static boolean canLoadClass(@Nullable ClassLoader target, String classNa
public static MethodHierarchyMatcher overridesOrImplementsMethodThat(ElementMatcher<? super MethodDescription> methodElementMatcher) {
return new MethodHierarchyMatcher(methodElementMatcher);
}

public static ElementMatcher.Junction<NamedElement> matches(final WildcardMatcher matcher) {
return new ElementMatcher.Junction.AbstractBase<NamedElement>() {
@Override
public boolean matches(NamedElement target) {
return matcher.matches(target.getActualName());
}

@Override
public String toString() {
return "matches(" + matcher + ")";
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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<WildcardMatcher> argumentMatchers;

private MethodMatcher(String stringRepresentation, @Nullable Integer modifier, WildcardMatcher classMatcher, WildcardMatcher methodMatcher, @Nullable List<WildcardMatcher> 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<WildcardMatcher> 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<WildcardMatcher> 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<WildcardMatcher> getArgumentMatchers() {
return argumentMatchers;
}

@Override
public String toString() {
return stringRepresentation;
}
}
Original file line number Diff line number Diff line change
@@ -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<? super TypeDescription> getTypeMatcher() {
return matches(methodMatcher.getClassMatcher())
.and(declaresMethod(matches(methodMatcher.getMethodMatcher())));
}

@Override
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
ElementMatcher.Junction<? super MethodDescription> 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<WildcardMatcher> 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<String> getInstrumentationGroupNames() {
return Collections.singletonList("method-matching");
}
}
Original file line number Diff line number Diff line change
@@ -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<MethodMatcher> {
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit f1611f9

Please sign in to comment.