Properly enable/disable metrics and health endpoints

Closes #11506

Co-authored-by: Dominik Guhr <dguhr@redhat.com>
This commit is contained in:
Pedro Igor 2022-07-21 13:42:44 -03:00 committed by Bruno Oliveira da Silva
parent a14501dd77
commit e14bd51656
15 changed files with 123 additions and 109 deletions

View file

@ -23,17 +23,33 @@ The result is returned in json format and it looks as follows:
----
{
"status": "UP",
"checks": [
{
"name": "Keycloak database connections health check",
"status": "UP"
}
]
"checks": []
}
----
== Enabling the health checks
Is possible to enable the health checks using the build time option `health-enabled`:
<@kc.build parameters="--health-enabled=true"/>
By default, no check is returned from the health endpoints.
== Available Checks
The table below shows the available checks.
|===
|*Check* | *Description* | *Requires Metrics*
|Database
|Returns the status of the database connection pool.
|Yes
|===
For some checks, you'll need to also enable metrics as indicated by the `Requires Metrics` column. To enable metrics
use the `metrics-enabled` option as follows:
<@kc.build parameters="--health-enabled=true --metrics-enabled=true"/>
</@tmpl.guide>

View file

@ -17,6 +17,8 @@
package org.keycloak.quarkus.deployment;
import static org.keycloak.quarkus.runtime.KeycloakRecorder.DEFAULT_HEALTH_ENDPOINT;
import static org.keycloak.quarkus.runtime.KeycloakRecorder.DEFAULT_METRICS_ENDPOINT;
import static org.keycloak.quarkus.runtime.Providers.getProviderManager;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS;
@ -54,6 +56,7 @@ import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.arc.deployment.BuildTimeConditionBuildItem;
import io.quarkus.datasource.deployment.spi.DevServicesDatasourceResultBuildItem;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.Consume;
@ -62,26 +65,21 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.StaticInitConfigSourceProviderBuildItem;
import io.quarkus.hibernate.orm.deployment.AdditionalJpaModelBuildItem;
import io.quarkus.hibernate.orm.deployment.HibernateOrmConfig;
import io.quarkus.hibernate.orm.deployment.PersistenceXmlDescriptorBuildItem;
import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationRuntimeConfiguredBuildItem;
import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentCustomizerBuildItem;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.configuration.ProfileManager;
import io.quarkus.smallrye.health.runtime.SmallRyeHealthHandler;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.smallrye.config.ConfigValue;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;
@ -91,7 +89,6 @@ import org.keycloak.Config;
import org.keycloak.config.StorageOptions;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.JpaConnectionSpi;
import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
import org.keycloak.quarkus.runtime.QuarkusProfile;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
@ -119,8 +116,6 @@ import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderManager;
import org.keycloak.provider.Spi;
import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter;
import org.keycloak.quarkus.runtime.dev.QuarkusDevRequestFilter;
import org.keycloak.quarkus.runtime.KeycloakRecorder;
import io.quarkus.deployment.annotations.BuildProducer;
@ -130,6 +125,7 @@ import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck;
import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory;
import org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaMapStorageProviderFactory;
import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFactory;
@ -153,8 +149,6 @@ class KeycloakProcessor {
private static final Logger logger = Logger.getLogger(KeycloakProcessor.class);
private static final String JAR_FILE_SEPARATOR = "!/";
private static final String DEFAULT_HEALTH_ENDPOINT = "/health";
private static final String DEFAULT_METRICS_ENDPOINT = "/metrics";
private static final Map<String, Function<ScriptProviderMetadata, ProviderFactory>> DEPLOYEABLE_SCRIPT_PROVIDERS = new HashMap<>();
private static final String KEYCLOAK_SCRIPTS_JSON_PATH = "META-INF/keycloak-scripts.json";
@ -481,48 +475,36 @@ class KeycloakProcessor {
indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-map-jpa"));
}
@BuildStep
void initializeFilter(BuildProducer<FilterBuildItem> filters, LaunchModeBuildItem launchModeBuildItem) {
QuarkusRequestFilter filter = new QuarkusRequestFilter();
LaunchMode launchMode = launchModeBuildItem.getLaunchMode();
if (launchMode.isDevOrTest()) {
filter = new QuarkusDevRequestFilter();
}
filters.produce(new FilterBuildItem(filter,FilterBuildItem.AUTHORIZATION - 10));
}
/**
* <p>Initialize metrics and health endpoints.
*
* <p>The only reason for manually registering these endpoints is that by default they run as blocking hence
* running in a different thread than the worker thread started by {@link QuarkusRequestFilter}.
* See https://github.com/quarkusio/quarkus/issues/12990.
*
* <p>By doing this, custom health checks such as {@link org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck} is
* executed within an active {@link org.keycloak.models.KeycloakSession}, making possible to use it when calculating the
* status.
*
* @param routes
*/
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void initializeMetrics(KeycloakRecorder recorder, BuildProducer<RouteBuildItem> routes, NonApplicationRootPathBuildItem nonAppRootPath) {
final Handler<RoutingContext> healthHandler = (isHealthEnabled()) ? new SmallRyeHealthHandler() : new NotFoundHandler();
Handler<RoutingContext> metricsHandler;
void initializeFilter(BuildProducer<FilterBuildItem> filters, KeycloakRecorder recorder) {
filters.produce(new FilterBuildItem(recorder.createRequestFilter(isHealthEnabled() || isMetricsEnabled()),FilterBuildItem.AUTHORIZATION - 10));
}
if (isMetricsEnabled()) {
String rootPath = nonAppRootPath.getNormalizedHttpRootPath();
metricsHandler = recorder.createMetricsHandler(rootPath.concat(DEFAULT_METRICS_ENDPOINT).replace("//", "/"));
} else {
metricsHandler = new NotFoundHandler();
@BuildStep
void disableMetricsEndpoint(BuildProducer<RouteBuildItem> routes) {
if (!isMetricsEnabled()) {
routes.produce(RouteBuildItem.builder().route(DEFAULT_METRICS_ENDPOINT.concat("/*")).handler(new NotFoundHandler()).build());
}
}
@BuildStep
void disableHealthEndpoint(BuildProducer<RouteBuildItem> routes, BuildProducer<BuildTimeConditionBuildItem> removeBeans,
CombinedIndexBuildItem index) {
boolean healthDisabled = !isHealthEnabled();
if (healthDisabled) {
routes.produce(RouteBuildItem.builder().route(DEFAULT_HEALTH_ENDPOINT.concat("/*")).handler(new NotFoundHandler()).build());
}
routes.produce(RouteBuildItem.builder().route(DEFAULT_HEALTH_ENDPOINT).handler(healthHandler).build());
routes.produce(RouteBuildItem.builder().route(DEFAULT_HEALTH_ENDPOINT.concat("/live")).handler(healthHandler).build());
routes.produce(RouteBuildItem.builder().route(DEFAULT_HEALTH_ENDPOINT.concat("/ready")).handler(healthHandler).build());
routes.produce(RouteBuildItem.builder().route(DEFAULT_METRICS_ENDPOINT).handler(metricsHandler).build());
boolean metricsDisabled = !isMetricsEnabled();
if (healthDisabled || metricsDisabled) {
// disables the single check we provide which depends on metrics enabled
ClassInfo disabledBean = index.getIndex()
.getClassByName(DotName.createSimple(KeycloakReadyHealthCheck.class.getName()));
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean.asClass(), false));
}
}
@BuildStep

View file

@ -6,3 +6,4 @@ db=dev-mem
db-username = sa
db-password = keycloak
health-enabled=true
metrics-enabled=true

View file

@ -21,6 +21,7 @@ import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import io.agroal.api.AgroalDataSource;
import io.quarkus.agroal.DataSource;
@ -40,6 +41,7 @@ import org.keycloak.common.Profile;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter;
import org.keycloak.quarkus.runtime.storage.database.liquibase.FastServiceLocator;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
@ -54,6 +56,9 @@ import liquibase.servicelocator.ServiceLocator;
@Recorder
public class KeycloakRecorder {
public static final String DEFAULT_HEALTH_ENDPOINT = "/health";
public static final String DEFAULT_METRICS_ENDPOINT = "/metrics";
public void configureLiquibase(Map<String, List<String>> services) {
ServiceLocator locator = Scope.getCurrentScope().getServiceLocator();
if (locator instanceof FastServiceLocator)
@ -133,4 +138,20 @@ public class KeycloakRecorder {
}
};
}
public QuarkusRequestFilter createRequestFilter(boolean healthOrMetricsEnabled) {
Predicate<RoutingContext> ignoreContext = null;
if (healthOrMetricsEnabled) {
// ignore metrics and health endpoints because they execute in their own worker thread
ignoreContext = new Predicate<>() {
@Override
public boolean test(RoutingContext context) {
return context.request().uri().startsWith("/health") || context.request().uri().startsWith("/metrics");
}
};
}
return new QuarkusRequestFilter(ignoreContext);
}
}

