KEYCLOAK-13639. Added metrics and custom healthcheck endpoints, both enabled via 'metrics.enabled' config parameter.
This commit is contained in:
parent
e35a4bcefc
commit
e8e5808aa9
12 changed files with 173 additions and 5 deletions
|
@ -66,6 +66,14 @@
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-bootstrap-core</artifactId>
|
<artifactId>quarkus-bootstrap-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-health-deployment</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-metrics-deployment</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-junit5-internal</artifactId>
|
<artifactId>quarkus-junit5-internal</artifactId>
|
||||||
|
|
|
@ -46,6 +46,7 @@ import org.keycloak.common.Profile;
|
||||||
import org.keycloak.config.ConfigProviderFactory;
|
import org.keycloak.config.ConfigProviderFactory;
|
||||||
import org.keycloak.configuration.Configuration;
|
import org.keycloak.configuration.Configuration;
|
||||||
import org.keycloak.configuration.KeycloakConfigSourceProvider;
|
import org.keycloak.configuration.KeycloakConfigSourceProvider;
|
||||||
|
import org.keycloak.configuration.MicroProfileConfigProvider;
|
||||||
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
|
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
|
||||||
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory;
|
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory;
|
||||||
import org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider;
|
import org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider;
|
||||||
|
@ -194,9 +195,13 @@ class KeycloakProcessor {
|
||||||
indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-services"));
|
indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-services"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Record(ExecutionTime.RUNTIME_INIT)
|
||||||
@BuildStep
|
@BuildStep
|
||||||
void initializeRouter(BuildProducer<FilterBuildItem> routes) {
|
void initializeFilter(BuildProducer<FilterBuildItem> routes, KeycloakRecorder recorder) {
|
||||||
routes.produce(new FilterBuildItem(new QuarkusRequestFilter(), FilterBuildItem.AUTHORIZATION - 10));
|
Optional<Boolean> metricsEnabled = Configuration.getOptionalBooleanValue(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX.concat("metrics.enabled"));
|
||||||
|
|
||||||
|
routes.produce(new FilterBuildItem(recorder.createFilter(metricsEnabled.orElse(false)),
|
||||||
|
FilterBuildItem.AUTHORIZATION - 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
@BuildStep(onlyIf = IsDevelopment.class)
|
@BuildStep(onlyIf = IsDevelopment.class)
|
||||||
|
|
|
@ -63,6 +63,14 @@
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-core</artifactId>
|
<artifactId>quarkus-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-health</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-metrics</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- CLI -->
|
<!-- CLI -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.configuration;
|
package org.keycloak.configuration;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import io.smallrye.config.ConfigValue;
|
import io.smallrye.config.ConfigValue;
|
||||||
import io.smallrye.config.SmallRyeConfig;
|
import io.smallrye.config.SmallRyeConfig;
|
||||||
|
@ -69,4 +70,13 @@ public final class Configuration {
|
||||||
public static Optional<String> getOptionalValue(String name) {
|
public static Optional<String> getOptionalValue(String name) {
|
||||||
return getConfig().getOptionalValue(name, String.class);
|
return getConfig().getOptionalValue(name, String.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Optional<Boolean> getOptionalBooleanValue(String name) {
|
||||||
|
return getConfig().getOptionalValue(name, String.class).map(new Function<String, Boolean>() {
|
||||||
|
@Override
|
||||||
|
public Boolean apply(String s) {
|
||||||
|
return Boolean.parseBoolean(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,10 @@ public class PropertyMapper {
|
||||||
return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, transformer, null, true, description, false));
|
return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, transformer, null, true, description, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PropertyMapper createBuildTimeProperty(String fromProperty, String toProperty, String description) {
|
||||||
|
return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, null, null, true, description, false));
|
||||||
|
}
|
||||||
|
|
||||||
static Map<String, PropertyMapper> MAPPERS = new HashMap<>();
|
static Map<String, PropertyMapper> MAPPERS = new HashMap<>();
|
||||||
|
|
||||||
static PropertyMapper IDENTITY = new PropertyMapper(null, null, null, null, null) {
|
static PropertyMapper IDENTITY = new PropertyMapper(null, null, null, null, null) {
|
||||||
|
|
|
@ -46,6 +46,7 @@ public final class PropertyMappers {
|
||||||
configureProxyMappers();
|
configureProxyMappers();
|
||||||
configureClustering();
|
configureClustering();
|
||||||
configureHostnameProviderMappers();
|
configureHostnameProviderMappers();
|
||||||
|
configureMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void configureHttpPropertyMappers() {
|
private static void configureHttpPropertyMappers() {
|
||||||
|
@ -150,6 +151,10 @@ public final class PropertyMappers {
|
||||||
create("hostname-force-backend-url-to-frontend-url ", "kc.spi.hostname.default.force-backend-url-to-frontend-url", "Forces backend requests to go through the URL defined as the frontend-url. Defaults to false. Possible values are true or false.");
|
create("hostname-force-backend-url-to-frontend-url ", "kc.spi.hostname.default.force-backend-url-to-frontend-url", "Forces backend requests to go through the URL defined as the frontend-url. Defaults to false. Possible values are true or false.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void configureMetrics() {
|
||||||
|
createBuildTimeProperty("metrics.enabled", "quarkus.datasource.metrics.enabled", "If the server should expose metrics and healthcheck. If enabled, metrics are available at the '/metrics' endpoint and healthcheck at the '/health' endpoint.");
|
||||||
|
}
|
||||||
|
|
||||||
static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
|
static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
|
||||||
return PropertyMapper.MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY)
|
return PropertyMapper.MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY)
|
||||||
.getOrDefault(name, context, context.proceed(name));
|
.getOrDefault(name, context, context.proceed(name));
|
||||||
|
|
|
@ -17,8 +17,9 @@
|
||||||
|
|
||||||
package org.keycloak.provider.quarkus;
|
package org.keycloak.provider.quarkus;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.services.filters.AbstractRequestFilter;
|
import org.keycloak.services.filters.AbstractRequestFilter;
|
||||||
|
|
||||||
import io.vertx.core.AsyncResult;
|
import io.vertx.core.AsyncResult;
|
||||||
|
@ -39,8 +40,15 @@ public class QuarkusRequestFilter extends AbstractRequestFilter implements Handl
|
||||||
// we don't really care about the result because any exception thrown should be handled by the parent class
|
// we don't really care about the result because any exception thrown should be handled by the parent class
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private Predicate<RoutingContext> enabledEndpoints;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(RoutingContext context) {
|
public void handle(RoutingContext context) {
|
||||||
|
if (!enabledEndpoints.test(context)) {
|
||||||
|
context.fail(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// our code should always be run as blocking until we don't provide a better support for running non-blocking code
|
// 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
|
// in the event loop
|
||||||
context.vertx().executeBlocking(promise -> {
|
context.vertx().executeBlocking(promise -> {
|
||||||
|
@ -94,4 +102,8 @@ public class QuarkusRequestFilter extends AbstractRequestFilter implements Handl
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setEnabledEndpoints(Predicate<RoutingContext> disabledEndpoints) {
|
||||||
|
this.enabledEndpoints = disabledEndpoints;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,8 @@ import java.util.function.Predicate;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
import io.smallrye.config.ConfigValue;
|
import io.smallrye.config.ConfigValue;
|
||||||
|
import io.vertx.core.Handler;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.QuarkusKeycloakSessionFactory;
|
import org.keycloak.QuarkusKeycloakSessionFactory;
|
||||||
import org.keycloak.cli.ShowConfigCommand;
|
import org.keycloak.cli.ShowConfigCommand;
|
||||||
|
@ -43,6 +45,7 @@ import org.keycloak.provider.Spi;
|
||||||
import io.quarkus.runtime.annotations.Recorder;
|
import io.quarkus.runtime.annotations.Recorder;
|
||||||
import liquibase.logging.LogFactory;
|
import liquibase.logging.LogFactory;
|
||||||
import liquibase.servicelocator.ServiceLocator;
|
import liquibase.servicelocator.ServiceLocator;
|
||||||
|
import org.keycloak.provider.quarkus.QuarkusRequestFilter;
|
||||||
import org.keycloak.util.Environment;
|
import org.keycloak.util.Environment;
|
||||||
|
|
||||||
@Recorder
|
@Recorder
|
||||||
|
@ -210,4 +213,23 @@ public class KeycloakRecorder {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Handler<RoutingContext> createFilter(boolean metricsEnabled) {
|
||||||
|
QuarkusRequestFilter handler = new QuarkusRequestFilter();
|
||||||
|
|
||||||
|
handler.setEnabledEndpoints(new Predicate<RoutingContext>() {
|
||||||
|
@Override
|
||||||
|
public boolean test(RoutingContext context) {
|
||||||
|
|
||||||
|
if (context.request().uri().startsWith("/metrics") ||
|
||||||
|
context.request().uri().startsWith("/health")) {
|
||||||
|
return metricsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.services.health;
|
||||||
|
|
||||||
|
import io.agroal.api.AgroalDataSource;
|
||||||
|
import io.quarkus.agroal.runtime.health.DataSourceHealthCheck;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
|
||||||
|
import org.eclipse.microprofile.health.Readiness;
|
||||||
|
|
||||||
|
import javax.enterprise.context.ApplicationScoped;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak Healthcheck Readiness Probe.
|
||||||
|
*
|
||||||
|
* Performs a hybrid between the passive and the active mode. If there are no healthy connections in the pool,
|
||||||
|
* it invokes the standard <code>DataSourceHealthCheck</code> that creates a new connection and checks if its valid.
|
||||||
|
*
|
||||||
|
* @see <a href="https://github.com/keycloak/keycloak-community/pull/55">Healthcheck API Design</a>
|
||||||
|
*/
|
||||||
|
@Readiness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class KeycloakReadyHealthCheck extends DataSourceHealthCheck {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date formatter, the same as used by Quarkus. This enables users to quickly compare the date printed
|
||||||
|
* by the probe with the logs.
|
||||||
|
*/
|
||||||
|
static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss,SSS").withZone(ZoneId.systemDefault());
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AgroalDataSource agroalDataSource;
|
||||||
|
|
||||||
|
AtomicReference<Instant> failingSince = new AtomicReference<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
HealthCheckResponseBuilder builder = HealthCheckResponse.named("Keycloak database connections health check").up();
|
||||||
|
long activeCount = agroalDataSource.getMetrics().activeCount();
|
||||||
|
long invalidCount = agroalDataSource.getMetrics().invalidCount();
|
||||||
|
if (activeCount < 1 || invalidCount > 0) {
|
||||||
|
HealthCheckResponse activeCheckResult = super.call();
|
||||||
|
if (activeCheckResult.getState() == HealthCheckResponse.State.DOWN) {
|
||||||
|
builder.down();
|
||||||
|
Instant failingTime = failingSince.updateAndGet(this::createInstanceIfNeeded);
|
||||||
|
builder.withData("Failing since", DATE_FORMATTER.format(failingTime));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failingSince.set(null);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant createInstanceIfNeeded(Instant instant) {
|
||||||
|
if (instant == null) {
|
||||||
|
return Instant.now();
|
||||||
|
}
|
||||||
|
return instant;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
# Default and non-production grade database vendor
|
# Default and non-production grade database vendor
|
||||||
db=h2-file
|
db=h2-file
|
||||||
|
|
||||||
# Default, and insecure, and non-production grade configuration for the development profile
|
# Default, and insecure, and non-production grade configuration for the development profile
|
||||||
|
@ -7,6 +7,9 @@ db=h2-file
|
||||||
%dev.db.password = keycloak
|
%dev.db.password = keycloak
|
||||||
%dev.cluster=local
|
%dev.cluster=local
|
||||||
|
|
||||||
|
# Metrics and healthcheck are disabled by default
|
||||||
|
metrics.enabled=false
|
||||||
|
|
||||||
# Logging configuration. INFO is the default level for most of the categories
|
# Logging configuration. INFO is the default level for most of the categories
|
||||||
#quarkus.log.level = DEBUG
|
#quarkus.log.level = DEBUG
|
||||||
quarkus.log.category."org.jboss.resteasy.resteasy_jaxrs.i18n".level=WARN
|
quarkus.log.category."org.jboss.resteasy.resteasy_jaxrs.i18n".level=WARN
|
||||||
|
|
|
@ -7,4 +7,8 @@ quarkus.package.main-class=keycloak
|
||||||
|
|
||||||
quarkus.http.root-path=/
|
quarkus.http.root-path=/
|
||||||
quarkus.application.name=Keycloak
|
quarkus.application.name=Keycloak
|
||||||
quarkus.banner.enabled=false
|
quarkus.banner.enabled=false
|
||||||
|
|
||||||
|
# Disable the default data source health check by Agroal extension, since we provide our own (default is true)
|
||||||
|
quarkus.datasource.health.enabled=false
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,10 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
|
||||||
@Override
|
@Override
|
||||||
public void commit() {
|
public void commit() {
|
||||||
try {
|
try {
|
||||||
|
if (Status.STATUS_NO_TRANSACTION == tm.getStatus() ||
|
||||||
|
Status.STATUS_ACTIVE != tm.getStatus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
logger.debug("JtaTransactionWrapper commit");
|
logger.debug("JtaTransactionWrapper commit");
|
||||||
tm.commit();
|
tm.commit();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -100,6 +104,10 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
|
||||||
@Override
|
@Override
|
||||||
public void rollback() {
|
public void rollback() {
|
||||||
try {
|
try {
|
||||||
|
if (Status.STATUS_NO_TRANSACTION == tm.getStatus() ||
|
||||||
|
Status.STATUS_ACTIVE != tm.getStatus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
logger.debug("JtaTransactionWrapper rollback");
|
logger.debug("JtaTransactionWrapper rollback");
|
||||||
tm.rollback();
|
tm.rollback();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
Loading…
Reference in a new issue