Skip to content

Commit

Permalink
Merge pull request #480 from nscuro/health-check
Browse files Browse the repository at this point in the history
Implement health endpoint based on MicroProfile Health
  • Loading branch information
stevespringett authored Mar 31, 2023
2 parents 3793e56 + ca39727 commit 6b07ef3
Show file tree
Hide file tree
Showing 13 changed files with 1,112 additions and 0 deletions.
14 changes: 14 additions & 0 deletions alpine-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<!-- Logging -->
<!-- Overriding OWASP Security Logging dependencies with newer versions -->
<dependency>
Expand All @@ -211,6 +215,11 @@
<groupId>org.owasp</groupId>
<artifactId>security-logging-logback</artifactId>
</dependency>
<!-- Health -->
<dependency>
<groupId>org.eclipse.microprofile.health</groupId>
<artifactId>microprofile-health-api</artifactId>
</dependency>
<!-- XSS prevention -->
<dependency>
<groupId>org.owasp.encoder</groupId>
Expand Down Expand Up @@ -261,6 +270,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.javacrumbs.json-unit</groupId>
<artifactId>json-unit-assertj</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* This file is part of Alpine.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package alpine.server.health;

import org.eclipse.microprofile.health.HealthCheck;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* A global registry for {@link HealthCheck}s.
* <p>
* Used by {@link alpine.server.servlets.HealthServlet} to lookup registered checks.
*
* @since 2.3.0
*/
public class HealthCheckRegistry {

private static final HealthCheckRegistry INSTANCE = new HealthCheckRegistry();

private final Map<String, HealthCheck> checks;

public HealthCheckRegistry() {
checks = new ConcurrentHashMap<>();
}

public static HealthCheckRegistry getInstance() {
return INSTANCE;
}

public Map<String, HealthCheck> getChecks() {
return Collections.unmodifiableMap(checks);
}

public void register(final String name, final HealthCheck check) {
checks.put(name, check);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* This file is part of Alpine.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package alpine.server.health;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.HealthCheckResponse.Status;

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

import static java.util.function.Predicate.not;

/**
* Implementation of the MicroProfile Health SPI.
*
* @see <a href="https://download.eclipse.org/microprofile/microprofile-health-3.1/microprofile-health-spec-3.1.html#_spi_usage">MicroProfile Health SPI Usage</a>
* @since 2.3.0
*/
class HealthCheckResponseBuilder extends org.eclipse.microprofile.health.HealthCheckResponseBuilder {

private String name;
private Status status = Status.DOWN;
private final Map<String, Object> data = new HashMap<>();

@Override
public org.eclipse.microprofile.health.HealthCheckResponseBuilder name(final String name) {
this.name = name;
return this;
}

@Override
public org.eclipse.microprofile.health.HealthCheckResponseBuilder withData(final String key, final String value) {
data.put(key, value);
return this;
}

@Override
public org.eclipse.microprofile.health.HealthCheckResponseBuilder withData(final String key, final long value) {
data.put(key, value);
return this;
}

@Override
public org.eclipse.microprofile.health.HealthCheckResponseBuilder withData(final String key, final boolean value) {
data.put(key, value);
return this;
}

@Override
public org.eclipse.microprofile.health.HealthCheckResponseBuilder up() {
status = Status.UP;
return this;
}

@Override
public org.eclipse.microprofile.health.HealthCheckResponseBuilder down() {
status = Status.DOWN;
return this;
}

@Override
public org.eclipse.microprofile.health.HealthCheckResponseBuilder status(final boolean up) {
status = up ? Status.UP : Status.DOWN;
return this;
}

@Override
public HealthCheckResponse build() {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("Health check responses must provide a name");
}

return new HealthCheckResponse(name, status, Optional.of(data).filter(not(Map::isEmpty)));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* This file is part of Alpine.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package alpine.server.health;

/**
* Implementation of the MicroProfile Health SPI.
*
* @see <a href="https://download.eclipse.org/microprofile/microprofile-health-3.1/microprofile-health-spec-3.1.html#_spi_usage">MicroProfile Health SPI Usage</a>
* @since 2.3.0
*/
public class HealthCheckResponseProvider implements org.eclipse.microprofile.health.spi.HealthCheckResponseProvider {

@Override
public org.eclipse.microprofile.health.HealthCheckResponseBuilder createResponseBuilder() {
return new HealthCheckResponseBuilder();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* This file is part of Alpine.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package alpine.server.health;

/**
* Defines types of health checks supported by MicroProfile Health.
*
* @see <a href="https://download.eclipse.org/microprofile/microprofile-health-3.1/microprofile-health-spec-3.1.html#_different_kinds_of_health_checks">MicroProfile Health Specification</a>
* @see <a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/">Probes in Kubernetes</a>
* @since 2.3.0
*/
public enum HealthCheckType {

/**
* Liveness probes may be used by service orchestrators to evaluate
* whether a service instance needs to be restarted.
*/
LIVENESS,

/**
* Readiness probes may be used by service orchestrators to evaluate
* whether a service instance is ready to accept traffic.
*/
READINESS,

/**
* Startup probes may be used by service orchestrators to evaluate
* whether a service instance has started.
*/
STARTUP,

/**
* Probes that either do not specify their type, or apply to all types.
*/
ALL

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* This file is part of Alpine.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package alpine.server.health.checks;

import alpine.common.logging.Logger;
import alpine.server.persistence.PersistenceManagerFactory;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.HealthCheckResponse.Status;
import org.eclipse.microprofile.health.Readiness;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;

/**
* @since 2.3.0
*/
@Readiness
public class DatabaseHealthCheck implements HealthCheck {

private static final Logger LOGGER = Logger.getLogger(DatabaseHealthCheck.class);

@Override
public HealthCheckResponse call() {
final var responseBuilder = HealthCheckResponse.named("database");

try (final PersistenceManager pm = PersistenceManagerFactory.createPersistenceManager()) {
// DataNucleus maintains different connection pools for transactional and
// non-transactional operations. Check both of them by executing a test query
// in a transactional and non-transactional context.
final Status nonTransactionalStatus = checkNonTransactionalConnectionPool(pm);
final Status transactionalStatus = checkTransactionalConnectionPool(pm);

responseBuilder
.status(nonTransactionalStatus == Status.UP && transactionalStatus == Status.UP)
.withData("nontx_connection_pool", nonTransactionalStatus.name())
.withData("tx_connection_pool", transactionalStatus.name());
} catch (Exception e) {
LOGGER.error("Executing database health check failed", e);
responseBuilder.down()
.withData("exception_message", e.getMessage());
}

return responseBuilder.build();
}

private Status checkNonTransactionalConnectionPool(final PersistenceManager pm) {
LOGGER.debug("Checking non-transactional connection pool");
try {
return executeQuery(pm);
} catch (Exception e) {
LOGGER.error("Checking non-transactional connection pool failed", e);
return Status.DOWN;
}
}

private Status checkTransactionalConnectionPool(final PersistenceManager pm) {
LOGGER.debug("Checking transactional connection pool");
final Transaction trx = pm.currentTransaction();
trx.setRollbackOnly();
try {
trx.begin();
return executeQuery(pm);
} catch (Exception e) {
LOGGER.error("Checking transactional connection pool failed", e);
return Status.DOWN;
} finally {
if (trx.isActive()) {
trx.rollback();
}
}
}

private Status executeQuery(final PersistenceManager pm) {
final Query<?> query = pm.newQuery(Query.SQL, "SELECT 1");
try {
query.executeResultUnique(Long.class);
return Status.UP;
} finally {
query.closeAll();
}
}

}
Loading

0 comments on commit 6b07ef3

Please sign in to comment.