Skip to content

Commit 5e6ff52

Browse files
[OpAMP] Enable/disable profiler via remote config (#2881)
* work in progress * simplify by making a test config class impl * factor out the creational stuff into a builder, add tests * default * rework tests * rename * import logger * work on tests * rename sequencer -> flusher. * spotless * POC * POC code refactored into production * Test fixed * Added possibility to turn off profiler from remote config * fix * code review followup * Additional JFR check added * Small refactoring of JfrContextStorage initialization. Initial health report sent * Code review followup --------- Co-authored-by: Jason Plumb <jplumb@splunk.com>
1 parent 4f20868 commit 5e6ff52

18 files changed

Lines changed: 1112 additions & 329 deletions

opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static java.util.logging.Level.WARNING;
2323

2424
import com.google.auto.service.AutoService;
25+
import com.splunk.opentelemetry.profiler.ProfilingSupervisor;
2526
import io.opentelemetry.javaagent.extension.AgentListener;
2627
import io.opentelemetry.opamp.client.OpampClient;
2728
import io.opentelemetry.opamp.client.OpampClientBuilder;
@@ -35,7 +36,9 @@
3536
import io.opentelemetry.sdk.resources.Resource;
3637
import java.io.IOException;
3738
import java.time.Duration;
39+
import java.time.Instant;
3840
import java.util.logging.Logger;
41+
import opamp.proto.ComponentHealth;
3942
import opamp.proto.ServerErrorResponse;
4043
import org.jetbrains.annotations.Nullable;
4144

@@ -56,7 +59,8 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetr
5659
createEffectiveConfigFactory(autoConfiguredOpenTelemetrySdk);
5760
State.EffectiveConfig effectiveConfig = buildEffectiveConfig(effectiveConfigFactory);
5861

59-
ServerToAgentMessageHandler serverToAgentMessageHandler = new ServerToAgentMessageHandler();
62+
ServerToAgentMessageHandler serverToAgentMessageHandler =
63+
new ServerToAgentMessageHandler(ProfilingSupervisor.SUPPLIER.get());
6064

6165
OpampClient client =
6266
startOpampClient(
@@ -99,6 +103,11 @@ public void onMessage(OpampClient opampClient, MessageData messageData) {
99103
}));
100104
}
101105

106+
@Override
107+
public int order() {
108+
return Integer.MAX_VALUE;
109+
}
110+
102111
private EffectiveConfigFactory createEffectiveConfigFactory(AutoConfiguredOpenTelemetrySdk sdk) {
103112
if (AutoConfigureUtil.isDeclarativeConfig(sdk)) {
104113
return new DeclarativeEffectiveConfigFileFactory();
@@ -132,6 +141,8 @@ static OpampClient startOpampClient(
132141
agentAttributes.addNonIdentifyingAttributes(builder);
133142

134143
builder.setEffectiveConfigState(effectiveConfig);
144+
builder.enableHealthReporting(createInitialHealthReport());
145+
135146
return builder.build(callbacks);
136147
}
137148

@@ -143,4 +154,15 @@ public opamp.proto.EffectiveConfig get() {
143154
}
144155
};
145156
}
157+
158+
private static ComponentHealth createInitialHealthReport() {
159+
Instant now = Instant.now();
160+
long nowNanos = now.getEpochSecond() * 1_000_000_000L + now.getNano();
161+
return new ComponentHealth.Builder()
162+
.healthy(true)
163+
.status("OK")
164+
.start_time_unix_nano(nowNanos)
165+
.status_time_unix_nano(nowNanos)
166+
.build();
167+
}
146168
}

opamp/src/main/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessor.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@
1616

1717
package com.splunk.opentelemetry.opamp;
1818

19+
import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty;
20+
21+
import com.google.common.annotations.VisibleForTesting;
22+
import com.splunk.opentelemetry.profiler.ProfilerDeclarativeConfiguration;
23+
import com.splunk.opentelemetry.profiler.ProfilingSupervisor;
24+
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
1925
import io.opentelemetry.opamp.client.OpampClient;
26+
import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfiguration;
27+
import java.io.ByteArrayInputStream;
2028
import java.util.Map;
2129
import java.util.Objects;
2230
import java.util.logging.Logger;
@@ -29,6 +37,13 @@ public class RemoteConfigProcessor {
2937
private static final Logger logger = Logger.getLogger(RemoteConfigProcessor.class.getName());
3038

3139
private static final String REMOTE_CONFIG_FILE_NAME = "splunk.remote.config";
40+
private static final String PROFILING_NODE_NAME = "profiling";
41+
42+
public ProfilingSupervisor profilingSupervisor;
43+
44+
public RemoteConfigProcessor(ProfilingSupervisor profilingSupervisor) {
45+
this.profilingSupervisor = Objects.requireNonNull(profilingSupervisor);
46+
}
3247

3348
public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient) {
3449
Objects.requireNonNull(opampClient);
@@ -44,7 +59,27 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient)
4459
return;
4560
}
4661

