Skip to content

Commit

Permalink
enhance class name detection (#3746)
Browse files Browse the repository at this point in the history
* enhance class name detection

* added fix and test for nested scala objects

* added changelog entry

* remove regex

* fix anonymous class names

- anonymous classes are now shown as "ParentClass$1" instead of "ParentClass"
- added more testcases

* fix unit tests
  • Loading branch information
wolframhaussig authored Sep 2, 2024
1 parent 803f6da commit 753bc86
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
[float]
===== Bug fixes
* Fix log4j2 log correlation with shaded application jar - {pull}3764[#3764]
* Improve automatic span class name detection for Scala and nested/anonymous classes - {pull}3746[#3746]
[float]
===== Features
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
package co.elastic.apm.agent.sdk.bytebuddy;

/**
* Utility class to extract the correct simple class name
*/
public class ClassNameParser {
/**
* Utility class, do not instantiate
*/
private ClassNameParser() {}
/**
* returns true if the string only contains digits
* @param str the string to check
* @return
*/
private static boolean isNumeric(String str) {
for(int i = 0; i< str.length(); i++) {
if(!Character.isDigit(str.charAt(i))) {
return false;
}
}
return true;
}
/**
* this method parses the anonymous class name from the full class name
* @param className
* @return
*/
private static String getAnonymousClassName(String className) {
int lastDollarIndex = className.lastIndexOf('$');
int currentDollarIndex;
String nestedClassName;
// we do not want to show just a number for anonymous classes, so we walk back until we find a named (normal or nested) class
do {
currentDollarIndex = className.lastIndexOf('$', lastDollarIndex -1);
nestedClassName = className.substring(currentDollarIndex + 1, lastDollarIndex);
lastDollarIndex = currentDollarIndex;
} while(currentDollarIndex != -1 && isNumeric(nestedClassName));
return className.substring(currentDollarIndex + 1);
}
/**
* Parses the class name from nested and anonymous classes (containing a $ sign)
* <p>
* Note: We cannot know if the $ sign is part of the class name or denotes a nested/anonymous class. As $ signs should not be used in class names anyway,
* we handle them always as a class separator
* </p>
* @param className
* @return
*/
private static String getNestedClassName(String className) {
int dollarIndex = className.lastIndexOf('$');
String innerClassName = className.substring(dollarIndex + 1);
if(isNumeric(innerClassName)) {
// this is an anonymous inner class
className = getAnonymousClassName(className);
} else {
className = innerClassName;
}
return className;
}
/**
* Parses the simple class name from a class name
* @param className
* @return
*/
public static String parse(String className) {
//remove package name
className = className.substring(className.lastIndexOf('.') + 1);
if(className.contains("$")) {
if(className.endsWith("$")) {
// this can happen if the source code was in Scala and the object keyword was used
// https://www.toptal.com/scala/scala-bytecode-and-the-jvm
className = className.substring(0, className.length() - 1);
if(className.contains("$"))
{
className = getNestedClassName(className);
}
} else {
className = getNestedClassName(className);
}
}
return className;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ public Advice.OffsetMapping make(ParameterDescription.InDefinedShape target,
public Target resolve(TypeDescription instrumentedType, MethodDescription instrumentedMethod, Assigner assigner,
Advice.ArgumentHandler argumentHandler, Sort sort) {
final String className = instrumentedMethod.getDeclaringType().getTypeName();
String simpleClassName = className.substring(className.lastIndexOf('$') + 1);
simpleClassName = simpleClassName.substring(simpleClassName.lastIndexOf('.') + 1);
final String simpleClassName = ClassNameParser.parse(className);
final String signature = String.format("%s#%s", simpleClassName, instrumentedMethod.getName());
return Target.ForStackManipulation.of(signature);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
package co.elastic.apm.agent.sdk.bytebuddy;

import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.Test;

import co.elastic.apm.agent.sdk.bytebuddy.clazzes.ParentClass.InnerClass;
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.ParentClass.InnerClass.NestedInnerClass;
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.ParentObject.ChildScalaObject$;
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.SimpleClass;
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.AnonymousClass;
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.AnonymousNestedClass;
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.Dollar$Class;
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.ScalaObject$;

class ClassNameParserTest {

@Test
void testSimpleClassName() {
String className = ClassNameParser.parse(SimpleClass.class.getName());
assertThat(className).isEqualTo("SimpleClass");
}
@Test
void testNestedClassName() {
String className = ClassNameParser.parse(InnerClass.class.getName());
assertThat(className).isEqualTo("InnerClass");
}
@Test
void testDoubleNestedClassName() {
String className = ClassNameParser.parse(NestedInnerClass.class.getName());
assertThat(className).isEqualTo("NestedInnerClass");
}
@Test
void testDollarClassName() {
String className = ClassNameParser.parse(Dollar$Class.class.getName());
//We have no way to know if the class name is Dollar$Class or Class is a nested class of Dollar
//As dollar signs should not be used in names, it should be fine to detect Class instead of Dollar$Class
assertThat(className).describedAs("Dollar signs should not be used in names").isEqualTo("Class");
}
@Test
void testAnonymousClassName() {
String className = ClassNameParser.parse(AnonymousClass.getAnonymousClass().getName());
assertThat(className).isEqualTo("AnonymousClass$1");
}
@Test
void testAnonymousNestedClassName() {
String className = ClassNameParser.parse(InnerClass.getAnonymousClass().getName());
assertThat(className).isEqualTo("InnerClass$1");
}
@Test
void testAnonymousInAnonymousClassName() {
String className = ClassNameParser.parse(AnonymousNestedClass.getAnonymousClass().getName());
assertThat(className).isEqualTo("AnonymousNestedClass$1$1");
}
@Test
void testScalaObjectClassName() {
String className = ClassNameParser.parse(ScalaObject$.class.getName());
assertThat(className).isEqualTo("ScalaObject");
}
@Test
void testNestedScalaObjectClassName() {
String className = ClassNameParser.parse(ChildScalaObject$.class.getName());
assertThat(className).isEqualTo("ChildScalaObject");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;

public class AnonymousClass {
public static Class<?> getAnonymousClass() {
return new Object() {
@Override
public String toString() {
return "foo";
}
}.getClass();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;

public class AnonymousNestedClass {
public static Class<?> getAnonymousClass() {
Factory f = new Factory() {
@Override
public Object getObject() {
return new Object() {
@Override
public String toString() {
return "foo";
}
};
}
};
return f.getObject().getClass();
}
public static interface Factory {
Object getObject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;

public class Dollar$Class {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;

public class ParentClass {

public static class InnerClass {
public static Class<?> getAnonymousClass() {
return new Object() {
@Override
public String toString() {
return "foo";
}
}.getClass();
}
public static class NestedInnerClass {

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;

public class ParentObject {
/**
* Scala objects generate a class with a dollar at the end
* https://www.toptal.com/scala/scala-bytecode-and-the-jvm
*/
public class ChildScalaObject$ {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;

/**
* Scala objects generate a class with a dollar at the end
* https://www.toptal.com/scala/scala-bytecode-and-the-jvm
*/
public class ScalaObject$ {

}
Loading

0 comments on commit 753bc86

Please sign in to comment.