diff --git a/changelog/@unreleased/pr-989.v2.yml b/changelog/@unreleased/pr-989.v2.yml new file mode 100644 index 000000000..6334bfb99 --- /dev/null +++ b/changelog/@unreleased/pr-989.v2.yml @@ -0,0 +1,5 @@ +type: improvement +improvement: + description: Safepoint metrics no longer return illegal access warnings + links: + - https://github.com/palantir/tritium/pull/989 diff --git a/tritium-metrics-jvm/build.gradle b/tritium-metrics-jvm/build.gradle index a8572d9ab..cb468e205 100644 --- a/tritium-metrics-jvm/build.gradle +++ b/tritium-metrics-jvm/build.gradle @@ -14,6 +14,7 @@ dependencies { implementation 'com.palantir.safe-logging:preconditions' implementation 'com.palantir.safe-logging:safe-logging' implementation 'org.slf4j:slf4j-api' + implementation 'net.bytebuddy:byte-buddy' testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/tritium-metrics-jvm/src/main/java/com/palantir/tritium/metrics/jvm/SafepointMetrics.java b/tritium-metrics-jvm/src/main/java/com/palantir/tritium/metrics/jvm/SafepointMetrics.java index 1d151b379..9dfac830b 100644 --- a/tritium-metrics-jvm/src/main/java/com/palantir/tritium/metrics/jvm/SafepointMetrics.java +++ b/tritium-metrics-jvm/src/main/java/com/palantir/tritium/metrics/jvm/SafepointMetrics.java @@ -18,8 +18,17 @@ import com.codahale.metrics.Gauge; import com.palantir.tritium.metrics.registry.TaggedMetricRegistry; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.Optional; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.implementation.bytecode.ByteCodeAppender.Size; +import net.bytebuddy.implementation.bytecode.StackManipulation; +import net.bytebuddy.implementation.bytecode.member.FieldAccess; +import net.bytebuddy.implementation.bytecode.member.MethodInvocation; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.matcher.ElementMatchers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,29 +38,77 @@ * essentially provides the information of '+PrintGCApplicationStoppedTime' programmatically. */ final class SafepointMetrics { + private static final String RUNTIME_FIELD = "runtime"; private static final Logger log = LoggerFactory.getLogger(SafepointMetrics.class); + private static final Optional> gauge = getGauge(); - // The reflection is so that we can use this on non-Hotspot JVMs - @SuppressWarnings("LiteralClassName") static void register(TaggedMetricRegistry registry) { - try { - Class managementFactoryHelper = Class.forName("sun.management.ManagementFactoryHelper"); - Method getHotspotRuntimeMBean = managementFactoryHelper.getMethod("getHotspotRuntimeMBean"); - Object hotspotRuntimeMBean = getHotspotRuntimeMBean.invoke(null); - Method getTotalSafepointTime = hotspotRuntimeMBean.getClass().getMethod("getTotalSafepointTime"); - getTotalSafepointTime.setAccessible(true); - Gauge gauge = () -> (Long) invoke(getTotalSafepointTime, hotspotRuntimeMBean); - InternalJvmMetrics.of(registry).safepointTime(gauge); - } catch (ReflectiveOperationException e) { - log.info("Could not get the total safepoint time, these metrics will not be registered.", e); - } + gauge.ifPresent(g -> InternalJvmMetrics.of(registry).safepointTime(g)); } - private static Object invoke(Method method, Object object) { + /** + * This is somewhat involved. Basically, Java 11+ does not let you compile against sun.management classes when using + * the --release flag, and they may or may not even be present. But the classes are present at runtime on JVMs we + * use, and the beans are available for diagnostics purposes (e.g. JMX). We used to use reflection to access + * this, but the reflection is caught by JDK internal security and eventually will be blocked by the module system. + * So, we generate a short class where we call the actual method, which does not have the same module boundary + * issues. + * + * Code should end up being equivalent to: + * + *
+     *     class SomeGauge implements Gauge {
+     *         private static final HotspotRuntimeMBean runtime = ManagementFactoryHelper.getHotspotRuntimeMBean();
+     *
+     *         public Object getValue() {
+     *             return runtime.getTotalSafepointTime();
+     *         }
+     *     }
+     * 
+ * + * but is generated at runtime. + * + */ + @SuppressWarnings("unchecked") + private static Optional> getGauge() { try { - return method.invoke(object); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException(e); + Class managementFactory = Class.forName("sun.management.ManagementFactoryHelper"); + Class runtimeMbean = Class.forName("sun.management.HotspotRuntimeMBean"); + Gauge gaugeImplementation = (Gauge) new ByteBuddy() + .subclass(Object.class) + .implement(Gauge.class) + .defineField( + RUNTIME_FIELD, runtimeMbean, Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL) + .initializer((methodVisitor, implementationContext, instrumentedMethod) -> { + StackManipulation.Size size = new StackManipulation.Compound( + // call method, put result on top of stack + MethodInvocation.invoke(new TypeDescription.ForLoadedType(managementFactory) + .getDeclaredMethods() + .filter(ElementMatchers.named("getHotspotRuntimeMBean")) + .getOnly()), + // write element on top of stack into appropriate field + FieldAccess.forField(implementationContext + .getInstrumentedType() + .getDeclaredFields() + .filter(ElementMatchers.named(RUNTIME_FIELD)) + .getOnly()) + .write()) + .apply(methodVisitor, implementationContext); + return new Size(size.getMaximalSize(), instrumentedMethod.getStackSize()); + }) + .method(ElementMatchers.named("getValue")) + .intercept(MethodCall.invoke(ElementMatchers.named("getTotalSafepointTime")) + .onField("runtime")) + .make() + .load(SafepointMetrics.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) + .getLoaded() + .getConstructor() + .newInstance(); + gaugeImplementation.getValue(); + return Optional.of(gaugeImplementation); + } catch (RuntimeException | ReflectiveOperationException | NoClassDefFoundError e) { + log.info("Could not get the total safepoint time, these metrics will not be registered.", e); + return Optional.empty(); } }