diff --git a/control/src/main/java/com/splunk/opamp/remotecontrol/CommandDispatcher.java b/control/src/main/java/com/splunk/opamp/remotecontrol/CommandDispatcher.java index 1bdc1085d..435c9c9ce 100644 --- a/control/src/main/java/com/splunk/opamp/remotecontrol/CommandDispatcher.java +++ b/control/src/main/java/com/splunk/opamp/remotecontrol/CommandDispatcher.java @@ -17,5 +17,5 @@ package com.splunk.opamp.remotecontrol; public interface CommandDispatcher { - void dispatch(String contentType, String body); + void dispatch(String body); } diff --git a/control/src/main/java/com/splunk/opamp/remotecontrol/CommandDispatcherImpl.java b/control/src/main/java/com/splunk/opamp/remotecontrol/CommandDispatcherImpl.java index ab24a93c8..fe4a5ac4a 100644 --- a/control/src/main/java/com/splunk/opamp/remotecontrol/CommandDispatcherImpl.java +++ b/control/src/main/java/com/splunk/opamp/remotecontrol/CommandDispatcherImpl.java @@ -38,7 +38,7 @@ public CommandDispatcherImpl(BigDumper threadDumper) { } @Override - public void dispatch(String contentType, String body) { + public void dispatch(String body) { String[] parts = body.split("\n"); if (parts.length == 0) { logger.warning("Missing useful command body."); diff --git a/control/src/main/java/com/splunk/opamp/remotecontrol/NoOpCommandDispatcher.java b/control/src/main/java/com/splunk/opamp/remotecontrol/NoOpCommandDispatcher.java index 7fed93a64..3dddf1706 100644 --- a/control/src/main/java/com/splunk/opamp/remotecontrol/NoOpCommandDispatcher.java +++ b/control/src/main/java/com/splunk/opamp/remotecontrol/NoOpCommandDispatcher.java @@ -18,7 +18,7 @@ public class NoOpCommandDispatcher implements CommandDispatcher { @Override - public void dispatch(String contentType, String body) { + public void dispatch(String body) { // nop } } diff --git a/control/src/test/java/com/splunk/opamp/remotecontrol/CommandDispatcherImplTest.java b/control/src/test/java/com/splunk/opamp/remotecontrol/CommandDispatcherImplTest.java index ab9b8dc70..0e5a8f7d2 100644 --- a/control/src/test/java/com/splunk/opamp/remotecontrol/CommandDispatcherImplTest.java +++ b/control/src/test/java/com/splunk/opamp/remotecontrol/CommandDispatcherImplTest.java @@ -33,7 +33,7 @@ void dispatchesThreadDumpWithJobIdCountAndInterval() { when(threadDumper.startPeriodicDumper("job-123", 3, Duration.ofMillis(250))).thenReturn(true); CommandDispatcher dispatcher = new CommandDispatcherImpl(threadDumper); - dispatcher.dispatch("text/plain", "thread.dump\njob-123\n3\n250"); + dispatcher.dispatch("thread.dump\njob-123\n3\n250"); verify(threadDumper).startPeriodicDumper("job-123", 3, Duration.ofMillis(250)); } @@ -43,7 +43,7 @@ void acceptsMaximumThreadDumpCountAndInterval() { BigDumper threadDumper = mock(BigDumper.class); CommandDispatcher dispatcher = new CommandDispatcherImpl(threadDumper); - dispatcher.dispatch("text/plain", "thread.dump\njob-123\n100\n300000"); + dispatcher.dispatch("thread.dump\njob-123\n100\n300000"); verify(threadDumper).startPeriodicDumper("job-123", 100, Duration.ofMinutes(5)); } @@ -54,7 +54,7 @@ void usesThreadDumpDefaults() { when(threadDumper.startPeriodicDumper("job-123", 1, Duration.ofMillis(1000))).thenReturn(true); CommandDispatcher dispatcher = new CommandDispatcherImpl(threadDumper); - dispatcher.dispatch("text/plain", "thread.dump\r\njob-123\r\n"); + dispatcher.dispatch("thread.dump\r\njob-123\r\n"); verify(threadDumper).startPeriodicDumper("job-123", 1, Duration.ofMillis(1000)); } @@ -64,7 +64,7 @@ void rejectsMissingJobId() { BigDumper threadDumper = mock(BigDumper.class); CommandDispatcher dispatcher = new CommandDispatcherImpl(threadDumper); - dispatcher.dispatch("text/plain", "thread.dump"); + dispatcher.dispatch("thread.dump"); verifyNoInteractions(threadDumper); } @@ -74,11 +74,10 @@ void rejectsInvalidCountAndIntervalWithoutThrowing() { BigDumper threadDumper = mock(BigDumper.class); CommandDispatcher dispatcher = new CommandDispatcherImpl(threadDumper); - assertDoesNotThrow(() -> dispatcher.dispatch("text/plain", "thread.dump\njob-123\n0\n1000")); - assertDoesNotThrow(() -> dispatcher.dispatch("text/plain", "thread.dump\njob-123\n101\n1000")); - assertDoesNotThrow(() -> dispatcher.dispatch("text/plain", "thread.dump\njob-123\n1\n300001")); - assertDoesNotThrow( - () -> dispatcher.dispatch("text/plain", "thread.dump\njob-123\n1\nnot-a-number")); + assertDoesNotThrow(() -> dispatcher.dispatch("thread.dump\njob-123\n0\n1000")); + assertDoesNotThrow(() -> dispatcher.dispatch("thread.dump\njob-123\n101\n1000")); + assertDoesNotThrow(() -> dispatcher.dispatch("thread.dump\njob-123\n1\n300001")); + assertDoesNotThrow(() -> dispatcher.dispatch("thread.dump\njob-123\n1\nnot-a-number")); verifyNoInteractions(threadDumper); } @@ -90,6 +89,6 @@ void containsThreadDumpFailures() { .thenThrow(new IllegalStateException("export failed")); CommandDispatcher dispatcher = new CommandDispatcherImpl(threadDumper); - assertDoesNotThrow(() -> dispatcher.dispatch("text/plain", "thread.dump\njob-123")); + assertDoesNotThrow(() -> dispatcher.dispatch("thread.dump\njob-123")); } } diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index b0394718e..8d12d324f 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -6,7 +6,7 @@ val otelVersion = "1.64.0" val otelAlphaVersion = otelVersion.replaceFirst("(-SNAPSHOT)?$".toRegex(), "-alpha$1") val otelInstrumentationVersion = "2.30.0" val otelInstrumentationAlphaVersion = otelInstrumentationVersion.replaceFirst("(-SNAPSHOT)?$".toRegex(), "-alpha$1") -val otelContribAlphaVersion = "1.58.0-alpha" +val otelContribAlphaVersion = "1.59.0-alpha" val autoValueVersion = "1.11.1" val dockerJavaVersion = "3.7.1" diff --git a/licenses/licenses.md b/licenses/licenses.md index 32edec35e..3749f7ed6 100644 --- a/licenses/licenses.md +++ b/licenses/licenses.md @@ -1,7 +1,7 @@ # splunk-otel-javaagent ## Dependency License Report -_2026-07-23 08:15:00 PDT_ +_2026-07-30 15:18:51 PDT_ ## Apache License, Version 2.0 **1** **Group:** `com.squareup.okhttp3` **Name:** `okhttp` **Version:** `5.4.0` @@ -20,11 +20,11 @@ _2026-07-23 08:15:00 PDT_ > - **POM Project URL**: [https://github.com/square/okio/](https://github.com/square/okio/) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**5** **Group:** `com.squareup.wire` **Name:** `wire-runtime` **Version:** `6.4.0` +**5** **Group:** `com.squareup.wire` **Name:** `wire-runtime` **Version:** `6.4.5` > - **POM Project URL**: [https://github.com/square/wire/](https://github.com/square/wire/) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**6** **Group:** `com.squareup.wire` **Name:** `wire-runtime-jvm` **Version:** `6.4.0` +**6** **Group:** `com.squareup.wire` **Name:** `wire-runtime-jvm` **Version:** `6.4.5` > - **POM Project URL**: [https://github.com/square/wire/](https://github.com/square/wire/) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) @@ -76,15 +76,15 @@ _2026-07-23 08:15:00 PDT_ > - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**19** **Group:** `io.opentelemetry.contrib` **Name:** `opentelemetry-opamp-client` **Version:** `1.58.0-alpha` +**19** **Group:** `io.opentelemetry.contrib` **Name:** `opentelemetry-opamp-client` **Version:** `1.59.0-alpha` > - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**20** **Group:** `io.opentelemetry.contrib` **Name:** `opentelemetry-resource-providers` **Version:** `1.58.0-alpha` +**20** **Group:** `io.opentelemetry.contrib` **Name:** `opentelemetry-resource-providers` **Version:** `1.59.0-alpha` > - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**21** **Group:** `io.opentelemetry.contrib` **Name:** `opentelemetry-samplers` **Version:** `1.58.0-alpha` +**21** **Group:** `io.opentelemetry.contrib` **Name:** `opentelemetry-samplers` **Version:** `1.59.0-alpha` > - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) 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 046700f6d..34c6d8fbd 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/OpampActivator.java @@ -171,13 +171,14 @@ static OpampClient startOpampClient( OpampClientBuilder builder = OpampClient.builder(); builder.enableEffectiveConfigReporting(); - // Remote control uses OpAMP remote configuration as its transport, so advertise the - // remote configuration capabilities when either feature is enabled. - if (opampClientConfiguration.isRemoteConfigurationEnabled() - || opampClientConfiguration.isRemoteControlAllowed()) { + if (opampClientConfiguration.isRemoteConfigurationEnabled()) { builder.enableRemoteConfig(); } + if (opampClientConfiguration.isRemoteControlAllowed()) { + builder.addCustomCapability(ServerToAgentMessageHandler.CMD_CAPABILITY); + } + String endpoint = opampClientConfiguration.getEndpoint(); long pollingDurationMillis = opampClientConfiguration.getPollingInterval(); if (endpoint != null) { 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 5cbe7a2eb..23796b728 100644 --- a/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java +++ b/opamp/src/main/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandler.java @@ -21,11 +21,12 @@ import com.splunk.opamp.remotecontrol.CommandDispatcher; import io.opentelemetry.opamp.client.OpampClient; import io.opentelemetry.opamp.client.internal.response.MessageData; -import opamp.proto.AgentConfigFile; import opamp.proto.AgentRemoteConfig; +import opamp.proto.CustomMessage; public class ServerToAgentMessageHandler { - public static final String MAGIC_CMD_STRING = "COMMAND_HACKS"; + public static final String CMD_CAPABILITY = "com.splunk.opamp.experimental_command/v1"; + public static final String CMD_TYPE = "command"; private final RemoteConfigProcessor remoteConfigProcessor; private final CommandDispatcher commandDispatcher; @@ -38,18 +39,14 @@ public class ServerToAgentMessageHandler { public void handleMessage(MessageData message, OpampClient opampClient) { AgentRemoteConfig remoteConfig = message.getRemoteConfig(); if (remoteConfig != null) { - - if (remoteConfig.config.config_map.containsKey(MAGIC_CMD_STRING)) { - AgentConfigFile agentConfigFile = remoteConfig.config.config_map.get(MAGIC_CMD_STRING); - String contentType = agentConfigFile.content_type; - String body = agentConfigFile.body.string(UTF_8); - commandDispatcher.dispatch(contentType, body); - if (remoteConfig.config.config_map.size() == 1) { // just this command - return; - } - } - remoteConfigProcessor.applyConfig(remoteConfig, opampClient); } + CustomMessage customMessage = message.getCustomMessage(); + if (customMessage != null + && CMD_CAPABILITY.equals(customMessage.capability) + && CMD_TYPE.equals(customMessage.type)) { + String body = customMessage.data.string(UTF_8); + commandDispatcher.dispatch(body); + } } } 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 f1e62cdcb..ca6497f70 100644 --- a/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/OpampActivatorTest.java @@ -44,20 +44,16 @@ import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension; import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.RecordedRequest; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import okio.ByteString; import opamp.proto.AgentCapabilities; -import opamp.proto.AgentConfigFile; -import opamp.proto.AgentConfigMap; -import opamp.proto.AgentRemoteConfig; import opamp.proto.AgentToServer; import opamp.proto.AnyValue; import opamp.proto.ArrayValue; +import opamp.proto.CustomMessage; import opamp.proto.KeyValue; import opamp.proto.ServerErrorResponse; import opamp.proto.ServerToAgent; @@ -119,15 +115,14 @@ void testOpamp() throws Exception { .put(AttributeKey.booleanArrayKey("boolobjarr"), Arrays.asList(true, true, false, true)) .build(); Resource resource = Resource.create(attributes); - Map configMap = - Collections.singletonMap( - "test-key", - new AgentConfigFile.Builder().body(ByteString.encodeUtf8("test-value")).build()); + String commandBody = "thread.dump\njob-123\n3\n250"; ServerToAgent response = new ServerToAgent.Builder() - .remote_config( - new AgentRemoteConfig.Builder() - .config(new AgentConfigMap.Builder().config_map(configMap).build()) + .custom_message( + new CustomMessage.Builder() + .capability(ServerToAgentMessageHandler.CMD_CAPABILITY) + .type(ServerToAgentMessageHandler.CMD_TYPE) + .data(ByteString.encodeUtf8(commandBody)) .build()) .build(); server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.X_PROTOBUF, response.encode())); @@ -141,6 +136,7 @@ void testOpamp() throws Exception { .withEndpoint(server.httpUri().toString()) .withPollingInterval(500) .withRemoteConfigurationEnabled(true) + .withRemoteControlAllowed(true) .build(); OpampClient client = OpampActivator.startOpampClient( @@ -172,17 +168,21 @@ public void onMessage(OpampClient opampClient, MessageData messageData) { // when MessageData message = result.get(5, TimeUnit.SECONDS); - AgentRemoteConfig remoteConfig = message.getRemoteConfig(); + CustomMessage customMessage = message.getCustomMessage(); // then - assertThat(remoteConfig).isNotNull(); - assertThat(remoteConfig.config.config_map.get("test-key").body.utf8()).isEqualTo("test-value"); + assertThat(customMessage).isNotNull(); + assertThat(customMessage.capability).isEqualTo(ServerToAgentMessageHandler.CMD_CAPABILITY); + assertThat(customMessage.type).isEqualTo(ServerToAgentMessageHandler.CMD_TYPE); + assertThat(customMessage.data.utf8()).isEqualTo(commandBody); RecordedRequest recordedRequest = server.takeRequest(); byte[] body = recordedRequest.request().content().array(); AgentToServer agentToServer = AgentToServer.ADAPTER.decode(body); assertRemoteConfigCapabilities(agentToServer, true); + assertThat(agentToServer.custom_capabilities.capabilities) + .containsExactly(ServerToAgentMessageHandler.CMD_CAPABILITY); assertIdentifyingString(agentToServer, SERVICE_NAME, "test-service"); assertIdentifyingString(agentToServer, SERVICE_INSTANCE_ID, "test-instance"); assertIdentifyingString(agentToServer, SERVICE_NAMESPACE, "test-ns"); @@ -266,10 +266,20 @@ public void onMessage(OpampClient opampClient, MessageData messageData) { } @Test - void shouldAdvertiseRemoteConfigCapabilitiesWhenRemoteControlIsAllowed() throws Exception { + void shouldAdvertiseCustomCapabilityWhenRemoteControlIsAllowed() throws Exception { AgentToServer agentToServer = startClientAndTakeInitialRequest(false, true); + assertRemoteConfigCapabilities(agentToServer, false); + assertThat(agentToServer.custom_capabilities.capabilities) + .containsExactly(ServerToAgentMessageHandler.CMD_CAPABILITY); + } + + @Test + void shouldAdvertiseRemoteConfigCapabilitiesOnlyWhenRemoteConfigIsEnabled() throws Exception { + AgentToServer agentToServer = startClientAndTakeInitialRequest(true, false); + assertRemoteConfigCapabilities(agentToServer, true); + assertThat(agentToServer.custom_capabilities).isNull(); } @Test @@ -277,6 +287,7 @@ void shouldNotAdvertiseRemoteConfigCapabilitiesWhenRemoteFeaturesAreDisabled() t AgentToServer agentToServer = startClientAndTakeInitialRequest(false, false); assertRemoteConfigCapabilities(agentToServer, false); + assertThat(agentToServer.custom_capabilities).isNull(); } private AgentToServer startClientAndTakeInitialRequest( diff --git a/opamp/src/test/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandlerTest.java b/opamp/src/test/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandlerTest.java new file mode 100644 index 000000000..4669fe49c --- /dev/null +++ b/opamp/src/test/java/com/splunk/opentelemetry/opamp/ServerToAgentMessageHandlerTest.java @@ -0,0 +1,124 @@ +/* + * 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 static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.splunk.opamp.remotecontrol.CommandDispatcher; +import io.opentelemetry.opamp.client.OpampClient; +import io.opentelemetry.opamp.client.internal.response.MessageData; +import okio.ByteString; +import opamp.proto.AgentRemoteConfig; +import opamp.proto.CustomMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ServerToAgentMessageHandlerTest { + + @Mock RemoteConfigProcessor remoteConfigProcessor; + @Mock CommandDispatcher commandDispatcher; + @Mock OpampClient opampClient; + private ServerToAgentMessageHandler handler; + + @BeforeEach + void setUp() { + handler = new ServerToAgentMessageHandler(remoteConfigProcessor, commandDispatcher); + } + + @Test + void dispatchesMatchingCustomCommand() { + String body = "thread.dump\njob-123\n3\n250"; + MessageData message = + MessageData.builder() + .setCustomMessage( + customMessage( + ServerToAgentMessageHandler.CMD_CAPABILITY, + ServerToAgentMessageHandler.CMD_TYPE, + body)) + .build(); + + handler.handleMessage(message, opampClient); + + verify(commandDispatcher).dispatch(body); + verifyNoInteractions(remoteConfigProcessor, opampClient); + } + + @Test + void ignoresCustomMessageForDifferentCapability() { + MessageData message = + MessageData.builder() + .setCustomMessage( + customMessage( + "com.example.other/v1", + ServerToAgentMessageHandler.CMD_TYPE, + "thread.dump\njob-123")) + .build(); + + handler.handleMessage(message, opampClient); + + verifyNoInteractions(commandDispatcher, remoteConfigProcessor, opampClient); + } + + @Test + void ignoresCustomMessageForDifferentType() { + MessageData message = + MessageData.builder() + .setCustomMessage( + customMessage( + ServerToAgentMessageHandler.CMD_CAPABILITY, + "not-a-command", + "thread.dump\njob-123")) + .build(); + + handler.handleMessage(message, opampClient); + + verifyNoInteractions(commandDispatcher, remoteConfigProcessor, opampClient); + } + + @Test + void handlesRemoteConfigAndCustomCommandFromSameMessage() { + String body = "thread.dump\njob-123"; + AgentRemoteConfig remoteConfig = new AgentRemoteConfig.Builder().build(); + MessageData message = + MessageData.builder() + .setRemoteConfig(remoteConfig) + .setCustomMessage( + customMessage( + ServerToAgentMessageHandler.CMD_CAPABILITY, + ServerToAgentMessageHandler.CMD_TYPE, + body)) + .build(); + + handler.handleMessage(message, opampClient); + + verify(remoteConfigProcessor).applyConfig(remoteConfig, opampClient); + verify(commandDispatcher).dispatch(body); + } + + private static CustomMessage customMessage(String capability, String type, String body) { + return new CustomMessage.Builder() + .capability(capability) + .type(type) + .data(ByteString.encodeUtf8(body)) + .build(); + } +}