View file

@ -12,7 +12,7 @@ final class HealthPropertyMappers {
public static PropertyMapper[] getHealthPropertyMappers() {
return new PropertyMapper[] {
fromOption(HealthOptions.HEALTH_ENABLED)
.to("quarkus.datasource.health.enabled")
.to("quarkus.health.extensions.enabled")
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
.build()
};

View file

@ -12,7 +12,7 @@ final class MetricsPropertyMappers {
public static PropertyMapper[] getMetricsPropertyMappers() {
return new PropertyMapper[] {
fromOption(MetricsOptions.METRICS_ENABLED)
.to("quarkus.datasource.metrics.enabled")
.to("quarkus.smallrye-metrics.extensions.enabled")
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
.build()
};

View file

@ -1,37 +0,0 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.keycloak.quarkus.runtime.dev;
import io.vertx.ext.web.RoutingContext;
import org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class QuarkusDevRequestFilter extends QuarkusRequestFilter {
@Override
public void handle(RoutingContext context) {
if (context.request().uri().startsWith("/q/")) {
// do not go through Keycloak request filter if serving Quarkus resources such as dev console
context.next();
return;
}
super.handle(context);
}
}

View file

@ -19,6 +19,7 @@ package org.keycloak.quarkus.runtime.integration.web;
import static org.keycloak.services.resources.KeycloakApplication.getSessionFactory;
import java.util.function.Predicate;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Resteasy;
import org.keycloak.models.KeycloakSession;
@ -49,13 +50,31 @@ public class QuarkusRequestFilter implements Handler<RoutingContext> {
// we don't really care about the result because any exception thrown should be handled by the parent class
};
private Predicate<RoutingContext> contextFilter;
public QuarkusRequestFilter() {
this(null);
}
public QuarkusRequestFilter(Predicate<RoutingContext> contextFilter) {
this.contextFilter = contextFilter;
}
@Override
public void handle(RoutingContext context) {
if (ignoreContext(context)) {
context.next();
return;
}
// our code should always be run as blocking until we don't provide a better support for running non-blocking code
// in the event loop
context.vertx().executeBlocking(createBlockingHandler(context), false, EMPTY_RESULT);
}
private boolean ignoreContext(RoutingContext context) {
return contextFilter != null && contextFilter.test(context);
}
private Handler<Promise<Object>> createBlockingHandler(RoutingContext context) {
return promise -> {
KeycloakSessionFactory sessionFactory = getSessionFactory();

View file

@ -7,6 +7,10 @@ quarkus.banner.enabled=false
# Disable health checks from extensions, since we provide our own (default is true)
quarkus.health.extensions.enabled=false
quarkus.datasource.health.enabled=false
# Enables metrics from other extensions if metrics is enabled
quarkus.datasource.metrics.enabled=${quarkus.smallrye-metrics.extensions.enabled}
# Default transaction timeout
quarkus.transaction-manager.default-transaction-timeout=300
@ -24,3 +28,6 @@ quarkus.package.include-dependency-list=false
# we do not want running dev services in distribution
quarkus.devservices.enabled=false
# We want to expose non-application paths (e.g. health) at the root path
quarkus.http.non-application-root-path=${quarkus.http.root-path}

View file

@ -445,7 +445,10 @@ public class ConfigurationTest {
public void testResolveHealthOption() {
System.setProperty(CLI_ARGS, "--health-enabled=true");
SmallRyeConfig config = createConfig();
assertEquals("true", config.getConfigValue("quarkus.datasource.health.enabled").getValue());
assertEquals("true", config.getConfigValue("quarkus.health.extensions.enabled").getValue());
System.setProperty(CLI_ARGS, "");
config = createConfig();
assertEquals("false", config.getConfigValue("quarkus.health.extensions.enabled").getValue());
}
@Test

View file

@ -320,8 +320,8 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
outputStream.clear();
errorStream.clear();
exitCode = -1;
keycloak = null;
shutdownOutputExecutor();
keycloak = null;
}
private Path prepareDistribution() {

View file

@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test;
import org.keycloak.it.junit5.extension.DistributionTest;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.containsString;
@DistributionTest(keepAlive =true)
public class HealthDistTest {
@ -32,10 +31,16 @@ public class HealthDistTest {
void testHealthEndpointNotEnabled() {
when().get("/health").then()
.statusCode(404);
when().get("/q/health").then()
.statusCode(404);
when().get("/health/live").then()
.statusCode(404);
when().get("/q/health/live").then()
.statusCode(404);
when().get("/health/ready").then()
.statusCode(404);
when().get("/q/health/ready").then()
.statusCode(404);
}
@Test
@ -47,14 +52,7 @@ public class HealthDistTest {
.statusCode(200);
when().get("/health/ready").then()
.statusCode(200);
// Metrics is endpoint independent
when().get("/metrics").then()
.statusCode(404);
}
@Test
@Launch({ "start-dev", "--health-enabled=true" })
void testHealthEndpointDoesNotEnableMetrics() {
// Metrics should not be enabled
when().get("/metrics").then()
.statusCode(404);
}

View file

@ -33,6 +33,8 @@ public class MetricsDistTest {
void testMetricsEndpointNotEnabled() {
when().get("/metrics").then()
.statusCode(404);
when().get("/q/metrics").then()
.statusCode(404);
}
@Test

View file

@ -105,7 +105,7 @@ public class QuarkusPropertiesAutoBuildDistTest {
}
@Test
@BeforeStartDistribution(EnableDatasourceMetrics.class)
@BeforeStartDistribution(EnableQuarkusMetrics.class)
@Launch({ "start", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
@Order(8)
void testWrappedBuildPropertyTriggersBuildButGetsIgnoredWhenSetByQuarkus(LaunchResult result) {
@ -163,11 +163,11 @@ public class QuarkusPropertiesAutoBuildDistTest {
}
}
public static class EnableDatasourceMetrics implements Consumer<KeycloakDistribution> {
public static class EnableQuarkusMetrics implements Consumer<KeycloakDistribution> {
@Override
public void accept(KeycloakDistribution distribution) {
distribution.setManualStop(true);
distribution.setQuarkusProperty("quarkus.datasource.metrics.enabled","true");
distribution.setQuarkusProperty("quarkus.smallrye-metrics.extensions.enabled","true");
}
}
}

View file

@ -35,7 +35,9 @@ public class KeycloakSecurityHeadersFilter implements ContainerResponseFilter {
public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class);
SecurityHeadersProvider securityHeadersProvider = session.getProvider(SecurityHeadersProvider.class);
securityHeadersProvider.addHeaders(containerRequestContext, containerResponseContext);
if (session != null) {
SecurityHeadersProvider securityHeadersProvider = session.getProvider(SecurityHeadersProvider.class);
securityHeadersProvider.addHeaders(containerRequestContext, containerResponseContext);
}
}
}