mirror of https://github.com/jenkinsci/jenkins.git
[JENKINS-75530] Implement dedicated healthcheck endpoint
This commit is contained in:
parent
4bd12d8549
commit
7488f84d5d
|
@ -0,0 +1,25 @@
|
|||
package jenkins.health;
|
||||
|
||||
import hudson.ExtensionPoint;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.Beta;
|
||||
|
||||
/**
|
||||
* Defines a health check that is critical to the operation of Jenkins.
|
||||
* @since XXX
|
||||
*/
|
||||
@Restricted(Beta.class)
|
||||
public interface HealthCheck extends ExtensionPoint {
|
||||
|
||||
/**
|
||||
* @return the name of the health check. Must be unique among health check implementations.
|
||||
*/
|
||||
default String getName() {
|
||||
return getClass().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the health check passed.
|
||||
*/
|
||||
boolean check();
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package jenkins.health;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.ExtensionList;
|
||||
import hudson.init.InitMilestone;
|
||||
import hudson.init.Initializer;
|
||||
import hudson.model.InvisibleAction;
|
||||
import hudson.model.UnprotectedRootAction;
|
||||
import java.util.HashMap;
|
||||
import java.util.logging.Logger;
|
||||
import net.sf.json.JSONArray;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.DoNotUse;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.json.JsonHttpResponse;
|
||||
|
||||
/**
|
||||
* Provides a health check action for Jenkins.
|
||||
*/
|
||||
@Extension
|
||||
@Restricted(DoNotUse.class)
|
||||
public final class HealthCheckAction extends InvisibleAction implements UnprotectedRootAction {
|
||||
private static final Logger LOGGER = Logger.getLogger(HealthCheckAction.class.getName());
|
||||
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "healthCheck";
|
||||
}
|
||||
|
||||
@Initializer(after = InitMilestone.EXTENSIONS_AUGMENTED)
|
||||
public static void init() {
|
||||
var names = new HashMap<String, String>();
|
||||
for (var healthCheck : ExtensionList.lookup(HealthCheck.class)) {
|
||||
var name = healthCheck.getName();
|
||||
var previousValue = names.put(name, healthCheck.getClass().getName());
|
||||
if (previousValue != null) {
|
||||
LOGGER.warning(() -> "Ignoring duplicate health check with name " + name + " from " + healthCheck.getClass().getName() + " as it is already defined by " + previousValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HttpResponse doIndex() {
|
||||
boolean success = true;
|
||||
var checks = new JSONArray();
|
||||
var names = new HashMap<String, String>();
|
||||
for (var healthCheck : ExtensionList.lookup(HealthCheck.class)) {
|
||||
var check = healthCheck.check();
|
||||
var name = healthCheck.getName();
|
||||
var previousValue = names.put(name, healthCheck.getClass().getName());
|
||||
if (previousValue == null) {
|
||||
success &= check;
|
||||
checks.add(new JSONObject().element("name", name).element("result", check));
|
||||
}
|
||||
}
|
||||
return new JsonHttpResponse(new JSONObject().element("status", success).element("data", checks), success ? 200 : 503);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package jenkins.health;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import net.sf.json.JSONArray;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.LoggerRule;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
|
||||
|
||||
public class HealthCheckActionTest {
|
||||
@Rule
|
||||
public JenkinsRule r = new JenkinsRule();
|
||||
|
||||
@Rule
|
||||
public LoggerRule loggingRule = new LoggerRule().record(HealthCheckAction.class, Level.WARNING).capture(10);
|
||||
|
||||
@Test
|
||||
public void healthCheck() throws Exception {
|
||||
try (var webClient = r.createWebClient()) {
|
||||
var page = webClient.goTo("healthCheck", "application/json");
|
||||
assertThat(page.getWebResponse().getStatusCode(), is(200));
|
||||
assertEquals(new JSONObject().element("status", true).element("data", new JSONArray()), JSONObject.fromObject(page.getWebResponse().getContentAsString()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void healthCheckSuccessExtension() throws Exception {
|
||||
try (var webClient = r.createWebClient()) {
|
||||
var page = webClient.goTo("healthCheck", "application/json");
|
||||
assertThat(page.getWebResponse().getStatusCode(), is(200));
|
||||
assertEquals(JSONObject.fromObject("""
|
||||
{
|
||||
"status": true,
|
||||
"data": [
|
||||
{
|
||||
"name": "success",
|
||||
"result": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"""), JSONObject.fromObject(page.getWebResponse().getContentAsString()));
|
||||
}
|
||||
}
|
||||
|
||||
@TestExtension({"healthCheckSuccessExtension", "healthCheckFailingExtension"})
|
||||
public static class SuccessHealthCheck implements HealthCheck {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "success";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void healthCheckFailingExtension() throws Exception {
|
||||
try (var webClient = r.createWebClient()) {
|
||||
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
webClient.getOptions().setPrintContentOnFailingStatusCode(false);
|
||||
var page = webClient.goTo("healthCheck", "application/json");
|
||||
assertThat(page.getWebResponse().getStatusCode(), is(503));
|
||||
assertEquals(JSONObject.fromObject("""
|
||||
{
|
||||
"status": false,
|
||||
"data": [
|
||||
{
|
||||
"name": "failing",
|
||||
"result": false
|
||||
},
|
||||
{
|
||||
"name": "success",
|
||||
"result": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"""), JSONObject.fromObject(page.getWebResponse().getContentAsString()));
|
||||
}
|
||||
}
|
||||
|
||||
@TestExtension("healthCheckFailingExtension")
|
||||
public static class FailingHealthCheck implements HealthCheck {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "failing";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void duplicateHealthCheckExtension() throws Exception {
|
||||
try (var webClient = r.createWebClient()) {
|
||||
var page = webClient.goTo("healthCheck", "application/json");
|
||||
assertThat(page.getWebResponse().getStatusCode(), is(200));
|
||||
assertEquals(JSONObject.fromObject("""
|
||||
{
|
||||
"status": true,
|
||||
"data": [
|
||||
{
|
||||
"name": "dupe",
|
||||
"result": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"""), JSONObject.fromObject(page.getWebResponse().getContentAsString()));
|
||||
}
|
||||
// TestExtension doesn't have ordinal, but I think by default extensions are ordered alphabetically
|
||||
assertThat(loggingRule.getMessages(), contains("Ignoring duplicate health check with name dupe from " + HealthCheck2.class.getName() + " as it is already defined by " + HealthCheck1.class.getName()));
|
||||
}
|
||||
|
||||
@TestExtension("duplicateHealthCheckExtension")
|
||||
public static class HealthCheck1 implements HealthCheck {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "dupe";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@TestExtension("duplicateHealthCheckExtension")
|
||||
public static class HealthCheck2 implements HealthCheck {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "dupe";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue