From 61e619ac4e345ab82e9a14420efecec7e603f28e Mon Sep 17 00:00:00 2001 From: robsunday Date: Tue, 26 May 2026 16:14:03 +0200 Subject: [PATCH 01/13] Added health reporting support --- .../opamp/client/OpampClient.java | 8 +++ .../opamp/client/OpampClientBuilder.java | 36 +++++++++++ .../client/internal/impl/OpampClientImpl.java | 13 ++++ .../internal/impl/OpampClientState.java | 4 ++ .../impl/recipe/AgentToServerAppenders.java | 6 ++ .../impl/recipe/appenders/HealthAppender.java | 34 +++++++++++ .../opamp/client/internal/request/Field.java | 1 + .../opamp/client/internal/state/State.java | 28 +++++++++ .../internal/impl/OpampClientImplTest.java | 59 ++++++++++++++++++- .../internal/impl/OpampClientStateTest.java | 1 + .../recipe/AgentToServerAppendersTest.java | 3 + .../recipe/appenders/HealthAppenderTest.java | 43 ++++++++++++++ 12 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppender.java create mode 100644 opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppenderTest.java diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClient.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClient.java index 735fa33401..48d008df5d 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClient.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClient.java @@ -9,6 +9,7 @@ import java.io.Closeable; import javax.annotation.Nullable; import opamp.proto.AgentDescription; +import opamp.proto.ComponentHealth; import opamp.proto.RemoteConfigStatus; import opamp.proto.ServerErrorResponse; @@ -35,6 +36,13 @@ static OpampClientBuilder builder() { */ void setRemoteConfigStatus(RemoteConfigStatus remoteConfigStatus); + /** + * Sets the current Agent health which will be sent in the next agent to server request. + * + * @param health The new component health. + */ + void setHealth(ComponentHealth health); + interface Callbacks { /** * Called when the connection is successfully established to the Server. For WebSocket clients diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java index d6af850fab..1045c8aca8 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java @@ -37,6 +37,7 @@ public final class OpampClientBuilder { HttpRequestService.create(OkHttpSender.create("http://localhost:4320/v1/opamp")); @Nullable private byte[] instanceUid; @Nullable private State.EffectiveConfig effectiveConfigState; + @Nullable private State.Health healthState; OpampClientBuilder() {} @@ -362,6 +363,19 @@ public OpampClientBuilder enableEffectiveConfigReporting() { return this; } + /** + * Adds the ReportsHealth capability to the Client so that the Server expects the Client's health + * status report, as explained here. + * + * @return this + */ + @CanIgnoreReturnValue + public OpampClientBuilder enableHealthReporting() { + capabilities = capabilities | AgentCapabilities.AgentCapabilities_ReportsHealth.getValue(); + return this; + } + /** * Sets the effective config state implementation. It should call {@link * State.EffectiveConfig#notifyUpdate()} whenever it has changes that have not been sent to the @@ -376,6 +390,20 @@ public OpampClientBuilder setEffectiveConfigState(State.EffectiveConfig effectiv return this; } + /** + * Sets the health state implementation. It should call {@link State.Health#notifyUpdate()} + * whenever it has changes that have not been sent to the server. Use {@link + * #enableHealthReporting()} to add the ReportsHealth capability. + * + * @param healthState The state implementation. + * @return this + */ + @CanIgnoreReturnValue + public OpampClientBuilder setHealthState(State.Health healthState) { + this.healthState = healthState; + return this; + } + public OpampClient build(OpampClient.Callbacks callbacks) { List protoIdentifyingAttributes = new ArrayList<>(); List protoNonIdentifyingAttributes = new ArrayList<>(); @@ -389,6 +417,9 @@ public OpampClient build(OpampClient.Callbacks callbacks) { if (effectiveConfigState == null) { effectiveConfigState = createEffectiveConfigNoop(); } + if (healthState == null) { + healthState = createHealthNoop(); + } OpampClientState state = new OpampClientState( new State.RemoteConfigStatus(new RemoteConfigStatus.Builder().build()), @@ -399,6 +430,7 @@ public OpampClient build(OpampClient.Callbacks callbacks) { .non_identifying_attributes(protoNonIdentifyingAttributes) .build()), new State.Capabilities(capabilities), + healthState, new State.InstanceUid(instanceUid), new State.Flags(0L), effectiveConfigState); @@ -415,6 +447,10 @@ public opamp.proto.EffectiveConfig get() { }; } + private static State.Health createHealthNoop() { + return new State.Health() {}; + } + private static AnyValue createStringValue(String value) { return new AnyValue.Builder().string_value(value).build(); } diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java index 1441093d7f..1525e52181 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java @@ -14,6 +14,7 @@ import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.CapabilitiesAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.EffectiveConfigAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.FlagsAppender; +import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.HealthAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.InstanceUidAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.RemoteConfigStatusAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.SequenceNumberAppender; @@ -28,12 +29,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import javax.annotation.Nonnull; import okio.ByteString; import opamp.proto.AgentDescription; import opamp.proto.AgentToServer; +import opamp.proto.ComponentHealth; import opamp.proto.RemoteConfigStatus; import opamp.proto.ServerErrorResponse; import opamp.proto.ServerToAgent; @@ -76,6 +79,7 @@ public final class OpampClientImpl // Compressable fields init List compressableFields = new ArrayList<>(); compressableFields.add(Field.AGENT_DESCRIPTION); + compressableFields.add(Field.HEALTH); compressableFields.add(Field.EFFECTIVE_CONFIG); compressableFields.add(Field.REMOTE_CONFIG_STATUS); COMPRESSABLE_FIELDS = Collections.unmodifiableList(compressableFields); @@ -90,6 +94,7 @@ public static OpampClientImpl create( RemoteConfigStatusAppender.create(state.remoteConfigStatus), SequenceNumberAppender.create(state.sequenceNum), CapabilitiesAppender.create(state.capabilities), + HealthAppender.create(state.health), InstanceUidAppender.create(state.instanceUid), FlagsAppender.create(state.flags), AgentDisconnectAppender.create()); @@ -144,6 +149,14 @@ public void setRemoteConfigStatus(@Nonnull RemoteConfigStatus remoteConfigStatus } } + @Override + public void setHealth(@Nonnull ComponentHealth health) { + if (!Objects.equals(state.health.get(), health)) { + state.health.set(health); + addFieldAndSend(Field.HEALTH); + } + } + @Override public void onConnectionSuccess() { callbacks.onConnect(this); diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientState.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientState.java index d8b26beeef..f72574db63 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientState.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientState.java @@ -19,6 +19,7 @@ public final class OpampClientState { public final State.SequenceNum sequenceNum; public final State.AgentDescription agentDescription; public final State.Capabilities capabilities; + public final State.Health health; public final State.InstanceUid instanceUid; public final State.Flags flags; public final State.EffectiveConfig effectiveConfig; @@ -29,6 +30,7 @@ public OpampClientState( State.SequenceNum sequenceNum, State.AgentDescription agentDescription, State.Capabilities capabilities, + State.Health health, State.InstanceUid instanceUid, State.Flags flags, State.EffectiveConfig effectiveConfig) { @@ -36,6 +38,7 @@ public OpampClientState( this.sequenceNum = sequenceNum; this.agentDescription = agentDescription; this.capabilities = capabilities; + this.health = health; this.instanceUid = instanceUid; this.flags = flags; this.effectiveConfig = effectiveConfig; @@ -45,6 +48,7 @@ public OpampClientState( providedItems.add(sequenceNum); providedItems.add(agentDescription); providedItems.add(capabilities); + providedItems.add(health); providedItems.add(instanceUid); providedItems.add(flags); providedItems.add(effectiveConfig); diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/recipe/AgentToServerAppenders.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/recipe/AgentToServerAppenders.java index bbde2def98..f6072ce426 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/recipe/AgentToServerAppenders.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/recipe/AgentToServerAppenders.java @@ -11,6 +11,7 @@ import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.CapabilitiesAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.EffectiveConfigAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.FlagsAppender; +import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.HealthAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.InstanceUidAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.RemoteConfigStatusAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.SequenceNumberAppender; @@ -29,17 +30,20 @@ public final class AgentToServerAppenders { public final RemoteConfigStatusAppender remoteConfigStatusAppender; public final SequenceNumberAppender sequenceNumberAppender; public final CapabilitiesAppender capabilitiesAppender; + public final HealthAppender healthAppender; public final InstanceUidAppender instanceUidAppender; public final FlagsAppender flagsAppender; public final AgentDisconnectAppender agentDisconnectAppender; private final Map allAppenders; + @SuppressWarnings("TooManyParameters") public AgentToServerAppenders( AgentDescriptionAppender agentDescriptionAppender, EffectiveConfigAppender effectiveConfigAppender, RemoteConfigStatusAppender remoteConfigStatusAppender, SequenceNumberAppender sequenceNumberAppender, CapabilitiesAppender capabilitiesAppender, + HealthAppender healthAppender, InstanceUidAppender instanceUidAppender, FlagsAppender flagsAppender, AgentDisconnectAppender agentDisconnectAppender) { @@ -48,6 +52,7 @@ public AgentToServerAppenders( this.remoteConfigStatusAppender = remoteConfigStatusAppender; this.sequenceNumberAppender = sequenceNumberAppender; this.capabilitiesAppender = capabilitiesAppender; + this.healthAppender = healthAppender; this.instanceUidAppender = instanceUidAppender; this.flagsAppender = flagsAppender; this.agentDisconnectAppender = agentDisconnectAppender; @@ -58,6 +63,7 @@ public AgentToServerAppenders( appenders.put(Field.REMOTE_CONFIG_STATUS, remoteConfigStatusAppender); appenders.put(Field.SEQUENCE_NUM, sequenceNumberAppender); appenders.put(Field.CAPABILITIES, capabilitiesAppender); + appenders.put(Field.HEALTH, healthAppender); appenders.put(Field.INSTANCE_UID, instanceUidAppender); appenders.put(Field.FLAGS, flagsAppender); appenders.put(Field.AGENT_DISCONNECT, agentDisconnectAppender); diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppender.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppender.java new file mode 100644 index 0000000000..49deb84938 --- /dev/null +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppender.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opamp.client.internal.impl.recipe.appenders; + +import java.util.function.Supplier; +import opamp.proto.AgentToServer; +import opamp.proto.ComponentHealth; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class HealthAppender implements AgentToServerAppender { + private final Supplier health; + + public static HealthAppender create(Supplier health) { + return new HealthAppender(health); + } + + private HealthAppender(Supplier health) { + this.health = health; + } + + @Override + public void appendTo(AgentToServer.Builder builder) { + ComponentHealth currentHealth = health.get(); + if (currentHealth != null) { + builder.health(currentHealth); + } + } +} diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/request/Field.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/request/Field.java index 277ecc2dd5..64cdc0494a 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/request/Field.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/request/Field.java @@ -18,6 +18,7 @@ public enum Field { SEQUENCE_NUM, AGENT_DESCRIPTION, CAPABILITIES, + HEALTH, EFFECTIVE_CONFIG, REMOTE_CONFIG_STATUS, AGENT_DISCONNECT, diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java index abb0a6a9db..846bb29340 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java @@ -7,8 +7,11 @@ import io.opentelemetry.opamp.client.internal.request.Field; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import opamp.proto.ComponentHealth; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -75,6 +78,31 @@ public Field getFieldType() { } } + class Health extends ObservableState { + private final AtomicReference state = new AtomicReference<>(); + + protected Health() {} + + public Health(@Nonnull ComponentHealth initialValue) { + state.set(Objects.requireNonNull(initialValue)); + } + + public void set(@Nonnull ComponentHealth value) { + state.set(Objects.requireNonNull(value)); + } + + @Nullable + @Override + public ComponentHealth get() { + return state.get(); + } + + @Override + public final Field getFieldType() { + return Field.HEALTH; + } + } + final class RemoteConfigStatus extends InMemoryState { public RemoteConfigStatus(opamp.proto.RemoteConfigStatus initialValue) { diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java index 020b1b2e2a..95ef19626b 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java @@ -37,6 +37,7 @@ import mockwebserver3.junit5.StartStop; import okio.Buffer; import okio.ByteString; +import opamp.proto.AgentCapabilities; import opamp.proto.AgentConfigFile; import opamp.proto.AgentConfigMap; import opamp.proto.AgentDescription; @@ -45,6 +46,7 @@ import opamp.proto.AgentToServer; import opamp.proto.AgentToServerFlags; import opamp.proto.AnyValue; +import opamp.proto.ComponentHealth; import opamp.proto.EffectiveConfig; import opamp.proto.KeyValue; import opamp.proto.RemoteConfigStatus; @@ -65,6 +67,7 @@ class OpampClientImplTest { private OpampClientState state; private OpampClientImpl client; private TestEffectiveConfig effectiveConfig; + private ComponentHealth initialHealth; private TestCallbacks callbacks; @StartStop private final MockWebServer server = new MockWebServer(); @@ -75,13 +78,22 @@ void setUp() { new EffectiveConfig.Builder() .config_map(createAgentConfigMap("first", "first content")) .build()); + initialHealth = + new ComponentHealth.Builder() + .healthy(true) + .start_time_unix_nano(123L) + .status("running") + .status_time_unix_nano(456L) + .build(); state = new OpampClientState( new State.RemoteConfigStatus( getRemoteConfigStatus(RemoteConfigStatuses.RemoteConfigStatuses_UNSET)), new State.SequenceNum(1L), new State.AgentDescription(new AgentDescription.Builder().build()), - new State.Capabilities(5L), + new State.Capabilities( + 5L | AgentCapabilities.AgentCapabilities_ReportsHealth.getValue()), + new State.Health(initialHealth), new State.InstanceUid(new byte[] {1, 2, 3}), new State.Flags((long) AgentToServerFlags.AgentToServerFlags_Unspecified.getValue()), effectiveConfig); @@ -105,6 +117,7 @@ void verifyFieldsSent() { assertThat(firstMessage.sequence_num).isEqualTo(1); assertThat(firstMessage.capabilities).isEqualTo(state.capabilities.get()); assertThat(firstMessage.agent_description).isEqualTo(state.agentDescription.get()); + assertThat(firstMessage.health).isEqualTo(initialHealth); assertThat(firstMessage.effective_config).isEqualTo(state.effectiveConfig.get()); assertThat(firstMessage.remote_config_status).isEqualTo(state.remoteConfigStatus.get()); @@ -124,6 +137,7 @@ void verifyFieldsSent() { assertThat(secondMessage.sequence_num).isEqualTo(2); assertThat(firstMessage.capabilities).isEqualTo(state.capabilities.get()); assertThat(secondMessage.agent_description).isNull(); + assertThat(secondMessage.health).isNull(); assertThat(secondMessage.effective_config).isNull(); assertThat(secondMessage.remote_config_status).isEqualTo(remoteConfigStatus); @@ -144,6 +158,7 @@ void verifyFieldsSent() { assertThat(thirdMessage.sequence_num).isEqualTo(3); assertThat(firstMessage.capabilities).isEqualTo(state.capabilities.get()); assertThat(thirdMessage.agent_description).isNull(); + assertThat(thirdMessage.health).isNull(); assertThat(thirdMessage.remote_config_status).isNull(); assertThat(thirdMessage.effective_config) .isEqualTo(otherConfig); // it was changed via observable state @@ -169,6 +184,7 @@ void verifyFieldsSent() { assertThat(fullRequestedMessage.sequence_num).isEqualTo(5); assertThat(fullRequestedMessage.capabilities).isEqualTo(state.capabilities.get()); assertThat(fullRequestedMessage.agent_description).isEqualTo(state.agentDescription.get()); + assertThat(fullRequestedMessage.health).isEqualTo(state.health.get()); assertThat(fullRequestedMessage.effective_config).isEqualTo(state.effectiveConfig.get()); assertThat(fullRequestedMessage.remote_config_status).isEqualTo(state.remoteConfigStatus.get()); } @@ -253,6 +269,47 @@ void verifyRemoteConfigStatusSetter() { assertThat(takeRequest()).isNull(); } + @Test + void verifyHealthSetter() { + initializeClient(); + ComponentHealth health = + new ComponentHealth.Builder() + .healthy(false) + .start_time_unix_nano(0L) + .last_error("failed") + .status("failed") + .status_time_unix_nano(789L) + .build(); + + // Update when changed + enqueueServerToAgentResponse(new ServerToAgent.Builder().build()); + client.setHealth(health); + assertThat(getAgentToServerMessage(takeRequest()).health).isEqualTo(health); + + // Ignore when the provided value is the same as the current one + enqueueServerToAgentResponse(new ServerToAgent.Builder().build()); + client.setHealth(health); + assertThat(takeRequest()).isNull(); + } + + @Test + void verifyHealthStateUpdate() { + initializeClient(); + ComponentHealth health = + new ComponentHealth.Builder() + .healthy(false) + .last_error("failed") + .status("failed") + .status_time_unix_nano(789L) + .build(); + + enqueueServerToAgentResponse(new ServerToAgent.Builder().build()); + state.health.set(health); + state.health.notifyUpdate(); + + assertThat(getAgentToServerMessage(takeRequest()).health).isEqualTo(health); + } + @Test void onConnectionSuccessful_notifyCallback() { initializeClient(); diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientStateTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientStateTest.java index 000b4a6fe6..fe272b12d5 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientStateTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientStateTest.java @@ -24,6 +24,7 @@ class OpampClientStateTest { @Mock private State.SequenceNum sequenceNum; @Mock private State.AgentDescription agentDescription; @Mock private State.Capabilities capabilities; + @Mock private State.Health health; @Mock private State.InstanceUid instanceUid; @Mock private State.Flags flags; @Mock private State.EffectiveConfig effectiveConfig; diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/AgentToServerAppendersTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/AgentToServerAppendersTest.java index 6bab5615f3..e9d3b46f19 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/AgentToServerAppendersTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/AgentToServerAppendersTest.java @@ -13,6 +13,7 @@ import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.CapabilitiesAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.EffectiveConfigAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.FlagsAppender; +import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.HealthAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.InstanceUidAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.RemoteConfigStatusAppender; import io.opentelemetry.opamp.client.internal.impl.recipe.appenders.SequenceNumberAppender; @@ -30,6 +31,7 @@ class AgentToServerAppendersTest { @Mock private RemoteConfigStatusAppender remoteConfigStatusAppender; @Mock private SequenceNumberAppender sequenceNumberAppender; @Mock private CapabilitiesAppender capabilitiesAppender; + @Mock private HealthAppender healthAppender; @Mock private FlagsAppender flagsAppender; @Mock private InstanceUidAppender instanceUidAppender; @Mock private AgentDisconnectAppender agentDisconnectAppender; @@ -42,6 +44,7 @@ void verifyAppenderList() { verifyMapping(Field.REMOTE_CONFIG_STATUS, remoteConfigStatusAppender); verifyMapping(Field.SEQUENCE_NUM, sequenceNumberAppender); verifyMapping(Field.CAPABILITIES, capabilitiesAppender); + verifyMapping(Field.HEALTH, healthAppender); verifyMapping(Field.INSTANCE_UID, instanceUidAppender); verifyMapping(Field.FLAGS, flagsAppender); verifyMapping(Field.AGENT_DISCONNECT, agentDisconnectAppender); diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppenderTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppenderTest.java new file mode 100644 index 0000000000..bf9450c77b --- /dev/null +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppenderTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opamp.client.internal.impl.recipe.appenders; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.atomic.AtomicReference; +import opamp.proto.AgentToServer; +import opamp.proto.ComponentHealth; +import org.junit.jupiter.api.Test; + +class HealthAppenderTest { + + @Test + void appendTo_withHealth_setsHealthField() { + ComponentHealth health = + new ComponentHealth.Builder() + .healthy(true) + .start_time_unix_nano(123L) + .status("running") + .status_time_unix_nano(456L) + .build(); + HealthAppender appender = HealthAppender.create(() -> health); + + AgentToServer.Builder builder = new AgentToServer.Builder(); + appender.appendTo(builder); + + assertThat(builder.build().health).isEqualTo(health); + } + + @Test + void appendTo_withNoHealth_leavesHealthFieldUnset() { + HealthAppender appender = HealthAppender.create(new AtomicReference()::get); + + AgentToServer.Builder builder = new AgentToServer.Builder(); + appender.appendTo(builder); + + assertThat(builder.build().health).isNull(); + } +} From 68085c47f51ca878c1702e8ae91afa6f15b82468 Mon Sep 17 00:00:00 2001 From: robsunday Date: Tue, 26 May 2026 16:23:49 +0200 Subject: [PATCH 02/13] Tests renamed --- .../internal/impl/recipe/appenders/HealthAppenderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppenderTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppenderTest.java index bf9450c77b..73d38f8526 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppenderTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/recipe/appenders/HealthAppenderTest.java @@ -15,7 +15,7 @@ class HealthAppenderTest { @Test - void appendTo_withHealth_setsHealthField() { + void shouldAppendProvidedHealth() { ComponentHealth health = new ComponentHealth.Builder() .healthy(true) @@ -32,7 +32,7 @@ void appendTo_withHealth_setsHealthField() { } @Test - void appendTo_withNoHealth_leavesHealthFieldUnset() { + void shouldNotAppendHealthIfNotProvided() { HealthAppender appender = HealthAppender.create(new AtomicReference()::get); AgentToServer.Builder builder = new AgentToServer.Builder(); From f5681476d95fd259ec32db036b832cc692d7376c Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 27 May 2026 11:24:39 +0200 Subject: [PATCH 03/13] HealthState refactored to inherit from InMemoryState --- .../opamp/client/OpampClientBuilder.java | 27 ++++++++++--------- .../opamp/client/internal/state/State.java | 22 +++------------ .../internal/impl/OpampClientImplTest.java | 18 ------------- 3 files changed, 18 insertions(+), 49 deletions(-) diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java index 1045c8aca8..4fcdf6dd5c 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java @@ -15,6 +15,7 @@ import io.opentelemetry.opamp.client.internal.state.State; import io.opentelemetry.opamp.client.request.service.RequestService; import java.nio.ByteBuffer; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -25,6 +26,7 @@ import opamp.proto.AgentDescription; import opamp.proto.AnyValue; import opamp.proto.ArrayValue; +import opamp.proto.ComponentHealth; import opamp.proto.KeyValue; import opamp.proto.RemoteConfigStatus; @@ -37,7 +39,7 @@ public final class OpampClientBuilder { HttpRequestService.create(OkHttpSender.create("http://localhost:4320/v1/opamp")); @Nullable private byte[] instanceUid; @Nullable private State.EffectiveConfig effectiveConfigState; - @Nullable private State.Health healthState; + @Nullable private ComponentHealth health; OpampClientBuilder() {} @@ -391,16 +393,15 @@ public OpampClientBuilder setEffectiveConfigState(State.EffectiveConfig effectiv } /** - * Sets the health state implementation. It should call {@link State.Health#notifyUpdate()} - * whenever it has changes that have not been sent to the server. Use {@link - * #enableHealthReporting()} to add the ReportsHealth capability. + * Sets the health object that is then passed to the corresponding state implementation. Use + * {@link #enableHealthReporting()} to add the ReportsHealth capability. * - * @param healthState The state implementation. + * @param health The component health * @return this */ @CanIgnoreReturnValue - public OpampClientBuilder setHealthState(State.Health healthState) { - this.healthState = healthState; + public OpampClientBuilder setHealth(ComponentHealth health) { + this.health = health; return this; } @@ -417,8 +418,8 @@ public OpampClient build(OpampClient.Callbacks callbacks) { if (effectiveConfigState == null) { effectiveConfigState = createEffectiveConfigNoop(); } - if (healthState == null) { - healthState = createHealthNoop(); + if (health == null) { + health = createInitialHealth(); } OpampClientState state = new OpampClientState( @@ -430,7 +431,7 @@ public OpampClient build(OpampClient.Callbacks callbacks) { .non_identifying_attributes(protoNonIdentifyingAttributes) .build()), new State.Capabilities(capabilities), - healthState, + new State.Health(health), new State.InstanceUid(instanceUid), new State.Flags(0L), effectiveConfigState); @@ -447,8 +448,10 @@ public opamp.proto.EffectiveConfig get() { }; } - private static State.Health createHealthNoop() { - return new State.Health() {}; + private static ComponentHealth createInitialHealth() { + return new ComponentHealth.Builder() + .start_time_unix_nano(Instant.now().toEpochMilli() * 1_000_000L) + .build(); } private static AnyValue createStringValue(String value) { diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java index 846bb29340..11a30fd573 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java @@ -7,10 +7,8 @@ import io.opentelemetry.opamp.client.internal.request.Field; import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import opamp.proto.ComponentHealth; /** @@ -78,27 +76,13 @@ public Field getFieldType() { } } - class Health extends ObservableState { - private final AtomicReference state = new AtomicReference<>(); - - protected Health() {} - + final class Health extends InMemoryState { public Health(@Nonnull ComponentHealth initialValue) { - state.set(Objects.requireNonNull(initialValue)); - } - - public void set(@Nonnull ComponentHealth value) { - state.set(Objects.requireNonNull(value)); - } - - @Nullable - @Override - public ComponentHealth get() { - return state.get(); + super(initialValue); } @Override - public final Field getFieldType() { + public Field getFieldType() { return Field.HEALTH; } } diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java index 95ef19626b..8e2e41ab7c 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java @@ -292,24 +292,6 @@ void verifyHealthSetter() { assertThat(takeRequest()).isNull(); } - @Test - void verifyHealthStateUpdate() { - initializeClient(); - ComponentHealth health = - new ComponentHealth.Builder() - .healthy(false) - .last_error("failed") - .status("failed") - .status_time_unix_nano(789L) - .build(); - - enqueueServerToAgentResponse(new ServerToAgent.Builder().build()); - state.health.set(health); - state.health.notifyUpdate(); - - assertThat(getAgentToServerMessage(takeRequest()).health).isEqualTo(health); - } - @Test void onConnectionSuccessful_notifyCallback() { initializeClient(); From 6899da9f08f6d4c9f70caba54441657422897e93 Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 27 May 2026 11:29:42 +0200 Subject: [PATCH 04/13] Fixed copy-paste errors in test --- .../opamp/client/internal/impl/OpampClientImplTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java index 8e2e41ab7c..eae0d547cf 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java @@ -135,7 +135,7 @@ void verifyFieldsSent() { // Verify only changed and required fields are present assertThat(secondMessage.instance_uid).isNotNull(); assertThat(secondMessage.sequence_num).isEqualTo(2); - assertThat(firstMessage.capabilities).isEqualTo(state.capabilities.get()); + assertThat(secondMessage.capabilities).isEqualTo(state.capabilities.get()); assertThat(secondMessage.agent_description).isNull(); assertThat(secondMessage.health).isNull(); assertThat(secondMessage.effective_config).isNull(); @@ -156,7 +156,7 @@ void verifyFieldsSent() { assertThat(thirdMessage.instance_uid).isNotNull(); assertThat(thirdMessage.sequence_num).isEqualTo(3); - assertThat(firstMessage.capabilities).isEqualTo(state.capabilities.get()); + assertThat(thirdMessage.capabilities).isEqualTo(state.capabilities.get()); assertThat(thirdMessage.agent_description).isNull(); assertThat(thirdMessage.health).isNull(); assertThat(thirdMessage.remote_config_status).isNull(); From 3888bc791a8c34a444fa9e3a0d5b1e8c7a6abbb4 Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 27 May 2026 11:44:02 +0200 Subject: [PATCH 05/13] Test readability improved --- .../opamp/client/internal/impl/OpampClientImplTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java index eae0d547cf..3138de0e70 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java @@ -92,7 +92,9 @@ void setUp() { new State.SequenceNum(1L), new State.AgentDescription(new AgentDescription.Builder().build()), new State.Capabilities( - 5L | AgentCapabilities.AgentCapabilities_ReportsHealth.getValue()), + (long) (AgentCapabilities.AgentCapabilities_ReportsStatus.getValue() + | AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig.getValue() + | AgentCapabilities.AgentCapabilities_ReportsHealth.getValue())), new State.Health(initialHealth), new State.InstanceUid(new byte[] {1, 2, 3}), new State.Flags((long) AgentToServerFlags.AgentToServerFlags_Unspecified.getValue()), From c40443a784c606b8d8a604a3dc03ad18b4d3579e Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 27 May 2026 11:46:57 +0200 Subject: [PATCH 06/13] Test readability improved --- .../opamp/client/internal/impl/OpampClientImplTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java index 3138de0e70..790b88438d 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java @@ -92,9 +92,10 @@ void setUp() { new State.SequenceNum(1L), new State.AgentDescription(new AgentDescription.Builder().build()), new State.Capabilities( - (long) (AgentCapabilities.AgentCapabilities_ReportsStatus.getValue() - | AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig.getValue() - | AgentCapabilities.AgentCapabilities_ReportsHealth.getValue())), + (long) + (AgentCapabilities.AgentCapabilities_ReportsStatus.getValue() + | AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig.getValue() + | AgentCapabilities.AgentCapabilities_ReportsHealth.getValue())), new State.Health(initialHealth), new State.InstanceUid(new byte[] {1, 2, 3}), new State.Flags((long) AgentToServerFlags.AgentToServerFlags_Unspecified.getValue()), From 846f6086af059919a18ce5323eb2aaef363062c1 Mon Sep 17 00:00:00 2001 From: Robert Niedziela <175605712+robsunday@users.noreply.github.com> Date: Wed, 27 May 2026 12:05:35 +0200 Subject: [PATCH 07/13] Start time precision improved Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/io/opentelemetry/opamp/client/OpampClientBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java index 4fcdf6dd5c..613ef297af 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java @@ -449,8 +449,9 @@ public opamp.proto.EffectiveConfig get() { } private static ComponentHealth createInitialHealth() { + Instant instant = Instant.now(); return new ComponentHealth.Builder() - .start_time_unix_nano(Instant.now().toEpochMilli() * 1_000_000L) + .start_time_unix_nano(instant.getEpochSecond() * 1_000_000_000L + instant.getNano()) .build(); } From e62c8f28082b6fe34297eec4e31f413f5a18ab5e Mon Sep 17 00:00:00 2001 From: robsunday Date: Wed, 27 May 2026 12:47:36 +0200 Subject: [PATCH 08/13] Code review followup --- .../java/io/opentelemetry/opamp/client/OpampClientBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java index 613ef297af..b3a3a3429f 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import opamp.proto.AgentCapabilities; import opamp.proto.AgentDescription; @@ -400,7 +401,7 @@ public OpampClientBuilder setEffectiveConfigState(State.EffectiveConfig effectiv * @return this */ @CanIgnoreReturnValue - public OpampClientBuilder setHealth(ComponentHealth health) { + public OpampClientBuilder setHealth(@Nonnull ComponentHealth health) { this.health = health; return this; } From 4f2c5ad1dbca7140471cd90be5d404eb918d281b Mon Sep 17 00:00:00 2001 From: robsunday Date: Thu, 28 May 2026 18:31:04 +0200 Subject: [PATCH 09/13] Code review followup --- .../opamp/client/OpampClientBuilder.java | 54 ++++++------------- .../client/internal/impl/OpampClientImpl.java | 4 +- .../client/internal/state/InMemoryState.java | 13 ++--- .../opamp/client/internal/state/State.java | 3 +- 4 files changed, 25 insertions(+), 49 deletions(-) diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java index b3a3a3429f..703c3bb9a0 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java @@ -15,11 +15,11 @@ import io.opentelemetry.opamp.client.internal.state.State; import io.opentelemetry.opamp.client.request.service.RequestService; import java.nio.ByteBuffer; -import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -40,7 +40,7 @@ public final class OpampClientBuilder { HttpRequestService.create(OkHttpSender.create("http://localhost:4320/v1/opamp")); @Nullable private byte[] instanceUid; @Nullable private State.EffectiveConfig effectiveConfigState; - @Nullable private ComponentHealth health; + @Nullable private ComponentHealth initialHealth; OpampClientBuilder() {} @@ -345,10 +345,8 @@ public OpampClientBuilder putNonIdentifyingAttribute(String key, double... value */ @CanIgnoreReturnValue public OpampClientBuilder enableRemoteConfig() { - capabilities = - capabilities - | AgentCapabilities.AgentCapabilities_AcceptsRemoteConfig.getValue() - | AgentCapabilities.AgentCapabilities_ReportsRemoteConfig.getValue(); + enableCapability(AgentCapabilities.AgentCapabilities_AcceptsRemoteConfig); + enableCapability(AgentCapabilities.AgentCapabilities_ReportsRemoteConfig); return this; } @@ -361,21 +359,7 @@ public OpampClientBuilder enableRemoteConfig() { */ @CanIgnoreReturnValue public OpampClientBuilder enableEffectiveConfigReporting() { - capabilities = - capabilities | AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig.getValue(); - return this; - } - - /** - * Adds the ReportsHealth capability to the Client so that the Server expects the Client's health - * status report, as explained here. - * - * @return this - */ - @CanIgnoreReturnValue - public OpampClientBuilder enableHealthReporting() { - capabilities = capabilities | AgentCapabilities.AgentCapabilities_ReportsHealth.getValue(); + enableCapability(AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig); return this; } @@ -394,15 +378,17 @@ public OpampClientBuilder setEffectiveConfigState(State.EffectiveConfig effectiv } /** - * Sets the health object that is then passed to the corresponding state implementation. Use - * {@link #enableHealthReporting()} to add the ReportsHealth capability. + * Enables health reporting and sets the initial health object that is then passed to the + * corresponding state implementation. + * This method automatically enables health reporting capability. * - * @param health The component health + * @param initialHealth The component health * @return this */ @CanIgnoreReturnValue - public OpampClientBuilder setHealth(@Nonnull ComponentHealth health) { - this.health = health; + public OpampClientBuilder enableHealthReporting(@Nonnull ComponentHealth initialHealth) { + this.initialHealth = Objects.requireNonNull(initialHealth); + enableCapability(AgentCapabilities.AgentCapabilities_ReportsHealth); return this; } @@ -419,9 +405,6 @@ public OpampClient build(OpampClient.Callbacks callbacks) { if (effectiveConfigState == null) { effectiveConfigState = createEffectiveConfigNoop(); } - if (health == null) { - health = createInitialHealth(); - } OpampClientState state = new OpampClientState( new State.RemoteConfigStatus(new RemoteConfigStatus.Builder().build()), @@ -432,13 +415,17 @@ public OpampClient build(OpampClient.Callbacks callbacks) { .non_identifying_attributes(protoNonIdentifyingAttributes) .build()), new State.Capabilities(capabilities), - new State.Health(health), + new State.Health(initialHealth), new State.InstanceUid(instanceUid), new State.Flags(0L), effectiveConfigState); return OpampClientImpl.create(service, state, callbacks); } + private void enableCapability(AgentCapabilities capability) { + capabilities = capabilities | capability.getValue(); + } + private static State.EffectiveConfig createEffectiveConfigNoop() { return new State.EffectiveConfig() { @Nullable @@ -449,13 +436,6 @@ public opamp.proto.EffectiveConfig get() { }; } - private static ComponentHealth createInitialHealth() { - Instant instant = Instant.now(); - return new ComponentHealth.Builder() - .start_time_unix_nano(instant.getEpochSecond() * 1_000_000_000L + instant.getNano()) - .build(); - } - private static AnyValue createStringValue(String value) { return new AnyValue.Builder().string_value(value).build(); } diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java index 1525e52181..3cae1bb13f 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java @@ -135,7 +135,7 @@ public void close() { @Override public void setAgentDescription(@Nonnull AgentDescription agentDescription) { - if (!state.agentDescription.get().equals(agentDescription)) { + if (!state.agentDescription.mustGet().equals(agentDescription)) { state.agentDescription.set(agentDescription); addFieldAndSend(Field.AGENT_DESCRIPTION); } @@ -143,7 +143,7 @@ public void setAgentDescription(@Nonnull AgentDescription agentDescription) { @Override public void setRemoteConfigStatus(@Nonnull RemoteConfigStatus remoteConfigStatus) { - if (!state.remoteConfigStatus.get().equals(remoteConfigStatus)) { + if (!state.remoteConfigStatus.mustGet().equals(remoteConfigStatus)) { state.remoteConfigStatus.set(remoteConfigStatus); addFieldAndSend(Field.REMOTE_CONFIG_STATUS); } diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java index be2d34215b..c9836cbf44 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java @@ -5,10 +5,8 @@ package io.opentelemetry.opamp.client.internal.state; -import static java.util.Objects.requireNonNull; - import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -17,10 +15,7 @@ abstract class InMemoryState implements State { private final AtomicReference state = new AtomicReference<>(); - public InMemoryState(T initialValue) { - if (initialValue == null) { - throw new IllegalArgumentException("The value must not be null"); - } + public InMemoryState(@Nullable T initialValue) { state.set(initialValue); } @@ -31,9 +26,9 @@ public void set(T value) { state.set(value); } - @Nonnull + @Nullable @Override public T get() { - return requireNonNull(state.get()); + return state.get(); } } diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java index 11a30fd573..09053b216c 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/State.java @@ -9,6 +9,7 @@ import java.util.Objects; import java.util.function.Supplier; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import opamp.proto.ComponentHealth; /** @@ -77,7 +78,7 @@ public Field getFieldType() { } final class Health extends InMemoryState { - public Health(@Nonnull ComponentHealth initialValue) { + public Health(@Nullable ComponentHealth initialValue) { super(initialValue); } From 0b3098a1f44e3da25675f7f53d1beddff1f9f6f8 Mon Sep 17 00:00:00 2001 From: robsunday Date: Thu, 28 May 2026 18:33:39 +0200 Subject: [PATCH 10/13] Spotless --- .../java/io/opentelemetry/opamp/client/OpampClientBuilder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java index 703c3bb9a0..c9ef52df9e 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/OpampClientBuilder.java @@ -380,7 +380,6 @@ public OpampClientBuilder setEffectiveConfigState(State.EffectiveConfig effectiv /** * Enables health reporting and sets the initial health object that is then passed to the * corresponding state implementation. - * This method automatically enables health reporting capability. * * @param initialHealth The component health * @return this From 7e0dbe4bd0fe82509bb07d77f2f1a83a7f22da5a Mon Sep 17 00:00:00 2001 From: robsunday Date: Fri, 29 May 2026 13:39:27 +0200 Subject: [PATCH 11/13] Thread safe setter returning boolean --- .../opamp/client/internal/impl/OpampClientImpl.java | 10 +++------- .../opamp/client/internal/state/InMemoryState.java | 13 +++++++++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java index 3cae1bb13f..2363cfca4e 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import javax.annotation.Nonnull; @@ -135,24 +134,21 @@ public void close() { @Override public void setAgentDescription(@Nonnull AgentDescription agentDescription) { - if (!state.agentDescription.mustGet().equals(agentDescription)) { - state.agentDescription.set(agentDescription); + if (state.agentDescription.set(agentDescription)) { addFieldAndSend(Field.AGENT_DESCRIPTION); } } @Override public void setRemoteConfigStatus(@Nonnull RemoteConfigStatus remoteConfigStatus) { - if (!state.remoteConfigStatus.mustGet().equals(remoteConfigStatus)) { - state.remoteConfigStatus.set(remoteConfigStatus); + if (state.remoteConfigStatus.set(remoteConfigStatus)) { addFieldAndSend(Field.REMOTE_CONFIG_STATUS); } } @Override public void setHealth(@Nonnull ComponentHealth health) { - if (!Objects.equals(state.health.get(), health)) { - state.health.set(health); + if (state.health.set(health)) { addFieldAndSend(Field.HEALTH); } } diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java index c9836cbf44..557be19843 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java @@ -5,6 +5,7 @@ package io.opentelemetry.opamp.client.internal.state; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; @@ -19,11 +20,19 @@ public InMemoryState(@Nullable T initialValue) { state.set(initialValue); } - public void set(T value) { + /** + * Set a new state's value in a thread safe way. + * + * @param value a new value stored in the state + * @return true if new value is different that the previous one, false + * otherwise. + */ + public boolean set(T value) { if (value == null) { throw new IllegalArgumentException("The value must not be null"); } - state.set(value); + T previousValue = state.getAndSet(value); + return !Objects.equals(previousValue, value); } @Nullable From 9b7a8e91cde2bb30a8a729a71d9abb9fb6fee0b5 Mon Sep 17 00:00:00 2001 From: robsunday Date: Tue, 9 Jun 2026 16:05:37 +0200 Subject: [PATCH 12/13] OpampClient setters check capabilities --- .../client/internal/impl/OpampClientImpl.java | 10 +++ .../internal/impl/OpampClientImplTest.java | 66 +++++++++++++------ 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java index 2363cfca4e..07cef7af8f 100644 --- a/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java +++ b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImpl.java @@ -33,6 +33,7 @@ import java.util.function.Supplier; import javax.annotation.Nonnull; import okio.ByteString; +import opamp.proto.AgentCapabilities; import opamp.proto.AgentDescription; import opamp.proto.AgentToServer; import opamp.proto.ComponentHealth; @@ -141,6 +142,7 @@ public void setAgentDescription(@Nonnull AgentDescription agentDescription) { @Override public void setRemoteConfigStatus(@Nonnull RemoteConfigStatus remoteConfigStatus) { + verifyCapability(AgentCapabilities.AgentCapabilities_ReportsRemoteConfig); if (state.remoteConfigStatus.set(remoteConfigStatus)) { addFieldAndSend(Field.REMOTE_CONFIG_STATUS); } @@ -148,6 +150,7 @@ public void setRemoteConfigStatus(@Nonnull RemoteConfigStatus remoteConfigStatus @Override public void setHealth(@Nonnull ComponentHealth health) { + verifyCapability(AgentCapabilities.AgentCapabilities_ReportsHealth); if (state.health.set(health)) { addFieldAndSend(Field.HEALTH); } @@ -261,4 +264,11 @@ private void addFieldAndSend(Field field) { recipeManager.next().addField(field); requestService.sendRequest(); } + + private void verifyCapability(AgentCapabilities capabilityToCheck) { + if ((state.capabilities.mustGet() & capabilityToCheck.getValue()) == 0) { + throw new IllegalStateException( + "Required capability " + capabilityToCheck + " is not enabled"); + } + } } diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java index 790b88438d..a3ecdb4148 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java @@ -7,6 +7,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; @@ -54,6 +55,7 @@ import opamp.proto.ServerErrorResponse; import opamp.proto.ServerToAgent; import opamp.proto.ServerToAgentFlags; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -85,21 +87,6 @@ void setUp() { .status("running") .status_time_unix_nano(456L) .build(); - state = - new OpampClientState( - new State.RemoteConfigStatus( - getRemoteConfigStatus(RemoteConfigStatuses.RemoteConfigStatuses_UNSET)), - new State.SequenceNum(1L), - new State.AgentDescription(new AgentDescription.Builder().build()), - new State.Capabilities( - (long) - (AgentCapabilities.AgentCapabilities_ReportsStatus.getValue() - | AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig.getValue() - | AgentCapabilities.AgentCapabilities_ReportsHealth.getValue())), - new State.Health(initialHealth), - new State.InstanceUid(new byte[] {1, 2, 3}), - new State.Flags((long) AgentToServerFlags.AgentToServerFlags_Unspecified.getValue()), - effectiveConfig); requestService = createHttpService(); } @@ -112,7 +99,13 @@ void tearDown() { void verifyFieldsSent() { // Check first request ServerToAgent response = new ServerToAgent.Builder().build(); - RecordedRequest firstRequest = initializeClient(response); + RecordedRequest firstRequest = + initializeClient( + response, + AgentCapabilities.AgentCapabilities_ReportsStatus, + AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig, + AgentCapabilities.AgentCapabilities_ReportsRemoteConfig, + AgentCapabilities.AgentCapabilities_ReportsHealth); AgentToServer firstMessage = getAgentToServerMessage(firstRequest); // Required first request fields @@ -257,7 +250,7 @@ void verifyAgentDescriptionSetter() { @Test void verifyRemoteConfigStatusSetter() { - initializeClient(); + initializeClient(AgentCapabilities.AgentCapabilities_ReportsRemoteConfig); RemoteConfigStatus remoteConfigStatus = getRemoteConfigStatus(RemoteConfigStatuses.RemoteConfigStatuses_APPLYING); @@ -274,7 +267,7 @@ void verifyRemoteConfigStatusSetter() { @Test void verifyHealthSetter() { - initializeClient(); + initializeClient(AgentCapabilities.AgentCapabilities_ReportsHealth); ComponentHealth health = new ComponentHealth.Builder() .healthy(false) @@ -295,6 +288,16 @@ void verifyHealthSetter() { assertThat(takeRequest()).isNull(); } + @Test + void verifyHealthSetter_throwsExceptionWhenCapabilityNotEnabled() { + initializeClient(); + ComponentHealth health = new ComponentHealth.Builder().build(); + + Exception exception = assertThrows(IllegalStateException.class, () -> client.setHealth(health)); + assertThat(exception.getMessage()) + .contains(AgentCapabilities.AgentCapabilities_ReportsHealth.toString()); + } + @Test void onConnectionSuccessful_notifyCallback() { initializeClient(); @@ -381,6 +384,25 @@ void whenServerProvidesNewInstanceUid_useIt() { assertThat(state.instanceUid.get()).isEqualTo(serverProvidedUid); } + @NotNull + private OpampClientState createState(AgentCapabilities... enabledCapabilities) { + long capabilities = 0; + for (AgentCapabilities capability : enabledCapabilities) { + capabilities |= capability.getValue(); + } + + return new OpampClientState( + new State.RemoteConfigStatus( + getRemoteConfigStatus(RemoteConfigStatuses.RemoteConfigStatuses_UNSET)), + new State.SequenceNum(1L), + new State.AgentDescription(new AgentDescription.Builder().build()), + new State.Capabilities(capabilities), + new State.Health(initialHealth), + new State.InstanceUid(new byte[] {1, 2, 3}), + new State.Flags((long) AgentToServerFlags.AgentToServerFlags_Unspecified.getValue()), + effectiveConfig); + } + private static AgentToServer getAgentToServerMessage(RecordedRequest request) { try { return AgentToServer.ADAPTER.decode(Objects.requireNonNull(request.getBody())); @@ -430,14 +452,16 @@ private static AgentDescription getAgentDescriptionWithOneIdentifyingValue( return new AgentDescription.Builder().identifying_attributes(keyValues).build(); } - private RecordedRequest initializeClient() { - return initializeClient(new ServerToAgent.Builder().build()); + private RecordedRequest initializeClient(AgentCapabilities... enabledCapabilities) { + return initializeClient(new ServerToAgent.Builder().build(), enabledCapabilities); } - private RecordedRequest initializeClient(ServerToAgent initialResponse) { + private RecordedRequest initializeClient( + ServerToAgent initialResponse, AgentCapabilities... enabledCapabilities) { // Prepare first request on start enqueueServerToAgentResponse(initialResponse); + state = createState(enabledCapabilities); callbacks = spy(new TestCallbacks()); client = OpampClientImpl.create(requestService, state, callbacks); From 712c534a87c2f3a620437a0eca4a002500f1d4dd Mon Sep 17 00:00:00 2001 From: robsunday Date: Tue, 9 Jun 2026 16:18:56 +0200 Subject: [PATCH 13/13] Test case added --- .../client/internal/impl/OpampClientImplTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java index a3ecdb4148..500b4bb919 100644 --- a/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java +++ b/opamp-client/src/test/java/io/opentelemetry/opamp/client/internal/impl/OpampClientImplTest.java @@ -265,6 +265,19 @@ void verifyRemoteConfigStatusSetter() { assertThat(takeRequest()).isNull(); } + @Test + void verifyRemoteConfigStatusSetter_throwsExceptionWhenCapabilityNotEnabled() { + initializeClient(); + RemoteConfigStatus remoteConfigStatus = + getRemoteConfigStatus(RemoteConfigStatuses.RemoteConfigStatuses_APPLYING); + + Exception exception = + assertThrows( + IllegalStateException.class, () -> client.setRemoteConfigStatus(remoteConfigStatus)); + assertThat(exception.getMessage()) + .contains(AgentCapabilities.AgentCapabilities_ReportsRemoteConfig.toString()); + } + @Test void verifyHealthSetter() { initializeClient(AgentCapabilities.AgentCapabilities_ReportsHealth);