From c5928541794129a2e4b7439d9f434b747347f5a5 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 8 Jun 2026 14:11:20 -0700 Subject: [PATCH 01/25] work in progress --- .../profiler/JfrAgentListener.java | 84 ++++++ ...ctivator.java => ProfilingSupervisor.java} | 138 ++++++---- .../profiler/JfrActivatorTest.java | 161 ----------- .../profiler/JfrAgentListenerTest.java | 168 ++++++++++++ .../profiler/ProfilingSupervisorTest.java | 250 ++++++++++++++++++ 5 files changed, 595 insertions(+), 206 deletions(-) create mode 100644 profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java rename profiler/src/main/java/com/splunk/opentelemetry/profiler/{JfrActivator.java => ProfilingSupervisor.java} (73%) delete mode 100644 profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrActivatorTest.java create mode 100644 profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java create mode 100644 profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java new file mode 100644 index 000000000..b9e0f4fe5 --- /dev/null +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -0,0 +1,84 @@ +/* + * 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 com.google.auto.service.AutoService; +import com.google.common.annotations.VisibleForTesting; +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +@AutoService(AgentListener.class) +public class JfrAgentListener implements AgentListener { + + private static final java.util.logging.Logger logger = + java.util.logging.Logger.getLogger(JfrAgentListener.class.getName()); + private final JFR jfr; + + public JfrAgentListener() { + this(JFR.getInstance()); + } + + @VisibleForTesting + JfrAgentListener(JFR jfr) { + this.jfr = jfr; + } + + @Override + public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { + + ProfilerConfiguration config = getProfilerConfiguration(sdk); + // Always start the supervisor, because we may need to start it later elsewhere. + ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk, config); + + if (notClearForTakeoff(config)) { + return; + } + supervisor.requestStart(); + } + + // Exists for testing + ProfilingSupervisor makeProfilingSupervisor( + AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + return ProfilingSupervisor.createAndStart(sdk, config); + } + + private static ProfilerConfiguration getProfilerConfiguration( + AutoConfiguredOpenTelemetrySdk sdk) { + if (ProfilerDeclarativeConfiguration.SUPPLIER.isConfigured()) { + return ProfilerDeclarativeConfiguration.SUPPLIER.get(); + } else { + ConfigProperties configProperties = AutoConfigureUtil.getConfig(sdk); + return new ProfilerEnvVarsConfiguration(configProperties); + } + } + + private boolean notClearForTakeoff(ProfilerConfiguration config) { + if (!config.isEnabled()) { + logger.fine("Profiler is not enabled."); + return true; + } + if (!jfr.isAvailable()) { + logger.warning( + "JDK Flight Recorder (JFR) is not available in this JVM. Profiling is disabled."); + return true; + } + + return false; + } +} diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java similarity index 73% rename from profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java rename to profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 47363abf0..5cd13f4af 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -16,12 +16,11 @@ package com.splunk.opentelemetry.profiler; -import static com.splunk.opentelemetry.profiler.util.Runnables.logUncaught; import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; import static io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil.getResource; +import static java.util.logging.Level.FINE; import static java.util.logging.Level.WARNING; -import com.google.auto.service.AutoService; import com.google.common.annotations.VisibleForTesting; import com.splunk.opentelemetry.profiler.allocation.exporter.AllocationEventExporter; import com.splunk.opentelemetry.profiler.allocation.exporter.PprofAllocationEventExporter; @@ -32,8 +31,6 @@ import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.api.logs.Logger; import io.opentelemetry.context.ContextStorage; -import io.opentelemetry.javaagent.extension.AgentListener; -import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.logs.LogRecordProcessor; @@ -47,67 +44,112 @@ import java.nio.file.Paths; import java.time.Duration; import java.util.Map; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicReference; -@AutoService(AgentListener.class) -public class JfrActivator implements AgentListener { +/** + * This class oversees the profiling subsystem. It runs for the entire time that the agent is + * running. + */ +public class ProfilingSupervisor { private static final java.util.logging.Logger logger = - java.util.logging.Logger.getLogger(JfrActivator.class.getName()); - private final ExecutorService executor; + java.util.logging.Logger.getLogger(ProfilingSupervisor.class.getName()); + private final ProfilerConfiguration config; private final JFR jfr; + private final AutoConfiguredOpenTelemetrySdk sdk; + private final BlockingQueue commandQueue; + private final AtomicReference sequencer = new AtomicReference<>(); + + private ProfilingSupervisor( + ProfilerConfiguration config, + JFR jfr, + AutoConfiguredOpenTelemetrySdk sdk, + BlockingQueue commandQueue) { + this.config = config; + this.jfr = jfr; + this.sdk = sdk; + this.commandQueue = commandQueue; + } - public JfrActivator() { - this(JFR.getInstance(), HelpfulExecutors.newSingleThreadExecutor("JFR Profiler")); + static ProfilingSupervisor createAndStart( + AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + return createAndStart( + config, JFR.getInstance(), HelpfulExecutors.newSingleThreadExecutor("JFR Profiler"), sdk); } @VisibleForTesting - JfrActivator(JFR jfr, ExecutorService executor) { - this.jfr = jfr; - this.executor = executor; + static ProfilingSupervisor createAndStart( + ProfilerConfiguration config, + JFR jfr, + ExecutorService executor, + AutoConfiguredOpenTelemetrySdk sdk) { + // TODO: What if already started? + BlockingQueue queue = new LinkedBlockingQueue<>(); + ProfilingSupervisor supervisor = new ProfilingSupervisor(config, jfr, sdk, queue); + executor.submit(supervisor::forever); + return supervisor; } - @Override - public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { - ProfilerConfiguration config = getProfilerConfiguration(sdk); - - if (notClearForTakeoff(config)) { - return; + private void forever() { + while (true) { + try { + ProfilingCommand command = commandQueue.take(); + handleCommand(command); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.fine("ProfilingSupervisor is shutting down"); + return; + } catch (Exception e) { + logger.log(FINE, "ProfilingSupervisor encountered an unexpected exception", e); + } } + } - config.log(); - logger.info("Profiler is active."); - setupContextStorage(); - - startJfr(getResource(sdk), config); + public void requestStart() { + commandQueue.add(ProfilingCommand.START); } - private static ProfilerConfiguration getProfilerConfiguration( - AutoConfiguredOpenTelemetrySdk sdk) { - if (ProfilerDeclarativeConfiguration.SUPPLIER.isConfigured()) { - return ProfilerDeclarativeConfiguration.SUPPLIER.get(); - } else { - ConfigProperties configProperties = AutoConfigureUtil.getConfig(sdk); - return new ProfilerEnvVarsConfiguration(configProperties); - } + public void requestStop() { + commandQueue.add(ProfilingCommand.STOP); } - private void startJfr(Resource resource, ProfilerConfiguration config) { - executor.submit(logUncaught(() -> activateJfrAndRunForever(config, resource))); + private void handleCommand(ProfilingCommand command) { + switch (command) { + case START: + tryStart(); + break; + case STOP: + // TODO: Build me + logger.warning("ProfilingSupervisor STOP not yet implemented"); + break; + } } - private boolean notClearForTakeoff(ProfilerConfiguration config) { - if (!config.isEnabled()) { - logger.fine("Profiler is not enabled."); - return true; + /** + * Try and start the profiler. This does not check configuration, just responds to a command + * request. + */ + private void tryStart() { + if (alreadyRunning()) { + logger.warning("JFR is already running, not starting again."); + return; } if (!jfr.isAvailable()) { logger.warning( - "JDK Flight Recorder (JFR) is not available in this JVM. Profiling is disabled."); - return true; + "JDK Flight Recorder (JFR) is not available in this JVM. Profiling will not start."); + return; } + config.log(); + logger.info("Profiler is active."); + setupContextStorage(); + activateJfrAndRunUntilStopped(config, getResource(sdk)); + } - return false; + private boolean alreadyRunning() { + return sequencer.get() != null; } private boolean checkOutputDir(Path outputDir) { @@ -137,7 +179,7 @@ private void outdirWarn(Path dir, String suffix) { logger.log(WARNING, "The configured output directory {0} {1}.", new Object[] {dir, suffix}); } - private void activateJfrAndRunForever(ProfilerConfiguration config, Resource resource) { + private void activateJfrAndRunUntilStopped(ProfilerConfiguration config, Resource resource) { boolean keepFiles = config.getKeepFiles(); Path outputDir = Paths.get(config.getProfilerDirectory()); if (keepFiles && !checkOutputDir(outputDir)) { @@ -199,13 +241,14 @@ private void activateJfrAndRunForever(ProfilerConfiguration config, Resource res .keepRecordingFiles(keepFiles) .build(); - RecordingSequencer sequencer = + RecordingSequencer recordingSequencer = RecordingSequencer.builder() .recordingDuration(recordingDuration) .recorder(recorder) .build(); - - sequencer.start(); + if (sequencer.compareAndSet(null, recordingSequencer)) { + recordingSequencer.start(); + } } private static LogRecordExporter createLogRecordExporter(Object configProperties) { @@ -264,4 +307,9 @@ private Map buildJfrSettings(ProfilerConfiguration config) { private static void setupContextStorage() { ContextStorage.addWrapper(JfrContextStorage::new); } + + enum ProfilingCommand { + START, + STOP + } } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrActivatorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrActivatorTest.java deleted file mode 100644 index c2b1e32ee..000000000 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrActivatorTest.java +++ /dev/null @@ -1,161 +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 com.splunk.opentelemetry.testing.declarativeconfig.DeclarativeConfigTestUtil.createAutoConfiguredSdk; -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 com.splunk.opentelemetry.profiler.snapshot.SnapshotProfilingDeclarativeConfiguration; -import io.opentelemetry.context.ContextStorage; -import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; -import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.ExecutorService; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.MockedStatic; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class JfrActivatorTest { - @RegisterExtension static final AutoCleanupExtension autoCleanup = AutoCleanupExtension.create(); - - @AfterEach - void resetDeclarativeConfigSuppliers() { - ProfilerDeclarativeConfiguration.SUPPLIER.reset(); - SnapshotProfilingDeclarativeConfiguration.SUPPLIER.reset(); - } - - @Test - void shouldActivateJfrRecording(@TempDir Path tempDir) throws IOException { - try (MockedStatic contextStorageMock = mockStatic(ContextStorage.class)) { - - // given - String yaml = - """ - file_format: "1.0" - distribution: - splunk: - profiling: - always_on: - """; - AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); - - 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 = - """ - file_format: "1.0" - distribution: - splunk: - profiling: - always_on: - """; - AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); - - 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); - } - } - - @ParameterizedTest - @MethodSource("generateNoProfilerYamlStrings") - void shouldNotActivateJfrRecording_profilerDisabled(String yaml, @TempDir Path tempDir) - throws IOException { - try (MockedStatic contextStorageMock = mockStatic(ContextStorage.class)) { - - // given - AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); - - 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); - } - } - - private List generateNoProfilerYamlStrings() { - return List.of( - Arguments.of("file_format: \"1.0\""), - Arguments.of( - """ - file_format: "1.0" - distribution: - """), - Arguments.of( - """ - file_format: "1.0" - distribution: - splunk: - """), - Arguments.of( - """ - file_format: "1.0" - distribution: - splunk: - something: - """)); - } -} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java new file mode 100644 index 000000000..6abe33dac --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java @@ -0,0 +1,168 @@ +/* + * 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 org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.splunk.opentelemetry.profiler.snapshot.SnapshotProfilingDeclarativeConfiguration; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class JfrAgentListenerTest { + @RegisterExtension static final AutoCleanupExtension autoCleanup = AutoCleanupExtension.create(); + + @AfterEach + void resetDeclarativeConfigSuppliers() { + ProfilerDeclarativeConfiguration.SUPPLIER.reset(); + SnapshotProfilingDeclarativeConfiguration.SUPPLIER.reset(); + } + + @Test + void shouldActivateJfrRecording(@TempDir Path tempDir) throws IOException { + // given + String yaml = + """ + file_format: "1.0" + distribution: + splunk: + profiling: + always_on: + """; + AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); + + var jfrMock = mock(JFR.class); + var supervisor = mock(ProfilingSupervisor.class); + + when(jfrMock.isAvailable()).thenReturn(true); + + JfrAgentListener listener = + new JfrAgentListener(jfrMock) { + @Override + ProfilingSupervisor makeProfilingSupervisor( + AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + return supervisor; + } + }; + + // when + listener.afterAgent(sdk); + + // then + verify(supervisor).requestStart(); + } + + @Test + void shouldNotActivateJfrRecording_JfrNotAvailable(@TempDir Path tempDir) throws IOException { + + // given + String yaml = + """ + file_format: "1.0" + distribution: + splunk: + profiling: + always_on: + """; + AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); + + var supervisor = mock(ProfilingSupervisor.class); + var jfrMock = mock(JFR.class); + when(jfrMock.isAvailable()).thenReturn(false); + + JfrAgentListener listener = + new JfrAgentListener(jfrMock) { + @Override + ProfilingSupervisor makeProfilingSupervisor( + AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + return supervisor; + } + }; + + // when + listener.afterAgent(sdk); + + // then + verifyNoInteractions(supervisor); + } + + @ParameterizedTest + @MethodSource("generateNoProfilerYamlStrings") + void shouldNotActivateJfrRecording_profilerDisabled(String yaml, @TempDir Path tempDir) + throws IOException { + + // given + AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); + + var jfrMock = mock(JFR.class); + var supervisor = mock(ProfilingSupervisor.class); + when(jfrMock.isAvailable()).thenReturn(true); + + JfrAgentListener listener = + new JfrAgentListener(jfrMock) { + @Override + ProfilingSupervisor makeProfilingSupervisor( + AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + return supervisor; + } + }; + + // when + listener.afterAgent(sdk); + + // then + verifyNoInteractions(supervisor); + } + + private List generateNoProfilerYamlStrings() { + return List.of( + Arguments.of("file_format: \"1.0\""), + Arguments.of( + """ + file_format: "1.0" + distribution: + """), + Arguments.of( + """ + file_format: "1.0" + distribution: + splunk: + """), + Arguments.of( + """ + file_format: "1.0" + distribution: + splunk: + something: + """)); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java new file mode 100644 index 000000000..bc87078aa --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java @@ -0,0 +1,250 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ProfilingSupervisorTest { + + @Mock JFR jfr; + @Mock ProfilerConfiguration config; + AutoConfiguredOpenTelemetrySdk sdk; + + ExecutorService executor; + ProfilingSupervisor supervisor; + + @BeforeEach + void setUp() { + executor = Executors.newSingleThreadExecutor(); + sdk = + AutoConfiguredOpenTelemetrySdk.builder() + .disableShutdownHook() + .addPropertiesSupplier( + () -> + Map.of( + "otel.traces.exporter", + "none", + "otel.metrics.exporter", + "none", + "otel.logs.exporter", + "none", + "otel.service.name", + "profiling-supervisor-test")) + .build(); + } + + @AfterEach + void tearDown() { + shutdownRecordingSequencer(); + executor.shutdownNow(); + } + + @Test + void requestStartDoesNotStartProfilerWhenJfrIsUnavailable() { + when(jfr.isAvailable()).thenReturn(false); + + supervisor = createSupervisor(); + + supervisor.requestStart(); + + await().untilAsserted(() -> verify(jfr).isAvailable()); + verify(config, never()).log(); + verify(jfr, never()).setStackDepth(anyInt()); + assertThat(currentSequencer()).isNull(); + } + + @Test + void requestStartBuildsAndStartsRecordingSequencer(@TempDir Path tempDir) { + int stackDepth = 4321; + Duration callStackInterval = Duration.ofMillis(123); + Duration recordingDuration = Duration.ofDays(1); + stubStartConfig(tempDir, stackDepth, callStackInterval, recordingDuration, false); + when(jfr.isAvailable()).thenReturn(true); + + supervisor = createSupervisor(); + + supervisor.requestStart(); + + RecordingSequencer sequencer = awaitSequencer(); + JfrRecorder recorder = fieldValue(sequencer, "recorder"); + Duration actualRecordingDuration = fieldValue(sequencer, "recordingDuration"); + Duration actualMaxAgeDuration = fieldValue(recorder, "maxAgeDuration"); + JFR actualJfr = fieldValue(recorder, "jfr"); + boolean actualKeepRecordingFiles = fieldValue(recorder, "keepRecordingFiles"); + JfrRecorder actualRecorder = fieldValue(sequencer, "recorder"); + Object recording = fieldValue(recorder, "recording"); + + verify(config).log(); + verify(jfr).setStackDepth(stackDepth); + assertThat(actualRecordingDuration).isEqualTo(recordingDuration); + assertThat(actualJfr).isSameAs(jfr); + assertThat(actualMaxAgeDuration).isEqualTo(recordingDuration.multipliedBy(10)); + assertThat(actualKeepRecordingFiles).isFalse(); + assertThat(actualRecorder).isSameAs(recorder); + assertThat(recording).isNotNull(); + + @SuppressWarnings("unchecked") + Map settings = fieldValue(recorder, "settings"); + assertThat(settings).containsEntry("jdk.ThreadDump#period", "123 ms"); + } + + @Test + void requestStartOnlyStartsOnce(@TempDir Path tempDir) { + stubStartConfig(tempDir, 1024, Duration.ofSeconds(10), Duration.ofDays(1), false); + when(jfr.isAvailable()).thenReturn(true); + + supervisor = createSupervisor(); + + supervisor.requestStart(); + RecordingSequencer sequencer = awaitSequencer(); + supervisor.requestStart(); + + await() + .during(Duration.ofMillis(200)) + .untilAsserted(() -> assertThat(currentSequencer()).isSameAs(sequencer)); + verify(config).log(); + verify(jfr).setStackDepth(1024); + } + + @Test + void requestStartCreatesConfiguredOutputDirectoryWhenKeepingFiles(@TempDir Path tempDir) { + Path outputDir = tempDir.resolve("profiling-output"); + stubStartConfig(outputDir, 1024, Duration.ZERO, Duration.ofDays(1), true); + when(jfr.isAvailable()).thenReturn(true); + + supervisor = createSupervisor(); + + supervisor.requestStart(); + + JfrRecorder recorder = fieldValue(awaitSequencer(), "recorder"); + boolean actualKeepRecordingFiles = fieldValue(recorder, "keepRecordingFiles"); + assertThat(outputDir).isDirectory(); + assertThat(actualKeepRecordingFiles).isTrue(); + } + + @Test + void requestStartDisablesKeepingFilesWhenOutputPathIsNotDirectory(@TempDir Path tempDir) + throws Exception { + Path outputPath = tempDir.resolve("not-a-directory"); + Files.writeString(outputPath, "already a file"); + stubStartConfig(outputPath, 1024, Duration.ZERO, Duration.ofDays(1), true); + when(jfr.isAvailable()).thenReturn(true); + + supervisor = createSupervisor(); + + supervisor.requestStart(); + + JfrRecorder recorder = fieldValue(awaitSequencer(), "recorder"); + boolean actualKeepRecordingFiles = fieldValue(recorder, "keepRecordingFiles"); + assertThat(actualKeepRecordingFiles).isFalse(); + } + + private ProfilingSupervisor createSupervisor() { + return ProfilingSupervisor.createAndStart(config, jfr, executor, sdk); + } + + private RecordingSequencer awaitSequencer() { + await().untilAsserted(() -> assertThat(currentSequencer()).isNotNull()); + return currentSequencer(); + } + + private RecordingSequencer currentSequencer() { + AtomicReference sequencerReference = fieldValue(supervisor, "sequencer"); + return sequencerReference.get(); + } + + private void shutdownRecordingSequencer() { + if (supervisor == null) { + return; + } + AtomicReference sequencerReference = fieldValue(supervisor, "sequencer"); + RecordingSequencer sequencer = sequencerReference.get(); + if (sequencer != null) { + ScheduledExecutorService sequencerExecutor = fieldValue(sequencer, "executor"); + sequencerExecutor.shutdownNow(); + JfrRecorder recorder = fieldValue(sequencer, "recorder"); + Object recording = fieldValue(recorder, "recording"); + if (recording != null) { + recorder.stop(); + } + } + } + + private void stubStartConfig( + Path outputDir, + int stackDepth, + Duration callStackInterval, + Duration recordingDuration, + boolean keepFiles) { + when(config.getProfilerDirectory()).thenReturn(outputDir.toString()); + when(config.getKeepFiles()).thenReturn(keepFiles); + when(config.getStackDepth()).thenReturn(stackDepth); + when(config.getRecordingDuration()).thenReturn(recordingDuration); + when(config.getCallStackInterval()).thenReturn(callStackInterval); + when(config.getIncludeAgentInternalStacks()).thenReturn(false); + when(config.getIncludeJvmInternalStacks()).thenReturn(false); + when(config.getTracingStacksOnly()).thenReturn(false); + when(config.getMemoryEnabled()).thenReturn(false); + when(config.getMemoryEventRateLimitEnabled()).thenReturn(true); + when(config.getMemoryEventRate()).thenReturn("150/s"); + when(config.getUseAllocationSampleEvent()).thenReturn(false); + when(config.getConfigProperties()) + .thenReturn( + DefaultConfigProperties.createFromMap( + Map.of( + "otel.exporter.otlp.protocol", + "http/protobuf", + "otel.exporter.otlp.endpoint", + "http://localhost:4318"))); + } + + @SuppressWarnings("unchecked") + private static T fieldValue(Object target, String fieldName) { + try { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(target); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Could not read field " + fieldName + " from " + target, e); + } + } +} From d80e8b9f46398ef04467b1bdba61ec709f8908d0 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 8 Jun 2026 14:24:27 -0700 Subject: [PATCH 02/25] simplify by making a test config class impl --- .../profiler/ProfilingSupervisorTest.java | 63 +++------ .../profiler/TestProfilingConfig.java | 131 ++++++++++++++++++ 2 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 profiler/src/test/java/com/splunk/opentelemetry/profiler/TestProfilingConfig.java diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java index bc87078aa..ce0fe9fa9 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java @@ -24,7 +24,6 @@ import static org.mockito.Mockito.when; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; @@ -46,7 +45,7 @@ class ProfilingSupervisorTest { @Mock JFR jfr; - @Mock ProfilerConfiguration config; + TestProfilingConfig config; AutoConfiguredOpenTelemetrySdk sdk; ExecutorService executor; @@ -55,6 +54,7 @@ class ProfilingSupervisorTest { @BeforeEach void setUp() { executor = Executors.newSingleThreadExecutor(); + config = new TestProfilingConfig(); sdk = AutoConfiguredOpenTelemetrySdk.builder() .disableShutdownHook() @@ -87,17 +87,17 @@ void requestStartDoesNotStartProfilerWhenJfrIsUnavailable() { supervisor.requestStart(); await().untilAsserted(() -> verify(jfr).isAvailable()); - verify(config, never()).log(); + assertThat(config.logCalled).isFalse(); verify(jfr, never()).setStackDepth(anyInt()); assertThat(currentSequencer()).isNull(); } @Test void requestStartBuildsAndStartsRecordingSequencer(@TempDir Path tempDir) { - int stackDepth = 4321; - Duration callStackInterval = Duration.ofMillis(123); - Duration recordingDuration = Duration.ofDays(1); - stubStartConfig(tempDir, stackDepth, callStackInterval, recordingDuration, false); + config.profilerDirectory = tempDir.toString(); + config.stackDepth = 4321; + config.callStackInterval = Duration.ofMillis(123); + config.recordingDuration = Duration.ofMinutes(1); when(jfr.isAvailable()).thenReturn(true); supervisor = createSupervisor(); @@ -113,23 +113,22 @@ void requestStartBuildsAndStartsRecordingSequencer(@TempDir Path tempDir) { JfrRecorder actualRecorder = fieldValue(sequencer, "recorder"); Object recording = fieldValue(recorder, "recording"); - verify(config).log(); - verify(jfr).setStackDepth(stackDepth); - assertThat(actualRecordingDuration).isEqualTo(recordingDuration); + assertThat(config.logCalled).isTrue(); + verify(jfr).setStackDepth(4321); + assertThat(actualRecordingDuration).isEqualTo(Duration.ofMinutes(1)); assertThat(actualJfr).isSameAs(jfr); - assertThat(actualMaxAgeDuration).isEqualTo(recordingDuration.multipliedBy(10)); + assertThat(actualMaxAgeDuration).isEqualTo(Duration.ofMinutes(1).multipliedBy(10)); assertThat(actualKeepRecordingFiles).isFalse(); assertThat(actualRecorder).isSameAs(recorder); assertThat(recording).isNotNull(); - @SuppressWarnings("unchecked") Map settings = fieldValue(recorder, "settings"); assertThat(settings).containsEntry("jdk.ThreadDump#period", "123 ms"); } @Test void requestStartOnlyStartsOnce(@TempDir Path tempDir) { - stubStartConfig(tempDir, 1024, Duration.ofSeconds(10), Duration.ofDays(1), false); + config.profilerDirectory = tempDir.toString(); when(jfr.isAvailable()).thenReturn(true); supervisor = createSupervisor(); @@ -141,14 +140,16 @@ void requestStartOnlyStartsOnce(@TempDir Path tempDir) { await() .during(Duration.ofMillis(200)) .untilAsserted(() -> assertThat(currentSequencer()).isSameAs(sequencer)); - verify(config).log(); + assertThat(config.logCalled).isTrue(); verify(jfr).setStackDepth(1024); } @Test void requestStartCreatesConfiguredOutputDirectoryWhenKeepingFiles(@TempDir Path tempDir) { Path outputDir = tempDir.resolve("profiling-output"); - stubStartConfig(outputDir, 1024, Duration.ZERO, Duration.ofDays(1), true); + config.profilerDirectory = outputDir.toString(); + config.callStackInterval = Duration.ZERO; + config.keepFiles = true; when(jfr.isAvailable()).thenReturn(true); supervisor = createSupervisor(); @@ -166,7 +167,9 @@ void requestStartDisablesKeepingFilesWhenOutputPathIsNotDirectory(@TempDir Path throws Exception { Path outputPath = tempDir.resolve("not-a-directory"); Files.writeString(outputPath, "already a file"); - stubStartConfig(outputPath, 1024, Duration.ZERO, Duration.ofDays(1), true); + config.profilerDirectory = outputPath.toString(); + config.callStackInterval = Duration.ZERO; + config.keepFiles = true; when(jfr.isAvailable()).thenReturn(true); supervisor = createSupervisor(); @@ -209,34 +212,6 @@ private void shutdownRecordingSequencer() { } } - private void stubStartConfig( - Path outputDir, - int stackDepth, - Duration callStackInterval, - Duration recordingDuration, - boolean keepFiles) { - when(config.getProfilerDirectory()).thenReturn(outputDir.toString()); - when(config.getKeepFiles()).thenReturn(keepFiles); - when(config.getStackDepth()).thenReturn(stackDepth); - when(config.getRecordingDuration()).thenReturn(recordingDuration); - when(config.getCallStackInterval()).thenReturn(callStackInterval); - when(config.getIncludeAgentInternalStacks()).thenReturn(false); - when(config.getIncludeJvmInternalStacks()).thenReturn(false); - when(config.getTracingStacksOnly()).thenReturn(false); - when(config.getMemoryEnabled()).thenReturn(false); - when(config.getMemoryEventRateLimitEnabled()).thenReturn(true); - when(config.getMemoryEventRate()).thenReturn("150/s"); - when(config.getUseAllocationSampleEvent()).thenReturn(false); - when(config.getConfigProperties()) - .thenReturn( - DefaultConfigProperties.createFromMap( - Map.of( - "otel.exporter.otlp.protocol", - "http/protobuf", - "otel.exporter.otlp.endpoint", - "http://localhost:4318"))); - } - @SuppressWarnings("unchecked") private static T fieldValue(Object target, String fieldName) { try { diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/TestProfilingConfig.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/TestProfilingConfig.java new file mode 100644 index 000000000..2981ae6a3 --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/TestProfilingConfig.java @@ -0,0 +1,131 @@ +/* + * 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 io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.time.Duration; +import java.util.Map; + +class TestProfilingConfig implements ProfilerConfiguration { + + boolean logCalled; + String profilerDirectory; + boolean keepFiles; + int stackDepth = 1024; + Duration recordingDuration = Duration.ofDays(1); + Duration callStackInterval = Duration.ofSeconds(10); + boolean includeAgentInternalStacks; + boolean includeJvmInternalStacks; + boolean tracingStacksOnly; + boolean memoryEnabled; + boolean memoryEventRateLimitEnabled = true; + String memoryEventRate = "150/s"; + boolean useAllocationSampleEvent; + ConfigProperties configProperties = + DefaultConfigProperties.createFromMap( + Map.of( + "otel.exporter.otlp.protocol", + "http/protobuf", + "otel.exporter.otlp.endpoint", + "http://localhost:4318")); + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public void log() { + logCalled = true; + } + + @Override + public String getIngestUrl() { + return "http://localhost:4318/v1/logs"; + } + + @Override + public String getOtlpProtocol() { + return "http/protobuf"; + } + + @Override + public boolean getMemoryEnabled() { + return memoryEnabled; + } + + @Override + public boolean getMemoryEventRateLimitEnabled() { + return memoryEventRateLimitEnabled; + } + + @Override + public String getMemoryEventRate() { + return memoryEventRate; + } + + @Override + public boolean getUseAllocationSampleEvent() { + return useAllocationSampleEvent; + } + + @Override + public Duration getCallStackInterval() { + return callStackInterval; + } + + @Override + public boolean getIncludeAgentInternalStacks() { + return includeAgentInternalStacks; + } + + @Override + public boolean getIncludeJvmInternalStacks() { + return includeJvmInternalStacks; + } + + @Override + public boolean getTracingStacksOnly() { + return tracingStacksOnly; + } + + @Override + public int getStackDepth() { + return stackDepth; + } + + @Override + public boolean getKeepFiles() { + return keepFiles; + } + + @Override + public String getProfilerDirectory() { + return profilerDirectory; + } + + @Override + public Duration getRecordingDuration() { + return recordingDuration; + } + + @Override + public Object getConfigProperties() { + return configProperties; + } +} From ff88ec81488937ef54446c894ea7c1557ca649f7 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 8 Jun 2026 15:30:45 -0700 Subject: [PATCH 03/25] factor out the creational stuff into a builder, add tests --- .../profiler/ProfilingSupervisor.java | 196 ++--------------- .../profiler/RecordingSequencerBuilder.java | 200 ++++++++++++++++++ .../profiler/ProfilingSupervisorTest.java | 173 +++++---------- 3 files changed, 273 insertions(+), 296 deletions(-) create mode 100644 profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 5cd13f4af..d9b776c51 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -16,34 +16,14 @@ package com.splunk.opentelemetry.profiler; -import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; import static io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil.getResource; import static java.util.logging.Level.FINE; -import static java.util.logging.Level.WARNING; import com.google.common.annotations.VisibleForTesting; -import com.splunk.opentelemetry.profiler.allocation.exporter.AllocationEventExporter; -import com.splunk.opentelemetry.profiler.allocation.exporter.PprofAllocationEventExporter; -import com.splunk.opentelemetry.profiler.context.SpanContextualizer; -import com.splunk.opentelemetry.profiler.exporter.CpuEventExporter; -import com.splunk.opentelemetry.profiler.exporter.PprofCpuEventExporter; import com.splunk.opentelemetry.profiler.util.HelpfulExecutors; -import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.logs.LogRecordProcessor; -import io.opentelemetry.sdk.logs.SdkLoggerProvider; -import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.resources.Resource; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -53,7 +33,7 @@ * This class oversees the profiling subsystem. It runs for the entire time that the agent is * running. */ -public class ProfilingSupervisor { +class ProfilingSupervisor { private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(ProfilingSupervisor.class.getName()); @@ -63,7 +43,8 @@ public class ProfilingSupervisor { private final BlockingQueue commandQueue; private final AtomicReference sequencer = new AtomicReference<>(); - private ProfilingSupervisor( + @VisibleForTesting + ProfilingSupervisor( ProfilerConfiguration config, JFR jfr, AutoConfiguredOpenTelemetrySdk sdk, @@ -76,23 +57,19 @@ private ProfilingSupervisor( static ProfilingSupervisor createAndStart( AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { - return createAndStart( - config, JFR.getInstance(), HelpfulExecutors.newSingleThreadExecutor("JFR Profiler"), sdk); - } - - @VisibleForTesting - static ProfilingSupervisor createAndStart( - ProfilerConfiguration config, - JFR jfr, - ExecutorService executor, - AutoConfiguredOpenTelemetrySdk sdk) { + ExecutorService executor = HelpfulExecutors.newSingleThreadExecutor("JFR Profiler"); // TODO: What if already started? BlockingQueue queue = new LinkedBlockingQueue<>(); - ProfilingSupervisor supervisor = new ProfilingSupervisor(config, jfr, sdk, queue); - executor.submit(supervisor::forever); + ProfilingSupervisor supervisor = new ProfilingSupervisor(config, JFR.getInstance(), sdk, queue); + supervisor.start(executor); return supervisor; } + @VisibleForTesting + void start(ExecutorService executor) { + executor.submit(this::forever); + } + private void forever() { while (true) { try { @@ -145,163 +122,24 @@ private void tryStart() { config.log(); logger.info("Profiler is active."); setupContextStorage(); - activateJfrAndRunUntilStopped(config, getResource(sdk)); + activateJfrAndRunUntilStopped(getResource(sdk)); } private boolean alreadyRunning() { return sequencer.get() != null; } - private boolean checkOutputDir(Path outputDir) { - if (!Files.exists(outputDir)) { - // Try creating the directory for the user... - try { - Files.createDirectories(outputDir); - } catch (IOException e) { - outdirWarn(outputDir, "does not exist and could not be created"); - return false; - } - } - if (!Files.isDirectory(outputDir)) { - outdirWarn(outputDir, "exists but is not a directory"); - return false; - } - - if (!Files.isWritable(outputDir)) { - outdirWarn(outputDir, "exists but is not writable"); - return false; - } - - return true; - } - - private void outdirWarn(Path dir, String suffix) { - logger.log(WARNING, "The configured output directory {0} {1}.", new Object[] {dir, suffix}); - } - - private void activateJfrAndRunUntilStopped(ProfilerConfiguration config, Resource resource) { - boolean keepFiles = config.getKeepFiles(); - Path outputDir = Paths.get(config.getProfilerDirectory()); - if (keepFiles && !checkOutputDir(outputDir)) { - keepFiles = false; - } - RecordingFileNamingConvention namingConvention = new RecordingFileNamingConvention(outputDir); - - int stackDepth = config.getStackDepth(); - jfr.setStackDepth(stackDepth); - - Duration recordingDuration = config.getRecordingDuration(); - Map jfrSettings = buildJfrSettings(config); - - EventReader eventReader = new EventReader(); - SpanContextualizer spanContextualizer = new SpanContextualizer(eventReader); - LogRecordExporter logsExporter = createLogRecordExporter(config.getConfigProperties()); - - CpuEventExporter cpuEventExporter = - PprofCpuEventExporter.builder() - .otelLogger(buildOtelLogger(SimpleLogRecordProcessor.create(logsExporter), resource)) - .period(config.getCallStackInterval()) - .stackDepth(stackDepth) - .build(); - - StackTraceFilter stackTraceFilter = buildStackTraceFilter(config, eventReader); - ThreadDumpProcessor threadDumpProcessor = - buildThreadDumpProcessor( - eventReader, spanContextualizer, cpuEventExporter, stackTraceFilter, config); - - AllocationEventExporter allocationEventExporter = - PprofAllocationEventExporter.builder() - .eventReader(eventReader) - .otelLogger(buildOtelLogger(SimpleLogRecordProcessor.create(logsExporter), resource)) - .stackDepth(stackDepth) - .build(); - - TLABProcessor tlabProcessor = - TLABProcessor.builder(config) - .eventReader(eventReader) - .allocationEventExporter(allocationEventExporter) - .spanContextualizer(spanContextualizer) - .stackTraceFilter(stackTraceFilter) - .build(); - - EventProcessingChain eventProcessingChain = - new EventProcessingChain( - eventReader, spanContextualizer, threadDumpProcessor, tlabProcessor); - - JfrRecordingHandler jfrRecordingHandler = - JfrRecordingHandler.builder().eventProcessingChain(eventProcessingChain).build(); - - JfrRecorder recorder = - JfrRecorder.builder() - .settings(jfrSettings) - .maxAgeDuration(recordingDuration.multipliedBy(10)) - .jfr(jfr) - .onNewRecording(jfrRecordingHandler) - .namingConvention(namingConvention) - .keepRecordingFiles(keepFiles) - .build(); - + private void activateJfrAndRunUntilStopped(Resource resource) { RecordingSequencer recordingSequencer = - RecordingSequencer.builder() - .recordingDuration(recordingDuration) - .recorder(recorder) - .build(); + makeRecordingSequencerBuilder().jfr(jfr).build(config, resource); if (sequencer.compareAndSet(null, recordingSequencer)) { recordingSequencer.start(); } } - private static LogRecordExporter createLogRecordExporter(Object configProperties) { - if (configProperties instanceof DeclarativeConfigProperties) { - DeclarativeConfigProperties exporterConfig = - ((DeclarativeConfigProperties) configProperties).getStructured("exporter", empty()); - return LogExporterBuilder.fromConfig(exporterConfig); - } - if (configProperties instanceof ConfigProperties) { - return LogExporterBuilder.fromConfig((ConfigProperties) configProperties); - } - throw new IllegalArgumentException( - "Unsupported config properties type: " + configProperties.getClass().getName()); - } - - private Logger buildOtelLogger(LogRecordProcessor logProcessor, Resource resource) { - return SdkLoggerProvider.builder() - .addLogRecordProcessor(logProcessor) - .setResource(resource) - .build() - .loggerBuilder(ProfilingSemanticAttributes.OTEL_INSTRUMENTATION_NAME) - .setInstrumentationVersion(ProfilingSemanticAttributes.OTEL_INSTRUMENTATION_VERSION) - .build(); - } - - private ThreadDumpProcessor buildThreadDumpProcessor( - EventReader eventReader, - SpanContextualizer spanContextualizer, - CpuEventExporter profilingEventExporter, - StackTraceFilter stackTraceFilter, - ProfilerConfiguration config) { - return ThreadDumpProcessor.builder() - .eventReader(eventReader) - .spanContextualizer(spanContextualizer) - .cpuEventExporter(profilingEventExporter) - .stackTraceFilter(stackTraceFilter) - .onlyTracingSpans(config.getTracingStacksOnly()) - .build(); - } - - /** Based on config, filters out agent internal stacks and/or JVM internal stacks */ - private StackTraceFilter buildStackTraceFilter( - ProfilerConfiguration config, EventReader eventReader) { - boolean includeAgentInternalStacks = config.getIncludeAgentInternalStacks(); - boolean includeJVMInternalStacks = config.getIncludeJvmInternalStacks(); - return new StackTraceFilter(eventReader, includeAgentInternalStacks, includeJVMInternalStacks); - } - - private Map buildJfrSettings(ProfilerConfiguration config) { - JfrSettingsReader settingsReader = new JfrSettingsReader(); - Map jfrSettings = settingsReader.read(); - JfrSettingsOverrides overrides = new JfrSettingsOverrides(config); - return overrides.apply(jfrSettings); + // Exists for testing + RecordingSequencerBuilder makeRecordingSequencerBuilder() { + return new RecordingSequencerBuilder(); } private static void setupContextStorage() { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java new file mode 100644 index 000000000..6f7ae91f0 --- /dev/null +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java @@ -0,0 +1,200 @@ +/* + * 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 io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; +import static java.util.logging.Level.WARNING; + +import com.splunk.opentelemetry.profiler.allocation.exporter.AllocationEventExporter; +import com.splunk.opentelemetry.profiler.allocation.exporter.PprofAllocationEventExporter; +import com.splunk.opentelemetry.profiler.context.SpanContextualizer; +import com.splunk.opentelemetry.profiler.exporter.CpuEventExporter; +import com.splunk.opentelemetry.profiler.exporter.PprofCpuEventExporter; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.logs.LogRecordProcessor; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.resources.Resource; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.Map; + +class RecordingSequencerBuilder { + private static final java.util.logging.Logger logger = + java.util.logging.Logger.getLogger(RecordingSequencerBuilder.class.getName()); + + private JFR jfr; + + RecordingSequencerBuilder jfr(JFR jfr) { + this.jfr = jfr; + return this; + } + + RecordingSequencer build(ProfilerConfiguration config, Resource resource) { + boolean keepFiles = config.getKeepFiles(); + Path outputDir = Paths.get(config.getProfilerDirectory()); + if (keepFiles && !checkOutputDir(outputDir)) { + keepFiles = false; + } + RecordingFileNamingConvention namingConvention = new RecordingFileNamingConvention(outputDir); + + int stackDepth = config.getStackDepth(); + jfr.setStackDepth(stackDepth); + + Duration recordingDuration = config.getRecordingDuration(); + Map jfrSettings = buildJfrSettings(config); + + EventReader eventReader = new EventReader(); + SpanContextualizer spanContextualizer = new SpanContextualizer(eventReader); + LogRecordExporter logsExporter = createLogRecordExporter(config.getConfigProperties()); + + CpuEventExporter cpuEventExporter = + PprofCpuEventExporter.builder() + .otelLogger(buildOtelLogger(SimpleLogRecordProcessor.create(logsExporter), resource)) + .period(config.getCallStackInterval()) + .stackDepth(stackDepth) + .build(); + + StackTraceFilter stackTraceFilter = buildStackTraceFilter(config, eventReader); + ThreadDumpProcessor threadDumpProcessor = + buildThreadDumpProcessor( + eventReader, spanContextualizer, cpuEventExporter, stackTraceFilter, config); + + AllocationEventExporter allocationEventExporter = + PprofAllocationEventExporter.builder() + .eventReader(eventReader) + .otelLogger(buildOtelLogger(SimpleLogRecordProcessor.create(logsExporter), resource)) + .stackDepth(stackDepth) + .build(); + + TLABProcessor tlabProcessor = + TLABProcessor.builder(config) + .eventReader(eventReader) + .allocationEventExporter(allocationEventExporter) + .spanContextualizer(spanContextualizer) + .stackTraceFilter(stackTraceFilter) + .build(); + + EventProcessingChain eventProcessingChain = + new EventProcessingChain( + eventReader, spanContextualizer, threadDumpProcessor, tlabProcessor); + + JfrRecordingHandler jfrRecordingHandler = + JfrRecordingHandler.builder().eventProcessingChain(eventProcessingChain).build(); + + JfrRecorder recorder = + JfrRecorder.builder() + .settings(jfrSettings) + .maxAgeDuration(recordingDuration.multipliedBy(10)) + .jfr(jfr) + .onNewRecording(jfrRecordingHandler) + .namingConvention(namingConvention) + .keepRecordingFiles(keepFiles) + .build(); + + return RecordingSequencer.builder() + .recordingDuration(recordingDuration) + .recorder(recorder) + .build(); + } + + private io.opentelemetry.api.logs.Logger buildOtelLogger( + LogRecordProcessor logProcessor, Resource resource) { + return SdkLoggerProvider.builder() + .addLogRecordProcessor(logProcessor) + .setResource(resource) + .build() + .loggerBuilder(ProfilingSemanticAttributes.OTEL_INSTRUMENTATION_NAME) + .setInstrumentationVersion(ProfilingSemanticAttributes.OTEL_INSTRUMENTATION_VERSION) + .build(); + } + + private ThreadDumpProcessor buildThreadDumpProcessor( + EventReader eventReader, + SpanContextualizer spanContextualizer, + CpuEventExporter profilingEventExporter, + StackTraceFilter stackTraceFilter, + ProfilerConfiguration config) { + return ThreadDumpProcessor.builder() + .eventReader(eventReader) + .spanContextualizer(spanContextualizer) + .cpuEventExporter(profilingEventExporter) + .stackTraceFilter(stackTraceFilter) + .onlyTracingSpans(config.getTracingStacksOnly()) + .build(); + } + + /** Based on config, filters out agent internal stacks and/or JVM internal stacks */ + private StackTraceFilter buildStackTraceFilter( + ProfilerConfiguration config, EventReader eventReader) { + boolean includeAgentInternalStacks = config.getIncludeAgentInternalStacks(); + boolean includeJVMInternalStacks = config.getIncludeJvmInternalStacks(); + return new StackTraceFilter(eventReader, includeAgentInternalStacks, includeJVMInternalStacks); + } + + private static LogRecordExporter createLogRecordExporter(Object configProperties) { + if (configProperties instanceof DeclarativeConfigProperties) { + DeclarativeConfigProperties exporterConfig = + ((DeclarativeConfigProperties) configProperties).getStructured("exporter", empty()); + return LogExporterBuilder.fromConfig(exporterConfig); + } + if (configProperties instanceof ConfigProperties) { + return LogExporterBuilder.fromConfig((ConfigProperties) configProperties); + } + throw new IllegalArgumentException( + "Unsupported config properties type: " + configProperties.getClass().getName()); + } + + private boolean checkOutputDir(Path outputDir) { + if (!Files.exists(outputDir)) { + // Try creating the directory for the user... + try { + Files.createDirectories(outputDir); + } catch (IOException e) { + outdirWarn(outputDir, "does not exist and could not be created"); + return false; + } + } + if (!Files.isDirectory(outputDir)) { + outdirWarn(outputDir, "exists but is not a directory"); + return false; + } + + if (!Files.isWritable(outputDir)) { + outdirWarn(outputDir, "exists but is not writable"); + return false; + } + + return true; + } + + private Map buildJfrSettings(ProfilerConfiguration config) { + JfrSettingsReader settingsReader = new JfrSettingsReader(); + Map jfrSettings = settingsReader.read(); + JfrSettingsOverrides overrides = new JfrSettingsOverrides(config); + return overrides.apply(jfrSettings); + } + + private void outdirWarn(Path dir, String suffix) { + logger.log(WARNING, "The configured output directory {0} {1}.", new Object[] {dir, suffix}); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java index ce0fe9fa9..001e68eab 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java @@ -19,20 +19,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import java.lang.reflect.Field; -import java.nio.file.Files; +import io.opentelemetry.sdk.resources.Resource; import java.nio.file.Path; import java.time.Duration; -import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.LinkedBlockingQueue; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,22 +43,25 @@ class ProfilingSupervisorTest { @Mock JFR jfr; + TestProfilingConfig config; + TestRecordingSequencerBuilder builder; AutoConfiguredOpenTelemetrySdk sdk; - ExecutorService executor; ProfilingSupervisor supervisor; @BeforeEach - void setUp() { - executor = Executors.newSingleThreadExecutor(); + void setUp(@TempDir Path tempDir) { config = new TestProfilingConfig(); + config.profilerDirectory = tempDir.toString(); + builder = new TestRecordingSequencerBuilder(); + executor = Executors.newSingleThreadExecutor(); sdk = AutoConfiguredOpenTelemetrySdk.builder() .disableShutdownHook() .addPropertiesSupplier( () -> - Map.of( + java.util.Map.of( "otel.traces.exporter", "none", "otel.metrics.exporter", @@ -70,11 +71,12 @@ void setUp() { "otel.service.name", "profiling-supervisor-test")) .build(); + supervisor = new TestProfilingSupervisor(config, jfr, sdk, builder); + supervisor.start(executor); } @AfterEach void tearDown() { - shutdownRecordingSequencer(); executor.shutdownNow(); } @@ -82,144 +84,81 @@ void tearDown() { void requestStartDoesNotStartProfilerWhenJfrIsUnavailable() { when(jfr.isAvailable()).thenReturn(false); - supervisor = createSupervisor(); - supervisor.requestStart(); await().untilAsserted(() -> verify(jfr).isAvailable()); assertThat(config.logCalled).isFalse(); verify(jfr, never()).setStackDepth(anyInt()); - assertThat(currentSequencer()).isNull(); + assertThat(builder.buildCalled).isFalse(); } @Test - void requestStartBuildsAndStartsRecordingSequencer(@TempDir Path tempDir) { - config.profilerDirectory = tempDir.toString(); + void requestStartBuildsAndStartsRecordingSequencer() { config.stackDepth = 4321; - config.callStackInterval = Duration.ofMillis(123); config.recordingDuration = Duration.ofMinutes(1); when(jfr.isAvailable()).thenReturn(true); - supervisor = createSupervisor(); - - supervisor.requestStart(); - - RecordingSequencer sequencer = awaitSequencer(); - JfrRecorder recorder = fieldValue(sequencer, "recorder"); - Duration actualRecordingDuration = fieldValue(sequencer, "recordingDuration"); - Duration actualMaxAgeDuration = fieldValue(recorder, "maxAgeDuration"); - JFR actualJfr = fieldValue(recorder, "jfr"); - boolean actualKeepRecordingFiles = fieldValue(recorder, "keepRecordingFiles"); - JfrRecorder actualRecorder = fieldValue(sequencer, "recorder"); - Object recording = fieldValue(recorder, "recording"); - - assertThat(config.logCalled).isTrue(); - verify(jfr).setStackDepth(4321); - assertThat(actualRecordingDuration).isEqualTo(Duration.ofMinutes(1)); - assertThat(actualJfr).isSameAs(jfr); - assertThat(actualMaxAgeDuration).isEqualTo(Duration.ofMinutes(1).multipliedBy(10)); - assertThat(actualKeepRecordingFiles).isFalse(); - assertThat(actualRecorder).isSameAs(recorder); - assertThat(recording).isNotNull(); - - Map settings = fieldValue(recorder, "settings"); - assertThat(settings).containsEntry("jdk.ThreadDump#period", "123 ms"); - } - - @Test - void requestStartOnlyStartsOnce(@TempDir Path tempDir) { - config.profilerDirectory = tempDir.toString(); - when(jfr.isAvailable()).thenReturn(true); - - supervisor = createSupervisor(); - - supervisor.requestStart(); - RecordingSequencer sequencer = awaitSequencer(); supervisor.requestStart(); - await() - .during(Duration.ofMillis(200)) - .untilAsserted(() -> assertThat(currentSequencer()).isSameAs(sequencer)); + await().untilAsserted(() -> assertThat(builder.buildCalled).isTrue()); assertThat(config.logCalled).isTrue(); - verify(jfr).setStackDepth(1024); + assertThat(builder.jfr).isSameAs(jfr); + assertThat(builder.config).isSameAs(config); + assertThat(builder.resource).isNotNull(); + verify(builder.sequencer).start(); } @Test - void requestStartCreatesConfiguredOutputDirectoryWhenKeepingFiles(@TempDir Path tempDir) { - Path outputDir = tempDir.resolve("profiling-output"); - config.profilerDirectory = outputDir.toString(); - config.callStackInterval = Duration.ZERO; - config.keepFiles = true; + void requestStartOnlyStartsOnce() { when(jfr.isAvailable()).thenReturn(true); - supervisor = createSupervisor(); - supervisor.requestStart(); - - JfrRecorder recorder = fieldValue(awaitSequencer(), "recorder"); - boolean actualKeepRecordingFiles = fieldValue(recorder, "keepRecordingFiles"); - assertThat(outputDir).isDirectory(); - assertThat(actualKeepRecordingFiles).isTrue(); - } - - @Test - void requestStartDisablesKeepingFilesWhenOutputPathIsNotDirectory(@TempDir Path tempDir) - throws Exception { - Path outputPath = tempDir.resolve("not-a-directory"); - Files.writeString(outputPath, "already a file"); - config.profilerDirectory = outputPath.toString(); - config.callStackInterval = Duration.ZERO; - config.keepFiles = true; - when(jfr.isAvailable()).thenReturn(true); - - supervisor = createSupervisor(); - + await().untilAsserted(() -> assertThat(builder.buildCalled).isTrue()); supervisor.requestStart(); - JfrRecorder recorder = fieldValue(awaitSequencer(), "recorder"); - boolean actualKeepRecordingFiles = fieldValue(recorder, "keepRecordingFiles"); - assertThat(actualKeepRecordingFiles).isFalse(); + await().during(Duration.ofMillis(200)).untilAsserted(() -> verify(builder.sequencer).start()); + assertThat(builder.buildCount).isEqualTo(1); } - private ProfilingSupervisor createSupervisor() { - return ProfilingSupervisor.createAndStart(config, jfr, executor, sdk); - } + private static class TestProfilingSupervisor extends ProfilingSupervisor { + private final RecordingSequencerBuilder builder; - private RecordingSequencer awaitSequencer() { - await().untilAsserted(() -> assertThat(currentSequencer()).isNotNull()); - return currentSequencer(); - } + TestProfilingSupervisor( + ProfilerConfiguration config, + JFR jfr, + AutoConfiguredOpenTelemetrySdk sdk, + RecordingSequencerBuilder builder) { + super(config, jfr, sdk, new LinkedBlockingQueue<>()); + this.builder = builder; + } - private RecordingSequencer currentSequencer() { - AtomicReference sequencerReference = fieldValue(supervisor, "sequencer"); - return sequencerReference.get(); + @Override + RecordingSequencerBuilder makeRecordingSequencerBuilder() { + return builder; + } } - private void shutdownRecordingSequencer() { - if (supervisor == null) { - return; + private static class TestRecordingSequencerBuilder extends RecordingSequencerBuilder { + final RecordingSequencer sequencer = mock(RecordingSequencer.class); + JFR jfr; + ProfilerConfiguration config; + Resource resource; + boolean buildCalled; + int buildCount; + + @Override + RecordingSequencerBuilder jfr(JFR jfr) { + this.jfr = jfr; + return this; } - AtomicReference sequencerReference = fieldValue(supervisor, "sequencer"); - RecordingSequencer sequencer = sequencerReference.get(); - if (sequencer != null) { - ScheduledExecutorService sequencerExecutor = fieldValue(sequencer, "executor"); - sequencerExecutor.shutdownNow(); - JfrRecorder recorder = fieldValue(sequencer, "recorder"); - Object recording = fieldValue(recorder, "recording"); - if (recording != null) { - recorder.stop(); - } - } - } - @SuppressWarnings("unchecked") - private static T fieldValue(Object target, String fieldName) { - try { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return (T) field.get(target); - } catch (ReflectiveOperationException e) { - throw new AssertionError("Could not read field " + fieldName + " from " + target, e); + @Override + RecordingSequencer build(ProfilerConfiguration config, Resource resource) { + this.config = config; + this.resource = resource; + buildCalled = true; + buildCount++; + return sequencer; } } } From daa85fff87721a663673866683fec8be252d377a Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 8 Jun 2026 15:32:57 -0700 Subject: [PATCH 04/25] default --- .../opentelemetry/profiler/RecordingSequencerBuilder.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java index 6f7ae91f0..3dbb18d5d 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java @@ -50,6 +50,9 @@ RecordingSequencerBuilder jfr(JFR jfr) { } RecordingSequencer build(ProfilerConfiguration config, Resource resource) { + if(jfr == null) { + jfr = JFR.getInstance(); + } boolean keepFiles = config.getKeepFiles(); Path outputDir = Paths.get(config.getProfilerDirectory()); if (keepFiles && !checkOutputDir(outputDir)) { From 07068708ca9ada6e1a93e875d0d25fe873b01bc8 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 8 Jun 2026 15:50:33 -0700 Subject: [PATCH 05/25] rework tests --- .../profiler/RecordingSequencerBuilder.java | 2 +- .../RecordingSequencerBuilderTest.java | 116 ++++++++++++++++++ .../profiler/TestProfilingConfig.java | 3 +- 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java index 3dbb18d5d..7e0b18587 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java @@ -50,7 +50,7 @@ RecordingSequencerBuilder jfr(JFR jfr) { } RecordingSequencer build(ProfilerConfiguration config, Resource resource) { - if(jfr == null) { + if (jfr == null) { jfr = JFR.getInstance(); } boolean keepFiles = config.getKeepFiles(); diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java new file mode 100644 index 000000000..152bd0eb3 --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java @@ -0,0 +1,116 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.sdk.resources.Resource; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedConstruction; + +class RecordingSequencerBuilderTest { + + @TempDir Path tempDir; + + @Test + void buildConfiguresJfrAndWiresRecorderIntoSequencer() { + JFR jfr = mock(JFR.class); + TestProfilingConfig config = config(tempDir); + config.stackDepth = 73; + config.recordingDuration = Duration.ofMillis(100); + + try (MockedConstruction recorderConstruction = + mockConstruction(JfrRecorder.class)) { + RecordingSequencer sequencer = + new RecordingSequencerBuilder().jfr(jfr).build(config, Resource.empty()); + + assertThat(sequencer).isNotNull(); + assertThat(recorderConstruction.constructed()).hasSize(1); + verify(jfr).setStackDepth(73); + + JfrRecorder recorder = recorderConstruction.constructed().get(0); + when(recorder.isStarted()).thenReturn(true); + + sequencer.handleInterval(); + + verify(recorder).flushSnapshot(); + } + } + + @Test + void buildCreatesMissingOutputDirectoryWhenKeepingFiles() { + Path outputDir = tempDir.resolve("profiler-output"); + JFR jfr = mock(JFR.class); + TestProfilingConfig config = config(outputDir); + config.keepFiles = true; + + try (MockedConstruction recorderConstruction = + mockConstruction(JfrRecorder.class)) { + RecordingSequencer sequencer = + new RecordingSequencerBuilder().jfr(jfr).build(config, Resource.empty()); + + assertThat(sequencer).isNotNull(); + assertThat(outputDir).isDirectory(); + assertThat(recorderConstruction.constructed()).hasSize(1); + } + } + + @Test + void buildContinuesWhenKeepFilesPathIsNotADirectory() throws Exception { + Path outputFile = tempDir.resolve("profiler-output"); + Files.createFile(outputFile); + JFR jfr = mock(JFR.class); + TestProfilingConfig config = config(outputFile); + config.keepFiles = true; + + try (MockedConstruction recorderConstruction = + mockConstruction(JfrRecorder.class)) { + RecordingSequencer sequencer = + new RecordingSequencerBuilder().jfr(jfr).build(config, Resource.empty()); + + assertThat(sequencer).isNotNull(); + assertThat(recorderConstruction.constructed()).hasSize(1); + } + } + + @Test + void buildRejectsUnsupportedConfigProperties() { + JFR jfr = mock(JFR.class); + TestProfilingConfig config = config(tempDir); + config.configProperties = new Object(); + + assertThatThrownBy( + () -> new RecordingSequencerBuilder().jfr(jfr).build(config, Resource.empty())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Unsupported config properties type:"); + } + + private TestProfilingConfig config(Path outputDir) { + TestProfilingConfig config = new TestProfilingConfig(); + config.profilerDirectory = outputDir.toString(); + return config; + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/TestProfilingConfig.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/TestProfilingConfig.java index 2981ae6a3..20cd32291 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/TestProfilingConfig.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/TestProfilingConfig.java @@ -16,7 +16,6 @@ package com.splunk.opentelemetry.profiler; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import java.time.Duration; import java.util.Map; @@ -36,7 +35,7 @@ class TestProfilingConfig implements ProfilerConfiguration { boolean memoryEventRateLimitEnabled = true; String memoryEventRate = "150/s"; boolean useAllocationSampleEvent; - ConfigProperties configProperties = + Object configProperties = DefaultConfigProperties.createFromMap( Map.of( "otel.exporter.otlp.protocol", From dfc76ede49151d81450a0b394a1128ad145d63dc Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 8 Jun 2026 15:57:59 -0700 Subject: [PATCH 06/25] rename --- .../profiler/JfrAgentListenerTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java index 6abe33dac..586818d07 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java @@ -60,13 +60,13 @@ void shouldActivateJfrRecording(@TempDir Path tempDir) throws IOException { """; AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); - var jfrMock = mock(JFR.class); + var jfr = mock(JFR.class); var supervisor = mock(ProfilingSupervisor.class); - when(jfrMock.isAvailable()).thenReturn(true); + when(jfr.isAvailable()).thenReturn(true); JfrAgentListener listener = - new JfrAgentListener(jfrMock) { + new JfrAgentListener(jfr) { @Override ProfilingSupervisor makeProfilingSupervisor( AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { @@ -96,11 +96,11 @@ void shouldNotActivateJfrRecording_JfrNotAvailable(@TempDir Path tempDir) throws AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); var supervisor = mock(ProfilingSupervisor.class); - var jfrMock = mock(JFR.class); - when(jfrMock.isAvailable()).thenReturn(false); + var jfr = mock(JFR.class); + when(jfr.isAvailable()).thenReturn(false); JfrAgentListener listener = - new JfrAgentListener(jfrMock) { + new JfrAgentListener(jfr) { @Override ProfilingSupervisor makeProfilingSupervisor( AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { @@ -123,12 +123,12 @@ void shouldNotActivateJfrRecording_profilerDisabled(String yaml, @TempDir Path t // given AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir, autoCleanup); - var jfrMock = mock(JFR.class); + var jfr = mock(JFR.class); var supervisor = mock(ProfilingSupervisor.class); - when(jfrMock.isAvailable()).thenReturn(true); + when(jfr.isAvailable()).thenReturn(true); JfrAgentListener listener = - new JfrAgentListener(jfrMock) { + new JfrAgentListener(jfr) { @Override ProfilingSupervisor makeProfilingSupervisor( AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { From 7f880ff770ea381965cb64f0d2606017046debde Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 10 Jun 2026 15:21:19 -0700 Subject: [PATCH 07/25] import logger --- .../com/splunk/opentelemetry/profiler/JfrAgentListener.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java index b9e0f4fe5..d28c7d8f3 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -22,12 +22,12 @@ import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.logging.Logger; @AutoService(AgentListener.class) public class JfrAgentListener implements AgentListener { - private static final java.util.logging.Logger logger = - java.util.logging.Logger.getLogger(JfrAgentListener.class.getName()); + private static final Logger logger = Logger.getLogger(JfrAgentListener.class.getName()); private final JFR jfr; public JfrAgentListener() { From 37e57764e37aa42630c3b0843f8e04a9d5297612 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 10 Jun 2026 17:19:45 -0700 Subject: [PATCH 08/25] work on tests --- .../profiler/ProfilingSupervisor.java | 6 ++-- .../profiler/RecordingSequencer.java | 30 ++++--------------- .../profiler/RecordingSequencerBuilder.java | 14 +++++---- .../profiler/ProfilingSupervisorTest.java | 18 ++++++----- .../RecordingSequencerBuilderTest.java | 8 ++--- .../profiler/RecordingSequencerTest.java | 3 +- 6 files changed, 35 insertions(+), 44 deletions(-) diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index d9b776c51..4adaa7f62 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -131,15 +131,15 @@ private boolean alreadyRunning() { private void activateJfrAndRunUntilStopped(Resource resource) { RecordingSequencer recordingSequencer = - makeRecordingSequencerBuilder().jfr(jfr).build(config, resource); + makeRecordingSequencerBuilder(resource).jfr(jfr).build(); if (sequencer.compareAndSet(null, recordingSequencer)) { recordingSequencer.start(); } } // Exists for testing - RecordingSequencerBuilder makeRecordingSequencerBuilder() { - return new RecordingSequencerBuilder(); + RecordingSequencerBuilder makeRecordingSequencerBuilder(Resource resource) { + return RecordingSequencer.builder(config, resource); } private static void setupContextStorage() { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencer.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencer.java index 5a31d2ecd..39bc83fce 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencer.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencer.java @@ -20,6 +20,7 @@ import com.google.common.annotations.VisibleForTesting; import com.splunk.opentelemetry.profiler.util.HelpfulExecutors; +import io.opentelemetry.sdk.resources.Resource; import java.time.Duration; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -34,9 +35,9 @@ class RecordingSequencer { private final Duration recordingDuration; private final JfrRecorder recorder; - private RecordingSequencer(Builder builder) { - this.recordingDuration = builder.recordingDuration; - this.recorder = builder.recorder; + RecordingSequencer(JfrRecorder recorder, Duration recordingDuration) { + this.recordingDuration = recordingDuration; + this.recorder = recorder; } public void start() { @@ -58,26 +59,7 @@ void handleInterval() { } } - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private Duration recordingDuration; - private JfrRecorder recorder; - - public Builder recordingDuration(Duration duration) { - this.recordingDuration = duration; - return this; - } - - public Builder recorder(JfrRecorder recorder) { - this.recorder = recorder; - return this; - } - - public RecordingSequencer build() { - return new RecordingSequencer(this); - } + public static RecordingSequencerBuilder builder(ProfilerConfiguration config, Resource resource) { + return new RecordingSequencerBuilder(config, resource); } } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java index 7e0b18587..c8fc2ab3c 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java @@ -41,15 +41,22 @@ class RecordingSequencerBuilder { private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(RecordingSequencerBuilder.class.getName()); + private final ProfilerConfiguration config; + private final Resource resource; private JFR jfr; + public RecordingSequencerBuilder(ProfilerConfiguration config, Resource resource) { + this.config = config; + this.resource = resource; + } + RecordingSequencerBuilder jfr(JFR jfr) { this.jfr = jfr; return this; } - RecordingSequencer build(ProfilerConfiguration config, Resource resource) { + RecordingSequencer build() { if (jfr == null) { jfr = JFR.getInstance(); } @@ -114,10 +121,7 @@ RecordingSequencer build(ProfilerConfiguration config, Resource resource) { .keepRecordingFiles(keepFiles) .build(); - return RecordingSequencer.builder() - .recordingDuration(recordingDuration) - .recorder(recorder) - .build(); + return new RecordingSequencer(recorder, recordingDuration); } private io.opentelemetry.api.logs.Logger buildOtelLogger( diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java index 001e68eab..79fbcfb1d 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java @@ -54,7 +54,7 @@ class ProfilingSupervisorTest { void setUp(@TempDir Path tempDir) { config = new TestProfilingConfig(); config.profilerDirectory = tempDir.toString(); - builder = new TestRecordingSequencerBuilder(); + builder = new TestRecordingSequencerBuilder(config, mock(Resource.class)); executor = Executors.newSingleThreadExecutor(); sdk = AutoConfiguredOpenTelemetrySdk.builder() @@ -133,19 +133,25 @@ private static class TestProfilingSupervisor extends ProfilingSupervisor { } @Override - RecordingSequencerBuilder makeRecordingSequencerBuilder() { + RecordingSequencerBuilder makeRecordingSequencerBuilder(Resource resource) { return builder; } } private static class TestRecordingSequencerBuilder extends RecordingSequencerBuilder { final RecordingSequencer sequencer = mock(RecordingSequencer.class); + private final ProfilerConfiguration config; + private final Resource resource; JFR jfr; - ProfilerConfiguration config; - Resource resource; boolean buildCalled; int buildCount; + public TestRecordingSequencerBuilder(ProfilerConfiguration config, Resource resource){ + super(config, resource); + this.config = config; + this.resource = resource; + } + @Override RecordingSequencerBuilder jfr(JFR jfr) { this.jfr = jfr; @@ -153,9 +159,7 @@ RecordingSequencerBuilder jfr(JFR jfr) { } @Override - RecordingSequencer build(ProfilerConfiguration config, Resource resource) { - this.config = config; - this.resource = resource; + RecordingSequencer build() { buildCalled = true; buildCount++; return sequencer; diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java index 152bd0eb3..509c1d9ca 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java @@ -45,7 +45,7 @@ void buildConfiguresJfrAndWiresRecorderIntoSequencer() { try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { RecordingSequencer sequencer = - new RecordingSequencerBuilder().jfr(jfr).build(config, Resource.empty()); + RecordingSequencer.builder(config, Resource.empty()).jfr(jfr).build(); assertThat(sequencer).isNotNull(); assertThat(recorderConstruction.constructed()).hasSize(1); @@ -70,7 +70,7 @@ void buildCreatesMissingOutputDirectoryWhenKeepingFiles() { try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { RecordingSequencer sequencer = - new RecordingSequencerBuilder().jfr(jfr).build(config, Resource.empty()); + RecordingSequencer.builder(config, Resource.empty()).jfr(jfr).build(); assertThat(sequencer).isNotNull(); assertThat(outputDir).isDirectory(); @@ -89,7 +89,7 @@ void buildContinuesWhenKeepFilesPathIsNotADirectory() throws Exception { try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { RecordingSequencer sequencer = - new RecordingSequencerBuilder().jfr(jfr).build(config, Resource.empty()); + RecordingSequencer.builder(config, Resource.empty()).jfr(jfr).build(); assertThat(sequencer).isNotNull(); assertThat(recorderConstruction.constructed()).hasSize(1); @@ -103,7 +103,7 @@ void buildRejectsUnsupportedConfigProperties() { config.configProperties = new Object(); assertThatThrownBy( - () -> new RecordingSequencerBuilder().jfr(jfr).build(config, Resource.empty())) + () -> RecordingSequencer.builder(config, Resource.empty()).jfr(jfr).build()) .isInstanceOf(IllegalArgumentException.class) .hasMessageStartingWith("Unsupported config properties type:"); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerTest.java index 756e179a2..56795c7ad 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import io.opentelemetry.sdk.resources.Resource; import java.time.Duration; import java.util.Collections; import java.util.concurrent.CountDownLatch; @@ -69,7 +70,7 @@ private RecordingSequencer buildSequencer() { } private RecordingSequencer buildSequencer(JfrRecorder recorder) { - return RecordingSequencer.builder().recordingDuration(duration).recorder(recorder).build(); + return new RecordingSequencer(recorder, duration); } private class MockRecorder extends JfrRecorder { From 16eb882795b777a81c300861c9707e5d2a919cb6 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 10 Jun 2026 17:31:18 -0700 Subject: [PATCH 09/25] rename sequencer -> flusher. --- ...uencer.java => PeriodicRecordingFlusher.java} | 6 +++--- .../profiler/ProfilingSupervisor.java | 10 +++++----- .../profiler/RecordingSequencerBuilder.java | 4 ++-- ... => PeriodicRecordingFlusherBuilderTest.java} | 16 ++++++++-------- ...st.java => PeriodicRecordingFlusherTest.java} | 15 +++++++-------- .../profiler/ProfilingSupervisorTest.java | 4 ++-- 6 files changed, 27 insertions(+), 28 deletions(-) rename profiler/src/main/java/com/splunk/opentelemetry/profiler/{RecordingSequencer.java => PeriodicRecordingFlusher.java} (90%) rename profiler/src/test/java/com/splunk/opentelemetry/profiler/{RecordingSequencerBuilderTest.java => PeriodicRecordingFlusherBuilderTest.java} (86%) rename profiler/src/test/java/com/splunk/opentelemetry/profiler/{RecordingSequencerTest.java => PeriodicRecordingFlusherTest.java} (87%) diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencer.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java similarity index 90% rename from profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencer.java rename to profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java index 39bc83fce..c3f11c58e 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencer.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java @@ -27,15 +27,15 @@ import java.util.logging.Logger; /** Responsible for periodically generating a sequence of JFR recordings. */ -class RecordingSequencer { - private static final Logger logger = Logger.getLogger(RecordingSequencer.class.getName()); +class PeriodicRecordingFlusher { + private static final Logger logger = Logger.getLogger(PeriodicRecordingFlusher.class.getName()); private final ScheduledExecutorService executor = HelpfulExecutors.newSingleThreadedScheduledExecutor("JFR Recording Sequencer"); private final Duration recordingDuration; private final JfrRecorder recorder; - RecordingSequencer(JfrRecorder recorder, Duration recordingDuration) { + PeriodicRecordingFlusher(JfrRecorder recorder, Duration recordingDuration) { this.recordingDuration = recordingDuration; this.recorder = recorder; } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 4adaa7f62..32c5a1851 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -41,7 +41,7 @@ class ProfilingSupervisor { private final JFR jfr; private final AutoConfiguredOpenTelemetrySdk sdk; private final BlockingQueue commandQueue; - private final AtomicReference sequencer = new AtomicReference<>(); + private final AtomicReference sequencer = new AtomicReference<>(); @VisibleForTesting ProfilingSupervisor( @@ -130,16 +130,16 @@ private boolean alreadyRunning() { } private void activateJfrAndRunUntilStopped(Resource resource) { - RecordingSequencer recordingSequencer = + PeriodicRecordingFlusher periodicRecordingFlusher = makeRecordingSequencerBuilder(resource).jfr(jfr).build(); - if (sequencer.compareAndSet(null, recordingSequencer)) { - recordingSequencer.start(); + if (sequencer.compareAndSet(null, periodicRecordingFlusher)) { + periodicRecordingFlusher.start(); } } // Exists for testing RecordingSequencerBuilder makeRecordingSequencerBuilder(Resource resource) { - return RecordingSequencer.builder(config, resource); + return PeriodicRecordingFlusher.builder(config, resource); } private static void setupContextStorage() { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java index c8fc2ab3c..6be9a3f29 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java @@ -56,7 +56,7 @@ RecordingSequencerBuilder jfr(JFR jfr) { return this; } - RecordingSequencer build() { + PeriodicRecordingFlusher build() { if (jfr == null) { jfr = JFR.getInstance(); } @@ -121,7 +121,7 @@ RecordingSequencer build() { .keepRecordingFiles(keepFiles) .build(); - return new RecordingSequencer(recorder, recordingDuration); + return new PeriodicRecordingFlusher(recorder, recordingDuration); } private io.opentelemetry.api.logs.Logger buildOtelLogger( diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java similarity index 86% rename from profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java rename to profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java index 509c1d9ca..3acf78423 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilderTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java @@ -31,7 +31,7 @@ import org.junit.jupiter.api.io.TempDir; import org.mockito.MockedConstruction; -class RecordingSequencerBuilderTest { +class PeriodicRecordingFlusherBuilderTest { @TempDir Path tempDir; @@ -44,8 +44,8 @@ void buildConfiguresJfrAndWiresRecorderIntoSequencer() { try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { - RecordingSequencer sequencer = - RecordingSequencer.builder(config, Resource.empty()).jfr(jfr).build(); + PeriodicRecordingFlusher sequencer = + PeriodicRecordingFlusher.builder(config, Resource.empty()).jfr(jfr).build(); assertThat(sequencer).isNotNull(); assertThat(recorderConstruction.constructed()).hasSize(1); @@ -69,8 +69,8 @@ void buildCreatesMissingOutputDirectoryWhenKeepingFiles() { try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { - RecordingSequencer sequencer = - RecordingSequencer.builder(config, Resource.empty()).jfr(jfr).build(); + PeriodicRecordingFlusher sequencer = + PeriodicRecordingFlusher.builder(config, Resource.empty()).jfr(jfr).build(); assertThat(sequencer).isNotNull(); assertThat(outputDir).isDirectory(); @@ -88,8 +88,8 @@ void buildContinuesWhenKeepFilesPathIsNotADirectory() throws Exception { try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { - RecordingSequencer sequencer = - RecordingSequencer.builder(config, Resource.empty()).jfr(jfr).build(); + PeriodicRecordingFlusher sequencer = + PeriodicRecordingFlusher.builder(config, Resource.empty()).jfr(jfr).build(); assertThat(sequencer).isNotNull(); assertThat(recorderConstruction.constructed()).hasSize(1); @@ -103,7 +103,7 @@ void buildRejectsUnsupportedConfigProperties() { config.configProperties = new Object(); assertThatThrownBy( - () -> RecordingSequencer.builder(config, Resource.empty()).jfr(jfr).build()) + () -> PeriodicRecordingFlusher.builder(config, Resource.empty()).jfr(jfr).build()) .isInstanceOf(IllegalArgumentException.class) .hasMessageStartingWith("Unsupported config properties type:"); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherTest.java similarity index 87% rename from profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerTest.java rename to profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherTest.java index 56795c7ad..1dc36b3f1 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/RecordingSequencerTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherTest.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -import io.opentelemetry.sdk.resources.Resource; import java.time.Duration; import java.util.Collections; import java.util.concurrent.CountDownLatch; @@ -31,7 +30,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class RecordingSequencerTest { +class PeriodicRecordingFlusherTest { Duration duration = Duration.ofMillis(10); @@ -41,7 +40,7 @@ class RecordingSequencerTest { @Test void canContinueNotStarted() { when(recorder.isStarted()).thenReturn(false); - RecordingSequencer sequencer = buildSequencer(); + PeriodicRecordingFlusher sequencer = buildSequencer(); sequencer.handleInterval(); verify(recorder).start(); verifyNoMoreInteractions(recorder); @@ -50,7 +49,7 @@ void canContinueNotStarted() { @Test void canContinueAlreadyStarted() { when(recorder.isStarted()).thenReturn(true); - RecordingSequencer sequencer = buildSequencer(); + PeriodicRecordingFlusher sequencer = buildSequencer(); sequencer.handleInterval(); verify(recorder).flushSnapshot(); verifyNoMoreInteractions(recorder); @@ -60,17 +59,17 @@ void canContinueAlreadyStarted() { void startThroughFlushSequence() throws Exception { CountDownLatch latch = new CountDownLatch(3); recorder = new MockRecorder(latch); - RecordingSequencer sequencer = buildSequencer(); + PeriodicRecordingFlusher sequencer = buildSequencer(); sequencer.start(); assertTrue(latch.await(5, SECONDS)); } - private RecordingSequencer buildSequencer() { + private PeriodicRecordingFlusher buildSequencer() { return buildSequencer(recorder); } - private RecordingSequencer buildSequencer(JfrRecorder recorder) { - return new RecordingSequencer(recorder, duration); + private PeriodicRecordingFlusher buildSequencer(JfrRecorder recorder) { + return new PeriodicRecordingFlusher(recorder, duration); } private class MockRecorder extends JfrRecorder { diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java index 79fbcfb1d..05479bd86 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java @@ -139,7 +139,7 @@ RecordingSequencerBuilder makeRecordingSequencerBuilder(Resource resource) { } private static class TestRecordingSequencerBuilder extends RecordingSequencerBuilder { - final RecordingSequencer sequencer = mock(RecordingSequencer.class); + final PeriodicRecordingFlusher sequencer = mock(PeriodicRecordingFlusher.class); private final ProfilerConfiguration config; private final Resource resource; JFR jfr; @@ -159,7 +159,7 @@ RecordingSequencerBuilder jfr(JFR jfr) { } @Override - RecordingSequencer build() { + PeriodicRecordingFlusher build() { buildCalled = true; buildCount++; return sequencer; From 2bec48f39bf1ce8e5b09b7d9255da47164d72d02 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 10 Jun 2026 17:31:37 -0700 Subject: [PATCH 10/25] spotless --- .../profiler/PeriodicRecordingFlusherBuilderTest.java | 2 +- .../splunk/opentelemetry/profiler/ProfilingSupervisorTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java index 3acf78423..d309ac1da 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java @@ -103,7 +103,7 @@ void buildRejectsUnsupportedConfigProperties() { config.configProperties = new Object(); assertThatThrownBy( - () -> PeriodicRecordingFlusher.builder(config, Resource.empty()).jfr(jfr).build()) + () -> PeriodicRecordingFlusher.builder(config, Resource.empty()).jfr(jfr).build()) .isInstanceOf(IllegalArgumentException.class) .hasMessageStartingWith("Unsupported config properties type:"); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java index 05479bd86..c5d1cdaa8 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java @@ -146,7 +146,7 @@ private static class TestRecordingSequencerBuilder extends RecordingSequencerBui boolean buildCalled; int buildCount; - public TestRecordingSequencerBuilder(ProfilerConfiguration config, Resource resource){ + public TestRecordingSequencerBuilder(ProfilerConfiguration config, Resource resource) { super(config, resource); this.config = config; this.resource = resource; From 3e8642b17d87d5aff9602f42bdeeb790bce7c5ec Mon Sep 17 00:00:00 2001 From: robsunday Date: Fri, 12 Jun 2026 16:35:08 +0200 Subject: [PATCH 11/25] POC --- .../opentelemetry/opamp/OpampActivator.java | 9 +++- .../opamp/RemoteConfigProcessor.java | 41 +++++++++++++++++++ .../opamp/ServerToAgentMessageHandler.java | 6 ++- .../opamp/RemoteConfigProcessorTest.java | 3 +- .../profiler/JfrAgentListener.java | 5 +++ .../profiler/JfrContextStorage.java | 15 ++++++- .../profiler/ProfilingSupervisor.java | 16 +++++++- 7 files changed, 88 insertions(+), 7 deletions(-) diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java index 40a1723f6..e95c15892 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java @@ -22,6 +22,7 @@ import static java.util.logging.Level.WARNING; import com.google.auto.service.AutoService; +import com.splunk.opentelemetry.profiler.JfrAgentListener; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.opamp.client.OpampClient; import io.opentelemetry.opamp.client.OpampClientBuilder; @@ -56,7 +57,8 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr createEffectiveConfigFactory(autoConfiguredOpenTelemetrySdk); State.EffectiveConfig effectiveConfig = buildEffectiveConfig(effectiveConfigFactory); - ServerToAgentMessageHandler serverToAgentMessageHandler = new ServerToAgentMessageHandler(); + ServerToAgentMessageHandler serverToAgentMessageHandler = + new ServerToAgentMessageHandler(JfrAgentListener.profilingSupervisor); OpampClient client = startOpampClient( @@ -99,6 +101,11 @@ public void onMessage(OpampClient opampClient, MessageData messageData) { })); } + @Override + public int order() { + return Integer.MAX_VALUE; + } + private EffectiveConfigFactory createEffectiveConfigFactory(AutoConfiguredOpenTelemetrySdk sdk) { if (AutoConfigureUtil.isDeclarativeConfig(sdk)) { return new DeclarativeEffectiveConfigFileFactory(); diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java index 49b8e5fc8..823559d1f 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java @@ -16,9 +16,18 @@ package com.splunk.opentelemetry.opamp; +import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; + +import com.google.common.annotations.VisibleForTesting; +import com.splunk.opentelemetry.profiler.ProfilerDeclarativeConfiguration; +import com.splunk.opentelemetry.profiler.ProfilingSupervisor; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.opamp.client.OpampClient; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfiguration; +import java.io.ByteArrayInputStream; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import opamp.proto.AgentConfigFile; import opamp.proto.AgentRemoteConfig; @@ -30,6 +39,12 @@ public class RemoteConfigProcessor { private static final String REMOTE_CONFIG_FILE_NAME = "splunk.remote.config"; + public AtomicReference profilingSupervisor; + + public RemoteConfigProcessor(AtomicReference profilingSupervisor) { + this.profilingSupervisor = profilingSupervisor; + } + public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) { Objects.requireNonNull(opampClient); @@ -45,6 +60,26 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) } // TODO: provide implementation + DeclarativeConfigProperties remoteConfigProperties = toDeclarativeConfigProperties(configFile); + DeclarativeConfigProperties profilingConfigProperties = + remoteConfigProperties + .getStructured("distribution", empty()) + .getStructured("splunk", empty()) + .getStructured("profiling"); + + // Update profiler configuration only when profiling node exists + if (profilingConfigProperties != null) { + ProfilerDeclarativeConfiguration profilingConfig = + new ProfilerDeclarativeConfiguration(profilingConfigProperties); + // TODO: should be merged with current profiling config. Probably we will need profiler + // configuration refactoring and some listeners implemented for profiler configuration + // changes. For POC use this temporary solution + if (profilingConfig.isEnabled()) { + profilingSupervisor.get().requestStart(); + } else { + profilingSupervisor.get().requestStop(); + } + } // Confirm to the OpAMP Server that remote config has been applied. opampClient.setRemoteConfigStatus( @@ -53,4 +88,10 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) .status(RemoteConfigStatuses.RemoteConfigStatuses_APPLIED) .build()); } + + @VisibleForTesting + static DeclarativeConfigProperties toDeclarativeConfigProperties(AgentConfigFile configFile) { + return DeclarativeConfiguration.toConfigProperties( + new ByteArrayInputStream(configFile.body.toByteArray())); + } } diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java index 3752369d1..a55b3f31f 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java @@ -17,14 +17,16 @@ package com.splunk.opentelemetry.opamp; import com.google.common.annotations.VisibleForTesting; +import com.splunk.opentelemetry.profiler.ProfilingSupervisor; import io.opentelemetry.opamp.client.OpampClient; import io.opentelemetry.opamp.client.internal.response.MessageData; +import java.util.concurrent.atomic.AtomicReference; public class ServerToAgentMessageHandler { private final RemoteConfigProcessor remoteConfigProcessor; - public ServerToAgentMessageHandler() { - this(new RemoteConfigProcessor()); + public ServerToAgentMessageHandler(AtomicReference profilingSupervisor) { + this(new RemoteConfigProcessor(profilingSupervisor)); } @VisibleForTesting diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java index f034d08f9..b032384d1 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java @@ -22,6 +22,7 @@ import io.opentelemetry.opamp.client.OpampClient; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import okio.ByteString; import opamp.proto.AgentConfigFile; import opamp.proto.AgentConfigMap; @@ -32,7 +33,7 @@ import org.mockito.ArgumentCaptor; class RemoteConfigProcessorTest { - private final RemoteConfigProcessor handler = new RemoteConfigProcessor(); + private final RemoteConfigProcessor handler = new RemoteConfigProcessor(new AtomicReference<>()); private final OpampClient opampClient = org.mockito.Mockito.mock(OpampClient.class); @Test diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java index d28c7d8f3..2277c3785 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -22,10 +22,14 @@ import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; @AutoService(AgentListener.class) public class JfrAgentListener implements AgentListener { + // Awful POC code + public static final AtomicReference profilingSupervisor = + new AtomicReference<>(); private static final Logger logger = Logger.getLogger(JfrAgentListener.class.getName()); private final JFR jfr; @@ -45,6 +49,7 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { ProfilerConfiguration config = getProfilerConfiguration(sdk); // Always start the supervisor, because we may need to start it later elsewhere. ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk, config); + profilingSupervisor.set(supervisor); if (notClearForTakeoff(config)) { return; diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java index afd2fba47..99adee5d3 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java @@ -24,15 +24,17 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.Scope; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import javax.annotation.Nullable; class JfrContextStorage implements ContextStorage { - private final ContextStorage delegate; private final Function newEvent; private final ThreadLocal activeSpan = ThreadLocal.withInitial(Span::getInvalid); + private static final AtomicBoolean enabled = new AtomicBoolean(false); + JfrContextStorage(ContextStorage delegate) { this(delegate, JfrContextStorage::newEvent); } @@ -43,6 +45,14 @@ class JfrContextStorage implements ContextStorage { this.newEvent = newEvent; } + public static void setEnabled(boolean enabled) { + JfrContextStorage.enabled.set(enabled); + } + + public static boolean isEnabled() { + return enabled.get(); + } + static ContextAttached newEvent(SpanContext spanContext) { if (spanContext.isValid()) { return new ContextAttached( @@ -54,6 +64,9 @@ static ContextAttached newEvent(SpanContext spanContext) { @Override public Scope attach(Context toAttach) { Scope delegatedScope = delegate.attach(toAttach); + if (!isEnabled()) { + return delegatedScope; + } Span span = Span.fromContext(toAttach); Span current = activeSpan.get(); // do nothing when active span didn't change diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 32c5a1851..3805af5f0 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -33,7 +33,10 @@ * This class oversees the profiling subsystem. It runs for the entire time that the agent is * running. */ -class ProfilingSupervisor { +public class ProfilingSupervisor { + static { + setupContextStorage(); + } private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(ProfilingSupervisor.class.getName()); @@ -100,11 +103,16 @@ private void handleCommand(ProfilingCommand command) { break; case STOP: // TODO: Build me + disableContextStorage(); logger.warning("ProfilingSupervisor STOP not yet implemented"); break; } } + private void disableContextStorage() { + JfrContextStorage.setEnabled(false); + } + /** * Try and start the profiler. This does not check configuration, just responds to a command * request. @@ -121,10 +129,14 @@ private void tryStart() { } config.log(); logger.info("Profiler is active."); - setupContextStorage(); + enableContextStorage(); activateJfrAndRunUntilStopped(getResource(sdk)); } + private void enableContextStorage() { + JfrContextStorage.setEnabled(true); + } + private boolean alreadyRunning() { return sequencer.get() != null; } From 6b553f9f60fb63eeba77bb11562e83a2754aa343 Mon Sep 17 00:00:00 2001 From: robsunday Date: Tue, 16 Jun 2026 20:44:43 +0200 Subject: [PATCH 12/25] POC code refactored into production --- .../opentelemetry/opamp/OpampActivator.java | 4 +-- .../opamp/RemoteConfigProcessor.java | 11 +++--- .../opamp/ServerToAgentMessageHandler.java | 3 +- .../opamp/RemoteConfigProcessorTest.java | 11 +++--- .../profiler/JfrAgentListener.java | 6 ---- .../profiler/JfrContextStorage.java | 11 +++--- .../profiler/ProfilingSupervisor.java | 36 +++++++++++++------ 7 files changed, 46 insertions(+), 36 deletions(-) diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java index e95c15892..88b88ecad 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java @@ -22,7 +22,7 @@ import static java.util.logging.Level.WARNING; import com.google.auto.service.AutoService; -import com.splunk.opentelemetry.profiler.JfrAgentListener; +import com.splunk.opentelemetry.profiler.ProfilingSupervisor; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.opamp.client.OpampClient; import io.opentelemetry.opamp.client.OpampClientBuilder; @@ -58,7 +58,7 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr State.EffectiveConfig effectiveConfig = buildEffectiveConfig(effectiveConfigFactory); ServerToAgentMessageHandler serverToAgentMessageHandler = - new ServerToAgentMessageHandler(JfrAgentListener.profilingSupervisor); + new ServerToAgentMessageHandler(ProfilingSupervisor.SUPPLIER.get()); OpampClient client = startOpampClient( diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java index 823559d1f..c574c9322 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java @@ -27,7 +27,6 @@ import java.io.ByteArrayInputStream; import java.util.Map; import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import opamp.proto.AgentConfigFile; import opamp.proto.AgentRemoteConfig; @@ -39,10 +38,10 @@ public class RemoteConfigProcessor { private static final String REMOTE_CONFIG_FILE_NAME = "splunk.remote.config"; - public AtomicReference profilingSupervisor; + public ProfilingSupervisor profilingSupervisor; - public RemoteConfigProcessor(AtomicReference profilingSupervisor) { - this.profilingSupervisor = profilingSupervisor; + public RemoteConfigProcessor(ProfilingSupervisor profilingSupervisor) { + this.profilingSupervisor = Objects.requireNonNull(profilingSupervisor); } public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) { @@ -75,9 +74,9 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) // configuration refactoring and some listeners implemented for profiler configuration // changes. For POC use this temporary solution if (profilingConfig.isEnabled()) { - profilingSupervisor.get().requestStart(); + profilingSupervisor.requestStart(); } else { - profilingSupervisor.get().requestStop(); + profilingSupervisor.requestStop(); } } diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java index a55b3f31f..f0ab02d36 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java @@ -20,12 +20,11 @@ import com.splunk.opentelemetry.profiler.ProfilingSupervisor; import io.opentelemetry.opamp.client.OpampClient; import io.opentelemetry.opamp.client.internal.response.MessageData; -import java.util.concurrent.atomic.AtomicReference; public class ServerToAgentMessageHandler { private final RemoteConfigProcessor remoteConfigProcessor; - public ServerToAgentMessageHandler(AtomicReference profilingSupervisor) { + public ServerToAgentMessageHandler(ProfilingSupervisor profilingSupervisor) { this(new RemoteConfigProcessor(profilingSupervisor)); } diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java index b032384d1..5f134fd19 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java @@ -17,12 +17,12 @@ package com.splunk.opentelemetry.opamp; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import io.opentelemetry.opamp.client.OpampClient; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; import okio.ByteString; import opamp.proto.AgentConfigFile; import opamp.proto.AgentConfigMap; @@ -33,19 +33,22 @@ import org.mockito.ArgumentCaptor; class RemoteConfigProcessorTest { - private final RemoteConfigProcessor handler = new RemoteConfigProcessor(new AtomicReference<>()); - private final OpampClient opampClient = org.mockito.Mockito.mock(OpampClient.class); + private final RemoteConfigProcessor handler = new RemoteConfigProcessor(mock()); + private final OpampClient opampClient = mock(OpampClient.class); @Test void shouldMarkRemoteConfigAsApplied() { // given + String remoteConfigYaml = "test-config:"; ByteString configHash = ByteString.encodeUtf8("test-config-hash"); AgentRemoteConfig remoteConfig = createRemoteConfig( configHash, Map.of( "splunk.remote.config", - new AgentConfigFile.Builder().body(ByteString.encodeUtf8("test-config")).build())); + new AgentConfigFile.Builder() + .body(ByteString.encodeUtf8(remoteConfigYaml)) + .build())); // when handler.applyConfig(remoteConfig, opampClient); diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java index 2277c3785..49d9b38f2 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -22,15 +22,10 @@ import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; @AutoService(AgentListener.class) public class JfrAgentListener implements AgentListener { - // Awful POC code - public static final AtomicReference profilingSupervisor = - new AtomicReference<>(); - private static final Logger logger = Logger.getLogger(JfrAgentListener.class.getName()); private final JFR jfr; @@ -49,7 +44,6 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { ProfilerConfiguration config = getProfilerConfiguration(sdk); // Always start the supervisor, because we may need to start it later elsewhere. ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk, config); - profilingSupervisor.set(supervisor); if (notClearForTakeoff(config)) { return; diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java index 99adee5d3..d20819e16 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java @@ -24,7 +24,6 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.Scope; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import javax.annotation.Nullable; @@ -33,7 +32,7 @@ class JfrContextStorage implements ContextStorage { private final Function newEvent; private final ThreadLocal activeSpan = ThreadLocal.withInitial(Span::getInvalid); - private static final AtomicBoolean enabled = new AtomicBoolean(false); + private volatile boolean enabled = false; JfrContextStorage(ContextStorage delegate) { this(delegate, JfrContextStorage::newEvent); @@ -45,12 +44,12 @@ class JfrContextStorage implements ContextStorage { this.newEvent = newEvent; } - public static void setEnabled(boolean enabled) { - JfrContextStorage.enabled.set(enabled); + public void setEnabled(boolean enabled) { + this.enabled = enabled; } - public static boolean isEnabled() { - return enabled.get(); + public boolean isEnabled() { + return enabled; } static ContextAttached newEvent(SpanContext spanContext) { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 3805af5f0..721d93281 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting; import com.splunk.opentelemetry.profiler.util.HelpfulExecutors; +import com.splunk.opentelemetry.profiler.util.OptionalConfigurableSupplier; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; @@ -38,13 +39,18 @@ public class ProfilingSupervisor { setupContextStorage(); } + public static final OptionalConfigurableSupplier SUPPLIER = + new OptionalConfigurableSupplier<>(); private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(ProfilingSupervisor.class.getName()); + private final ProfilerConfiguration config; private final JFR jfr; private final AutoConfiguredOpenTelemetrySdk sdk; private final BlockingQueue commandQueue; private final AtomicReference sequencer = new AtomicReference<>(); + private static final AtomicReference jfrContextStorage = + new AtomicReference<>(); @VisibleForTesting ProfilingSupervisor( @@ -60,11 +66,15 @@ public class ProfilingSupervisor { static ProfilingSupervisor createAndStart( AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + if (SUPPLIER.isConfigured()) { + throw new IllegalStateException("Already started"); + } ExecutorService executor = HelpfulExecutors.newSingleThreadExecutor("JFR Profiler"); - // TODO: What if already started? BlockingQueue queue = new LinkedBlockingQueue<>(); ProfilingSupervisor supervisor = new ProfilingSupervisor(config, JFR.getInstance(), sdk, queue); + SUPPLIER.configure(supervisor); supervisor.start(executor); + return supervisor; } @@ -103,14 +113,17 @@ private void handleCommand(ProfilingCommand command) { break; case STOP: // TODO: Build me - disableContextStorage(); + setJfrContextStorageEnabled(false); logger.warning("ProfilingSupervisor STOP not yet implemented"); break; } } - private void disableContextStorage() { - JfrContextStorage.setEnabled(false); + private void setJfrContextStorageEnabled(boolean enabled) { + JfrContextStorage contextStorage = jfrContextStorage.get(); + if (contextStorage != null) { + contextStorage.setEnabled(enabled); + } } /** @@ -129,14 +142,10 @@ private void tryStart() { } config.log(); logger.info("Profiler is active."); - enableContextStorage(); + setJfrContextStorageEnabled(true); activateJfrAndRunUntilStopped(getResource(sdk)); } - private void enableContextStorage() { - JfrContextStorage.setEnabled(true); - } - private boolean alreadyRunning() { return sequencer.get() != null; } @@ -155,7 +164,14 @@ RecordingSequencerBuilder makeRecordingSequencerBuilder(Resource resource) { } private static void setupContextStorage() { - ContextStorage.addWrapper(JfrContextStorage::new); + if (JFR.getInstance().isAvailable()) { + ContextStorage.addWrapper( + (delegate) -> { + JfrContextStorage storage = new JfrContextStorage(delegate); + jfrContextStorage.set(storage); + return storage; + }); + } } enum ProfilingCommand { From da2bd25eaf44846a0baa5e86dbd823bb1c182baa Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 17 Jun 2026 10:57:40 +0200 Subject: [PATCH 13/25] Test fixed --- .../com/splunk/opentelemetry/profiler/JfrContextStorageTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrContextStorageTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrContextStorageTest.java index 8ce8cf45d..e98352a8f 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrContextStorageTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrContextStorageTest.java @@ -83,6 +83,7 @@ void testAttachLifecycle() { when(newEvent.apply(SpanContext.getInvalid())).thenReturn(outEvent); JfrContextStorage contextStorage = new JfrContextStorage(delegate, newEvent); + contextStorage.setEnabled(true); Scope resultScope = contextStorage.attach(newContext); verify(inEvent).begin(); From 67ad6540ac4c22161ba762b5007b3e8abb5d6ee2 Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 17 Jun 2026 16:41:28 +0200 Subject: [PATCH 14/25] Added possibility to turn off profiler from remote config --- .../opamp/RemoteConfigProcessor.java | 32 ++++++---- .../profiler/JfrAgentListener.java | 2 +- .../opentelemetry/profiler/JfrRecorder.java | 15 +++-- .../profiler/PeriodicRecordingFlusher.java | 20 ++++-- ...a => PeriodicRecordingFlusherBuilder.java} | 8 +-- .../profiler/ProfilingSupervisor.java | 51 +++++++++------ .../profiler/JfrAgentListenerTest.java | 2 +- .../profiler/JfrRecorderTest.java | 14 ++++ .../PeriodicRecordingFlusherTest.java | 31 ++++++--- .../profiler/ProfilingSupervisorTest.java | 64 +++++++++++++------ 10 files changed, 165 insertions(+), 74 deletions(-) rename profiler/src/main/java/com/splunk/opentelemetry/profiler/{RecordingSequencerBuilder.java => PeriodicRecordingFlusherBuilder.java} (96%) diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java index c574c9322..a66d4f8d6 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java @@ -37,6 +37,7 @@ public class RemoteConfigProcessor { private static final Logger logger = Logger.getLogger(RemoteConfigProcessor.class.getName()); private static final String REMOTE_CONFIG_FILE_NAME = "splunk.remote.config"; + private static final String PROFILING_NODE_NAME = "profiling"; public ProfilingSupervisor profilingSupervisor; @@ -60,24 +61,27 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) // TODO: provide implementation DeclarativeConfigProperties remoteConfigProperties = toDeclarativeConfigProperties(configFile); - DeclarativeConfigProperties profilingConfigProperties = + DeclarativeConfigProperties splunkDistributionConfigProperties = remoteConfigProperties .getStructured("distribution", empty()) - .getStructured("splunk", empty()) - .getStructured("profiling"); + .getStructured("splunk", empty()); // Update profiler configuration only when profiling node exists - if (profilingConfigProperties != null) { - ProfilerDeclarativeConfiguration profilingConfig = - new ProfilerDeclarativeConfiguration(profilingConfigProperties); - // TODO: should be merged with current profiling config. Probably we will need profiler - // configuration refactoring and some listeners implemented for profiler configuration - // changes. For POC use this temporary solution - if (profilingConfig.isEnabled()) { - profilingSupervisor.requestStart(); - } else { - profilingSupervisor.requestStop(); - } + if (!splunkDistributionConfigProperties.getPropertyKeys().contains(PROFILING_NODE_NAME)) { + return; + } + + DeclarativeConfigProperties profilingConfigProperties = + splunkDistributionConfigProperties.getStructured(PROFILING_NODE_NAME, empty()); + ProfilerDeclarativeConfiguration profilingConfig = + new ProfilerDeclarativeConfiguration(profilingConfigProperties); + // TODO: should be merged with current profiling config. Probably we will need profiler + // configuration refactoring and some listeners implemented for profiler configuration + // changes. For POC use this temporary solution + if (profilingConfig.isEnabled()) { + profilingSupervisor.requestStartProfiling(); + } else { + profilingSupervisor.requestStopProfiling(); } // Confirm to the OpAMP Server that remote config has been applied. diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java index 49d9b38f2..41ab0c9de 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -48,7 +48,7 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { if (notClearForTakeoff(config)) { return; } - supervisor.requestStart(); + supervisor.requestStartProfiling(); } // Exists for testing 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 15251c667..f3043b03d 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrRecorder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrRecorder.java @@ -60,6 +60,9 @@ class JfrRecorder { } public void start() { + if (isStarted()) { + throw new IllegalStateException("Already started"); + } logger.fine("Profiler is starting a JFR recording"); recording = newRecording(); recording.setSettings(settings); @@ -70,6 +73,13 @@ public void start() { recording.start(); } + public void stop() { + if (isStarted()) { + recording.stop(); + } + recording = null; + } + @VisibleForTesting Recording newRecording() { return new Recording(); @@ -119,11 +129,6 @@ public boolean isStarted() { return (recording != null) && RecordingState.RUNNING.equals(recording.getState()); } - public void stop() { - recording.stop(); - recording = null; - } - public static Builder builder() { return new Builder(); } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java index c3f11c58e..37509288e 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java @@ -23,6 +23,7 @@ import io.opentelemetry.sdk.resources.Resource; import java.time.Duration; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; @@ -34,6 +35,7 @@ class PeriodicRecordingFlusher { HelpfulExecutors.newSingleThreadedScheduledExecutor("JFR Recording Sequencer"); private final Duration recordingDuration; private final JfrRecorder recorder; + private ScheduledFuture scheduledFlushFuture = null; PeriodicRecordingFlusher(JfrRecorder recorder, Duration recordingDuration) { this.recordingDuration = recordingDuration; @@ -42,8 +44,17 @@ class PeriodicRecordingFlusher { public void start() { recorder.start(); - executor.scheduleAtFixedRate( - this::handleInterval, 0, recordingDuration.toMillis(), TimeUnit.MILLISECONDS); + scheduledFlushFuture = + executor.scheduleAtFixedRate( + this::handleInterval, 0, recordingDuration.toMillis(), TimeUnit.MILLISECONDS); + } + + public void stop() { + if (scheduledFlushFuture != null) { + scheduledFlushFuture.cancel(true); + scheduledFlushFuture = null; + } + recorder.stop(); } @VisibleForTesting @@ -59,7 +70,8 @@ void handleInterval() { } } - public static RecordingSequencerBuilder builder(ProfilerConfiguration config, Resource resource) { - return new RecordingSequencerBuilder(config, resource); + public static PeriodicRecordingFlusherBuilder builder( + ProfilerConfiguration config, Resource resource) { + return new PeriodicRecordingFlusherBuilder(config, resource); } } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilder.java similarity index 96% rename from profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java rename to profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilder.java index 6be9a3f29..00efd8a62 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/RecordingSequencerBuilder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilder.java @@ -38,20 +38,20 @@ import java.time.Duration; import java.util.Map; -class RecordingSequencerBuilder { +class PeriodicRecordingFlusherBuilder { private static final java.util.logging.Logger logger = - java.util.logging.Logger.getLogger(RecordingSequencerBuilder.class.getName()); + java.util.logging.Logger.getLogger(PeriodicRecordingFlusherBuilder.class.getName()); private final ProfilerConfiguration config; private final Resource resource; private JFR jfr; - public RecordingSequencerBuilder(ProfilerConfiguration config, Resource resource) { + public PeriodicRecordingFlusherBuilder(ProfilerConfiguration config, Resource resource) { this.config = config; this.resource = resource; } - RecordingSequencerBuilder jfr(JFR jfr) { + PeriodicRecordingFlusherBuilder jfr(JFR jfr) { this.jfr = jfr; return this; } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 721d93281..58b3fcca7 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -48,7 +48,8 @@ public class ProfilingSupervisor { private final JFR jfr; private final AutoConfiguredOpenTelemetrySdk sdk; private final BlockingQueue commandQueue; - private final AtomicReference sequencer = new AtomicReference<>(); + private final AtomicReference recordingFlusher = + new AtomicReference<>(); private static final AtomicReference jfrContextStorage = new AtomicReference<>(); @@ -80,10 +81,10 @@ static ProfilingSupervisor createAndStart( @VisibleForTesting void start(ExecutorService executor) { - executor.submit(this::forever); + executor.submit(this::commandLoop); } - private void forever() { + private void commandLoop() { while (true) { try { ProfilingCommand command = commandQueue.take(); @@ -98,11 +99,11 @@ private void forever() { } } - public void requestStart() { + public void requestStartProfiling() { commandQueue.add(ProfilingCommand.START); } - public void requestStop() { + public void requestStopProfiling() { commandQueue.add(ProfilingCommand.STOP); } @@ -112,9 +113,7 @@ private void handleCommand(ProfilingCommand command) { tryStart(); break; case STOP: - // TODO: Build me - setJfrContextStorageEnabled(false); - logger.warning("ProfilingSupervisor STOP not yet implemented"); + tryStop(); break; } } @@ -131,7 +130,7 @@ private void setJfrContextStorageEnabled(boolean enabled) { * request. */ private void tryStart() { - if (alreadyRunning()) { + if (isJfrRecordingActive()) { logger.warning("JFR is already running, not starting again."); return; } @@ -143,23 +142,39 @@ private void tryStart() { config.log(); logger.info("Profiler is active."); setJfrContextStorageEnabled(true); - activateJfrAndRunUntilStopped(getResource(sdk)); + activateJfrRecording(getResource(sdk)); + } + + private void tryStop() { + if (!isJfrRecordingActive()) { + logger.warning("JFR is not running already, not stopping again."); + return; + } + setJfrContextStorageEnabled(false); + deactivateJfrRecording(); + } + + private boolean isJfrRecordingActive() { + return recordingFlusher.get() != null; } - private boolean alreadyRunning() { - return sequencer.get() != null; + private void activateJfrRecording(Resource resource) { + PeriodicRecordingFlusher recordingFlusher = + makeRecordingFlusherBuilder(resource).jfr(jfr).build(); + if (this.recordingFlusher.compareAndSet(null, recordingFlusher)) { + recordingFlusher.start(); + } } - private void activateJfrAndRunUntilStopped(Resource resource) { - PeriodicRecordingFlusher periodicRecordingFlusher = - makeRecordingSequencerBuilder(resource).jfr(jfr).build(); - if (sequencer.compareAndSet(null, periodicRecordingFlusher)) { - periodicRecordingFlusher.start(); + private void deactivateJfrRecording() { + PeriodicRecordingFlusher recordingFlusher = this.recordingFlusher.getAndSet(null); + if (recordingFlusher != null) { + recordingFlusher.stop(); } } // Exists for testing - RecordingSequencerBuilder makeRecordingSequencerBuilder(Resource resource) { + PeriodicRecordingFlusherBuilder makeRecordingFlusherBuilder(Resource resource) { return PeriodicRecordingFlusher.builder(config, resource); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java index 586818d07..9f87f91d0 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java @@ -78,7 +78,7 @@ ProfilingSupervisor makeProfilingSupervisor( listener.afterAgent(sdk); // then - verify(supervisor).requestStart(); + verify(supervisor).requestStartProfiling(); } @Test diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrRecorderTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrRecorderTest.java index b47059c9d..f2168f2e3 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrRecorderTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrRecorderTest.java @@ -65,6 +65,17 @@ void testStart() { verify(recording).start(); } + @Test + void testStop() { + JfrRecorder jfrRecorder = buildJfrRecorder(mock(JFR.class)); + jfrRecorder.start(); + when(recording.getState()).thenReturn(RecordingState.RUNNING); + + jfrRecorder.stop(); + + verify(recording).stop(); + } + @Test void testFlushSnapshot() throws Exception { JFR jfr = mock(JFR.class); @@ -105,8 +116,11 @@ void testIsStop() { JFR jfr = mock(JFR.class); JfrRecorder jfrRecorder = buildJfrRecorder(jfr); assertFalse(jfrRecorder.isStarted()); + jfrRecorder.start(); verify(recording, never()).stop(); + + when(recording.getState()).thenReturn(RecordingState.RUNNING); jfrRecorder.stop(); verify(recording).stop(); assertFalse(jfrRecorder.isStarted()); diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherTest.java index 1dc36b3f1..85b140dca 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherTest.java @@ -40,8 +40,8 @@ class PeriodicRecordingFlusherTest { @Test void canContinueNotStarted() { when(recorder.isStarted()).thenReturn(false); - PeriodicRecordingFlusher sequencer = buildSequencer(); - sequencer.handleInterval(); + PeriodicRecordingFlusher recordingFlusher = buildRecordingFlusher(); + recordingFlusher.handleInterval(); verify(recorder).start(); verifyNoMoreInteractions(recorder); } @@ -49,8 +49,8 @@ void canContinueNotStarted() { @Test void canContinueAlreadyStarted() { when(recorder.isStarted()).thenReturn(true); - PeriodicRecordingFlusher sequencer = buildSequencer(); - sequencer.handleInterval(); + PeriodicRecordingFlusher recordingFlusher = buildRecordingFlusher(); + recordingFlusher.handleInterval(); verify(recorder).flushSnapshot(); verifyNoMoreInteractions(recorder); } @@ -59,16 +59,29 @@ void canContinueAlreadyStarted() { void startThroughFlushSequence() throws Exception { CountDownLatch latch = new CountDownLatch(3); recorder = new MockRecorder(latch); - PeriodicRecordingFlusher sequencer = buildSequencer(); - sequencer.start(); + PeriodicRecordingFlusher recordingFlusher = buildRecordingFlusher(); + recordingFlusher.start(); assertTrue(latch.await(5, SECONDS)); } - private PeriodicRecordingFlusher buildSequencer() { - return buildSequencer(recorder); + @Test + void stopCancelsScheduledFutureAndStopsRecorder() { + @SuppressWarnings("unchecked") + PeriodicRecordingFlusher recordingFlusher = buildRecordingFlusher(); + + recordingFlusher.start(); + recordingFlusher.stop(); + + verify(recorder).start(); + verify(recorder).stop(); + verifyNoMoreInteractions(recorder); + } + + private PeriodicRecordingFlusher buildRecordingFlusher() { + return buildRecordingFlusher(recorder); } - private PeriodicRecordingFlusher buildSequencer(JfrRecorder recorder) { + private PeriodicRecordingFlusher buildRecordingFlusher(JfrRecorder recorder) { return new PeriodicRecordingFlusher(recorder, duration); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java index c5d1cdaa8..3d2e133e5 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,7 +46,7 @@ class ProfilingSupervisorTest { @Mock JFR jfr; TestProfilingConfig config; - TestRecordingSequencerBuilder builder; + TestPeriodicRecordingFlusherBuilder builder; AutoConfiguredOpenTelemetrySdk sdk; ExecutorService executor; ProfilingSupervisor supervisor; @@ -54,7 +55,7 @@ class ProfilingSupervisorTest { void setUp(@TempDir Path tempDir) { config = new TestProfilingConfig(); config.profilerDirectory = tempDir.toString(); - builder = new TestRecordingSequencerBuilder(config, mock(Resource.class)); + builder = new TestPeriodicRecordingFlusherBuilder(config, mock(Resource.class)); executor = Executors.newSingleThreadExecutor(); sdk = AutoConfiguredOpenTelemetrySdk.builder() @@ -84,7 +85,7 @@ void tearDown() { void requestStartDoesNotStartProfilerWhenJfrIsUnavailable() { when(jfr.isAvailable()).thenReturn(false); - supervisor.requestStart(); + supervisor.requestStartProfiling(); await().untilAsserted(() -> verify(jfr).isAvailable()); assertThat(config.logCalled).isFalse(); @@ -93,67 +94,94 @@ void requestStartDoesNotStartProfilerWhenJfrIsUnavailable() { } @Test - void requestStartBuildsAndStartsRecordingSequencer() { + void requestStartProfilingBuildsAndStartsRecordingSequencer() { config.stackDepth = 4321; config.recordingDuration = Duration.ofMinutes(1); when(jfr.isAvailable()).thenReturn(true); - supervisor.requestStart(); + supervisor.requestStartProfiling(); await().untilAsserted(() -> assertThat(builder.buildCalled).isTrue()); assertThat(config.logCalled).isTrue(); assertThat(builder.jfr).isSameAs(jfr); assertThat(builder.config).isSameAs(config); assertThat(builder.resource).isNotNull(); - verify(builder.sequencer).start(); + verify(builder.flusher).start(); } @Test - void requestStartOnlyStartsOnce() { + void requestStartProfilingOnlyStartsOnce() { when(jfr.isAvailable()).thenReturn(true); - supervisor.requestStart(); + supervisor.requestStartProfiling(); await().untilAsserted(() -> assertThat(builder.buildCalled).isTrue()); - supervisor.requestStart(); + supervisor.requestStartProfiling(); - await().during(Duration.ofMillis(200)).untilAsserted(() -> verify(builder.sequencer).start()); + await().during(Duration.ofMillis(200)).untilAsserted(() -> verify(builder.flusher).start()); assertThat(builder.buildCount).isEqualTo(1); } + @Test + void requestStopProfilingStopsActiveRecordingFlusher() { + when(jfr.isAvailable()).thenReturn(true); + + supervisor.requestStartProfiling(); + await().untilAsserted(() -> verify(builder.flusher).start()); + + supervisor.requestStopProfiling(); + + await().untilAsserted(() -> verify(builder.flusher).stop()); + } + + @Test + void requestStartProfilingCanStartAgainAfterStop() { + when(jfr.isAvailable()).thenReturn(true); + + supervisor.requestStartProfiling(); + await().untilAsserted(() -> verify(builder.flusher).start()); + supervisor.requestStopProfiling(); + await().untilAsserted(() -> verify(builder.flusher).stop()); + + supervisor.requestStartProfiling(); + + await().untilAsserted(() -> verify(builder.flusher, times(2)).start()); + assertThat(builder.buildCount).isEqualTo(2); + } + private static class TestProfilingSupervisor extends ProfilingSupervisor { - private final RecordingSequencerBuilder builder; + private final PeriodicRecordingFlusherBuilder builder; TestProfilingSupervisor( ProfilerConfiguration config, JFR jfr, AutoConfiguredOpenTelemetrySdk sdk, - RecordingSequencerBuilder builder) { + PeriodicRecordingFlusherBuilder builder) { super(config, jfr, sdk, new LinkedBlockingQueue<>()); this.builder = builder; } @Override - RecordingSequencerBuilder makeRecordingSequencerBuilder(Resource resource) { + PeriodicRecordingFlusherBuilder makeRecordingFlusherBuilder(Resource resource) { return builder; } } - private static class TestRecordingSequencerBuilder extends RecordingSequencerBuilder { - final PeriodicRecordingFlusher sequencer = mock(PeriodicRecordingFlusher.class); + private static class TestPeriodicRecordingFlusherBuilder extends PeriodicRecordingFlusherBuilder { + final PeriodicRecordingFlusher flusher = mock(PeriodicRecordingFlusher.class); private final ProfilerConfiguration config; private final Resource resource; JFR jfr; boolean buildCalled; int buildCount; - public TestRecordingSequencerBuilder(ProfilerConfiguration config, Resource resource) { + public TestPeriodicRecordingFlusherBuilder(ProfilerConfiguration config, Resource resource) { super(config, resource); this.config = config; this.resource = resource; } @Override - RecordingSequencerBuilder jfr(JFR jfr) { + PeriodicRecordingFlusherBuilder jfr(JFR jfr) { this.jfr = jfr; return this; } @@ -162,7 +190,7 @@ RecordingSequencerBuilder jfr(JFR jfr) { PeriodicRecordingFlusher build() { buildCalled = true; buildCount++; - return sequencer; + return flusher; } } } From 76762383f007fe69647c1bc128961d6180e3c0cf Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 17 Jun 2026 17:02:03 +0200 Subject: [PATCH 15/25] fix --- .../opamp/RemoteConfigProcessor.java | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java index a66d4f8d6..d25adfff0 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java @@ -59,7 +59,6 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) return; } - // TODO: provide implementation DeclarativeConfigProperties remoteConfigProperties = toDeclarativeConfigProperties(configFile); DeclarativeConfigProperties splunkDistributionConfigProperties = remoteConfigProperties @@ -67,21 +66,19 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) .getStructured("splunk", empty()); // Update profiler configuration only when profiling node exists - if (!splunkDistributionConfigProperties.getPropertyKeys().contains(PROFILING_NODE_NAME)) { - return; - } - - DeclarativeConfigProperties profilingConfigProperties = - splunkDistributionConfigProperties.getStructured(PROFILING_NODE_NAME, empty()); - ProfilerDeclarativeConfiguration profilingConfig = - new ProfilerDeclarativeConfiguration(profilingConfigProperties); - // TODO: should be merged with current profiling config. Probably we will need profiler - // configuration refactoring and some listeners implemented for profiler configuration - // changes. For POC use this temporary solution - if (profilingConfig.isEnabled()) { - profilingSupervisor.requestStartProfiling(); - } else { - profilingSupervisor.requestStopProfiling(); + if (splunkDistributionConfigProperties.getPropertyKeys().contains(PROFILING_NODE_NAME)) { + DeclarativeConfigProperties profilingConfigProperties = + splunkDistributionConfigProperties.getStructured(PROFILING_NODE_NAME, empty()); + ProfilerDeclarativeConfiguration profilingConfig = + new ProfilerDeclarativeConfiguration(profilingConfigProperties); + // TODO: should be merged with current profiling config. Probably we will need profiler + // configuration refactoring and some listeners implemented for profiler configuration + // changes. For POC use this temporary solution + if (profilingConfig.isEnabled()) { + profilingSupervisor.requestStartProfiling(); + } else { + profilingSupervisor.requestStopProfiling(); + } } // Confirm to the OpAMP Server that remote config has been applied. From 7bf5bc4cbe48da87f52f18d0898e21d11b6cce74 Mon Sep 17 00:00:00 2001 From: robsunday Date: Thu, 18 Jun 2026 18:56:35 +0200 Subject: [PATCH 16/25] code review followup --- .../profiler/JfrAgentListener.java | 2 +- .../JfrContextStorageInitializer.java | 47 +++++++++++++++++++ .../profiler/PeriodicRecordingFlusher.java | 4 +- .../profiler/ProfilingSupervisor.java | 24 +++++----- 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java index 41ab0c9de..574fbd549 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -42,7 +42,7 @@ public JfrAgentListener() { public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { ProfilerConfiguration config = getProfilerConfiguration(sdk); - // Always start the supervisor, because we may need to start it later elsewhere. + // Always start the supervisor, so it can start profiling later elsewhere. ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk, config); if (notClearForTakeoff(config)) { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java new file mode 100644 index 000000000..c7135f46b --- /dev/null +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java @@ -0,0 +1,47 @@ +/* + * 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 com.google.auto.service.AutoService; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import org.checkerframework.checker.nullness.qual.NonNull; + +@AutoService({ + AutoConfigurationCustomizerProvider.class, + DeclarativeConfigurationCustomizerProvider.class +}) +public final class JfrContextStorageInitializer + implements AutoConfigurationCustomizerProvider, DeclarativeConfigurationCustomizerProvider { + + @Override + public void customize(@NonNull AutoConfigurationCustomizer autoConfiguration) { + ProfilingSupervisor.setupJfrContextStorage(); + } + + @Override + public void customize(DeclarativeConfigurationCustomizer configurationCustomizer) { + ProfilingSupervisor.setupJfrContextStorage(); + } + + @Override + public int order() { + return Integer.MIN_VALUE; + } +} diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java index 37509288e..314de153f 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusher.java @@ -32,7 +32,7 @@ class PeriodicRecordingFlusher { private static final Logger logger = Logger.getLogger(PeriodicRecordingFlusher.class.getName()); private final ScheduledExecutorService executor = - HelpfulExecutors.newSingleThreadedScheduledExecutor("JFR Recording Sequencer"); + HelpfulExecutors.newSingleThreadedScheduledExecutor("JFR Recording Flusher"); private final Duration recordingDuration; private final JfrRecorder recorder; private ScheduledFuture scheduledFlushFuture = null; @@ -50,11 +50,11 @@ public void start() { } public void stop() { + recorder.stop(); if (scheduledFlushFuture != null) { scheduledFlushFuture.cancel(true); scheduledFlushFuture = null; } - recorder.stop(); } @VisibleForTesting diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 58b3fcca7..c1575c40c 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -28,6 +28,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** @@ -35,10 +36,6 @@ * running. */ public class ProfilingSupervisor { - static { - setupContextStorage(); - } - public static final OptionalConfigurableSupplier SUPPLIER = new OptionalConfigurableSupplier<>(); private static final java.util.logging.Logger logger = @@ -52,6 +49,7 @@ public class ProfilingSupervisor { new AtomicReference<>(); private static final AtomicReference jfrContextStorage = new AtomicReference<>(); + private static final AtomicBoolean contextStorageSetup = new AtomicBoolean(); @VisibleForTesting ProfilingSupervisor( @@ -178,15 +176,17 @@ PeriodicRecordingFlusherBuilder makeRecordingFlusherBuilder(Resource resource) { return PeriodicRecordingFlusher.builder(config, resource); } - private static void setupContextStorage() { - if (JFR.getInstance().isAvailable()) { - ContextStorage.addWrapper( - (delegate) -> { - JfrContextStorage storage = new JfrContextStorage(delegate); - jfrContextStorage.set(storage); - return storage; - }); + static void setupJfrContextStorage() { + if (!contextStorageSetup.compareAndSet(false, true)) { + return; } + + ContextStorage.addWrapper( + (delegate) -> { + JfrContextStorage storage = new JfrContextStorage(delegate); + jfrContextStorage.set(storage); + return storage; + }); } enum ProfilingCommand { From dd1c3b106be376c98701e80ccb430b74f6f09876 Mon Sep 17 00:00:00 2001 From: robsunday Date: Fri, 19 Jun 2026 10:32:26 +0200 Subject: [PATCH 17/25] Additional JFR check added --- .../profiler/JfrContextStorageInitializer.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java index c7135f46b..9904e0afc 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java @@ -32,16 +32,22 @@ public final class JfrContextStorageInitializer @Override public void customize(@NonNull AutoConfigurationCustomizer autoConfiguration) { - ProfilingSupervisor.setupJfrContextStorage(); + setupJfrContextStorage(); } @Override public void customize(DeclarativeConfigurationCustomizer configurationCustomizer) { - ProfilingSupervisor.setupJfrContextStorage(); + setupJfrContextStorage(); } @Override public int order() { return Integer.MIN_VALUE; } + + private void setupJfrContextStorage() { + if (JFR.getInstance().isAvailable()) { + ProfilingSupervisor.setupJfrContextStorage(); + } + } } From d34578466ca6fab7bddf54111973c2b7b8464926 Mon Sep 17 00:00:00 2001 From: robsunday Date: Fri, 19 Jun 2026 17:48:43 +0200 Subject: [PATCH 18/25] Enable reporting effective config after change --- .../DeclarativeConfigurationInterceptor.java | 2 +- .../opentelemetry/opamp/OpampActivator.java | 31 +++++-------------- .../opamp/RemoteConfigProcessor.java | 14 ++++++--- .../opamp/ServerToAgentMessageHandler.java | 6 ++-- ...DeclarativeEffectiveConfigFileFactory.java | 3 +- .../EffectiveConfigBuilder.java | 2 +- .../EffectiveConfigFactory.java | 2 +- .../EnvVarsEffectiveConfigFileFactory.java | 2 +- .../YamlNodeBuilder.java | 2 +- .../opamp/OpampActivatorTest.java | 8 ++--- .../opamp/RemoteConfigProcessorTest.java | 17 ++++++++-- ...arativeEffectiveConfigFileFactoryTest.java | 3 +- ...EnvVarsEffectiveConfigFileFactoryTest.java | 2 +- .../YamlNodeBuilderTest.java | 3 +- 14 files changed, 51 insertions(+), 46 deletions(-) rename opamp/src/main/java/com/splunk/opentelemetry/opamp/{ => effectiveconfig}/DeclarativeEffectiveConfigFileFactory.java (98%) rename opamp/src/main/java/com/splunk/opentelemetry/opamp/{ => effectiveconfig}/EffectiveConfigBuilder.java (95%) rename opamp/src/main/java/com/splunk/opentelemetry/opamp/{ => effectiveconfig}/EffectiveConfigFactory.java (97%) rename opamp/src/main/java/com/splunk/opentelemetry/opamp/{ => effectiveconfig}/EnvVarsEffectiveConfigFileFactory.java (98%) rename opamp/src/main/java/com/splunk/opentelemetry/opamp/{ => effectiveconfig}/YamlNodeBuilder.java (99%) rename opamp/src/test/java/com/splunk/opentelemetry/opamp/{ => effectiveconfig}/DeclarativeEffectiveConfigFileFactoryTest.java (99%) rename opamp/src/test/java/com/splunk/opentelemetry/opamp/{ => effectiveconfig}/EnvVarsEffectiveConfigFileFactoryTest.java (99%) rename opamp/src/test/java/com/splunk/opentelemetry/opamp/{ => effectiveconfig}/YamlNodeBuilderTest.java (98%) diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/DeclarativeConfigurationInterceptor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/DeclarativeConfigurationInterceptor.java index b3a46a6e1..ffb3e452b 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/DeclarativeConfigurationInterceptor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/DeclarativeConfigurationInterceptor.java @@ -35,7 +35,7 @@ public static OpenTelemetryConfigurationModel getConfigurationModel() { } @VisibleForTesting - static void reset() { + public static void reset() { configurationModel = null; } diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java index 88b88ecad..4ee2dcd11 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java @@ -17,11 +17,12 @@ package com.splunk.opentelemetry.opamp; import static io.opentelemetry.opamp.client.internal.request.service.HttpRequestService.DEFAULT_DELAY_BETWEEN_RETRIES; -import static io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil.getConfig; import static io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil.getResource; import static java.util.logging.Level.WARNING; import com.google.auto.service.AutoService; +import com.splunk.opentelemetry.opamp.effectiveconfig.EffectiveConfigReporter; +import com.splunk.opentelemetry.opamp.effectiveconfig.UpdatableEffectiveConfigState; import com.splunk.opentelemetry.profiler.ProfilingSupervisor; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.opamp.client.OpampClient; @@ -31,7 +32,6 @@ import io.opentelemetry.opamp.client.internal.request.service.HttpRequestService; import io.opentelemetry.opamp.client.internal.response.MessageData; import io.opentelemetry.opamp.client.internal.state.State; -import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import java.io.IOException; @@ -53,18 +53,19 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr } Resource resource = getResource(autoConfiguredOpenTelemetrySdk); - EffectiveConfigFactory effectiveConfigFactory = - createEffectiveConfigFactory(autoConfiguredOpenTelemetrySdk); - State.EffectiveConfig effectiveConfig = buildEffectiveConfig(effectiveConfigFactory); + UpdatableEffectiveConfigState effectiveConfigState = new UpdatableEffectiveConfigState(); + EffectiveConfigReporter effectiveConfigReporter = + EffectiveConfigReporter.create(autoConfiguredOpenTelemetrySdk, effectiveConfigState); + effectiveConfigReporter.reportEffectiveConfigIfChanged(); ServerToAgentMessageHandler serverToAgentMessageHandler = - new ServerToAgentMessageHandler(ProfilingSupervisor.SUPPLIER.get()); + new ServerToAgentMessageHandler(ProfilingSupervisor.SUPPLIER.get(), effectiveConfigReporter); OpampClient client = startOpampClient( opampClientConfiguration, resource, - effectiveConfig, + effectiveConfigState, new OpampClient.Callbacks() { @Override public void onConnect(OpampClient opampClient) { @@ -106,13 +107,6 @@ public int order() { return Integer.MAX_VALUE; } - private EffectiveConfigFactory createEffectiveConfigFactory(AutoConfiguredOpenTelemetrySdk sdk) { - if (AutoConfigureUtil.isDeclarativeConfig(sdk)) { - return new DeclarativeEffectiveConfigFileFactory(); - } - return new EnvVarsEffectiveConfigFileFactory(getConfig(sdk)); - } - static OpampClient startOpampClient( OpampClientConfiguration opampClientConfiguration, Resource resource, @@ -141,13 +135,4 @@ static OpampClient startOpampClient( builder.setEffectiveConfigState(effectiveConfig); return builder.build(callbacks); } - - static State.EffectiveConfig buildEffectiveConfig(EffectiveConfigFactory effectiveConfigFactory) { - return new State.EffectiveConfig() { - @Override - public opamp.proto.EffectiveConfig get() { - return new opamp.proto.EffectiveConfig(effectiveConfigFactory.createEffectiveConfigMap()); - } - }; - } } diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java index d25adfff0..93cd96547 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java @@ -19,6 +19,7 @@ import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; import com.google.common.annotations.VisibleForTesting; +import com.splunk.opentelemetry.opamp.effectiveconfig.EffectiveConfigReporter; import com.splunk.opentelemetry.profiler.ProfilerDeclarativeConfiguration; import com.splunk.opentelemetry.profiler.ProfilingSupervisor; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; @@ -39,10 +40,13 @@ public class RemoteConfigProcessor { private static final String REMOTE_CONFIG_FILE_NAME = "splunk.remote.config"; private static final String PROFILING_NODE_NAME = "profiling"; - public ProfilingSupervisor profilingSupervisor; + private final ProfilingSupervisor profilingSupervisor; + private final EffectiveConfigReporter effectiveConfigReporter; - public RemoteConfigProcessor(ProfilingSupervisor profilingSupervisor) { + public RemoteConfigProcessor(ProfilingSupervisor profilingSupervisor, + EffectiveConfigReporter effectiveConfigReporter) { this.profilingSupervisor = Objects.requireNonNull(profilingSupervisor); + this.effectiveConfigReporter = effectiveConfigReporter; } public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) { @@ -71,9 +75,6 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) splunkDistributionConfigProperties.getStructured(PROFILING_NODE_NAME, empty()); ProfilerDeclarativeConfiguration profilingConfig = new ProfilerDeclarativeConfiguration(profilingConfigProperties); - // TODO: should be merged with current profiling config. Probably we will need profiler - // configuration refactoring and some listeners implemented for profiler configuration - // changes. For POC use this temporary solution if (profilingConfig.isEnabled()) { profilingSupervisor.requestStartProfiling(); } else { @@ -87,6 +88,9 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) .last_remote_config_hash(remoteConfig.config_hash) .status(RemoteConfigStatuses.RemoteConfigStatuses_APPLIED) .build()); + + // TODO: Maybe should be postponed after profiler is enabled/disabled? + effectiveConfigReporter.reportEffectiveConfigIfChanged(); } @VisibleForTesting diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java index f0ab02d36..c5aeeca85 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java @@ -17,6 +17,7 @@ package com.splunk.opentelemetry.opamp; import com.google.common.annotations.VisibleForTesting; +import com.splunk.opentelemetry.opamp.effectiveconfig.EffectiveConfigReporter; import com.splunk.opentelemetry.profiler.ProfilingSupervisor; import io.opentelemetry.opamp.client.OpampClient; import io.opentelemetry.opamp.client.internal.response.MessageData; @@ -24,8 +25,9 @@ public class ServerToAgentMessageHandler { private final RemoteConfigProcessor remoteConfigProcessor; - public ServerToAgentMessageHandler(ProfilingSupervisor profilingSupervisor) { - this(new RemoteConfigProcessor(profilingSupervisor)); + public ServerToAgentMessageHandler(ProfilingSupervisor profilingSupervisor, + EffectiveConfigReporter effectiveConfigReporter) { + this(new RemoteConfigProcessor(profilingSupervisor, effectiveConfigReporter)); } @VisibleForTesting diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/DeclarativeEffectiveConfigFileFactory.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/DeclarativeEffectiveConfigFileFactory.java similarity index 98% rename from opamp/src/main/java/com/splunk/opentelemetry/opamp/DeclarativeEffectiveConfigFileFactory.java rename to opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/DeclarativeEffectiveConfigFileFactory.java index a3410f895..160fbb32e 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/DeclarativeEffectiveConfigFileFactory.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/DeclarativeEffectiveConfigFileFactory.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.splunk.opentelemetry.opamp; +package com.splunk.opentelemetry.opamp.effectiveconfig; import com.google.common.annotations.VisibleForTesting; +import com.splunk.opentelemetry.opamp.DeclarativeConfigurationInterceptor; import com.splunk.opentelemetry.profiler.ProfilerConfiguration; import com.splunk.opentelemetry.profiler.ProfilerDeclarativeConfiguration; import com.splunk.opentelemetry.profiler.snapshot.SnapshotProfilingConfiguration; diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/EffectiveConfigBuilder.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigBuilder.java similarity index 95% rename from opamp/src/main/java/com/splunk/opentelemetry/opamp/EffectiveConfigBuilder.java rename to opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigBuilder.java index 08ab87bbb..8bfa48a54 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/EffectiveConfigBuilder.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigBuilder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.opentelemetry.opamp; +package com.splunk.opentelemetry.opamp.effectiveconfig; import java.time.Duration; diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/EffectiveConfigFactory.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigFactory.java similarity index 97% rename from opamp/src/main/java/com/splunk/opentelemetry/opamp/EffectiveConfigFactory.java rename to opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigFactory.java index 13c454783..a68a7466f 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/EffectiveConfigFactory.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.opentelemetry.opamp; +package com.splunk.opentelemetry.opamp.effectiveconfig; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/EnvVarsEffectiveConfigFileFactory.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactory.java similarity index 98% rename from opamp/src/main/java/com/splunk/opentelemetry/opamp/EnvVarsEffectiveConfigFileFactory.java rename to opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactory.java index 62186f822..678a07b2b 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/EnvVarsEffectiveConfigFileFactory.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.opentelemetry.opamp; +package com.splunk.opentelemetry.opamp.effectiveconfig; import com.splunk.opentelemetry.profiler.ProfilerConfiguration; import com.splunk.opentelemetry.profiler.ProfilerEnvVarsConfiguration; diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/YamlNodeBuilder.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilder.java similarity index 99% rename from opamp/src/main/java/com/splunk/opentelemetry/opamp/YamlNodeBuilder.java rename to opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilder.java index 1bd57418d..e38bfe946 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/YamlNodeBuilder.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.opentelemetry.opamp; +package com.splunk.opentelemetry.opamp.effectiveconfig; import java.util.LinkedHashMap; import java.util.Map; diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java index ca85b97da..ca87584b3 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java @@ -16,7 +16,6 @@ package com.splunk.opentelemetry.opamp; -import static com.splunk.opentelemetry.opamp.OpampActivator.buildEffectiveConfig; import static io.opentelemetry.api.common.AttributeKey.booleanKey; import static io.opentelemetry.api.common.AttributeKey.doubleKey; import static io.opentelemetry.api.common.AttributeKey.longKey; @@ -29,14 +28,15 @@ import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_TYPE; import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_VERSION; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import com.splunk.opentelemetry.opamp.effectiveconfig.UpdatableEffectiveConfigState; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.Value; import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; import io.opentelemetry.opamp.client.OpampClient; import io.opentelemetry.opamp.client.internal.response.MessageData; -import io.opentelemetry.opamp.client.internal.state.State; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.resources.Resource; @@ -133,9 +133,7 @@ void testOpamp() throws Exception { .build(); server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.X_PROTOBUF, response.encode())); - ConfigProperties config = DefaultConfigProperties.createFromMap(Map.of()); - State.EffectiveConfig effectiveConfig = - buildEffectiveConfig(new EnvVarsEffectiveConfigFileFactory(config)); + UpdatableEffectiveConfigState effectiveConfig = mock(); CompletableFuture result = new CompletableFuture<>(); OpampClientConfiguration opampClientConfiguration = diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java index 5f134fd19..aefa5542d 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import com.splunk.opentelemetry.opamp.effectiveconfig.EffectiveConfigReporter; import io.opentelemetry.opamp.client.OpampClient; import java.util.Map; import okio.ByteString; @@ -29,12 +30,23 @@ import opamp.proto.AgentRemoteConfig; import opamp.proto.RemoteConfigStatus; import opamp.proto.RemoteConfigStatuses; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class RemoteConfigProcessorTest { - private final RemoteConfigProcessor handler = new RemoteConfigProcessor(mock()); - private final OpampClient opampClient = mock(OpampClient.class); + @Mock EffectiveConfigReporter effectiveConfigReporter; + @Mock OpampClient opampClient; + private RemoteConfigProcessor handler; + + @BeforeEach + void setUp() { + handler = new RemoteConfigProcessor(mock(), effectiveConfigReporter); + } @Test void shouldMarkRemoteConfigAsApplied() { @@ -60,6 +72,7 @@ void shouldMarkRemoteConfigAsApplied() { RemoteConfigStatus status = statusCaptor.getValue(); assertThat(status.last_remote_config_hash).isEqualTo(configHash); assertThat(status.status).isEqualTo(RemoteConfigStatuses.RemoteConfigStatuses_APPLIED); + verify(effectiveConfigReporter).reportEffectiveConfigIfChanged(); } @Test diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/DeclarativeEffectiveConfigFileFactoryTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/DeclarativeEffectiveConfigFileFactoryTest.java similarity index 99% rename from opamp/src/test/java/com/splunk/opentelemetry/opamp/DeclarativeEffectiveConfigFileFactoryTest.java rename to opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/DeclarativeEffectiveConfigFileFactoryTest.java index 0bbf444ff..3e92535e4 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/DeclarativeEffectiveConfigFileFactoryTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/DeclarativeEffectiveConfigFileFactoryTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.opentelemetry.opamp; +package com.splunk.opentelemetry.opamp.effectiveconfig; import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; import static io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil.getDistributionConfig; @@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; +import com.splunk.opentelemetry.opamp.DeclarativeConfigurationInterceptor; import com.splunk.opentelemetry.profiler.ProfilerConfiguration; import com.splunk.opentelemetry.profiler.ProfilerDeclarativeConfiguration; import com.splunk.opentelemetry.profiler.snapshot.SnapshotProfilingConfiguration; diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/EnvVarsEffectiveConfigFileFactoryTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactoryTest.java similarity index 99% rename from opamp/src/test/java/com/splunk/opentelemetry/opamp/EnvVarsEffectiveConfigFileFactoryTest.java rename to opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactoryTest.java index f48165485..ecf927e2c 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/EnvVarsEffectiveConfigFileFactoryTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactoryTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.opentelemetry.opamp; +package com.splunk.opentelemetry.opamp.effectiveconfig; import static org.assertj.core.api.Assertions.assertThat; diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/YamlNodeBuilderTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilderTest.java similarity index 98% rename from opamp/src/test/java/com/splunk/opentelemetry/opamp/YamlNodeBuilderTest.java rename to opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilderTest.java index fd9d7e751..bcf04857c 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/YamlNodeBuilderTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilderTest.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package com.splunk.opentelemetry.opamp; +package com.splunk.opentelemetry.opamp.effectiveconfig; import static org.assertj.core.api.Assertions.assertThat; import java.util.Map; + import org.junit.jupiter.api.Test; class YamlNodeBuilderTest { From 14ae567ae5c8f5829fe79b5b6a3a4013f81db3fa Mon Sep 17 00:00:00 2001 From: robsunday Date: Fri, 19 Jun 2026 17:48:48 +0200 Subject: [PATCH 19/25] Enable reporting effective config after change --- .../EffectiveConfigReporter.java | 75 +++++++++++++++++++ .../UpdatableEffectiveConfigState.java | 34 +++++++++ 2 files changed, 109 insertions(+) create mode 100644 opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigReporter.java create mode 100644 opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/UpdatableEffectiveConfigState.java diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigReporter.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigReporter.java new file mode 100644 index 000000000..587021bd9 --- /dev/null +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EffectiveConfigReporter.java @@ -0,0 +1,75 @@ +/* + * 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.opamp.effectiveconfig; + +import static io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil.getConfig; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.annotations.VisibleForTesting; +import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import java.util.HashMap; +import java.util.Map; +import okio.ByteString; +import opamp.proto.AgentConfigFile; +import opamp.proto.AgentConfigMap; + +public class EffectiveConfigReporter { + private final UpdatableEffectiveConfigState effectiveConfigState; + private final EffectiveConfigFactory effectiveConfigFactory; + private String lastReportedConfigContent; + + @VisibleForTesting + EffectiveConfigReporter( + EffectiveConfigFactory effectiveConfigFactory, + UpdatableEffectiveConfigState effectiveConfigState) { + this.effectiveConfigFactory = effectiveConfigFactory; + this.effectiveConfigState = effectiveConfigState; + } + + public static EffectiveConfigReporter create( + AutoConfiguredOpenTelemetrySdk sdk, UpdatableEffectiveConfigState effectiveConfigState) { + return new EffectiveConfigReporter(createEffectiveConfigFactory(sdk), effectiveConfigState); + } + + public boolean reportEffectiveConfigIfChanged() { + // Detect if effectiveConfig changed and needs to be reported + String configContent = effectiveConfigFactory.createEffectiveConfigContent(); + if (configContent.equals(lastReportedConfigContent)) { + return false; + } + + Map configMap = new HashMap<>(); + AgentConfigFile configFile = + new AgentConfigFile( + new ByteString(configContent.getBytes(UTF_8)), effectiveConfigFactory.getContentType()); + configMap.put(effectiveConfigFactory.getFileName(), configFile); + + effectiveConfigState.set(new AgentConfigMap(configMap)); + lastReportedConfigContent = configContent; + + return true; + } + + private static EffectiveConfigFactory createEffectiveConfigFactory( + AutoConfiguredOpenTelemetrySdk sdk) { + if (AutoConfigureUtil.isDeclarativeConfig(sdk)) { + return new DeclarativeEffectiveConfigFileFactory(); + } + return new EnvVarsEffectiveConfigFileFactory(getConfig(sdk)); + } +} diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/UpdatableEffectiveConfigState.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/UpdatableEffectiveConfigState.java new file mode 100644 index 000000000..242109ab5 --- /dev/null +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/UpdatableEffectiveConfigState.java @@ -0,0 +1,34 @@ +/* + * 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.opamp.effectiveconfig; + +import io.opentelemetry.opamp.client.internal.state.State; +import opamp.proto.AgentConfigMap; + +public class UpdatableEffectiveConfigState extends State.EffectiveConfig { + private AgentConfigMap agentConfigMap; + + public void set(AgentConfigMap configMap) { + agentConfigMap = configMap; + notifyUpdate(); + } + + @Override + public opamp.proto.EffectiveConfig get() { + return new opamp.proto.EffectiveConfig(agentConfigMap); + } +} From 8f66b55b2c8a5e7c123dae2abd6851375998d8dd Mon Sep 17 00:00:00 2001 From: robsunday Date: Fri, 19 Jun 2026 17:49:19 +0200 Subject: [PATCH 20/25] Spotless --- .../java/com/splunk/opentelemetry/opamp/OpampActivator.java | 3 ++- .../com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java | 4 ++-- .../opentelemetry/opamp/ServerToAgentMessageHandler.java | 4 ++-- .../com/splunk/opentelemetry/opamp/OpampActivatorTest.java | 2 -- .../opamp/effectiveconfig/YamlNodeBuilderTest.java | 1 - 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java index 4ee2dcd11..e1d0037cf 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java @@ -59,7 +59,8 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr effectiveConfigReporter.reportEffectiveConfigIfChanged(); ServerToAgentMessageHandler serverToAgentMessageHandler = - new ServerToAgentMessageHandler(ProfilingSupervisor.SUPPLIER.get(), effectiveConfigReporter); + new ServerToAgentMessageHandler( + ProfilingSupervisor.SUPPLIER.get(), effectiveConfigReporter); OpampClient client = startOpampClient( diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java index 93cd96547..cbf17362a 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java @@ -43,8 +43,8 @@ public class RemoteConfigProcessor { private final ProfilingSupervisor profilingSupervisor; private final EffectiveConfigReporter effectiveConfigReporter; - public RemoteConfigProcessor(ProfilingSupervisor profilingSupervisor, - EffectiveConfigReporter effectiveConfigReporter) { + public RemoteConfigProcessor( + ProfilingSupervisor profilingSupervisor, EffectiveConfigReporter effectiveConfigReporter) { this.profilingSupervisor = Objects.requireNonNull(profilingSupervisor); this.effectiveConfigReporter = effectiveConfigReporter; } diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java index c5aeeca85..30d938135 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java @@ -25,8 +25,8 @@ public class ServerToAgentMessageHandler { private final RemoteConfigProcessor remoteConfigProcessor; - public ServerToAgentMessageHandler(ProfilingSupervisor profilingSupervisor, - EffectiveConfigReporter effectiveConfigReporter) { + public ServerToAgentMessageHandler( + ProfilingSupervisor profilingSupervisor, EffectiveConfigReporter effectiveConfigReporter) { this(new RemoteConfigProcessor(profilingSupervisor, effectiveConfigReporter)); } diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java index ca87584b3..689590a19 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java @@ -37,8 +37,6 @@ import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; import io.opentelemetry.opamp.client.OpampClient; import io.opentelemetry.opamp.client.internal.response.MessageData; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.testing.internal.armeria.common.HttpResponse; import io.opentelemetry.testing.internal.armeria.common.HttpStatus; diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilderTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilderTest.java index bcf04857c..319902ea2 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilderTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/YamlNodeBuilderTest.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.Map; - import org.junit.jupiter.api.Test; class YamlNodeBuilderTest { From 944e0b8eb4db3b0ae755af143cb15764131ca60b Mon Sep 17 00:00:00 2001 From: robsunday Date: Mon, 22 Jun 2026 17:02:07 +0200 Subject: [PATCH 21/25] Merge fix --- .../com/splunk/opentelemetry/opamp/OpampActivator.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java index 7c9fcb13a..d98bd2a60 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java @@ -141,15 +141,6 @@ static OpampClient startOpampClient( return builder.build(callbacks); } - static State.EffectiveConfig buildEffectiveConfig(EffectiveConfigFactory effectiveConfigFactory) { - return new State.EffectiveConfig() { - @Override - public opamp.proto.EffectiveConfig get() { - return new opamp.proto.EffectiveConfig(effectiveConfigFactory.createEffectiveConfigMap()); - } - }; - } - private static ComponentHealth createInitialHealthReport() { Instant now = Instant.now(); long nowNanos = now.getEpochSecond() * 1_000_000_000L + now.getNano(); From 0e10073b9deefd5396828cd87cb6a5f86038838e Mon Sep 17 00:00:00 2001 From: robsunday Date: Tue, 23 Jun 2026 12:29:02 +0200 Subject: [PATCH 22/25] Remote config status reporting improved --- .../opamp/RemoteConfigProcessor.java | 77 ++++++++++++------- .../profiler/ProfilingSupervisor.java | 3 +- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java index f40556647..37920f30a 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Objects; import java.util.logging.Logger; +import okio.ByteString; import opamp.proto.AgentConfigFile; import opamp.proto.AgentRemoteConfig; import opamp.proto.RemoteConfigStatus; @@ -63,34 +64,45 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) return; } - DeclarativeConfigProperties remoteConfigProperties = toDeclarativeConfigProperties(configFile); - DeclarativeConfigProperties splunkDistributionConfigProperties = - remoteConfigProperties - .getStructured("distribution", empty()) - .getStructured("splunk", empty()); - - // Update profiler configuration only when profiling node exists - if (splunkDistributionConfigProperties.getPropertyKeys().contains(PROFILING_NODE_NAME)) { - DeclarativeConfigProperties profilingConfigProperties = - splunkDistributionConfigProperties.getStructured(PROFILING_NODE_NAME, empty()); - ProfilerDeclarativeConfiguration profilingConfig = - new ProfilerDeclarativeConfiguration(profilingConfigProperties); - // TODO: should be merged with current profiling config. Probably we will need profiler - // configuration refactoring and some listeners implemented for profiler configuration - // changes. For POC use this temporary solution - if (profilingConfig.isEnabled()) { - profilingSupervisor.requestStartProfiling(); - } else { - profilingSupervisor.requestStopProfiling(); + try { + DeclarativeConfigProperties remoteConfigProperties = + toDeclarativeConfigProperties(configFile); + DeclarativeConfigProperties splunkDistributionConfigProperties = + remoteConfigProperties + .getStructured("distribution", empty()) + .getStructured("splunk", empty()); + + // Update profiler configuration only when profiling node exists + if (splunkDistributionConfigProperties.getPropertyKeys().contains(PROFILING_NODE_NAME)) { + DeclarativeConfigProperties profilingConfigProperties = + splunkDistributionConfigProperties.getStructured(PROFILING_NODE_NAME, empty()); + ProfilerDeclarativeConfiguration profilingRemoteConfig = + new ProfilerDeclarativeConfiguration(profilingConfigProperties); + // TODO: should be merged with current profiling config. Probably we will need profiler + // configuration refactoring and some listeners implemented for profiler configuration + // changes. For POC use this temporary solution + if (profilingRemoteConfig.isEnabled()) { + profilingSupervisor.requestStartProfiling(); + } else { + profilingSupervisor.requestStopProfiling(); + } } - } - // Confirm to the OpAMP Server that remote config has been applied. - opampClient.setRemoteConfigStatus( - new RemoteConfigStatus.Builder() - .last_remote_config_hash(remoteConfig.config_hash) - .status(RemoteConfigStatuses.RemoteConfigStatuses_APPLIED) - .build()); + // Confirm to the OpAMP Server that remote config has been applied. + reportRemoteConfigStatus( + remoteConfig.config_hash, + RemoteConfigStatuses.RemoteConfigStatuses_APPLIED, + null, + opampClient); + + } catch (Exception e) { + reportRemoteConfigStatus( + remoteConfig.config_hash, + RemoteConfigStatuses.RemoteConfigStatuses_FAILED, + "Exception occurred: " + e.getMessage(), + opampClient); + throw e; + } // TODO: Maybe should be postponed after profiler is enabled/disabled? effectiveConfigReporter.reportEffectiveConfigIfChanged(); @@ -101,4 +113,17 @@ static DeclarativeConfigProperties toDeclarativeConfigProperties(AgentConfigFile return DeclarativeConfiguration.toConfigProperties( new ByteArrayInputStream(configFile.body.toByteArray())); } + + private void reportRemoteConfigStatus( + ByteString configHash, + RemoteConfigStatuses status, + String errorMessage, + OpampClient opampClient) { + opampClient.setRemoteConfigStatus( + new RemoteConfigStatus.Builder() + .last_remote_config_hash(configHash) + .error_message(errorMessage) + .status(status) + .build()); + } } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index c1575c40c..2d6e73e75 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -138,9 +138,9 @@ private void tryStart() { return; } config.log(); - logger.info("Profiler is active."); setJfrContextStorageEnabled(true); activateJfrRecording(getResource(sdk)); + logger.info("Profiler is active."); } private void tryStop() { @@ -150,6 +150,7 @@ private void tryStop() { } setJfrContextStorageEnabled(false); deactivateJfrRecording(); + logger.info("Profiler is deactivated."); } private boolean isJfrRecordingActive() { From 06c7fe92634a2846cfc1ce36f009dad62457ed27 Mon Sep 17 00:00:00 2001 From: robsunday Date: Tue, 23 Jun 2026 18:49:43 +0200 Subject: [PATCH 23/25] Refactoring of profiler config Sending effective config when configuration changed --- .../jvmmetrics/JvmMetricsInstaller.java | 13 +- .../jvmmetrics/JvmMetricsInstallerTest.java | 10 ++ .../opentelemetry/opamp/OpampActivator.java | 15 +- .../opamp/ProfilerRemoteConfiguration.java | 135 ++++++++++++++++++ .../opamp/RemoteConfigProcessor.java | 29 ++-- .../opamp/ServerToAgentMessageHandler.java | 8 +- .../EnvVarsEffectiveConfigFileFactory.java | 3 +- .../opamp/RemoteConfigProcessorTest.java | 95 ++++++++++-- ...EnvVarsEffectiveConfigFileFactoryTest.java | 9 ++ .../profiler/JfrAgentListener.java | 21 +-- .../profiler/LogExporterBuilder.java | 16 +-- .../profiler/OtelLoggerFactory.java | 13 +- .../PeriodicRecordingFlusherBuilder.java | 9 +- .../profiler/ProfilerConfiguration.java | 4 + ... => ProfilerConfigurationInitializer.java} | 36 ++++- .../ProfilerDeclarativeConfiguration.java | 4 - .../profiler/ProfilingSupervisor.java | 16 ++- .../profiler/JfrAgentListenerTest.java | 11 +- .../profiler/LogExporterBuilderTest.java | 10 +- .../profiler/OtelLoggerFactoryTest.java | 2 +- .../PeriodicRecordingFlusherBuilderTest.java | 22 ++- .../profiler/ProfilingSupervisorTest.java | 11 +- .../ConcurrentServiceEntrySamplingTest.java | 2 +- .../snapshot/GracefulShutdownTest.java | 2 +- .../SnapshotProfilingLogExportingTest.java | 2 +- 25 files changed, 377 insertions(+), 121 deletions(-) create mode 100644 opamp/src/main/java/com/splunk/opentelemetry/opamp/ProfilerRemoteConfiguration.java rename profiler/src/main/java/com/splunk/opentelemetry/profiler/{ProfilerDeclarativeConfigurationInitializer.java => ProfilerConfigurationInitializer.java} (61%) diff --git a/instrumentation/jvm-metrics/src/main/java/com/splunk/opentelemetry/instrumentation/jvmmetrics/JvmMetricsInstaller.java b/instrumentation/jvm-metrics/src/main/java/com/splunk/opentelemetry/instrumentation/jvmmetrics/JvmMetricsInstaller.java index fed727687..c0e0980ec 100644 --- a/instrumentation/jvm-metrics/src/main/java/com/splunk/opentelemetry/instrumentation/jvmmetrics/JvmMetricsInstaller.java +++ b/instrumentation/jvm-metrics/src/main/java/com/splunk/opentelemetry/instrumentation/jvmmetrics/JvmMetricsInstaller.java @@ -22,8 +22,6 @@ import com.splunk.opentelemetry.instrumentation.jvmmetrics.otel.OtelAllocatedMemoryMetrics; import com.splunk.opentelemetry.instrumentation.jvmmetrics.otel.OtelGcMemoryMetrics; import com.splunk.opentelemetry.profiler.ProfilerConfiguration; -import com.splunk.opentelemetry.profiler.ProfilerDeclarativeConfiguration; -import com.splunk.opentelemetry.profiler.ProfilerEnvVarsConfiguration; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -33,7 +31,7 @@ public class JvmMetricsInstaller implements AgentListener { @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { ConfigProperties config = getConfig(autoConfiguredOpenTelemetrySdk); - boolean metricsEnabled = isProfilerMemoryEnabled(config); + boolean metricsEnabled = isProfilerMemoryEnabled(); if (!config.getBoolean("otel.instrumentation.jvm-metrics-splunk.enabled", metricsEnabled)) { return; @@ -43,12 +41,7 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr new OtelGcMemoryMetrics().install(); } - private boolean isProfilerMemoryEnabled(ConfigProperties config) { - ProfilerConfiguration profilerConfiguration = - ProfilerDeclarativeConfiguration.SUPPLIER.isConfigured() - ? ProfilerDeclarativeConfiguration.SUPPLIER.get() - : new ProfilerEnvVarsConfiguration(config); - - return profilerConfiguration.getMemoryEnabled(); + private boolean isProfilerMemoryEnabled() { + return ProfilerConfiguration.SUPPLIER.get().getMemoryEnabled(); } } diff --git a/instrumentation/jvm-metrics/src/test/java/com/splunk/opentelemetry/instrumentation/jvmmetrics/JvmMetricsInstallerTest.java b/instrumentation/jvm-metrics/src/test/java/com/splunk/opentelemetry/instrumentation/jvmmetrics/JvmMetricsInstallerTest.java index 7aa2c2e95..21b67dbb8 100644 --- a/instrumentation/jvm-metrics/src/test/java/com/splunk/opentelemetry/instrumentation/jvmmetrics/JvmMetricsInstallerTest.java +++ b/instrumentation/jvm-metrics/src/test/java/com/splunk/opentelemetry/instrumentation/jvmmetrics/JvmMetricsInstallerTest.java @@ -23,6 +23,8 @@ import static org.mockito.Mockito.verify; import com.splunk.opentelemetry.instrumentation.jvmmetrics.otel.OtelAllocatedMemoryMetrics; +import com.splunk.opentelemetry.profiler.ProfilerConfiguration; +import com.splunk.opentelemetry.profiler.ProfilerEnvVarsConfiguration; import com.splunk.opentelemetry.testing.declarativeconfig.DeclarativeConfigTestUtil; import io.opentelemetry.instrumentation.config.bridge.DeclarativeConfigPropertiesBridgeBuilder; import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; @@ -30,12 +32,18 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import java.util.Map; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; class JvmMetricsInstallerTest { + @AfterEach + void tearDown() { + ProfilerConfiguration.SUPPLIER.reset(); + } + @Test void shouldInstallJvmMetrics_declarativeConfig() { // given @@ -53,6 +61,7 @@ void shouldInstallJvmMetrics_declarativeConfig() { new DeclarativeConfigPropertiesBridgeBuilder() .build( AutoConfigureUtil.getInstrumentationConfig(DeclarativeConfigTestUtil.parse(yaml))); + ProfilerConfiguration.SUPPLIER.configure(new ProfilerEnvVarsConfiguration(config)); try (MockedStatic autoConfigureUtil = mockStatic(AutoConfigureUtil.class); MockedConstruction allocatedMetrics = @@ -78,6 +87,7 @@ void shouldInstallJvmMetrics_envVarsConfig() { ConfigProperties config = DefaultConfigProperties.createFromMap( Map.of("otel.instrumentation.jvm-metrics-splunk.enabled", "true")); + ProfilerConfiguration.SUPPLIER.configure(new ProfilerEnvVarsConfiguration(config)); try (MockedStatic autoConfigureUtil = mockStatic(AutoConfigureUtil.class); MockedConstruction allocatedMetrics = diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java index d98bd2a60..c1bdc7569 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java @@ -23,6 +23,7 @@ import com.google.auto.service.AutoService; import com.splunk.opentelemetry.opamp.effectiveconfig.EffectiveConfigReporter; import com.splunk.opentelemetry.opamp.effectiveconfig.UpdatableEffectiveConfigState; +import com.splunk.opentelemetry.profiler.ProfilerConfiguration; import com.splunk.opentelemetry.profiler.ProfilingSupervisor; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.opamp.client.OpampClient; @@ -54,6 +55,7 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr return; } + ProfilerRemoteConfiguration profilerRemoteConfiguration = setupProfilerConfiguration(); Resource resource = getResource(autoConfiguredOpenTelemetrySdk); UpdatableEffectiveConfigState effectiveConfigState = new UpdatableEffectiveConfigState(); EffectiveConfigReporter effectiveConfigReporter = @@ -62,7 +64,9 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr ServerToAgentMessageHandler serverToAgentMessageHandler = new ServerToAgentMessageHandler( - ProfilingSupervisor.SUPPLIER.get(), effectiveConfigReporter); + profilerRemoteConfiguration, + ProfilingSupervisor.SUPPLIER.get(), + effectiveConfigReporter); OpampClient client = startOpampClient( @@ -105,6 +109,15 @@ public void onMessage(OpampClient opampClient, MessageData messageData) { })); } + private ProfilerRemoteConfiguration setupProfilerConfiguration() { + ProfilerConfiguration originalConfig = ProfilerConfiguration.SUPPLIER.get(); + ProfilerRemoteConfiguration remoteConfig = new ProfilerRemoteConfiguration(originalConfig); + + ProfilerConfiguration.SUPPLIER.configure(remoteConfig); + + return remoteConfig; + } + @Override public int order() { return Integer.MAX_VALUE; diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ProfilerRemoteConfiguration.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ProfilerRemoteConfiguration.java new file mode 100644 index 000000000..7dfe8fce5 --- /dev/null +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ProfilerRemoteConfiguration.java @@ -0,0 +1,135 @@ +/* + * 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.opamp; + +import com.splunk.opentelemetry.profiler.ProfilerConfiguration; +import java.time.Duration; +import java.util.Objects; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +public class ProfilerRemoteConfiguration implements ProfilerConfiguration { + private static final Logger logger = + Logger.getLogger(ProfilerRemoteConfiguration.class.getName()); + + private final ProfilerConfiguration delegate; + + private boolean enabled; + + ProfilerRemoteConfiguration(ProfilerConfiguration delegate) { + this.delegate = Objects.requireNonNull(delegate); + + enabled = delegate.isEnabled(); + } + + void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void log() { + logger.info("-----------------------"); + logger.info("Remote profiler configuration overwrites:"); + log("Enabled", isEnabled()); + logger.info("Base profiler configuration:"); + delegate.log(); + } + + private static void log(String key, @Nullable Object value) { + logger.info(String.format("%30s : %s", key, value)); + } + + @Override + public String getIngestUrl() { + return delegate.getIngestUrl(); + } + + @Override + public String getOtlpProtocol() { + return delegate.getOtlpProtocol(); + } + + @Override + public boolean getMemoryEnabled() { + return delegate.getMemoryEnabled(); + } + + @Override + public boolean getMemoryEventRateLimitEnabled() { + return delegate.getMemoryEventRateLimitEnabled(); + } + + @Override + public String getMemoryEventRate() { + return delegate.getMemoryEventRate(); + } + + @Override + public boolean getUseAllocationSampleEvent() { + return delegate.getUseAllocationSampleEvent(); + } + + @Override + public Duration getCallStackInterval() { + return delegate.getCallStackInterval(); + } + + @Override + public boolean getIncludeAgentInternalStacks() { + return delegate.getIncludeAgentInternalStacks(); + } + + @Override + public boolean getIncludeJvmInternalStacks() { + return delegate.getIncludeJvmInternalStacks(); + } + + @Override + public boolean getTracingStacksOnly() { + return delegate.getTracingStacksOnly(); + } + + @Override + public int getStackDepth() { + return delegate.getStackDepth(); + } + + @Override + public boolean getKeepFiles() { + return delegate.getKeepFiles(); + } + + @Override + public String getProfilerDirectory() { + return delegate.getProfilerDirectory(); + } + + @Override + public Duration getRecordingDuration() { + return delegate.getRecordingDuration(); + } + + @Override + public Object getConfigProperties() { + return delegate.getConfigProperties(); + } +} diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java index 37920f30a..11585577d 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java @@ -41,13 +41,17 @@ public class RemoteConfigProcessor { private static final String REMOTE_CONFIG_FILE_NAME = "splunk.remote.config"; private static final String PROFILING_NODE_NAME = "profiling"; + private final ProfilerRemoteConfiguration profilerRemoteConfiguration; private final ProfilingSupervisor profilingSupervisor; private final EffectiveConfigReporter effectiveConfigReporter; public RemoteConfigProcessor( - ProfilingSupervisor profilingSupervisor, EffectiveConfigReporter effectiveConfigReporter) { + ProfilerRemoteConfiguration profilerRemoteConfiguration, + ProfilingSupervisor profilingSupervisor, + EffectiveConfigReporter effectiveConfigReporter) { + this.profilerRemoteConfiguration = Objects.requireNonNull(profilerRemoteConfiguration); this.profilingSupervisor = Objects.requireNonNull(profilingSupervisor); - this.effectiveConfigReporter = effectiveConfigReporter; + this.effectiveConfigReporter = Objects.requireNonNull(effectiveConfigReporter); } public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) { @@ -67,21 +71,20 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) try { DeclarativeConfigProperties remoteConfigProperties = toDeclarativeConfigProperties(configFile); - DeclarativeConfigProperties splunkDistributionConfigProperties = + DeclarativeConfigProperties distributionRemoteConfigProperties = remoteConfigProperties .getStructured("distribution", empty()) .getStructured("splunk", empty()); // Update profiler configuration only when profiling node exists - if (splunkDistributionConfigProperties.getPropertyKeys().contains(PROFILING_NODE_NAME)) { - DeclarativeConfigProperties profilingConfigProperties = - splunkDistributionConfigProperties.getStructured(PROFILING_NODE_NAME, empty()); - ProfilerDeclarativeConfiguration profilingRemoteConfig = - new ProfilerDeclarativeConfiguration(profilingConfigProperties); - // TODO: should be merged with current profiling config. Probably we will need profiler - // configuration refactoring and some listeners implemented for profiler configuration - // changes. For POC use this temporary solution - if (profilingRemoteConfig.isEnabled()) { + if (distributionRemoteConfigProperties.getPropertyKeys().contains(PROFILING_NODE_NAME)) { + ProfilerDeclarativeConfiguration receivedProfilerConfig = + new ProfilerDeclarativeConfiguration( + distributionRemoteConfigProperties.getStructured(PROFILING_NODE_NAME, empty())); + + profilerRemoteConfiguration.setEnabled(receivedProfilerConfig.isEnabled()); + + if (profilerRemoteConfiguration.isEnabled()) { profilingSupervisor.requestStartProfiling(); } else { profilingSupervisor.requestStopProfiling(); @@ -92,7 +95,7 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) reportRemoteConfigStatus( remoteConfig.config_hash, RemoteConfigStatuses.RemoteConfigStatuses_APPLIED, - null, + "", opampClient); } catch (Exception e) { diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java index 30d938135..dd3899d71 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java @@ -26,8 +26,12 @@ public class ServerToAgentMessageHandler { private final RemoteConfigProcessor remoteConfigProcessor; public ServerToAgentMessageHandler( - ProfilingSupervisor profilingSupervisor, EffectiveConfigReporter effectiveConfigReporter) { - this(new RemoteConfigProcessor(profilingSupervisor, effectiveConfigReporter)); + ProfilerRemoteConfiguration profilerRemoteConfiguration, + ProfilingSupervisor profilingSupervisor, + EffectiveConfigReporter effectiveConfigReporter) { + this( + new RemoteConfigProcessor( + profilerRemoteConfiguration, profilingSupervisor, effectiveConfigReporter)); } @VisibleForTesting diff --git a/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactory.java b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactory.java index 678a07b2b..687fda85a 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactory.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactory.java @@ -17,7 +17,6 @@ package com.splunk.opentelemetry.opamp.effectiveconfig; import com.splunk.opentelemetry.profiler.ProfilerConfiguration; -import com.splunk.opentelemetry.profiler.ProfilerEnvVarsConfiguration; import com.splunk.opentelemetry.profiler.snapshot.SnapshotProfilingConfiguration; import com.splunk.opentelemetry.profiler.snapshot.SnapshotProfilingEnvVarsConfiguration; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -49,7 +48,7 @@ public String createEffectiveConfigContent() { } private void addSplunkEnvVars(EffectiveConfigBuilder builder) { - ProfilerConfiguration profilerConfiguration = new ProfilerEnvVarsConfiguration(config); + ProfilerConfiguration profilerConfiguration = ProfilerConfiguration.SUPPLIER.get(); SnapshotProfilingConfiguration snapshotConfiguration = new SnapshotProfilingEnvVarsConfiguration(config); diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java index aefa5542d..951c1a7bc 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java @@ -17,11 +17,13 @@ package com.splunk.opentelemetry.opamp; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import com.splunk.opentelemetry.opamp.effectiveconfig.EffectiveConfigReporter; +import com.splunk.opentelemetry.profiler.ProfilerConfiguration; +import com.splunk.opentelemetry.profiler.ProfilingSupervisor; import io.opentelemetry.opamp.client.OpampClient; import java.util.Map; import okio.ByteString; @@ -39,17 +41,23 @@ @ExtendWith(MockitoExtension.class) class RemoteConfigProcessorTest { + @Mock ProfilerConfiguration profilerConfiguration; + @Mock ProfilingSupervisor profilingSupervisor; @Mock EffectiveConfigReporter effectiveConfigReporter; @Mock OpampClient opampClient; + private ProfilerRemoteConfiguration profilerRemoteConfiguration; private RemoteConfigProcessor handler; @BeforeEach void setUp() { - handler = new RemoteConfigProcessor(mock(), effectiveConfigReporter); + profilerRemoteConfiguration = new ProfilerRemoteConfiguration(profilerConfiguration); + handler = + new RemoteConfigProcessor( + profilerRemoteConfiguration, profilingSupervisor, effectiveConfigReporter); } @Test - void shouldMarkRemoteConfigAsApplied() { + void shouldMarkRemoteConfigAsAppliedWhenProfilingConfigIsNotProvided() { // given String remoteConfigYaml = "test-config:"; ByteString configHash = ByteString.encodeUtf8("test-config-hash"); @@ -66,12 +74,66 @@ void shouldMarkRemoteConfigAsApplied() { handler.applyConfig(remoteConfig, opampClient); // then - ArgumentCaptor statusCaptor = - ArgumentCaptor.forClass(RemoteConfigStatus.class); - verify(opampClient).setRemoteConfigStatus(statusCaptor.capture()); - RemoteConfigStatus status = statusCaptor.getValue(); + RemoteConfigStatus status = getReportedRemoteConfigStatus(); + assertThat(status.last_remote_config_hash).isEqualTo(configHash); + assertThat(status.status).isEqualTo(RemoteConfigStatuses.RemoteConfigStatuses_APPLIED); + assertThat(status.error_message).isEmpty(); + assertThat(profilerRemoteConfiguration.isEnabled()).isFalse(); + verify(effectiveConfigReporter).reportEffectiveConfigIfChanged(); + verifyNoInteractions(profilingSupervisor); + } + + @Test + void shouldStartProfilingWhenRemoteConfigEnablesProfiler() { + // given + String remoteConfigYaml = + """ + distribution: + splunk: + profiling: + always_on: + cpu_profiler: + """; + ByteString configHash = ByteString.encodeUtf8("test-config-hash"); + AgentRemoteConfig remoteConfig = createRemoteConfig(configHash, remoteConfigYaml); + + // when + handler.applyConfig(remoteConfig, opampClient); + + // then + RemoteConfigStatus status = getReportedRemoteConfigStatus(); + assertThat(status.last_remote_config_hash).isEqualTo(configHash); + assertThat(status.status).isEqualTo(RemoteConfigStatuses.RemoteConfigStatuses_APPLIED); + assertThat(status.error_message).isEmpty(); + assertThat(profilerRemoteConfiguration.isEnabled()).isTrue(); + verify(profilingSupervisor).requestStartProfiling(); + verify(profilingSupervisor, never()).requestStopProfiling(); + verify(effectiveConfigReporter).reportEffectiveConfigIfChanged(); + } + + @Test + void shouldStopProfilingWhenRemoteConfigDisablesProfiler() { + // given + String remoteConfigYaml = + """ + distribution: + splunk: + profiling: + """; + ByteString configHash = ByteString.encodeUtf8("test-config-hash"); + AgentRemoteConfig remoteConfig = createRemoteConfig(configHash, remoteConfigYaml); + + // when + handler.applyConfig(remoteConfig, opampClient); + + // then + RemoteConfigStatus status = getReportedRemoteConfigStatus(); assertThat(status.last_remote_config_hash).isEqualTo(configHash); assertThat(status.status).isEqualTo(RemoteConfigStatuses.RemoteConfigStatuses_APPLIED); + assertThat(status.error_message).isEmpty(); + assertThat(profilerRemoteConfiguration.isEnabled()).isFalse(); + verify(profilingSupervisor).requestStopProfiling(); + verify(profilingSupervisor, never()).requestStartProfiling(); verify(effectiveConfigReporter).reportEffectiveConfigIfChanged(); } @@ -89,7 +151,24 @@ void shouldIgnoreRemoteConfigWithoutExpectedConfigFile() { handler.applyConfig(remoteConfig, opampClient); // then - verify(opampClient, never()).setRemoteConfigStatus(org.mockito.ArgumentMatchers.any()); + assertThat(profilerRemoteConfiguration.isEnabled()).isFalse(); + verifyNoInteractions(opampClient, profilingSupervisor); + verify(effectiveConfigReporter, never()).reportEffectiveConfigIfChanged(); + } + + private RemoteConfigStatus getReportedRemoteConfigStatus() { + ArgumentCaptor statusCaptor = + ArgumentCaptor.forClass(RemoteConfigStatus.class); + verify(opampClient).setRemoteConfigStatus(statusCaptor.capture()); + return statusCaptor.getValue(); + } + + private static AgentRemoteConfig createRemoteConfig(ByteString configHash, String config) { + return createRemoteConfig( + configHash, + Map.of( + "splunk.remote.config", + new AgentConfigFile.Builder().body(ByteString.encodeUtf8(config)).build())); } private static AgentRemoteConfig createRemoteConfig( diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactoryTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactoryTest.java index ecf927e2c..0940e0a3b 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactoryTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/effectiveconfig/EnvVarsEffectiveConfigFileFactoryTest.java @@ -18,15 +18,23 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.splunk.opentelemetry.profiler.ProfilerConfiguration; +import com.splunk.opentelemetry.profiler.ProfilerEnvVarsConfiguration; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import java.io.IOException; import java.io.StringReader; import java.util.Map; import java.util.Properties; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; class EnvVarsEffectiveConfigFileFactoryTest { + @AfterEach + void tearDown() { + ProfilerConfiguration.SUPPLIER.reset(); + } + @Test void createFile_reportsCorrectContentType() { DefaultConfigProperties config = DefaultConfigProperties.createFromMap(Map.of()); @@ -154,6 +162,7 @@ void buildFileContent_usesSignalSpecificProtocolWhenResolvingEndpoints() throws private static Properties createFileContent(Map configMap) throws IOException { DefaultConfigProperties config = DefaultConfigProperties.createFromMap(configMap); + ProfilerConfiguration.SUPPLIER.configure(new ProfilerEnvVarsConfiguration(config)); String fileContent = new EnvVarsEffectiveConfigFileFactory(config).createEffectiveConfigContent(); diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java index fc978ef53..11ad6eb04 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -19,9 +19,7 @@ import com.google.auto.service.AutoService; import com.google.common.annotations.VisibleForTesting; import io.opentelemetry.javaagent.extension.AgentListener; -import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.logging.Logger; @AutoService(AgentListener.class) @@ -42,9 +40,9 @@ public JfrAgentListener() { public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { ProfilingSupervisor.setupJfrContextStorage(); - ProfilerConfiguration config = getProfilerConfiguration(sdk); + ProfilerConfiguration config = ProfilerConfiguration.SUPPLIER.get(); // Always start the supervisor, so it can start profiling later elsewhere. - ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk, config); + ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk); if (notClearForTakeoff(config)) { return; @@ -60,19 +58,8 @@ public int order() { } // Exists for testing - ProfilingSupervisor makeProfilingSupervisor( - AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { - return ProfilingSupervisor.createAndStart(sdk, config); - } - - private static ProfilerConfiguration getProfilerConfiguration( - AutoConfiguredOpenTelemetrySdk sdk) { - if (ProfilerDeclarativeConfiguration.SUPPLIER.isConfigured()) { - return ProfilerDeclarativeConfiguration.SUPPLIER.get(); - } else { - ConfigProperties configProperties = AutoConfigureUtil.getConfig(sdk); - return new ProfilerEnvVarsConfiguration(configProperties); - } + ProfilingSupervisor makeProfilingSupervisor(AutoConfiguredOpenTelemetrySdk sdk) { + return ProfilingSupervisor.createAndStart(sdk, ProfilerConfiguration.SUPPLIER); } private boolean notClearForTakeoff(ProfilerConfiguration config) { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/LogExporterBuilder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/LogExporterBuilder.java index ea7c781c6..ec4e5f364 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/LogExporterBuilder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/LogExporterBuilder.java @@ -44,7 +44,8 @@ class LogExporterBuilder { static final String EXTRA_CONTENT_TYPE = "Extra-Content-Type"; static final String STACKTRACES_HEADER_VALUE = "otel-profiling-stacktraces"; - static LogRecordExporter fromConfig(DeclarativeConfigProperties exporterConfigProperties) { + static LogRecordExporter fromDeclarativeConfig( + DeclarativeConfigProperties exporterConfigProperties) { if (exporterConfigProperties != null) { Set propertyKeys = exporterConfigProperties.getPropertyKeys(); @@ -68,11 +69,8 @@ static LogRecordExporter fromConfig(DeclarativeConfigProperties exporterConfigPr throw new ConfigurationException("Profiler exporter configuration is invalid"); } - static LogRecordExporter fromConfig(ConfigProperties config) { - return fromConfig(new ProfilerEnvVarsConfiguration(config)); - } - - static LogRecordExporter fromConfig(ProfilerEnvVarsConfiguration config) { + static LogRecordExporter fromEnvironmentConfig() { + ProfilerConfiguration config = ProfilerConfiguration.SUPPLIER.get(); String protocol = config.getOtlpProtocol(); if ("http/protobuf".equals(protocol)) { return buildHttpExporter(config, OtlpHttpLogRecordExporter::builder); @@ -84,7 +82,7 @@ static LogRecordExporter fromConfig(ProfilerEnvVarsConfiguration config) { @VisibleForTesting static LogRecordExporter buildGrpcExporter( - ProfilerEnvVarsConfiguration config, Supplier makeBuilder) { + ProfilerConfiguration config, Supplier makeBuilder) { OtlpGrpcLogRecordExporterBuilder builder = makeBuilder.get(); String ingestUrl = config.getIngestUrl(); builder.setEndpoint(ingestUrl); @@ -94,13 +92,13 @@ static LogRecordExporter buildGrpcExporter( @VisibleForTesting static LogRecordExporter buildHttpExporter( - ProfilerEnvVarsConfiguration config, Supplier makeBuilder) { + ProfilerConfiguration config, Supplier makeBuilder) { OtlpHttpLogRecordExporterBuilder builder = makeBuilder.get(); String ingestUrl = config.getIngestUrl(); OtlpConfigUtil.configureOtlpExporterBuilder( DATA_TYPE_LOGS, - config.getConfigProperties(), + (ConfigProperties) config.getConfigProperties(), builder::setComponentLoader, builder::setEndpoint, builder::addHeader, diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/OtelLoggerFactory.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/OtelLoggerFactory.java index f164fc902..2bddc274a 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/OtelLoggerFactory.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/OtelLoggerFactory.java @@ -26,26 +26,27 @@ import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.resources.Resource; import java.util.function.Function; +import java.util.function.Supplier; public class OtelLoggerFactory { - private final Function logRecordExporter; + private final Supplier logRecordExporter; private final Function declarativeLogRecordExporter; public OtelLoggerFactory() { - this(LogExporterBuilder::fromConfig, LogExporterBuilder::fromConfig); + this(LogExporterBuilder::fromEnvironmentConfig, LogExporterBuilder::fromDeclarativeConfig); } @VisibleForTesting public OtelLoggerFactory( - Function logRecordExporter, + Supplier logRecordExporter, Function declarativeLogRecordExporter) { this.logRecordExporter = logRecordExporter; this.declarativeLogRecordExporter = declarativeLogRecordExporter; } public Logger build(ConfigProperties properties, Resource resource) { - LogRecordExporter exporter = createLogRecordExporter(properties); + LogRecordExporter exporter = createLogRecordExporter(); LogRecordProcessor processor = SimpleLogRecordProcessor.create(exporter); return buildOtelLogger(processor, resource); } @@ -56,8 +57,8 @@ public Logger build(DeclarativeConfigProperties properties, Resource resource) { return buildOtelLogger(processor, resource); } - private LogRecordExporter createLogRecordExporter(ConfigProperties properties) { - return logRecordExporter.apply(properties); + private LogRecordExporter createLogRecordExporter() { + return logRecordExporter.get(); } private LogRecordExporter createLogRecordExporter(DeclarativeConfigProperties properties) { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilder.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilder.java index 00efd8a62..54297452a 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilder.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilder.java @@ -25,7 +25,6 @@ import com.splunk.opentelemetry.profiler.exporter.CpuEventExporter; import com.splunk.opentelemetry.profiler.exporter.PprofCpuEventExporter; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.logs.LogRecordProcessor; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.export.LogRecordExporter; @@ -162,13 +161,9 @@ private static LogRecordExporter createLogRecordExporter(Object configProperties if (configProperties instanceof DeclarativeConfigProperties) { DeclarativeConfigProperties exporterConfig = ((DeclarativeConfigProperties) configProperties).getStructured("exporter", empty()); - return LogExporterBuilder.fromConfig(exporterConfig); + return LogExporterBuilder.fromDeclarativeConfig(exporterConfig); } - if (configProperties instanceof ConfigProperties) { - return LogExporterBuilder.fromConfig((ConfigProperties) configProperties); - } - throw new IllegalArgumentException( - "Unsupported config properties type: " + configProperties.getClass().getName()); + return LogExporterBuilder.fromEnvironmentConfig(); } private boolean checkOutputDir(Path outputDir) { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerConfiguration.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerConfiguration.java index 6e2aaf69f..3f7c600ce 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerConfiguration.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerConfiguration.java @@ -16,9 +16,13 @@ package com.splunk.opentelemetry.profiler; +import com.splunk.opentelemetry.profiler.util.OptionalConfigurableSupplier; import java.time.Duration; public interface ProfilerConfiguration { + OptionalConfigurableSupplier SUPPLIER = + new OptionalConfigurableSupplier<>(); + boolean HAS_OBJECT_ALLOCATION_SAMPLE_EVENT = getJavaVersion() >= 16; boolean isEnabled(); diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerDeclarativeConfigurationInitializer.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerConfigurationInitializer.java similarity index 61% rename from profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerDeclarativeConfigurationInitializer.java rename to profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerConfigurationInitializer.java index 72fba6659..e0041a0d8 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerDeclarativeConfigurationInitializer.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerConfigurationInitializer.java @@ -23,16 +23,25 @@ import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import java.util.Collections; /** * Purpose of this class is to configure the supplier of ProfilerDeclarativeConfiguration. * ProfilerDeclarativeConfiguration class object can then be used in code executed after SDK is * created, such as AgentListeners. */ -@AutoService(DeclarativeConfigurationCustomizerProvider.class) -public class ProfilerDeclarativeConfigurationInitializer - implements DeclarativeConfigurationCustomizerProvider { +@AutoService({ + DeclarativeConfigurationCustomizerProvider.class, + AutoConfigurationCustomizerProvider.class +}) +public class ProfilerConfigurationInitializer + implements DeclarativeConfigurationCustomizerProvider, AutoConfigurationCustomizerProvider { + + @Override public void customize(DeclarativeConfigurationCustomizer configurationCustomizer) { + // Initialize profiler configuration from declarative config configurationCustomizer.addModelCustomizer( (model) -> { DeclarativeConfigProperties distributionConfig = @@ -40,10 +49,27 @@ public void customize(DeclarativeConfigurationCustomizer configurationCustomizer DeclarativeConfigProperties profilingConfig = distributionConfig.getStructured("profiling", empty()); - ProfilerDeclarativeConfiguration.SUPPLIER.configure( - new ProfilerDeclarativeConfiguration(profilingConfig)); + ProfilerDeclarativeConfiguration profilerConfiguration = + new ProfilerDeclarativeConfiguration(profilingConfig); + ProfilerConfiguration.SUPPLIER.configure(profilerConfiguration); return model; }); } + + @Override + public void customize(AutoConfigurationCustomizer autoConfiguration) { + // Initialize profiler configuration from environment config + autoConfiguration.addPropertiesCustomizer( + configProperties -> { + ProfilerConfiguration.SUPPLIER.configure( + new ProfilerEnvVarsConfiguration(configProperties)); + return Collections.emptyMap(); + }); + } + + @Override + public int order() { + return Integer.MAX_VALUE; + } } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerDeclarativeConfiguration.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerDeclarativeConfiguration.java index 1da14893f..029f8d61c 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerDeclarativeConfiguration.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilerDeclarativeConfiguration.java @@ -18,16 +18,12 @@ import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; -import com.splunk.opentelemetry.profiler.util.OptionalConfigurableSupplier; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import java.time.Duration; import java.util.logging.Logger; import javax.annotation.Nullable; public class ProfilerDeclarativeConfiguration implements ProfilerConfiguration { - public static final OptionalConfigurableSupplier SUPPLIER = - new OptionalConfigurableSupplier<>(); - private static final Logger logger = Logger.getLogger(ProfilerDeclarativeConfiguration.class.getName()); diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 2d6e73e75..037cab914 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -41,7 +41,7 @@ public class ProfilingSupervisor { private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(ProfilingSupervisor.class.getName()); - private final ProfilerConfiguration config; + private final OptionalConfigurableSupplier configSupplier; private final JFR jfr; private final AutoConfiguredOpenTelemetrySdk sdk; private final BlockingQueue commandQueue; @@ -53,24 +53,26 @@ public class ProfilingSupervisor { @VisibleForTesting ProfilingSupervisor( - ProfilerConfiguration config, + OptionalConfigurableSupplier configSupplier, JFR jfr, AutoConfiguredOpenTelemetrySdk sdk, BlockingQueue commandQueue) { - this.config = config; + this.configSupplier = configSupplier; this.jfr = jfr; this.sdk = sdk; this.commandQueue = commandQueue; } static ProfilingSupervisor createAndStart( - AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + AutoConfiguredOpenTelemetrySdk sdk, + OptionalConfigurableSupplier configSupplier) { if (SUPPLIER.isConfigured()) { throw new IllegalStateException("Already started"); } ExecutorService executor = HelpfulExecutors.newSingleThreadExecutor("JFR Profiler"); BlockingQueue queue = new LinkedBlockingQueue<>(); - ProfilingSupervisor supervisor = new ProfilingSupervisor(config, JFR.getInstance(), sdk, queue); + ProfilingSupervisor supervisor = + new ProfilingSupervisor(configSupplier, JFR.getInstance(), sdk, queue); SUPPLIER.configure(supervisor); supervisor.start(executor); @@ -137,7 +139,7 @@ private void tryStart() { "JDK Flight Recorder (JFR) is not available in this JVM. Profiling will not start."); return; } - config.log(); + configSupplier.get().log(); setJfrContextStorageEnabled(true); activateJfrRecording(getResource(sdk)); logger.info("Profiler is active."); @@ -174,7 +176,7 @@ private void deactivateJfrRecording() { // Exists for testing PeriodicRecordingFlusherBuilder makeRecordingFlusherBuilder(Resource resource) { - return PeriodicRecordingFlusher.builder(config, resource); + return PeriodicRecordingFlusher.builder(configSupplier.get(), resource); } static void setupJfrContextStorage() { diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java index 9f87f91d0..3d7d28fe2 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/JfrAgentListenerTest.java @@ -43,7 +43,7 @@ class JfrAgentListenerTest { @AfterEach void resetDeclarativeConfigSuppliers() { - ProfilerDeclarativeConfiguration.SUPPLIER.reset(); + ProfilerConfiguration.SUPPLIER.reset(); SnapshotProfilingDeclarativeConfiguration.SUPPLIER.reset(); } @@ -68,8 +68,7 @@ void shouldActivateJfrRecording(@TempDir Path tempDir) throws IOException { JfrAgentListener listener = new JfrAgentListener(jfr) { @Override - ProfilingSupervisor makeProfilingSupervisor( - AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + ProfilingSupervisor makeProfilingSupervisor(AutoConfiguredOpenTelemetrySdk sdk) { return supervisor; } }; @@ -102,8 +101,7 @@ void shouldNotActivateJfrRecording_JfrNotAvailable(@TempDir Path tempDir) throws JfrAgentListener listener = new JfrAgentListener(jfr) { @Override - ProfilingSupervisor makeProfilingSupervisor( - AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + ProfilingSupervisor makeProfilingSupervisor(AutoConfiguredOpenTelemetrySdk sdk) { return supervisor; } }; @@ -130,8 +128,7 @@ void shouldNotActivateJfrRecording_profilerDisabled(String yaml, @TempDir Path t JfrAgentListener listener = new JfrAgentListener(jfr) { @Override - ProfilingSupervisor makeProfilingSupervisor( - AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) { + ProfilingSupervisor makeProfilingSupervisor(AutoConfiguredOpenTelemetrySdk sdk) { return supervisor; } }; diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/LogExporterBuilderTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/LogExporterBuilderTest.java index 973be8856..3b0f5ca0b 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/LogExporterBuilderTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/LogExporterBuilderTest.java @@ -173,7 +173,7 @@ void shouldCreateHttpExporter() { DeclarativeConfigProperties exporterConfig = getExporterConfig(model); // when - LogRecordExporter exporter = LogExporterBuilder.fromConfig(exporterConfig); + LogRecordExporter exporter = LogExporterBuilder.fromDeclarativeConfig(exporterConfig); // then assertThat(exporter).isNotNull(); @@ -197,7 +197,7 @@ void shouldCreateGrpcExporter() { DeclarativeConfigProperties exporterConfig = getExporterConfig(model); // when - LogRecordExporter exporter = LogExporterBuilder.fromConfig(exporterConfig); + LogRecordExporter exporter = LogExporterBuilder.fromDeclarativeConfig(exporterConfig); // then assertThat(exporter).isNotNull(); @@ -220,7 +220,7 @@ void shouldCreateHttpExporter_defaultEndpoint() { DeclarativeConfigProperties exporterConfig = getExporterConfig(model); // when - LogRecordExporter exporter = LogExporterBuilder.fromConfig(exporterConfig); + LogRecordExporter exporter = LogExporterBuilder.fromDeclarativeConfig(exporterConfig); // then assertThat(exporter).isNotNull(); @@ -243,7 +243,7 @@ void shouldCreateGrpcExporter_defaultEndpoint() { DeclarativeConfigProperties exporterConfig = getExporterConfig(model); // when - LogRecordExporter exporter = LogExporterBuilder.fromConfig(exporterConfig); + LogRecordExporter exporter = LogExporterBuilder.fromDeclarativeConfig(exporterConfig); // then assertThat(exporter).isNotNull(); @@ -266,7 +266,7 @@ void shouldThrowExceptionForInvalidProtocol() { DeclarativeConfigProperties exporterConfig = getExporterConfig(model); // when, then - assertThatThrownBy(() -> LogExporterBuilder.fromConfig(exporterConfig)) + assertThatThrownBy(() -> LogExporterBuilder.fromDeclarativeConfig(exporterConfig)) .isInstanceOf(ConfigurationException.class); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/OtelLoggerFactoryTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/OtelLoggerFactoryTest.java index 67ceaa988..a75309a9c 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/OtelLoggerFactoryTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/OtelLoggerFactoryTest.java @@ -33,7 +33,7 @@ class OtelLoggerFactoryTest { private final InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create(); private final OtelLoggerFactory factory = - new OtelLoggerFactory(properties -> exporter, declarativeConfigProperties -> exporter); + new OtelLoggerFactory(() -> exporter, declarativeConfigProperties -> exporter); @Test void configureLoggerWithProfilingInstrumentationScopeName() { diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java index d309ac1da..b491b4c42 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/PeriodicRecordingFlusherBuilderTest.java @@ -17,7 +17,6 @@ package com.splunk.opentelemetry.profiler; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.verify; @@ -27,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.mockito.MockedConstruction; @@ -35,12 +35,18 @@ class PeriodicRecordingFlusherBuilderTest { @TempDir Path tempDir; + @AfterEach + void tearDown() { + ProfilerConfiguration.SUPPLIER.reset(); + } + @Test void buildConfiguresJfrAndWiresRecorderIntoSequencer() { JFR jfr = mock(JFR.class); TestProfilingConfig config = config(tempDir); config.stackDepth = 73; config.recordingDuration = Duration.ofMillis(100); + ProfilerConfiguration.SUPPLIER.configure(config); try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { @@ -66,6 +72,7 @@ void buildCreatesMissingOutputDirectoryWhenKeepingFiles() { JFR jfr = mock(JFR.class); TestProfilingConfig config = config(outputDir); config.keepFiles = true; + ProfilerConfiguration.SUPPLIER.configure(config); try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { @@ -85,6 +92,7 @@ void buildContinuesWhenKeepFilesPathIsNotADirectory() throws Exception { JFR jfr = mock(JFR.class); TestProfilingConfig config = config(outputFile); config.keepFiles = true; + ProfilerConfiguration.SUPPLIER.configure(config); try (MockedConstruction recorderConstruction = mockConstruction(JfrRecorder.class)) { @@ -96,18 +104,6 @@ void buildContinuesWhenKeepFilesPathIsNotADirectory() throws Exception { } } - @Test - void buildRejectsUnsupportedConfigProperties() { - JFR jfr = mock(JFR.class); - TestProfilingConfig config = config(tempDir); - config.configProperties = new Object(); - - assertThatThrownBy( - () -> PeriodicRecordingFlusher.builder(config, Resource.empty()).jfr(jfr).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageStartingWith("Unsupported config properties type:"); - } - private TestProfilingConfig config(Path outputDir) { TestProfilingConfig config = new TestProfilingConfig(); config.profilerDirectory = outputDir.toString(); diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java index 3d2e133e5..8a338180c 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ProfilingSupervisorTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.splunk.opentelemetry.profiler.util.OptionalConfigurableSupplier; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import java.nio.file.Path; @@ -156,7 +157,7 @@ private static class TestProfilingSupervisor extends ProfilingSupervisor { JFR jfr, AutoConfiguredOpenTelemetrySdk sdk, PeriodicRecordingFlusherBuilder builder) { - super(config, jfr, sdk, new LinkedBlockingQueue<>()); + super(configSupplier(config), jfr, sdk, new LinkedBlockingQueue<>()); this.builder = builder; } @@ -164,6 +165,14 @@ private static class TestProfilingSupervisor extends ProfilingSupervisor { PeriodicRecordingFlusherBuilder makeRecordingFlusherBuilder(Resource resource) { return builder; } + + private static OptionalConfigurableSupplier configSupplier( + ProfilerConfiguration config) { + OptionalConfigurableSupplier supplier = + new OptionalConfigurableSupplier<>(); + supplier.configure(config); + return supplier; + } } private static class TestPeriodicRecordingFlusherBuilder extends PeriodicRecordingFlusherBuilder { diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/ConcurrentServiceEntrySamplingTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/ConcurrentServiceEntrySamplingTest.java index 327e9bc68..70f6a8934 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/ConcurrentServiceEntrySamplingTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/ConcurrentServiceEntrySamplingTest.java @@ -66,7 +66,7 @@ private StackTraceSampler newSampler(StagingArea staging) { .with( new StackTraceExporterActivator( new OtelLoggerFactory( - properties -> logExporter, declarativeConfigProperties -> logExporter))) + () -> logExporter, declarativeConfigProperties -> logExporter))) .build(); @RegisterExtension diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/GracefulShutdownTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/GracefulShutdownTest.java index c54ad34dc..5b80afa7a 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/GracefulShutdownTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/GracefulShutdownTest.java @@ -43,7 +43,7 @@ class GracefulShutdownTest { .with( new StackTraceExporterActivator( new OtelLoggerFactory( - properties -> logExporter, declarativeConfigProperties -> logExporter))) + () -> logExporter, declarativeConfigProperties -> logExporter))) .build(); @Test diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingLogExportingTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingLogExportingTest.java index ee1b18bab..db71b2917 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingLogExportingTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingLogExportingTest.java @@ -63,7 +63,7 @@ class SnapshotProfilingLogExportingTest { .with( new StackTraceExporterActivator( new OtelLoggerFactory( - properties -> logExporter, declarativeConfigProperties -> logExporter))) + () -> logExporter, declarativeConfigProperties -> logExporter))) .build(); @AfterEach From 3c46ecdd3f314a0fcb1785f11fd5f61b7939f84d Mon Sep 17 00:00:00 2001 From: robsunday Date: Tue, 23 Jun 2026 18:56:45 +0200 Subject: [PATCH 24/25] Use configuration supplier in few additional places --- .../com/splunk/opentelemetry/profiler/JfrAgentListener.java | 4 ++-- .../splunk/opentelemetry/profiler/ProfilingSupervisor.java | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java index 11ad6eb04..bbf807dff 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -40,10 +40,10 @@ public JfrAgentListener() { public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { ProfilingSupervisor.setupJfrContextStorage(); - ProfilerConfiguration config = ProfilerConfiguration.SUPPLIER.get(); // Always start the supervisor, so it can start profiling later elsewhere. ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk); + ProfilerConfiguration config = ProfilerConfiguration.SUPPLIER.get(); if (notClearForTakeoff(config)) { return; } @@ -59,7 +59,7 @@ public int order() { // Exists for testing ProfilingSupervisor makeProfilingSupervisor(AutoConfiguredOpenTelemetrySdk sdk) { - return ProfilingSupervisor.createAndStart(sdk, ProfilerConfiguration.SUPPLIER); + return ProfilingSupervisor.createAndStart(sdk); } private boolean notClearForTakeoff(ProfilerConfiguration config) { diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java index 037cab914..2b9269ffd 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/ProfilingSupervisor.java @@ -63,16 +63,14 @@ public class ProfilingSupervisor { this.commandQueue = commandQueue; } - static ProfilingSupervisor createAndStart( - AutoConfiguredOpenTelemetrySdk sdk, - OptionalConfigurableSupplier configSupplier) { + static ProfilingSupervisor createAndStart(AutoConfiguredOpenTelemetrySdk sdk) { if (SUPPLIER.isConfigured()) { throw new IllegalStateException("Already started"); } ExecutorService executor = HelpfulExecutors.newSingleThreadExecutor("JFR Profiler"); BlockingQueue queue = new LinkedBlockingQueue<>(); ProfilingSupervisor supervisor = - new ProfilingSupervisor(configSupplier, JFR.getInstance(), sdk, queue); + new ProfilingSupervisor(ProfilerConfiguration.SUPPLIER, JFR.getInstance(), sdk, queue); SUPPLIER.configure(supervisor); supervisor.start(executor); From dfda42b7f1aa77253e23064369a1113eff8e2601 Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 24 Jun 2026 10:30:18 +0200 Subject: [PATCH 25/25] cleanup --- .../profiler/JfrAgentListener.java | 4 +- .../JfrContextStorageInitializer.java | 53 ------------------- 2 files changed, 3 insertions(+), 54 deletions(-) delete mode 100644 profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java index bbf807dff..cf8ea81e7 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrAgentListener.java @@ -38,7 +38,9 @@ public JfrAgentListener() { @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { - ProfilingSupervisor.setupJfrContextStorage(); + if (jfr.isAvailable()) { + ProfilingSupervisor.setupJfrContextStorage(); + } // Always start the supervisor, so it can start profiling later elsewhere. ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk); diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java deleted file mode 100644 index 9904e0afc..000000000 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorageInitializer.java +++ /dev/null @@ -1,53 +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 com.google.auto.service.AutoService; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizer; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider; -import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; -import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; -import org.checkerframework.checker.nullness.qual.NonNull; - -@AutoService({ - AutoConfigurationCustomizerProvider.class, - DeclarativeConfigurationCustomizerProvider.class -}) -public final class JfrContextStorageInitializer - implements AutoConfigurationCustomizerProvider, DeclarativeConfigurationCustomizerProvider { - - @Override - public void customize(@NonNull AutoConfigurationCustomizer autoConfiguration) { - setupJfrContextStorage(); - } - - @Override - public void customize(DeclarativeConfigurationCustomizer configurationCustomizer) { - setupJfrContextStorage(); - } - - @Override - public int order() { - return Integer.MIN_VALUE; - } - - private void setupJfrContextStorage() { - if (JFR.getInstance().isAvailable()) { - ProfilingSupervisor.setupJfrContextStorage(); - } - } -}