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..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 @@ -19,12 +19,15 @@ 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; import opamp.proto.AgentCapabilities; 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,6 +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 initialHealth; OpampClientBuilder() {} @@ -341,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; } @@ -357,8 +359,7 @@ public OpampClientBuilder enableRemoteConfig() { */ @CanIgnoreReturnValue public OpampClientBuilder enableEffectiveConfigReporting() { - capabilities = - capabilities | AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig.getValue(); + enableCapability(AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig); return this; } @@ -376,6 +377,20 @@ public OpampClientBuilder setEffectiveConfigState(State.EffectiveConfig effectiv return this; } + /** + * Enables health reporting and sets the initial health object that is then passed to the + * corresponding state implementation. + * + * @param initialHealth The component health + * @return this + */ + @CanIgnoreReturnValue + public OpampClientBuilder enableHealthReporting(@Nonnull ComponentHealth initialHealth) { + this.initialHealth = Objects.requireNonNull(initialHealth); + enableCapability(AgentCapabilities.AgentCapabilities_ReportsHealth); + return this; + } + public OpampClient build(OpampClient.Callbacks callbacks) { List protoIdentifyingAttributes = new ArrayList<>(); List protoNonIdentifyingAttributes = new ArrayList<>(); @@ -399,12 +414,17 @@ public OpampClient build(OpampClient.Callbacks callbacks) { .non_identifying_attributes(protoNonIdentifyingAttributes) .build()), new State.Capabilities(capabilities), + 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 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..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 @@ -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; @@ -32,8 +33,10 @@ 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; 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()); @@ -130,20 +135,27 @@ public void close() { @Override public void setAgentDescription(@Nonnull AgentDescription agentDescription) { - if (!state.agentDescription.get().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.get().equals(remoteConfigStatus)) { - state.remoteConfigStatus.set(remoteConfigStatus); + verifyCapability(AgentCapabilities.AgentCapabilities_ReportsRemoteConfig); + if (state.remoteConfigStatus.set(remoteConfigStatus)) { addFieldAndSend(Field.REMOTE_CONFIG_STATUS); } } + @Override + public void setHealth(@Nonnull ComponentHealth health) { + verifyCapability(AgentCapabilities.AgentCapabilities_ReportsHealth); + if (state.health.set(health)) { + addFieldAndSend(Field.HEALTH); + } + } + @Override public void onConnectionSuccess() { callbacks.onConnect(this); @@ -252,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/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/InMemoryState.java b/opamp-client/src/main/java/io/opentelemetry/opamp/client/internal/state/InMemoryState.java index be2d34215b..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,10 +5,9 @@ package io.opentelemetry.opamp.client.internal.state; -import static java.util.Objects.requireNonNull; - +import java.util.Objects; 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,23 +16,28 @@ 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); } - 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); } - @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 abb0a6a9db..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,8 @@ import java.util.Objects; 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 +77,17 @@ public Field getFieldType() { } } + final class Health extends InMemoryState { + public Health(@Nullable ComponentHealth initialValue) { + super(initialValue); + } + + @Override + public 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..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 @@ -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; @@ -37,6 +38,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 +47,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; @@ -52,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; @@ -65,6 +69,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,16 +80,13 @@ void setUp() { new EffectiveConfig.Builder() .config_map(createAgentConfigMap("first", "first content")) .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.InstanceUid(new byte[] {1, 2, 3}), - new State.Flags((long) AgentToServerFlags.AgentToServerFlags_Unspecified.getValue()), - effectiveConfig); + initialHealth = + new ComponentHealth.Builder() + .healthy(true) + .start_time_unix_nano(123L) + .status("running") + .status_time_unix_nano(456L) + .build(); requestService = createHttpService(); } @@ -97,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 @@ -105,6 +113,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()); @@ -122,8 +131,9 @@ 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(); assertThat(secondMessage.remote_config_status).isEqualTo(remoteConfigStatus); @@ -142,8 +152,9 @@ 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(); assertThat(thirdMessage.effective_config) .isEqualTo(otherConfig); // it was changed via observable state @@ -169,6 +180,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()); } @@ -238,7 +250,7 @@ void verifyAgentDescriptionSetter() { @Test void verifyRemoteConfigStatusSetter() { - initializeClient(); + initializeClient(AgentCapabilities.AgentCapabilities_ReportsRemoteConfig); RemoteConfigStatus remoteConfigStatus = getRemoteConfigStatus(RemoteConfigStatuses.RemoteConfigStatuses_APPLYING); @@ -253,6 +265,52 @@ 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); + 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 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(); @@ -339,6 +397,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())); @@ -388,14 +465,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); 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..73d38f8526 --- /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 shouldAppendProvidedHealth() { + 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 shouldNotAppendHealthIfNotProvided() { + HealthAppender appender = HealthAppender.create(new AtomicReference()::get); + + AgentToServer.Builder builder = new AgentToServer.Builder(); + appender.appendTo(builder); + + assertThat(builder.build().health).isNull(); + } +}