Skip to content

Commit 753bc86

Browse files
enhance class name detection (#3746)
* 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
1 parent 803f6da commit 753bc86

File tree

12 files changed

+395
-3
lines changed

12 files changed

+395
-3
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
3434
[float]
3535
===== Bug fixes
3636
* Fix log4j2 log correlation with shaded application jar - {pull}3764[#3764]
37+
* Improve automatic span class name detection for Scala and nested/anonymous classes - {pull}3746[#3746]
3738
3839
[float]
3940
===== Features
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.sdk.bytebuddy;
20+
21+
/**
22+
* Utility class to extract the correct simple class name
23+
*/
24+
public class ClassNameParser {
25+
/**
26+
* Utility class, do not instantiate
27+
*/
28+
private ClassNameParser() {}
29+
/**
30+
* returns true if the string only contains digits
31+
* @param str the string to check
32+
* @return
33+
*/
34+
private static boolean isNumeric(String str) {
35+
for(int i = 0; i< str.length(); i++) {
36+
if(!Character.isDigit(str.charAt(i))) {
37+
return false;
38+
}
39+
}
40+
return true;
41+
}
42+
/**
43+
* this method parses the anonymous class name from the full class name
44+
* @param className
45+
* @return
46+
*/
47+
private static String getAnonymousClassName(String className) {
48+
int lastDollarIndex = className.lastIndexOf('$');
49+
int currentDollarIndex;
50+
String nestedClassName;
51+
// 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
52+
do {
53+
currentDollarIndex = className.lastIndexOf('$', lastDollarIndex -1);
54+
nestedClassName = className.substring(currentDollarIndex + 1, lastDollarIndex);
55+
lastDollarIndex = currentDollarIndex;
56+
} while(currentDollarIndex != -1 && isNumeric(nestedClassName));
57+
return className.substring(currentDollarIndex + 1);
58+
}
59+
/**
60+
* Parses the class name from nested and anonymous classes (containing a $ sign)
61+
* <p>
62+
* 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,
63+
* we handle them always as a class separator
64+
* </p>
65+
* @param className
66+
* @return
67+
*/
68+
private static String getNestedClassName(String className) {
69+
int dollarIndex = className.lastIndexOf('$');
70+
String innerClassName = className.substring(dollarIndex + 1);
71+
if(isNumeric(innerClassName)) {
72+
// this is an anonymous inner class
73+
className = getAnonymousClassName(className);
74+
} else {
75+
className = innerClassName;
76+
}
77+
return className;
78+
}
79+
/**
80+
* Parses the simple class name from a class name
81+
* @param className
82+
* @return
83+
*/
84+
public static String parse(String className) {
85+
//remove package name
86+
className = className.substring(className.lastIndexOf('.') + 1);
87+
if(className.contains("$")) {
88+
if(className.endsWith("$")) {
89+
// this can happen if the source code was in Scala and the object keyword was used
90+
// https://www.toptal.com/scala/scala-bytecode-and-the-jvm
91+
className = className.substring(0, className.length() - 1);
92+
if(className.contains("$"))
93+
{
94+
className = getNestedClassName(className);
95+
}
96+
} else {
97+
className = getNestedClassName(className);
98+
}
99+
}
100+
return className;
101+
}
102+
}

apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/bytebuddy/SimpleMethodSignatureOffsetMappingFactory.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ public Advice.OffsetMapping make(ParameterDescription.InDefinedShape target,
5151
public Target resolve(TypeDescription instrumentedType, MethodDescription instrumentedMethod, Assigner assigner,
5252
Advice.ArgumentHandler argumentHandler, Sort sort) {
5353
final String className = instrumentedMethod.getDeclaringType().getTypeName();
54-
String simpleClassName = className.substring(className.lastIndexOf('$') + 1);
55-
simpleClassName = simpleClassName.substring(simpleClassName.lastIndexOf('.') + 1);
54+
final String simpleClassName = ClassNameParser.parse(className);
5655
final String signature = String.format("%s#%s", simpleClassName, instrumentedMethod.getName());
5756
return Target.ForStackManipulation.of(signature);
5857
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.sdk.bytebuddy;
20+
21+
import static org.assertj.core.api.Assertions.*;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.ParentClass.InnerClass;
26+
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.ParentClass.InnerClass.NestedInnerClass;
27+
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.ParentObject.ChildScalaObject$;
28+
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.SimpleClass;
29+
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.AnonymousClass;
30+
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.AnonymousNestedClass;
31+
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.Dollar$Class;
32+
import co.elastic.apm.agent.sdk.bytebuddy.clazzes.ScalaObject$;
33+
34+
class ClassNameParserTest {
35+
36+
@Test
37+
void testSimpleClassName() {
38+
String className = ClassNameParser.parse(SimpleClass.class.getName());
39+
assertThat(className).isEqualTo("SimpleClass");
40+
}
41+
@Test
42+
void testNestedClassName() {
43+
String className = ClassNameParser.parse(InnerClass.class.getName());
44+
assertThat(className).isEqualTo("InnerClass");
45+
}
46+
@Test
47+
void testDoubleNestedClassName() {
48+
String className = ClassNameParser.parse(NestedInnerClass.class.getName());
49+
assertThat(className).isEqualTo("NestedInnerClass");
50+
}
51+
@Test
52+
void testDollarClassName() {
53+
String className = ClassNameParser.parse(Dollar$Class.class.getName());
54+
//We have no way to know if the class name is Dollar$Class or Class is a nested class of Dollar
55+
//As dollar signs should not be used in names, it should be fine to detect Class instead of Dollar$Class
56+
assertThat(className).describedAs("Dollar signs should not be used in names").isEqualTo("Class");
57+
}
58+
@Test
59+
void testAnonymousClassName() {
60+
String className = ClassNameParser.parse(AnonymousClass.getAnonymousClass().getName());
61+
assertThat(className).isEqualTo("AnonymousClass$1");
62+
}
63+
@Test
64+
void testAnonymousNestedClassName() {
65+
String className = ClassNameParser.parse(InnerClass.getAnonymousClass().getName());
66+
assertThat(className).isEqualTo("InnerClass$1");
67+
}
68+
@Test
69+
void testAnonymousInAnonymousClassName() {
70+
String className = ClassNameParser.parse(AnonymousNestedClass.getAnonymousClass().getName());
71+
assertThat(className).isEqualTo("AnonymousNestedClass$1$1");
72+
}
73+
@Test
74+
void testScalaObjectClassName() {
75+
String className = ClassNameParser.parse(ScalaObject$.class.getName());
76+
assertThat(className).isEqualTo("ScalaObject");
77+
}
78+
@Test
79+
void testNestedScalaObjectClassName() {
80+
String className = ClassNameParser.parse(ChildScalaObject$.class.getName());
81+
assertThat(className).isEqualTo("ChildScalaObject");
82+
}
83+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;
20+
21+
public class AnonymousClass {
22+
public static Class<?> getAnonymousClass() {
23+
return new Object() {
24+
@Override
25+
public String toString() {
26+
return "foo";
27+
}
28+
}.getClass();
29+
}
30+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;
20+
21+
public class AnonymousNestedClass {
22+
public static Class<?> getAnonymousClass() {
23+
Factory f = new Factory() {
24+
@Override
25+
public Object getObject() {
26+
return new Object() {
27+
@Override
28+
public String toString() {
29+
return "foo";
30+
}
31+
};
32+
}
33+
};
34+
return f.getObject().getClass();
35+
}
36+
public static interface Factory {
37+
Object getObject();
38+
}
39+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;
20+
21+
public class Dollar$Class {
22+
23+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;
20+
21+
public class ParentClass {
22+
23+
public static class InnerClass {
24+
public static Class<?> getAnonymousClass() {
25+
return new Object() {
26+
@Override
27+
public String toString() {
28+
return "foo";
29+
}
30+
}.getClass();
31+
}
32+
public static class NestedInnerClass {
33+
34+
}
35+
}
36+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;
20+
21+
public class ParentObject {
22+
/**
23+
* Scala objects generate a class with a dollar at the end
24+
* https://www.toptal.com/scala/scala-bytecode-and-the-jvm
25+
*/
26+
public class ChildScalaObject$ {
27+
28+
}
29+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.sdk.bytebuddy.clazzes;
20+
21+
/**
22+
* Scala objects generate a class with a dollar at the end
23+
* https://www.toptal.com/scala/scala-bytecode-and-the-jvm
24+
*/
25+
public class ScalaObject$ {
26+
27+
}

0 commit comments

Comments
 (0)