47-
// TODO: provide implementation
62+
DeclarativeConfigProperties remoteConfigProperties = toDeclarativeConfigProperties(configFile);
63+
DeclarativeConfigProperties splunkDistributionConfigProperties =
64+
remoteConfigProperties
65+
.getStructured("distribution", empty())
66+
.getStructured("splunk", empty());
67+
68+
// Update profiler configuration only when profiling node exists
69+
if (splunkDistributionConfigProperties.getPropertyKeys().contains(PROFILING_NODE_NAME)) {
70+
DeclarativeConfigProperties profilingConfigProperties =
71+
splunkDistributionConfigProperties.getStructured(PROFILING_NODE_NAME, empty());
72+
ProfilerDeclarativeConfiguration profilingConfig =
73+
new ProfilerDeclarativeConfiguration(profilingConfigProperties);
74+
// TODO: should be merged with current profiling config. Probably we will need profiler
75+
// configuration refactoring and some listeners implemented for profiler configuration
76+
// changes. For POC use this temporary solution
77+
if (profilingConfig.isEnabled()) {
78+
profilingSupervisor.requestStartProfiling();
79+
} else {
80+
profilingSupervisor.requestStopProfiling();
81+
}
82+
}
4883

4984
// Confirm to the OpAMP Server that remote config has been applied.
5085
opampClient.setRemoteConfigStatus(
@@ -53,4 +88,10 @@ public void applyConfig(AgentRemoteConfig remoteConfig, OpampClient opampClient)
5388
.status(RemoteConfigStatuses.RemoteConfigStatuses_APPLIED)
5489
.build());
5590
}
91+
92+
@VisibleForTesting
93+
static DeclarativeConfigProperties toDeclarativeConfigProperties(AgentConfigFile configFile) {
94+
return DeclarativeConfiguration.toConfigProperties(
95+
new ByteArrayInputStream(configFile.body.toByteArray()));
96+
}
5697
}

opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
package com.splunk.opentelemetry.opamp;
1818

1919
import com.google.common.annotations.VisibleForTesting;
20+
import com.splunk.opentelemetry.profiler.ProfilingSupervisor;
2021
import io.opentelemetry.opamp.client.OpampClient;
2122
import io.opentelemetry.opamp.client.internal.response.MessageData;
2223

