Rework HealthEndpoint and HealthIndicator
With this commit the state of a component or subsystem becomes a first-class citizen in Boot's application health support. HealthIndicators now return a Health instance with status and some contextual details. An aggregation strategy has been introduced to aggregate several Health instances into one final application Health instance. Out of the box OrderedHealthAggregator can be configured to allow different ordering or a custom HealthAggregator bean can be registered.pull/931/merge
parent
d59cbc830a
commit
4648188782
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2014 the original author or authors.
|
||||||
|
*
|
||||||
|
* 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.springframework.boot.actuate.health;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value object used to carry information about the health information of a component or
|
||||||
|
* subsystem.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@link Health} contains a {@link Status} to express the state of a component or
|
||||||
|
* subsystem and some additional details to carry some contextual information.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@link Health} has a fluent API to make it easy to construct instances. Typical usage
|
||||||
|
* in a {@link HealthIndicator} would be:
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* Health health = new Health();
|
||||||
|
* try {
|
||||||
|
* // do some test to determine state of component
|
||||||
|
*
|
||||||
|
* health.up().withDetail("version", "1.1.2");
|
||||||
|
* }
|
||||||
|
* catch (Exception ex) {
|
||||||
|
* health.down().withException(ex);
|
||||||
|
* }
|
||||||
|
* return health;
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @author Christian Dupuis
|
||||||
|
* @since 1.1.0
|
||||||
|
*/
|
||||||
|
@JsonInclude(Include.NON_EMPTY)
|
||||||
|
public class Health {
|
||||||
|
|
||||||
|
private Status status;
|
||||||
|
|
||||||
|
private Map<String, Object> details;
|
||||||
|
|
||||||
|
public Health() {
|
||||||
|
this(Status.UNKOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Health(Status status) {
|
||||||
|
this.status = status;
|
||||||
|
this.details = new LinkedHashMap<String, Object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Health status(Status status) {
|
||||||
|
Assert.notNull(status, "Status must not be null");
|
||||||
|
this.status = status;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Health up() {
|
||||||
|
return status(Status.UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Health down() {
|
||||||
|
return status(Status.DOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Health withException(Exception ex) {
|
||||||
|
Assert.notNull(ex, "Exception must not be null");
|
||||||
|
return withDetail("error", ex.getClass().getName() + ": " + ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonAnySetter
|
||||||
|
public Health withDetail(String key, Object data) {
|
||||||
|
Assert.notNull(key, "Key must not be null");
|
||||||
|
Assert.notNull(data, "Data must not be null");
|
||||||
|
this.details.put(key, data);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonUnwrapped
|
||||||
|
public Status getStatus() {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonAnyGetter
|
||||||
|
public Map<String, Object> getDetails() {
|
||||||
|
return this.details;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj != null && obj instanceof Health) {
|
||||||
|
return ObjectUtils.nullSafeEquals(this.status, ((Health) obj).status)
|
||||||
|
&& ObjectUtils.nullSafeEquals(this.details, ((Health) obj).details);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hashCode = 0;
|
||||||
|
if (this.status != null) {
|
||||||
|
hashCode = this.status.hashCode();
|
||||||
|
}
|
||||||
|
return 13 * hashCode + this.details.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2014 the original author or authors.
|
||||||
|
*
|
||||||
|
* 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.springframework.boot.actuate.health;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy interface used by {@link CompositeHealthIndicator} to aggregate {@link Health}
|
||||||
|
* instances into a final one.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This is especially useful to combine subsystem states expressed through
|
||||||
|
* {@link Health#getStatus()} into one state for the entire system. The default
|
||||||
|
* implementation {@link OrderedHealthAggregator} sorts {@link Status} instances based on
|
||||||
|
* a priority list.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It is possible to add more complex {@link Status} types to the system. In that case
|
||||||
|
* either the {@link OrderedHealthAggregator} needs to be properly configured or users
|
||||||
|
* need to register a custom {@link HealthAggregator} as bean.
|
||||||
|
*
|
||||||
|
* @author Christian Dupuis
|
||||||
|
* @since 1.1.0
|
||||||
|
*/
|
||||||
|
public interface HealthAggregator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregate several given {@link Health} instances into one.
|
||||||
|
*/
|
||||||
|
Health aggregate(Map<String, Health> healths);
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2014 the original author or authors.
|
||||||
|
*
|
||||||
|
* 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.springframework.boot.actuate.health;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link HealthAggregator} implementation that aggregates {@link Health}
|
||||||
|
* instances and determines the final system state based on a simple ordered list.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If a different order is required or a new {@link Status} type will be used, the order
|
||||||
|
* can be set by calling {@link #setStatusOrder(List)}.
|
||||||
|
*
|
||||||
|
* @author Christian Dupuis
|
||||||
|
* @since 1.1.0
|
||||||
|
*/
|
||||||
|
public class OrderedHealthAggregator implements HealthAggregator {
|
||||||
|
|
||||||
|
private List<String> statusOrder = Arrays.asList("DOWN", "OUT_OF_SERVICE", "UP",
|
||||||
|
"UNKOWN");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Health aggregate(Map<String, Health> healths) {
|
||||||
|
Health health = new Health();
|
||||||
|
|
||||||
|
List<Status> status = new ArrayList<Status>();
|
||||||
|
for (Map.Entry<String, Health> h : healths.entrySet()) {
|
||||||
|
health.withDetail(h.getKey(), h.getValue());
|
||||||
|
status.add(h.getValue().getStatus());
|
||||||
|
}
|
||||||
|
health.status(aggregateStatus(status));
|
||||||
|
return health;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusOrder(List<String> statusOrder) {
|
||||||
|
this.statusOrder = statusOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Status aggregateStatus(List<Status> status) {
|
||||||
|
|
||||||
|
if (status.size() == 0) {
|
||||||
|
return Status.UNKOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.sort(new Comparator<Status>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(Status s1, Status s2) {
|
||||||
|
return Integer.valueOf(
|
||||||
|
OrderedHealthAggregator.this.statusOrder.indexOf(s1.getStatus())).compareTo(
|
||||||
|
Integer.valueOf(OrderedHealthAggregator.this.statusOrder.indexOf(s2.getStatus())));
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return status.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2014 the original author or authors.
|
||||||
|
*
|
||||||
|
* 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.springframework.boot.actuate.health;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value object to express state of a component or subsystem.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Status provides convenient constants for commonly used states like {@link #UP},
|
||||||
|
* {@link #DOWN} or {@link #OUT_OF_SERVICE}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Custom states can also be created and used throughout the Spring Boot Health subsystem.
|
||||||
|
*
|
||||||
|
* @author Christian Dupuis
|
||||||
|
* @since 1.1.0
|
||||||
|
*/
|
||||||
|
@JsonInclude(Include.NON_EMPTY)
|
||||||
|
public class Status {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient constant value representing unknown state
|
||||||
|
*/
|
||||||
|
public static final Status UNKOWN = new Status("UNKOWN");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient constant value representing up state
|
||||||
|
*/
|
||||||
|
public static final Status UP = new Status("UP");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient constant value representing down state
|
||||||
|
*/
|
||||||
|
public static final Status DOWN = new Status("DOWN");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient constant value representing out-of-service state
|
||||||
|
*/
|
||||||
|
public static final Status OUT_OF_SERVICE = new Status("OUT_OF_SERVICE");
|
||||||
|
|
||||||
|
private final String status;
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
public Status(String code) {
|
||||||
|
this(code, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Status(String code, String description) {
|
||||||
|
Assert.notNull(code, "Status must not be null");
|
||||||
|
Assert.notNull(description, "Description must not be null");
|
||||||
|
this.status = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonInclude(Include.NON_EMPTY)
|
||||||
|
public String getDescription() {
|
||||||
|
return this.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj != null && obj instanceof Status) {
|
||||||
|
return ObjectUtils.nullSafeEquals(this.status, ((Status) obj).status);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.status.hashCode();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2014 the original author or authors.
|
||||||
|
*
|
||||||
|
* 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.springframework.boot.actuate.health;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link OrderedHealthAggregator}.
|
||||||
|
*
|
||||||
|
* @author Christian Dupuis
|
||||||
|
*/
|
||||||
|
public class OrderedHealthAggregatorTests {
|
||||||
|
|
||||||
|
private OrderedHealthAggregator healthAggregator;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
this.healthAggregator = new OrderedHealthAggregator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultOrdering() {
|
||||||
|
Map<String, Health> healths = new HashMap<String, Health>();
|
||||||
|
healths.put("h1", new Health(Status.DOWN));
|
||||||
|
healths.put("h2", new Health(Status.UP));
|
||||||
|
healths.put("h3", new Health(Status.UNKOWN));
|
||||||
|
healths.put("h4", new Health(Status.OUT_OF_SERVICE));
|
||||||
|
assertEquals(Status.DOWN, this.healthAggregator.aggregate(healths).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultOrderingWithCustomStatus() {
|
||||||
|
Map<String, Health> healths = new HashMap<String, Health>();
|
||||||
|
healths.put("h1", new Health(Status.DOWN));
|
||||||
|
healths.put("h2", new Health(Status.UP));
|
||||||
|
healths.put("h3", new Health(Status.UNKOWN));
|
||||||
|
healths.put("h4", new Health(Status.OUT_OF_SERVICE));
|
||||||
|
healths.put("h5", new Health(new Status("CUSTOM")));
|
||||||
|
assertEquals(new Status("CUSTOM"),
|
||||||
|
this.healthAggregator.aggregate(healths).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultOrderingWithCustomStatusAndOrder() {
|
||||||
|
this.healthAggregator.setStatusOrder(Arrays.asList("DOWN", "OUT_OF_SERVICE",
|
||||||
|
"UP", "UNKOWN", "CUSTOM"));
|
||||||
|
Map<String, Health> healths = new HashMap<String, Health>();
|
||||||
|
healths.put("h1", new Health(Status.DOWN));
|
||||||
|
healths.put("h2", new Health(Status.UP));
|
||||||
|
healths.put("h3", new Health(Status.UNKOWN));
|
||||||
|
healths.put("h4", new Health(Status.OUT_OF_SERVICE));
|
||||||
|
healths.put("h5", new Health(new Status("CUSTOM")));
|
||||||
|
assertEquals(Status.DOWN, this.healthAggregator.aggregate(healths).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue