Skip to content

Commit 78a0fa7

Browse files
authored
Support dots in jmx attribute names (#2921)
1 parent cfa1329 commit 78a0fa7

File tree

10 files changed

+235
-14
lines changed

10 files changed

+235
-14
lines changed

agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/perfcounter/AvailableJmxMetricLogger.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ private static Set<String> getAttributes(MBeanServer server, ObjectName objectNa
114114
Set<String> attributes = new HashSet<>();
115115
for (MBeanAttributeInfo attribute : mbeanInfo.getAttributes()) {
116116
if (!attribute.isReadable()) {
117-
attributes.add(attribute.getName() + " (not readable)");
117+
attributes.add(escapedName(attribute) + " (not readable)");
118118
continue;
119119
}
120120
try {
@@ -125,7 +125,7 @@ private static Set<String> getAttributes(MBeanServer server, ObjectName objectNa
125125
// "java.lang.UnsupportedOperationException: CollectionUsage threshold is not supported"
126126
// and available jmx metrics are already only logged at debug
127127
logger.trace(e.getMessage(), e);
128-
attributes.add(attribute.getName() + " (exception)");
128+
attributes.add(escapedName(attribute) + " (exception)");
129129
}
130130
}
131131
return attributes;
@@ -148,14 +148,14 @@ private static List<String> getAttributes(MBeanAttributeInfo attribute, @Nullabl
148148
}
149149
}
150150

151-
return singletonList(attribute.getName() + " (" + valueType(value) + ")");
151+
return singletonList(escapedName(attribute) + " (" + valueType(value) + ")");
152152
}
153153

154154
private static List<String> getCompositeTypeAttributes(
155155
MBeanAttributeInfo attribute, @Nullable Object compositeData, CompositeType compositeType) {
156156
List<String> attributes = new ArrayList<>();
157157
for (String itemName : compositeType.keySet()) {
158-
String attributeName = attribute.getName() + "." + itemName;
158+
String attributeName = escapedName(attribute) + "." + itemName;
159159
OpenType<?> itemType = compositeType.getType(itemName);
160160
if (itemType == null) {
161161
attributes.add(attributeName + " (null)");
@@ -173,6 +173,10 @@ private static List<String> getCompositeTypeAttributes(
173173
return attributes;
174174
}
175175

176+
private static String escapedName(MBeanAttributeInfo attribute) {
177+
return attribute.getName().replace(".", "\\.");
178+
}
179+
176180
private static String valueType(@Nullable Object value) {
177181
if (value instanceof Number) {
178182
return "number";

agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/perfcounter/JmxDataFetcher.java

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,14 @@ private static List<Object> fetch(
7272
InstanceNotFoundException {
7373
ArrayList<Object> result = new ArrayList<>();
7474

75-
String[] inners = attributeName.split("\\.");
75+
List<String> inners = splitByDot(attributeName);
7676

7777
for (ObjectName object : objects) {
78-
79-
Object value;
80-
81-
if (inners.length == 1) {
82-
value = server.getAttribute(object, attributeName);
83-
} else {
84-
value = server.getAttribute(object, inners[0]);
78+
Object value = server.getAttribute(object, inners.get(0));
79+
if (inners.size() > 1) {
8580
if (value != null) {
86-
value = ((CompositeData) value).get(inners[1]);
81+
// TODO (trask) will support more nesting after moving to upstream otel jmx component
82+
value = ((CompositeData) value).get(inners.get(1));
8783
}
8884
}
8985
if (value != null) {
@@ -94,5 +90,54 @@ private static List<Object> fetch(
9490
return result;
9591
}
9692

93+
// This code is copied in from upstream otel java instrumentation repository
94+
// until we move to upstream version
95+
private static List<String> splitByDot(String rawName) {
96+
List<String> components = new ArrayList<>();
97+
try {
98+
StringBuilder currentSegment = new StringBuilder();
99+
boolean escaped = false;
100+
for (int i = 0; i < rawName.length(); ++i) {
101+
char ch = rawName.charAt(i);
102+
if (escaped) {
103+
// Allow only '\' and '.' to be escaped
104+
if (ch != '\\' && ch != '.') {
105+
throw new IllegalArgumentException(
106+
"Invalid escape sequence in attribute name '" + rawName + "'");
107+
}
108+
currentSegment.append(ch);
109+
escaped = false;
110+
} else {
111+
if (ch == '\\') {
112+
escaped = true;
113+
} else if (ch == '.') {
114+
// this is a segment separator
115+
verifyAndAddNameSegment(components, currentSegment);
116+
currentSegment = new StringBuilder();
117+
} else {
118+
currentSegment.append(ch);
119+
}
120+
}
121+
}
122+
123+
// The returned list is never empty ...
124+
verifyAndAddNameSegment(components, currentSegment);
125+
126+
} catch (IllegalArgumentException unused) {
127+
// Drop the original exception. We have more meaningful context here.
128+
throw new IllegalArgumentException("Invalid attribute name '" + rawName + "'");
129+
}
130+
131+
return components;
132+
}
133+
134+
private static void verifyAndAddNameSegment(List<String> segments, StringBuilder candidate) {
135+
String newSegment = candidate.toString().trim();
136+
if (newSegment.isEmpty()) {
137+
throw new IllegalArgumentException();
138+
}
139+
segments.add(newSegment);
140+
}
141+
97142
private JmxDataFetcher() {}
98143
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ hideFromDependabot(":smoke-tests:apps:CustomDimensions")
9292
hideFromDependabot(":smoke-tests:apps:CustomInstrumentation")
9393
hideFromDependabot(":smoke-tests:apps:DiagnosticExtension:MockExtension")
9494
hideFromDependabot(":smoke-tests:apps:DiagnosticExtension")
95+
hideFromDependabot(":smoke-tests:apps:DotInJmxMetric")
9596
hideFromDependabot(":smoke-tests:apps:gRPC")
9697
hideFromDependabot(":smoke-tests:apps:HeartBeat")
9798
hideFromDependabot(":smoke-tests:apps:HttpClients")

smoke-tests/apps/ActuatorMetrics/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/ActuatorMetricsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ abstract class ActuatorMetricsTest {
3131
@Test
3232
@TargetUri("/test")
3333
void doMostBasicTest() throws Exception {
34-
testing.mockedIngestion.waitForItems("RequestData", 1);
34+
testing.getTelemetry(0);
3535

3636
List<Envelope> metricItems =
3737
testing.mockedIngestion.waitForItems(
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
plugins {
2+
id("ai.smoke-test-war")
3+
}
4+
5+
dependencies {
6+
implementation("org.springframework.boot:spring-boot-starter-web:2.1.7.RELEASE") {
7+
exclude("org.springframework.boot", "spring-boot-starter-tomcat")
8+
}
9+
// this dependency is needed to make wildfly happy
10+
implementation("org.reactivestreams:reactive-streams:1.0.3")
11+
12+
implementation("org.weakref:jmxutils:1.22")
13+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.applicationinsights.smoketestapp;
5+
6+
import java.lang.management.ManagementFactory;
7+
import org.springframework.boot.autoconfigure.SpringBootApplication;
8+
import org.springframework.boot.builder.SpringApplicationBuilder;
9+
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
10+
import org.weakref.jmx.MBeanExporter;
11+
import org.weakref.jmx.Managed;
12+
import org.weakref.jmx.Nested;
13+
14+
@SpringBootApplication
15+
public class SpringBootApp extends SpringBootServletInitializer {
16+
17+
@Override
18+
protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) {
19+
20+
MBeanExporter exporter = new MBeanExporter(ManagementFactory.getPlatformMBeanServer());
21+
exporter.export("test:name=X", new NestedExample());
22+
23+
return applicationBuilder.sources(SpringBootApp.class);
24+
}
25+
26+
public static class NestedExample {
27+
private final NestedObject nestedObject = new NestedObject();
28+
29+
@Nested
30+
public NestedObject getNestedObject() {
31+
return nestedObject;
32+
}
33+
34+
public static final class NestedObject {
35+
@Managed
36+
public int getValue() {
37+
return 5;
38+
}
39+
}
40+
}
41+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.applicationinsights.smoketestapp;
5+
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
@RestController
10+
public class TestController {
11+
12+
@GetMapping("/")
13+
public String root() {
14+
return "OK";
15+
}
16+
17+
@GetMapping("/test")
18+
public String test() {
19+
return "OK!";
20+
}
21+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.applicationinsights.smoketest;
5+
6+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_11;
7+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_17;
8+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_19;
9+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_20;
10+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_8;
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
import com.microsoft.applicationinsights.smoketest.schemav2.Data;
14+
import com.microsoft.applicationinsights.smoketest.schemav2.DataPoint;
15+
import com.microsoft.applicationinsights.smoketest.schemav2.Envelope;
16+
import com.microsoft.applicationinsights.smoketest.schemav2.MetricData;
17+
import java.util.List;
18+
import java.util.concurrent.TimeUnit;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.RegisterExtension;
21+
22+
@UseAgent
23+
abstract class DotInJmxMetricTest {
24+
25+
@RegisterExtension
26+
static final SmokeTestExtension testing =
27+
SmokeTestExtension.builder().setSelfDiagnosticsLevel("debug").build();
28+
29+
@Test
30+
@TargetUri("/test")
31+
void doMostBasicTest() throws Exception {
32+
testing.getTelemetry(0);
33+
34+
List<Envelope> metricItems =
35+
testing.mockedIngestion.waitForItems(
36+
envelope -> isMetricWithName(envelope, "NameWithDot"), 1, 10, TimeUnit.SECONDS);
37+
38+
MetricData data = (MetricData) ((Data<?>) metricItems.get(0).getData()).getBaseData();
39+
List<DataPoint> points = data.getMetrics();
40+
assertThat(points).hasSize(1);
41+
42+
DataPoint point = points.get(0);
43+
assertThat(point.getValue()).isEqualTo(5);
44+
}
45+
46+
private static boolean isMetricWithName(Envelope envelope, String metricName) {
47+
if (!envelope.getData().getBaseType().equals("MetricData")) {
48+
return false;
49+
}
50+
MetricData md = SmokeTestExtension.getBaseData(envelope);
51+
return metricName.equals(md.getMetrics().get(0).getName());
52+
}
53+
54+
@Environment(TOMCAT_8_JAVA_8)
55+
static class Tomcat8Java8Test extends DotInJmxMetricTest {}
56+
57+
@Environment(TOMCAT_8_JAVA_11)
58+
static class Tomcat8Java11Test extends DotInJmxMetricTest {}
59+
60+
@Environment(TOMCAT_8_JAVA_17)
61+
static class Tomcat8Java17Test extends DotInJmxMetricTest {}
62+
63+
@Environment(TOMCAT_8_JAVA_19)
64+
static class Tomcat8Java19Test extends DotInJmxMetricTest {}
65+
66+
@Environment(TOMCAT_8_JAVA_20)
67+
static class Tomcat8Java20Test extends DotInJmxMetricTest {}
68+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"role": {
3+
"name": "testrolename",
4+
"instance": "testroleinstance"
5+
},
6+
"sampling": {
7+
"percentage": 100
8+
},
9+
"jmxMetrics": [
10+
{
11+
"name": "NameWithDot",
12+
"objectName": "test:name=X",
13+
"attribute": "NestedObject\\.Value"
14+
}
15+
],
16+
"metricIntervalSeconds": 5
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<configuration>
3+
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
4+
<encoder>
5+
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
6+
</encoder>
7+
</appender>
8+
<root level="warn">
9+
<appender-ref ref="CONSOLE"/>
10+
</root>
11+
</configuration>

0 commit comments

Comments
 (0)