8000 Properly enable/disable metrics and health endpoints by pedroigor · Pull Request #11507 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions docs/guides/src/main/server/health.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, just thinking now: should we still promote the build so much, or use start instead with the new behaviour around optimized? wdyt? not hard on either, though

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought and I initially started by using start (now we have pre-build by default). But I think we need to first see what it takes to update all guides and not a single one, in favor of consistency.


</@tmpl.guide>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +56,7 @@
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;
Expand All @@ -62,26 +65,21 @@
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;
Expand All @@ -91,7 +89,6 @@
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;
Expand Down Expand Up @@ -119,8 +116,6 @@
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;
Expand All @@ -130,6 +125,7 @@
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;
Expand All @@ -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";

Expand Down Expand Up @@ -482,48 +476,36 @@ void indexJpaStore(BuildProducer<IndexDependencyBuildItem> indexDependencyBuildI
indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-map-jpa"));
}

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void initializeFilter(BuildProducer<FilterBuildItem> filters, LaunchModeBuildItem launchModeBuildItem) {
QuarkusRequestFilter filter = new QuarkusRequestFilter();
LaunchMode launchMode = launchModeBuildItem.getLaunchMode();
void initializeFilter(BuildProducer<FilterBuildItem> filters, KeycloakRecorder recorder) {
filters.produce(new FilterBuildItem(recorder.createRequestFilter(isHealthEnabled() || isMetricsEnabled()),FilterBuildItem.AUTHORIZATION - 10));
Comment thread
pedroigor marked this conversation as resolved.
Resolved
}

if (launchMode.isDevOrTest()) {
filter = new QuarkusDevRequestFilter();
@BuildStep
void disableMetricsEndpoint(BuildProducer<RouteBuildItem> routes) {
if (!isMetricsEnabled()) {
routes.produce(RouteBuildItem.builder().route(DEFAULT_METRICS_ENDPOINT.concat("/*")).handler(new NotFoundHandler()).build());
}

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 disableHealthEndpoint(BuildProducer<RouteBuildItem> routes, BuildProducer<BuildTimeConditionBuildItem> removeBeans,
CombinedIndexBuildItem index) {
boolean healthDisabled = !isHealthEnabled();

if (isMetricsEnabled()) {
String rootPath = nonAppRootPath.getNormalizedHttpRootPath();
metricsHandler = recorder.createMetricsHandler(rootPath.concat(DEFAULT_METRICS_ENDPOINT).replace("//", "/"));
} else {
metricsHandler = new NotFoundHandler();
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
Expand Down
1 change: 1 addition & 0 deletions quarkus/deployment/src/test/resources/keycloak.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ db=dev-mem
db-username = sa
db-password = keycloak
health-enabled=true
metrics-enabled=true
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand All @@ -40,6 +41,7 @@
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;
Expand All @@ -54,6 +56,9 @@
@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)
Expand Down Expand Up @@ -133,4 +138,20 @@ public void contributeRuntimeProperties(BiConsumer<String, Object> propertyColle
}
};
}

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ private 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()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ private 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()
};
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

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;
Expand Down Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions quarkus/runtime/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Comment thread
pedroigor marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,10 @@ public void testTransactionTypeChangesDriver() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,8 @@ private void reset() {
outputStream.clear();
errorStream.clear();
exitCode = -1;
keycloak = null;
shutdownOutputExecutor();
keycloak = null;
Comment thread
pedroigor marked this conversation as resolved.
Resolved
}

private Path prepareDistribution() {
Expand Down
Loading
0