Skip to content

Commit

Permalink
Provide component status next to details
Browse files Browse the repository at this point in the history
Adjust the AxonServerHealthIndicator to share a status field as well.
This status field should include the additional status "WARN" for when
one of the contexts the app is connected with, is down. To support this
thoroughly, a StatusAggregate should be included.

#1964
  • Loading branch information
smcvb committed Mar 9, 2022
1 parent f453a74 commit 1c558ec
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2010-2022. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.axonframework.actuator;

import org.springframework.boot.actuate.health.Status;

/**
* Utility class holding additional {@link Status} instances.
*
* @author Steven van Beelen
* @author Marc Gathier
* @since 4.6.0
*/
public abstract class HealthStatus {

/**
* A {@link Status} suggesting the connection is still working but not at full capacity.
*/
public static final Status WARN = new Status("WARN");
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@
* limitations under the License.
*/

package org.axonframework.actuator;
package org.axonframework.actuator.axonserver;

import org.axonframework.actuator.HealthStatus;
import org.axonframework.axonserver.connector.AxonServerConnectionManager;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;

import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* An {@link AbstractHealthIndicator} implementation exposing the health of the connections made through the {@link
* AxonServerConnectionManager}. Shares information per connected context under {@code
* "{context-name}.connection.active"}.
* AxonServerConnectionManager}. This status is exposed through the {@code "axonServer"} component.
* <p>
* The status is regarded as {@link Status#UP} if <b>all</b> {@link AxonServerConnectionManager#connections()} are up.
* If one of them is down, the status is {@link HealthStatus#WARN}. If all of them are down the status will be {@link
* Status#DOWN}. This {@link org.springframework.boot.actuate.health.HealthIndicator} also shares connection details per
* context under {@code "{context-name}.connection.active"}.
*
* @author Steven van Beelen
* @since 4.6.0
Expand All @@ -45,7 +54,23 @@ public AxonServerHealthIndicator(AxonServerConnectionManager connectionManager)

@Override
protected void doHealthCheck(Health.Builder builder) {
connectionManager.connections()
.forEach((key, value) -> builder.withDetail(String.format(CONNECTION, key), value));
builder.up();

AtomicBoolean anyConnectionUp = new AtomicBoolean(false);
Map<String, Boolean> connections = connectionManager.connections();

connections.forEach((context, connectionStatus) -> {
if (!connectionStatus) {
builder.status(HealthStatus.WARN);
} else {
anyConnectionUp.compareAndSet(false, true);
}
builder.withDetail(String.format(CONNECTION, context),
connectionStatus ? Status.UP.getCode() : Status.DOWN.getCode());
});

if (!connections.isEmpty() && !anyConnectionUp.get()) {
builder.down();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2010-2022. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.axonframework.actuator.axonserver;

import org.axonframework.actuator.HealthStatus;
import org.springframework.boot.actuate.health.SimpleStatusAggregator;
import org.springframework.boot.actuate.health.Status;

/**
* A {@link SimpleStatusAggregator} implementation determining the overall health of an Axon Framework application using
* Axon Server. Adds the {@link HealthStatus#WARN} status to the regular set of {@link Status statuses}.
*
* @author Steven van Beelen
* @author Marc Gathier
* @since 4.6.0
*/
public class AxonServerStatusAggregator extends SimpleStatusAggregator {

/**
* Constructs a default Axon Server specific {@link SimpleStatusAggregator}. Adds the {@link HealthStatus#WARN}
* after {@link Status#OUT_OF_SERVICE} and before {@link Status#UP}.
*/
public AxonServerStatusAggregator() {
super(Status.DOWN, Status.OUT_OF_SERVICE, HealthStatus.WARN, Status.UP, Status.UNKNOWN);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

package org.axonframework.springboot.autoconfig;

import org.axonframework.actuator.AxonServerHealthIndicator;
import org.axonframework.actuator.axonserver.AxonServerHealthIndicator;
import org.axonframework.actuator.axonserver.AxonServerStatusAggregator;
import org.axonframework.axonserver.connector.AxonServerConnectionManager;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Expand All @@ -41,4 +42,9 @@ public class AxonServerActuatorAutoConfiguration {
public AxonServerHealthIndicator axonServerHealthIndicator(AxonServerConnectionManager connectionManager) {
return new AxonServerHealthIndicator(connectionManager);
}

@Bean
public AxonServerStatusAggregator axonServerStatusAggregator() {
return new AxonServerStatusAggregator();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright (c) 2010-2022. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.axonframework.actuator.axonserver;

import org.axonframework.actuator.HealthStatus;
import org.axonframework.axonserver.connector.AxonServerConnectionManager;
import org.junit.jupiter.api.*;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
* Test class validating the {@link AxonServerHealthIndicator}.
*
* @author Steven van Beelen
*/
class AxonServerHealthIndicatorTest {

private AxonServerConnectionManager connectionManager;

private AxonServerHealthIndicator testSubject;

@BeforeEach
void setUp() {
connectionManager = mock(AxonServerConnectionManager.class);
testSubject = new AxonServerHealthIndicator(connectionManager);
}

@Test
void testDoHealthCheckStatusReturnsUp() {
String testContextOne = "context-one";
String testContextTwo = "context-two";
String expectedDetailsContextOne = testContextOne + ".connection.active";
String expectedDetailsContextTwo = testContextTwo + ".connection.active";

Map<String, Boolean> testConnections = new HashMap<>();
testConnections.put(testContextOne, true);
testConnections.put(testContextTwo, true);
when(connectionManager.connections()).thenReturn(testConnections);

Health.Builder healthBuilder = new Health.Builder();

testSubject.doHealthCheck(healthBuilder);

Health result = healthBuilder.build();
assertEquals(Status.UP.getCode(), result.getStatus().getCode());

Map<String, Object> resultDetails = result.getDetails();
assertFalse(resultDetails.isEmpty());
assertEquals(2, resultDetails.size());
String detailsContextOne = (String) resultDetails.get(expectedDetailsContextOne);
assertNotNull(detailsContextOne);
assertEquals(Status.UP.getCode(), detailsContextOne);
String detailsContextTwo = (String) resultDetails.get(expectedDetailsContextTwo);
assertNotNull(detailsContextTwo);
assertEquals(Status.UP.getCode(), detailsContextTwo);
}

@Test
void testDoHealthCheckStatusReturnsWarning() {
String testContextOne = "context-one";
String testContextTwo = "context-two";
String expectedDetailsContextOne = testContextOne + ".connection.active";
String expectedDetailsContextTwo = testContextTwo + ".connection.active";

Map<String, Boolean> testConnections = new HashMap<>();
testConnections.put(testContextOne, true);
testConnections.put(testContextTwo, false);
when(connectionManager.connections()).thenReturn(testConnections);

Health.Builder healthBuilder = new Health.Builder();

testSubject.doHealthCheck(healthBuilder);

Health result = healthBuilder.build();
assertEquals(HealthStatus.WARN, result.getStatus());

Map<String, Object> resultDetails = result.getDetails();
assertFalse(resultDetails.isEmpty());
assertEquals(2, resultDetails.size());
String detailsContextOne = (String) resultDetails.get(expectedDetailsContextOne);
assertNotNull(detailsContextOne);
assertEquals(Status.UP.getCode(), detailsContextOne);
String detailsContextTwo = (String) resultDetails.get(expectedDetailsContextTwo);
assertNotNull(detailsContextTwo);
assertEquals(Status.DOWN.getCode(), detailsContextTwo);
}

@Test
void testDoHealthCheckStatusReturnsDown() {
String testContextOne = "context-one";
String testContextTwo = "context-two";
String expectedDetailsContextOne = testContextOne + ".connection.active";
String expectedDetailsContextTwo = testContextTwo + ".connection.active";

Map<String, Boolean> testConnections = new HashMap<>();
testConnections.put(testContextOne, false);
testConnections.put(testContextTwo, false);

when(connectionManager.connections()).thenReturn(testConnections);

Health.Builder healthBuilder = new Health.Builder();

testSubject.doHealthCheck(healthBuilder);

Health result = healthBuilder.build();
assertEquals(Status.DOWN.getCode(), result.getStatus().getCode());

Map<String, Object> resultDetails = result.getDetails();
assertFalse(resultDetails.isEmpty());
assertEquals(2, resultDetails.size());
String detailsContextOne = (String) resultDetails.get(expectedDetailsContextOne);
assertNotNull(detailsContextOne);
assertEquals(Status.DOWN.getCode(), detailsContextOne);
String detailsContextTwo = (String) resultDetails.get(expectedDetailsContextTwo);
assertNotNull(detailsContextTwo);
assertEquals(Status.DOWN.getCode(), detailsContextTwo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

package org.axonframework.springboot.autoconfig;

import org.axonframework.actuator.AxonServerHealthIndicator;
import org.axonframework.actuator.axonserver.AxonServerHealthIndicator;
import org.axonframework.actuator.axonserver.AxonServerStatusAggregator;
import org.junit.jupiter.api.*;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
Expand Down Expand Up @@ -44,14 +45,20 @@ void setUp() {
void testAxonServerHealthIndicatorIsNotCreatedForAxonServerDisabled() {
testApplicationContext.withUserConfiguration(TestContext.class)
.withPropertyValues("axon.axonserver.enabled:false")
.run(context -> assertThat(context).doesNotHaveBean(AxonServerHealthIndicator.class));
.run(context -> {
assertThat(context).doesNotHaveBean(AxonServerHealthIndicator.class);
assertThat(context).doesNotHaveBean(AxonServerStatusAggregator.class);
});
}

@Test
void testAxonServerHealthIndicatorIsCreated() {
testApplicationContext.withUserConfiguration(TestContext.class)
.withPropertyValues("axon.axonserver.enabled:true")
.run(context -> assertThat(context).hasSingleBean(AxonServerHealthIndicator.class));
.run(context -> {
assertThat(context).hasSingleBean(AxonServerHealthIndicator.class);
assertThat(context).hasSingleBean(AxonServerStatusAggregator.class);
});
}

@ContextConfiguration
Expand Down

0 comments on commit 1c558ec

Please sign in to comment.