Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions profiler/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
compileOnly("com.google.auto.service:auto-service")

testImplementation(project(":custom"))
testImplementation(project(":testing:common"))
testImplementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common")
testImplementation("io.grpc:grpc-netty")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@
class JFR {
private static final Logger logger = Logger.getLogger(JFR.class.getName());

public static final JFR instance = new JFR();
private static final JFR instance = new JFR();
private static final boolean jfrAvailable = checkJfr();

public static JFR getInstance() {
Copy link
Copy Markdown
Contributor Author

@robsunday robsunday Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[for reviewer] Refactored to make it more OO and mockable.

return instance;
}

private static boolean checkJfr() {
try {
JFR.class.getClassLoader().loadClass("jdk.jfr.FlightRecorder");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static java.util.logging.Level.WARNING;

import com.google.auto.service.AutoService;
import com.google.common.annotations.VisibleForTesting;
import com.splunk.opentelemetry.SplunkConfiguration;
import com.splunk.opentelemetry.profiler.allocation.exporter.AllocationEventExporter;
import com.splunk.opentelemetry.profiler.allocation.exporter.PprofAllocationEventExporter;
Expand All @@ -30,6 +31,7 @@
import com.splunk.opentelemetry.profiler.exporter.PprofCpuEventExporter;
import com.splunk.opentelemetry.profiler.util.HelpfulExecutors;
import io.opentelemetry.api.logs.Logger;
import io.opentelemetry.context.ContextStorage;
import io.opentelemetry.javaagent.extension.AgentListener;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
Expand All @@ -51,7 +53,16 @@ public class JfrActivator implements AgentListener {

private static final java.util.logging.Logger logger =
java.util.logging.Logger.getLogger(JfrActivator.class.getName());
private final ExecutorService executor = HelpfulExecutors.newSingleThreadExecutor("JFR Profiler");
private final ExecutorService executor;

public JfrActivator() {
this(HelpfulExecutors.newSingleThreadExecutor("JFR Profiler"));
}

@VisibleForTesting
JfrActivator(ExecutorService executor) {
this.executor = executor;
}

@Override
public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
Expand All @@ -62,6 +73,8 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr

Configuration.log(config);
logger.info("Profiler is active.");
setupContextStorage();

executor.submit(
logUncaught(
() -> activateJfrAndRunForever(config, getResource(autoConfiguredOpenTelemetrySdk))));
Expand All @@ -72,7 +85,7 @@ private boolean notClearForTakeoff(ConfigProperties config) {
logger.fine("Profiler is not enabled.");
return true;
}
if (!JFR.instance.isAvailable()) {
if (!JFR.getInstance().isAvailable()) {
logger.warning(
"JDK Flight Recorder (JFR) is not available in this JVM. Profiling is disabled.");
return true;
Expand Down Expand Up @@ -117,7 +130,7 @@ private void activateJfrAndRunForever(ConfigProperties config, Resource resource
RecordingFileNamingConvention namingConvention = new RecordingFileNamingConvention(outputDir);

int stackDepth = Configuration.getStackDepth(config);
JFR.instance.setStackDepth(stackDepth);
JFR.getInstance().setStackDepth(stackDepth);

// can't be null, default value is set in Configuration.getProperties
Duration recordingDuration = Configuration.getRecordingDuration(config);
Expand Down Expand Up @@ -165,7 +178,7 @@ private void activateJfrAndRunForever(ConfigProperties config, Resource resource
JfrRecorder.builder()
.settings(jfrSettings)
.maxAgeDuration(recordingDuration.multipliedBy(10))
.jfr(JFR.instance)
.jfr(JFR.getInstance())
.onNewRecording(jfrRecordingHandler)
.namingConvention(namingConvention)
.keepRecordingFiles(keepFiles)
Expand Down Expand Up @@ -218,4 +231,8 @@ private Map<String, String> buildJfrSettings(ConfigProperties config) {
JfrSettingsOverrides overrides = new JfrSettingsOverrides(config);
return overrides.apply(jfrSettings);
}

private static void setupContextStorage() {
ContextStorage.addWrapper(JfrContextStorage::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static class Builder {
private RecordingFileNamingConvention namingConvention;
private Map<String, String> settings;
private Duration maxAgeDuration;
private JFR jfr = JFR.instance;
private JFR jfr = JFR.getInstance();
private Consumer<InputStream> onNewRecording;
private boolean keepRecordingFiles;

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.util;

import static io.opentelemetry.sdk.autoconfigure.AdditionalPropertiesUtil.getAdditionalPropertyOrDefault;

import com.splunk.opentelemetry.SplunkConfiguration;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalLanguageSpecificInstrumentationModel;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel;
import java.util.Map;

public class ProfilerDeclarativeConfigUtil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this class used anywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not any longer, removed

public static boolean isProfilerEnabled(OpenTelemetryConfigurationModel model) {
return getBoolean(model, SplunkConfiguration.PROFILER_ENABLED_PROPERTY);
}

private static boolean getBoolean(OpenTelemetryConfigurationModel model, String propertyName) {
if (model.getInstrumentationDevelopment() != null) {
ExperimentalLanguageSpecificInstrumentationModel java =
model.getInstrumentationDevelopment().getJava();

if (java != null) {
Map<String, Object> javaInstrumentationProperties = java.getAdditionalProperties();
return getBoolean(javaInstrumentationProperties, propertyName);
}
}
return false;
}

private static boolean getBoolean(
Map<String, Object> javaInstrumentationProperties, String propertyName) {
return getAdditionalPropertyOrDefault(javaInstrumentationProperties, propertyName, false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.splunk.opentelemetry.profiler;

import static com.splunk.opentelemetry.testing.declarativeconfig.DeclarativeConfigTestUtil.createAutoConfiguredSdk;
import static com.splunk.opentelemetry.testing.declarativeconfig.DeclarativeConfigTestUtil.toYamlString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import io.opentelemetry.context.ContextStorage;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;

class JfrActivatorTest {
@Test
void shouldActivateJfrRecording(@TempDir Path tempDir) throws IOException {
try (MockedStatic<JFR> jfrMock = mockStatic(JFR.class);
MockedStatic<ContextStorage> contextStorageMock = mockStatic(ContextStorage.class)) {

// given
String yaml = toYamlString(
"file_format: \"1.0-rc.1\"",
"instrumentation/development:",
" java:",
" splunk:",
" profiler:",
" enabled: true"
);
AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir);

var jfrInstanceMock = mock(JFR.class);
when(jfrInstanceMock.isAvailable()).thenReturn(true);
jfrMock.when(JFR::getInstance).thenReturn(jfrInstanceMock);

ExecutorService executorMock = mock(ExecutorService.class);
JfrActivator activator = new JfrActivator(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<JFR> jfrMock = mockStatic(JFR.class);
MockedStatic<ContextStorage> contextStorageMock = mockStatic(ContextStorage.class)) {

// given
String yaml = toYamlString(
"file_format: \"1.0-rc.1\"",
"instrumentation/development:",
" java:",
" splunk:",
" profiler:",
" enabled: true"
);
AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir);

var jfrInstanceMock = mock(JFR.class);
when(jfrInstanceMock.isAvailable()).thenReturn(false);
jfrMock.when(JFR::getInstance).thenReturn(jfrInstanceMock);

ExecutorService executorMock = mock(ExecutorService.class);
JfrActivator activator = new JfrActivator(executorMock);

// when
activator.afterAgent(sdk);

// then
contextStorageMock.verifyNoInteractions();
verifyNoInteractions(executorMock);
}
}

@Test
void shouldNotActivateJfrRecording_profilerDisabled(@TempDir Path tempDir) throws IOException {
try (MockedStatic<JFR> jfrMock = mockStatic(JFR.class);
MockedStatic<ContextStorage> contextStorageMock = mockStatic(ContextStorage.class)) {

// given
String yaml = toYamlString(
"file_format: \"1.0-rc.1\"",
"instrumentation/development:",
" java:"
);
AutoConfiguredOpenTelemetrySdk sdk = createAutoConfiguredSdk(yaml, tempDir);

var jfrInstanceMock = mock(JFR.class);
when(jfrInstanceMock.isAvailable()).thenReturn(true);
jfrMock.when(JFR::getInstance).thenReturn(jfrInstanceMock);

ExecutorService executorMock = mock(ExecutorService.class);
JfrActivator activator = new JfrActivator(executorMock);

// when
activator.afterAgent(sdk);

// then
contextStorageMock.verifyNoInteractions();
verifyNoInteractions(executorMock);
}
}
}
Loading