diff --git a/profiler/build.gradle.kts b/profiler/build.gradle.kts index 0b8d83682..113fea5e2 100644 --- a/profiler/build.gradle.kts +++ b/profiler/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { compileOnly("com.google.auto.service:auto-service") testImplementation(project(":custom")) + testImplementation(project(":testing:common")) testImplementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common") testImplementation("io.grpc:grpc-netty") diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JFR.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JFR.java index 2863bcba7..3e8d6b5d9 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JFR.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JFR.java @@ -36,9 +36,13 @@ class JFR { private static final Logger logger = Logger.getLogger(JFR.class.getName()); - public static final JFR instance = new JFR(); + private static final JFR instance = new JFR(); private static final boolean jfrAvailable = checkJfr(); + public static JFR getInstance() { + return instance; + } + private static boolean checkJfr() { try { JFR.class.getClassLoader().loadClass("jdk.jfr.FlightRecorder"); diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java index 41b3bc9ba..209b5e3d4 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java @@ -22,6 +22,7 @@ import static java.util.logging.Level.WARNING; import com.google.auto.service.AutoService; +import com.google.common.annotations.VisibleForTesting; import com.splunk.opentelemetry.SplunkConfiguration; import com.splunk.opentelemetry.profiler.allocation.exporter.AllocationEventExporter; import com.splunk.opentelemetry.profiler.allocation.exporter.PprofAllocationEventExporter; @@ -30,6 +31,7 @@ import com.splunk.opentelemetry.profiler.exporter.PprofCpuEventExporter; import com.splunk.opentelemetry.profiler.util.HelpfulExecutors; import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.context.ContextStorage; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -51,7 +53,18 @@ public class JfrActivator implements AgentListener { private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(JfrActivator.class.getName()); - private final ExecutorService executor = HelpfulExecutors.newSingleThreadExecutor("JFR Profiler"); + private final ExecutorService executor; + private final JFR jfr; + + public JfrActivator() { + this(JFR.getInstance(), HelpfulExecutors.newSingleThreadExecutor("JFR Profiler")); + } + + @VisibleForTesting + JfrActivator(JFR jfr, ExecutorService executor) { + this.jfr = jfr; + this.executor = executor; + } @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { @@ -62,6 +75,8 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr Configuration.log(config); logger.info("Profiler is active."); + setupContextStorage(); + executor.submit( logUncaught( () -> activateJfrAndRunForever(config, getResource(autoConfiguredOpenTelemetrySdk)))); @@ -72,7 +87,7 @@ private boolean notClearForTakeoff(ConfigProperties config) { logger.fine("Profiler is not enabled."); return true; } - if (!JFR.instance.isAvailable()) { + if (!jfr.isAvailable()) { logger.warning( "JDK Flight Recorder (JFR) is not available in this JVM. Profiling is disabled."); return true; @@ -117,7 +132,7 @@ private void activateJfrAndRunForever(ConfigProperties config, Resource resource RecordingFileNamingConvention namingConvention = new RecordingFileNamingConvention(outputDir); int stackDepth = Configuration.getStackDepth(config); - JFR.instance.setStackDepth(stackDepth); + jfr.setStackDepth(stackDepth); // can't be null, default value is set in Configuration.getProperties Duration recordingDuration = Configuration.getRecordingDuration(config); @@ -165,7 +180,7 @@ private void activateJfrAndRunForever(ConfigProperties config, Resource resource JfrRecorder.builder() .settings(jfrSettings) .maxAgeDuration(recordingDuration.multipliedBy(10)) - .jfr(JFR.instance) + .jfr(jfr) .onNewRecording(jfrRecordingHandler) .namingConvention(namingConvention) .keepRecordingFiles(keepFiles) @@ -218,4 +233,8 @@ private Map buildJfrSettings(ConfigProperties config) { JfrSettingsOverrides overrides = new JfrSettingsOverrides(config); return overrides.apply(jfrSettings); } + + private static void setupContextStorage() { + ContextStorage.addWrapper(JfrContextStorage::new); + } } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrRecorder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrRecorder.java index 24ff15538..15251c667 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrRecorder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrRecorder.java @@ -132,7 +132,7 @@ public static class Builder { private RecordingFileNamingConvention namingConvention; private Map settings; private Duration maxAgeDuration; - private JFR jfr = JFR.instance; + private JFR jfr = JFR.getInstance(); private Consumer onNewRecording; private boolean keepRecordingFiles; diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/SdkCustomizer.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/SdkCustomizer.java deleted file mode 100644 index 21c215af0..000000000 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/SdkCustomizer.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Splunk Inc. - * - * Licensed 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 com.splunk.opentelemetry.profiler; - -import static java.util.Collections.emptyMap; - -import com.google.auto.service.AutoService; -import com.splunk.opentelemetry.SplunkConfiguration; -import io.opentelemetry.context.ContextStorage; -import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; -import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; - -@AutoService(AutoConfigurationCustomizerProvider.class) -public class SdkCustomizer implements AutoConfigurationCustomizerProvider { - - @Override - public void customize(AutoConfigurationCustomizer autoConfigurationCustomizer) { - autoConfigurationCustomizer.addPropertiesCustomizer( - config -> { - if (jfrIsAvailable() && jfrIsEnabledInConfig(config)) { - ContextStorage.addWrapper(JfrContextStorage::new); - } - return emptyMap(); - }); - } - - private boolean jfrIsAvailable() { - return JFR.instance.isAvailable(); - } - - private boolean jfrIsEnabledInConfig(ConfigProperties config) { - return SplunkConfiguration.isProfilerEnabled(config); - } -} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrActivatorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrActivatorTest.java new file mode 100644 index 000000000..c66a8413f --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrActivatorTest.java @@ -0,0 +1,121 @@ +/* + * Copyright Splunk Inc. + * + * Licensed 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 com.splunk.opentelemetry.profiler; + +import static com.splunk.opentelemetry.testing.declarativeconfig.DeclarativeConfigTestUtil.createAutoConfiguredSdk; +import static com.splunk.opentelemetry.testing.declarativeconfig.DeclarativeConfigTestUtil.toYamlString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import java.io.IOException; +import java.nio.file.Path; +import java.util.concurrent.ExecutorService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; + +class JfrActivatorTest { + @Test + void shouldActivateJfrRecording(@TempDir Path tempDir) throws IOException { + try (MockedStatic contextStorageMock = mockStatic(ContextStorage.class)) { + + // given + String yaml = + toYamlString( + "file_format: \"1.0-rc.1\"", + "instrumentation/development:", + " java:", + " splunk:", + " profiler:", + " enabled: true"); + AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir); + + var jfrMock = mock(JFR.class); + when(jfrMock.isAvailable()).thenReturn(true); + + ExecutorService executorMock = mock(ExecutorService.class); + JfrActivator activator = new JfrActivator(jfrMock, executorMock); + + // when + activator.afterAgent(sdk); + + // then + contextStorageMock.verify(() -> ContextStorage.addWrapper(any())); + verify(executorMock).submit(any(Runnable.class)); + } + } + + @Test + void shouldNotActivateJfrRecording_JfrNotAvailable(@TempDir Path tempDir) throws IOException { + try (MockedStatic contextStorageMock = mockStatic(ContextStorage.class)) { + + // given + String yaml = + toYamlString( + "file_format: \"1.0-rc.1\"", + "instrumentation/development:", + " java:", + " splunk:", + " profiler:", + " enabled: true"); + AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir); + + var jfrMock = mock(JFR.class); + when(jfrMock.isAvailable()).thenReturn(false); + + ExecutorService executorMock = mock(ExecutorService.class); + JfrActivator activator = new JfrActivator(jfrMock, executorMock); + + // when + activator.afterAgent(sdk); + + // then + contextStorageMock.verifyNoInteractions(); + verifyNoInteractions(executorMock); + } + } + + @Test + void shouldNotActivateJfrRecording_profilerDisabled(@TempDir Path tempDir) throws IOException { + try (MockedStatic contextStorageMock = mockStatic(ContextStorage.class)) { + + // given + String yaml = + toYamlString("file_format: \"1.0-rc.1\"", "instrumentation/development:", " java:"); + AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir); + + var jfrMock = mock(JFR.class); + when(jfrMock.isAvailable()).thenReturn(true); + + ExecutorService executorMock = mock(ExecutorService.class); + JfrActivator activator = new JfrActivator(jfrMock, executorMock); + + // when + activator.afterAgent(sdk); + + // then + contextStorageMock.verifyNoInteractions(); + verifyNoInteractions(executorMock); + } + } +}