2324
public class ServerToAgentMessageHandler {
2425
private final RemoteConfigProcessor remoteConfigProcessor;
2526

26-
public ServerToAgentMessageHandler() {
27-
this(new RemoteConfigProcessor());
27+
public ServerToAgentMessageHandler(ProfilingSupervisor profilingSupervisor) {
28+
this(new RemoteConfigProcessor(profilingSupervisor));
2829
}
2930

3031
@VisibleForTesting

opamp/src/test/java/com/splunk/opentelemetry/opamp/RemoteConfigProcessorTest.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.splunk.opentelemetry.opamp;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.mock;
2021
import static org.mockito.Mockito.never;
2122
import static org.mockito.Mockito.verify;
2223

@@ -32,19 +33,22 @@
3233
import org.mockito.ArgumentCaptor;
3334

3435
class RemoteConfigProcessorTest {
35-
private final RemoteConfigProcessor handler = new RemoteConfigProcessor();
36-
private final OpampClient opampClient = org.mockito.Mockito.mock(OpampClient.class);
36+
private final RemoteConfigProcessor handler = new RemoteConfigProcessor(mock());
37+
private final OpampClient opampClient = mock(OpampClient.class);
3738

3839
@Test
3940
void shouldMarkRemoteConfigAsApplied() {
4041
// given
42+
String remoteConfigYaml = "test-config:";
4143
ByteString configHash = ByteString.encodeUtf8("test-config-hash");
4244
AgentRemoteConfig remoteConfig =
4345
createRemoteConfig(
4446
configHash,
4547
Map.of(
4648
"splunk.remote.config",
47-
new AgentConfigFile.Builder().body(ByteString.encodeUtf8("test-config")).build()));
49+
new AgentConfigFile.Builder()
50+
.body(ByteString.encodeUtf8(remoteConfigYaml))
51+
.build()));
4852

4953
// when
5054
handler.applyConfig(remoteConfig, opampClient);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright Splunk Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.splunk.opentelemetry.profiler;
18+
19+
import com.google.auto.service.AutoService;
20+
import com.google.common.annotations.VisibleForTesting;
21+
import io.opentelemetry.javaagent.extension.AgentListener;
22+
import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil;
23+
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
24+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
25+
import java.util.logging.Logger;
26+
27+
@AutoService(AgentListener.class)
28+
public class JfrAgentListener implements AgentListener {
29+
private static final Logger logger = Logger.getLogger(JfrAgentListener.class.getName());
30+
private final JFR jfr;
31+
32+
public JfrAgentListener() {
33+
this(JFR.getInstance());
34+
}
35+
36+
@VisibleForTesting
37+
JfrAgentListener(JFR jfr) {
38+
this.jfr = jfr;
39+
}
40+
41+
@Override
42+
public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) {
43+
ProfilingSupervisor.setupJfrContextStorage();
44+
45+
ProfilerConfiguration config = getProfilerConfiguration(sdk);
46+
// Always start the supervisor, so it can start profiling later elsewhere.
47+
ProfilingSupervisor supervisor = makeProfilingSupervisor(sdk, config);
48+
49+
if (notClearForTakeoff(config)) {
50+
return;
51+
}
52+
53+
supervisor.requestStartProfiling();
54+
}
55+
56+
@Override
57+
public int order() {
58+
// Run it a bit earlier than listeners with default priority
59+
return -1;
60+
}
61+
62+
// Exists for testing
63+
ProfilingSupervisor makeProfilingSupervisor(
64+
AutoConfiguredOpenTelemetrySdk sdk, ProfilerConfiguration config) {
65+
return ProfilingSupervisor.createAndStart(sdk, config);
66+
}
67+
68+
private static ProfilerConfiguration getProfilerConfiguration(
69+
AutoConfiguredOpenTelemetrySdk sdk) {
70+
if (ProfilerDeclarativeConfiguration.SUPPLIER.isConfigured()) {
71+
return ProfilerDeclarativeConfiguration.SUPPLIER.get();
72+
} else {
73+
ConfigProperties configProperties = AutoConfigureUtil.getConfig(sdk);
74+
return new ProfilerEnvVarsConfiguration(configProperties);
75+
}
76+
}
77+
78+
private boolean notClearForTakeoff(ProfilerConfiguration config) {
79+
if (!config.isEnabled()) {
80+
logger.fine("Profiler is not enabled.");
81+
return true;
82+
}
83+
if (!jfr.isAvailable()) {
84+
logger.warning(
85+
"JDK Flight Recorder (JFR) is not available in this JVM. Profiling is disabled.");
86+
return true;
87+
}
88+
89+
return false;
90+
}
91+
}

profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrContextStorage.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@
2828
import javax.annotation.Nullable;
2929

3030
class JfrContextStorage implements ContextStorage {
31-
3231
private final ContextStorage delegate;
3332
private final Function<SpanContext, ContextAttached> newEvent;
3433
private final ThreadLocal<Span> activeSpan = ThreadLocal.withInitial(Span::getInvalid);
3534

35+
private volatile boolean enabled = false;
36+
3637
JfrContextStorage(ContextStorage delegate) {
3738
this(delegate, JfrContextStorage::newEvent);
3839
}
@@ -43,6 +44,14 @@ class JfrContextStorage implements ContextStorage {
4344
this.newEvent = newEvent;
4445
}
4546

47+
public void setEnabled(boolean enabled) {
48+
this.enabled = enabled;
49+
}
50+
51+
public boolean isEnabled() {
52+
return enabled;
53+
}
54+
4655
static ContextAttached newEvent(SpanContext spanContext) {
4756
if (spanContext.isValid()) {
4857
return new ContextAttached(
@@ -54,6 +63,9 @@ static ContextAttached newEvent(SpanContext spanContext) {
5463
@Override
5564
public Scope attach(Context toAttach) {
5665
Scope delegatedScope = delegate.attach(toAttach);
66+
if (!isEnabled()) {
67+
return delegatedScope;
68+
}
5769
Span span = Span.fromContext(toAttach);
5870
Span current = activeSpan.get();
5971
// do nothing when active span didn't change

profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrRecorder.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ class JfrRecorder {
6060
}
6161

6262
public void start() {
63+
if (isStarted()) {
64+
throw new IllegalStateException("Already started");
65+
}
6366
logger.fine("Profiler is starting a JFR recording");
6467
recording = newRecording();
6568
recording.setSettings(settings);
@@ -70,6 +73,13 @@ public void start() {
7073
recording.start();
7174
}
7275

76+
public void stop() {
77+
if (isStarted()) {
78+
recording.stop();
79+
}
80+
recording = null;
81+
}
82+
7383
@VisibleForTesting
7484
Recording newRecording() {
7585
return new Recording();
@@ -119,11 +129,6 @@ public boolean isStarted() {
119129
return (recording != null) && RecordingState.RUNNING.equals(recording.getState());
120130
}
121131

122-
public void stop() {
123-
recording.stop();
124-
recording = null;
125-
}
126-
127132
public static Builder builder() {
128133
return new Builder();
129134
}

0 commit comments

Comments
 (0)