jfr-connection: fixes for using diagnostic command to start a recording (#1352)
Co-authored-by: Jean Bisutti <jean.bisutti@gmail.com>
This commit is contained in:
parent
6e7673b11f
commit
63361ccbe8
|
@ -9,4 +9,5 @@ otelJava.moduleName.set("io.opentelemetry.contrib.jfr.connection")
|
|||
dependencies {
|
||||
testImplementation("org.openjdk.jmc:common:8.3.1")
|
||||
testImplementation("org.openjdk.jmc:flightrecorder:8.3.1")
|
||||
testImplementation("org.mockito:mockito-inline")
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ package io.opentelemetry.contrib.jfr.connection;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -33,7 +35,7 @@ import javax.management.ReflectionException;
|
|||
final class FlightRecorderDiagnosticCommandConnection implements FlightRecorderConnection {
|
||||
private static final String DIAGNOSTIC_COMMAND_OBJECT_NAME =
|
||||
"com.sun.management:type=DiagnosticCommand";
|
||||
private static final String JFR_START_REGEX = "Started recording (.+?)\\. .*";
|
||||
private static final String JFR_START_REGEX = "Started recording (\\d+?)\\.";
|
||||
private static final Pattern JFR_START_PATTERN = Pattern.compile(JFR_START_REGEX, Pattern.DOTALL);
|
||||
|
||||
// All JFR commands take String[] parameters
|
||||
|
@ -56,7 +58,11 @@ final class FlightRecorderDiagnosticCommandConnection implements FlightRecorderC
|
|||
ObjectInstance objectInstance =
|
||||
mBeanServerConnection.getObjectInstance(new ObjectName(DIAGNOSTIC_COMMAND_OBJECT_NAME));
|
||||
ObjectName objectName = objectInstance.getObjectName();
|
||||
assertCommercialFeaturesUnlocked(mBeanServerConnection, objectName);
|
||||
|
||||
if (jdkHasUnlockCommercialFeatures(mBeanServerConnection)) {
|
||||
assertCommercialFeaturesUnlocked(mBeanServerConnection, objectName);
|
||||
}
|
||||
|
||||
return new FlightRecorderDiagnosticCommandConnection(
|
||||
mBeanServerConnection, objectInstance.getObjectName());
|
||||
} catch (MalformedObjectNameException e) {
|
||||
|
@ -179,7 +185,7 @@ final class FlightRecorderDiagnosticCommandConnection implements FlightRecorderC
|
|||
@Override
|
||||
public void dumpRecording(long id, String outputFile) throws IOException, JfrConnectionException {
|
||||
try {
|
||||
Object[] params = mkParams("filename=" + outputFile, "recording=" + id, "compress=true");
|
||||
Object[] params = mkParams("filename=" + outputFile, "name=" + id);
|
||||
mBeanServerConnection.invoke(objectName, "jfrDump", params, signature);
|
||||
} catch (InstanceNotFoundException | MBeanException | ReflectionException e) {
|
||||
throw JfrConnectionException.canonicalJfrConnectionException(getClass(), "dumpRecording", e);
|
||||
|
@ -210,26 +216,41 @@ final class FlightRecorderDiagnosticCommandConnection implements FlightRecorderC
|
|||
throw new UnsupportedOperationException("closeRecording not supported on Java 8");
|
||||
}
|
||||
|
||||
// Do this check separate from assertCommercialFeatures because reliance
|
||||
// on System properties makes it difficult to test.
|
||||
static boolean jdkHasUnlockCommercialFeatures(MBeanServerConnection mBeanServerConnection) {
|
||||
try {
|
||||
RuntimeMXBean runtimeMxBean =
|
||||
ManagementFactory.getPlatformMXBean(mBeanServerConnection, RuntimeMXBean.class);
|
||||
String javaVmVendor = runtimeMxBean.getVmVendor();
|
||||
String javaVersion = runtimeMxBean.getVmVersion();
|
||||
return javaVmVendor.contains("Oracle Corporation")
|
||||
&& javaVersion.matches("(?:^1\\.8|9|10).*");
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
static void assertCommercialFeaturesUnlocked(
|
||||
MBeanServerConnection mBeanServerConnection, ObjectName objectName)
|
||||
throws IOException, JfrConnectionException {
|
||||
|
||||
try {
|
||||
Object unlockedMessage =
|
||||
mBeanServerConnection.invoke(objectName, "vmCheckCommercialFeatures", null, null);
|
||||
if (unlockedMessage instanceof String) {
|
||||
boolean unlocked = ((String) unlockedMessage).contains("unlocked");
|
||||
if (!unlocked) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Unlocking commercial features may be required. This must be explicitly enabled by adding -XX:+UnlockCommercialFeatures");
|
||||
throw JfrConnectionException.canonicalJfrConnectionException(
|
||||
FlightRecorderDiagnosticCommandConnection.class,
|
||||
"assertCommercialFeaturesUnlocked",
|
||||
new UnsupportedOperationException(
|
||||
"Unlocking commercial features may be required. This must be explicitly enabled by adding -XX:+UnlockCommercialFeatures"));
|
||||
}
|
||||
}
|
||||
} catch (InstanceNotFoundException
|
||||
| MBeanException
|
||||
| ReflectionException
|
||||
| UnsupportedOperationException e) {
|
||||
throw JfrConnectionException.canonicalJfrConnectionException(
|
||||
FlightRecorderDiagnosticCommandConnection.class, "assertCommercialFeaturesUnlocked", e);
|
||||
} catch (InstanceNotFoundException | MBeanException | ReflectionException ignored) {
|
||||
// If the MBean doesn't have the vmCheckCommercialFeatures method, then we can't check it.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -207,7 +207,7 @@ public class RecordingOptions {
|
|||
* <li>"d" (days)
|
||||
* </ul>
|
||||
*
|
||||
* For example, {@code "2 h"}, {@code "3 m"}.
|
||||
* For example, {@code "2h"}, {@code "3m"}.
|
||||
*
|
||||
* <p>If the value is {@code null}, {@code duration} will be set to the default value, which is
|
||||
* "no limit".
|
||||
|
@ -388,7 +388,7 @@ public class RecordingOptions {
|
|||
case "m":
|
||||
case "h":
|
||||
case "d":
|
||||
return Long.toString(value) + " " + units;
|
||||
return value + units;
|
||||
default:
|
||||
// fall through
|
||||
}
|
||||
|
|
|
@ -10,14 +10,67 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.errorprone.annotations.Keep;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.util.stream.Stream;
|
||||
import javax.management.MBeanServerConnection;
|
||||
import javax.management.ObjectName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
class FlightRecorderDiagnosticCommandConnectionTest {
|
||||
|
||||
@Keep
|
||||
static Stream<Arguments> assertJdkHasUnlockCommercialFeatures() {
|
||||
return Stream.of(
|
||||
Arguments.of("Oracle Corporation", "1.8.0_401", true),
|
||||
Arguments.of("AdoptOpenJDK", "1.8.0_282", false),
|
||||
Arguments.of("Oracle Corporation", "10.0.2", true),
|
||||
Arguments.of("Oracle Corporation", "9.0.4", true),
|
||||
Arguments.of("Oracle Corporation", "11.0.22", false),
|
||||
Arguments.of("Microsoft", "11.0.13", false),
|
||||
Arguments.of("Microsoft", "17.0.3", false),
|
||||
Arguments.of("Oracle Corporation", "21.0.3", false));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void assertJdkHasUnlockCommercialFeatures(String vmVendor, String vmVersion, boolean expected)
|
||||
throws Exception {
|
||||
|
||||
MBeanServerConnection mBeanServerConnection = mock(MBeanServerConnection.class);
|
||||
|
||||
try (MockedStatic<ManagementFactory> mockedStatic = mockStatic(ManagementFactory.class)) {
|
||||
mockedStatic
|
||||
.when(
|
||||
() -> ManagementFactory.getPlatformMXBean(mBeanServerConnection, RuntimeMXBean.class))
|
||||
.thenAnswer(
|
||||
new Answer<RuntimeMXBean>() {
|
||||
@Override
|
||||
public RuntimeMXBean answer(InvocationOnMock invocation) {
|
||||
RuntimeMXBean mockedRuntimeMxBean = mock(RuntimeMXBean.class);
|
||||
when(mockedRuntimeMxBean.getVmVendor()).thenReturn(vmVendor);
|
||||
when(mockedRuntimeMxBean.getVmVersion()).thenReturn(vmVersion);
|
||||
return mockedRuntimeMxBean;
|
||||
}
|
||||
});
|
||||
|
||||
boolean actual =
|
||||
FlightRecorderDiagnosticCommandConnection.jdkHasUnlockCommercialFeatures(
|
||||
mBeanServerConnection);
|
||||
assertEquals(expected, actual, "Expected " + expected + " for " + vmVendor + " " + vmVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertCommercialFeaturesUnlocked() throws Exception {
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
|
|
|
@ -46,18 +46,18 @@ class RecordingOptionsTest {
|
|||
@Keep
|
||||
static Stream<Arguments> testGetMaxAge() {
|
||||
return Stream.of(
|
||||
Arguments.of("3 ns", "3 ns"),
|
||||
Arguments.of("3 us", "3 us"),
|
||||
Arguments.of("3 ms", "3 ms"),
|
||||
Arguments.of("3 s", "3 s"),
|
||||
Arguments.of("3 m", "3 m"),
|
||||
Arguments.of("3 h", "3 h"),
|
||||
Arguments.of("3 h", "3 h"),
|
||||
Arguments.of("+3 d", "3 d"),
|
||||
Arguments.of("3ms", "3 ms"),
|
||||
Arguments.of("3 ns", "3ns"),
|
||||
Arguments.of("3 us", "3us"),
|
||||
Arguments.of("3 ms", "3ms"),
|
||||
Arguments.of("3 s", "3s"),
|
||||
Arguments.of("3 m", "3m"),
|
||||
Arguments.of("3 h", "3h"),
|
||||
Arguments.of("3 h", "3h"),
|
||||
Arguments.of("+3 d", "3d"),
|
||||
Arguments.of("3ms", "3ms"),
|
||||
Arguments.of("0", "0"),
|
||||
Arguments.of("", "0"),
|
||||
Arguments.of((String) null, "0"));
|
||||
Arguments.of(null, "0"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
@ -209,15 +209,15 @@ class RecordingOptionsTest {
|
|||
@Keep
|
||||
private static Stream<Arguments> testGetDuration() {
|
||||
return Stream.of(
|
||||
Arguments.of("3 ns", "3 ns"),
|
||||
Arguments.of("3 us", "3 us"),
|
||||
Arguments.of("3 ms", "3 ms"),
|
||||
Arguments.of("3 s", "3 s"),
|
||||
Arguments.of("3 m", "3 m"),
|
||||
Arguments.of("3 h", "3 h"),
|
||||
Arguments.of("3 h", "3 h"),
|
||||
Arguments.of("+3 d", "3 d"),
|
||||
Arguments.of("3ms", "3 ms"),
|
||||
Arguments.of("3 ns", "3ns"),
|
||||
Arguments.of("3 us", "3us"),
|
||||
Arguments.of("3 ms", "3ms"),
|
||||
Arguments.of("3 s", "3s"),
|
||||
Arguments.of("3 m", "3m"),
|
||||
Arguments.of("3 h", "3h"),
|
||||
Arguments.of("3 h", "3h"),
|
||||
Arguments.of("+3 d", "3d"),
|
||||
Arguments.of("3ms", "3ms"),
|
||||
Arguments.of("0", "0"),
|
||||
Arguments.of("", "0"),
|
||||
Arguments.of(null, "0"));
|
||||
|
@ -263,12 +263,12 @@ class RecordingOptionsTest {
|
|||
void testGetRecordingOptions() {
|
||||
Map<String, String> expected = new HashMap<>();
|
||||
expected.put("name", "test");
|
||||
expected.put("maxAge", "3 m");
|
||||
expected.put("maxAge", "3m");
|
||||
expected.put("maxSize", "1048576");
|
||||
expected.put("dumpOnExit", "true");
|
||||
expected.put("destination", "test.jfr");
|
||||
expected.put("disk", "true");
|
||||
expected.put("duration", "120 s");
|
||||
expected.put("duration", "120s");
|
||||
RecordingOptions opts =
|
||||
new RecordingOptions.Builder()
|
||||
.name("test")
|
||||
|
|
|
@ -48,6 +48,7 @@ import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
|
|||
import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.junit.platform.commons.logging.Logger;
|
||||
import org.junit.platform.commons.logging.LoggerFactory;
|
||||
|
||||
|
@ -329,6 +330,13 @@ class RecordingTest {
|
|||
String getter =
|
||||
"get" + key.substring(0, 1).toUpperCase(Locale.ROOT) + key.substring(1);
|
||||
String expected = (String) compositeData.get("value");
|
||||
|
||||
// Special case for duration values. The FlightRecorderMXBean wants "<number><unit>"
|
||||
// but returns "<number> <unit>", so we need to normalize the expected value.
|
||||
if (expected != null && expected.matches("([-+]?\\d+)\\s*(\\w*)")) {
|
||||
expected = expected.replaceAll("\\s", "");
|
||||
}
|
||||
|
||||
try {
|
||||
Method method = RecordingOptions.class.getMethod(getter);
|
||||
String actual = (String) method.invoke(recordingOptions);
|
||||
|
@ -505,4 +513,43 @@ class RecordingTest {
|
|||
fail("Error thrown by MBean server or FlightRecorderMXBean: ", badBean);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"5 s", "5m"})
|
||||
void assertDiagnosticCommandCanRecordWithDuration(String duration) {
|
||||
// Issue #1338
|
||||
MBeanServerConnection mBeanServer = ManagementFactory.getPlatformMBeanServer();
|
||||
try {
|
||||
FlightRecorderConnection connection =
|
||||
FlightRecorderDiagnosticCommandConnection.connect(mBeanServer);
|
||||
assertCanRecord(connection, duration);
|
||||
} catch (IOException | JfrConnectionException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"5 s", "5m"})
|
||||
void assertMbeanCanRecordWithDuration(String duration) {
|
||||
// Ensure fix for issue #1338 doesn't regress MBean recording
|
||||
MBeanServerConnection mBeanServer = ManagementFactory.getPlatformMBeanServer();
|
||||
try {
|
||||
FlightRecorderConnection connection = FlightRecorderConnection.connect(mBeanServer);
|
||||
assertCanRecord(connection, duration);
|
||||
} catch (IOException | JfrConnectionException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertCanRecord(FlightRecorderConnection connection, String duration)
|
||||
throws JfrConnectionException {
|
||||
RecordingOptions recordingOptions = new RecordingOptions.Builder().duration(duration).build();
|
||||
RecordingConfiguration recordingConfiguration = RecordingConfiguration.PROFILE_CONFIGURATION;
|
||||
try (Recording recording = connection.newRecording(recordingOptions, recordingConfiguration)) {
|
||||
recording.start();
|
||||
recording.stop();
|
||||
} catch (IOException e) {
|
||||
// Recording is autoclose
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue