chore(quarkus): only show OTel Metrics in community build (#49002)

* Closes: https://github.com/keycloak/keycloak/issues/48997

Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>
This commit is contained in:
Michal Vavřík 2026-05-15 14:01:29 +02:00 committed by GitHub
parent ce12c7184c
commit fc667a827a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 99 additions and 21 deletions

View file

@ -137,7 +137,9 @@ public class Profile {
OPENTELEMETRY("OpenTelemetry support", Type.DEFAULT),
OPENTELEMETRY_LOGS("OpenTelemetry Logs support", Type.PREVIEW, OPENTELEMETRY),
OPENTELEMETRY_METRICS("Micrometer to OpenTelemetry bridge support for metrics", Type.EXPERIMENTAL, OPENTELEMETRY),
OPENTELEMETRY_METRICS("Micrometer to OpenTelemetry bridge support for metrics", Type.EXPERIMENTAL, 1, false, true,
() -> isClassAvailable("io.quarkus.micrometer.opentelemetry.runtime.MicrometerOtelBridgeRecorder"),
null, OPENTELEMETRY),
DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL),
@ -187,6 +189,7 @@ public class Profile {
private final String unversionedKey;
private final String key;
private final BooleanSupplier isAvailable;
private final boolean hideWhenUnavailable;
private final FeatureUpdatePolicy updatePolicy;
private final Set<Feature> dependencies;
private final boolean deprecated;
@ -213,11 +216,16 @@ public class Profile {
}
Feature(String label, Type type, int version, boolean deprecated, BooleanSupplier isAvailable, FeatureUpdatePolicy updatePolicy, Feature... dependencies) {
this(label, type, version, deprecated, false, isAvailable, updatePolicy, dependencies);
}
Feature(String label, Type type, int version, boolean deprecated, boolean hideWhenUnavailable, BooleanSupplier isAvailable, FeatureUpdatePolicy updatePolicy, Feature... dependencies) {
this.label = label;
this.type = type;
this.version = version;
this.deprecated = type == Type.DEPRECATED || deprecated;
this.isAvailable = isAvailable;
this.hideWhenUnavailable = hideWhenUnavailable;
this.updatePolicy = updatePolicy == null ? FeatureUpdatePolicy.ROLLING : updatePolicy;
this.key = name().toLowerCase().replaceAll("_", "-");
if (this.name().endsWith("_V" + version)) {
@ -283,6 +291,10 @@ public class Profile {
return updatePolicy;
}
private boolean isVisible() {
return isAvailable() || !hideWhenUnavailable;
}
public enum Type {
// in priority order
@ -459,11 +471,18 @@ public class Profile {
}
public static Set<String> getAllUnversionedFeatureNames() {
return Collections.unmodifiableSet(getOrderedFeatures().keySet());
return Collections.unmodifiableSet(getOrderedFeatures().entrySet().stream()
.filter(e -> e.getValue().stream().anyMatch(Feature::isVisible))
.map(Map.Entry::getKey)
.collect(Collectors.toSet()));
}
public static Set<String> getDisableableUnversionedFeatureNames() {
return getOrderedFeatures().keySet().stream().filter(f -> !ESSENTIAL_FEATURES.contains(f)).collect(Collectors.toSet());
return getOrderedFeatures().entrySet().stream()
.filter(e -> !ESSENTIAL_FEATURES.contains(e.getKey()))
.filter(e -> e.getValue().stream().anyMatch(Feature::isVisible))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
/**
@ -582,6 +601,15 @@ public class Profile {
}
}
private static boolean isClassAvailable(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
public enum FeatureUpdatePolicy {
// Always allow a rolling update when the Feature is enabled/disabled
ROLLING,

View file

@ -152,6 +152,8 @@ As stated for the general configuration of headers, you can configure custom req
<@kc.start parameters="--telemetry-logs-header-Authorization='Bearer logs-token'"/>
<@profile.ifCommunity>
== Metrics
WARNING: The OpenTelemetry Metrics feature is currently experimental, and it is not recommended for use in production.
@ -178,8 +180,6 @@ As stated for the general configuration of headers, you can configure custom req
<@kc.start parameters="--telemetry-metrics-header-Authorization='Bearer metrics-token'"/>
<@profile.ifCommunity>
== Development setup
For development purposes, you can use the https://github.com/grafana/docker-otel-lgtm[Grafana OTel-LGTM service], containing OpenTelemetry Collector and backends for logs (Loki), metrics (Prometheus), and traces (Tempo).
@ -206,8 +206,10 @@ Then, you can navigate to Grafana UI by accessing `+localhost:3000+` and then yo
=== Logs
<@opts.includeOptions includedOptions="telemetry-logs-*"/>
<@profile.ifCommunity>
=== Metrics
<@opts.includeOptions includedOptions="metrics-enabled telemetry-metrics-*"/>
</@profile.ifCommunity>
</@opts.printRelevantOptions>

View file

@ -276,11 +276,6 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-opentelemetry-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
@ -338,4 +333,21 @@
</plugins>
</build>
<profiles>
<profile>
<id>community</id>
<activation>
<property>
<name>!product</name>
</property>
</activation>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-opentelemetry-deployment</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

View file

@ -125,11 +125,6 @@
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-apache-httpclient-4.3</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>com.apicatalog</groupId>
<artifactId>titanium-json-ld</artifactId>
@ -860,5 +855,19 @@
</dependency>
</dependencies>
</profile>
<profile>
<id>community</id>
<activation>
<property>
<name>!product</name>
</property>
</activation>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-opentelemetry</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

View file

@ -28,6 +28,7 @@ import org.keycloak.config.MetricsOptions;
import org.keycloak.config.OpenApiOptions;
import org.keycloak.config.TelemetryOptions;
import org.keycloak.config.database.Database;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import static java.util.Collections.emptySet;
@ -187,6 +188,9 @@ public class IgnoredArtifacts {
private static Set<String> otelMetrics() {
boolean isOtelMetricsEnabled = Configuration.isTrue(TelemetryOptions.TELEMETRY_METRICS_ENABLED);
if (isOtelMetricsEnabled && !Profile.Feature.OPENTELEMETRY_METRICS.isAvailable()) {
throw new PropertyException("The OpenTelemetry Metrics feature is not available in this distribution.");
}
return !isOtelMetricsEnabled ? OTEL_METRICS : emptySet();
}

View file

@ -58,6 +58,7 @@ public class TelemetryPropertyMappers implements PropertyMapperGrouping{
@Override
public List<? extends PropertyMapper<?>> getPropertyMappers() {
TELEMETRY_HEADERS_CACHE = null;
boolean metricsAvailable = Profile.Feature.OPENTELEMETRY_METRICS.isAvailable();
return List.of(
fromFeature(Profile.Feature.OPENTELEMETRY)
.transformer(TelemetryPropertyMappers::checkIfDependantsAreEnabled)
@ -127,36 +128,36 @@ public class TelemetryPropertyMappers implements PropertyMapperGrouping{
.isMasked(true) // it may contain sensitive information
.build(),
// Telemetry Metrics
fromOption(TELEMETRY_METRICS_ENABLED)
fromOption(metricsAvailable ? TELEMETRY_METRICS_ENABLED : toHidden(TELEMETRY_METRICS_ENABLED))
.isEnabled(TelemetryPropertyMappers::isOtelMetricsFeatureEnabled, OTEL_METRICS_FEATURE_ENABLED_MSG)
.to("quarkus.otel.metrics.enabled")
.build(),
fromOption(TELEMETRY_METRICS_ENDPOINT)
fromOption(metricsAvailable ? TELEMETRY_METRICS_ENDPOINT : toHidden(TELEMETRY_METRICS_ENDPOINT))
.isEnabled(TelemetryPropertyMappers::isTelemetryMetricsEnabled, OTEL_METRICS_ENABLED_MSG)
.mapFrom(TelemetryOptions.TELEMETRY_ENDPOINT)
.to("quarkus.otel.exporter.otlp.metrics.endpoint")
.paramLabel("url")
.validator(TelemetryPropertyMappers::validateEndpoint)
.build(),
fromOption(TELEMETRY_METRICS_PROTOCOL)
fromOption(metricsAvailable ? TELEMETRY_METRICS_PROTOCOL : toHidden(TELEMETRY_METRICS_PROTOCOL))
.isEnabled(TelemetryPropertyMappers::isTelemetryMetricsEnabled, OTEL_METRICS_ENABLED_MSG)
.mapFrom(TelemetryOptions.TELEMETRY_PROTOCOL)
.to("quarkus.otel.exporter.otlp.metrics.protocol")
.paramLabel("protocol")
.build(),
fromOption(TELEMETRY_METRICS_INTERVAL)
fromOption(metricsAvailable ? TELEMETRY_METRICS_INTERVAL : toHidden(TELEMETRY_METRICS_INTERVAL))
.isEnabled(TelemetryPropertyMappers::isTelemetryMetricsEnabled, OTEL_METRICS_ENABLED_MSG)
.to("quarkus.otel.metric.export.interval")
.paramLabel("duration")
.validator(TelemetryPropertyMappers::validateDuration)
.build(),
fromOption(TELEMETRY_METRICS_HEADERS)
fromOption(metricsAvailable ? TELEMETRY_METRICS_HEADERS : toHidden(TELEMETRY_METRICS_HEADERS))
.isEnabled(TelemetryPropertyMappers::isTelemetryMetricsEnabled, OTEL_METRICS_ENABLED_MSG)
.to("quarkus.otel.exporter.otlp.metrics.headers")
.transformer((value, context) -> transformTelemetryHeaders(TELEMETRY_METRICS_HEADER, value))
.isMasked(true)
.build(),
fromOption(TELEMETRY_METRICS_HEADER)
fromOption(metricsAvailable ? TELEMETRY_METRICS_HEADER : toHidden(TELEMETRY_METRICS_HEADER))
.isEnabled(TelemetryPropertyMappers::isTelemetryMetricsEnabled, OTEL_METRICS_ENABLED_MSG)
.paramLabel("<value>")
.isMasked(true) // it may contain sensitive information
@ -164,6 +165,10 @@ public class TelemetryPropertyMappers implements PropertyMapperGrouping{
);
}
private static <T> Option<T> toHidden(Option<T> option) {
return option.toBuilder().hidden().build();
}
private static String checkIfDependantsAreEnabled(String value, ConfigSourceInterceptorContext context) {
if (TelemetryPropertyMappers.isTelemetryLogsEnabled() || TelemetryPropertyMappers.isTelemetryMetricsEnabled() || TracingPropertyMappers.isTracingEnabled()) {
return Boolean.TRUE.toString();

View file

@ -36,6 +36,7 @@ import org.keycloak.quarkus.runtime.configuration.AbstractConfigurationTest;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.apache.commons.io.FileUtils;
import org.junit.Assume;
import org.junit.Ignore;
import org.junit.Test;
import picocli.CommandLine;
@ -1064,6 +1065,7 @@ public class PicocliTest extends AbstractConfigurationTest {
@Test
public void telemetryParentHeaders() {
Assume.assumeTrue(Profile.Feature.OPENTELEMETRY_METRICS.isAvailable());
// tracing enabled
var nonRunningPicocli = pseudoLaunch("start-dev", "--tracing-enabled=true", "--telemetry-header-Authorization=Bearer asdlkfjadsflkj");
assertNoError(nonRunningPicocli);
@ -1152,6 +1154,7 @@ public class PicocliTest extends AbstractConfigurationTest {
@Test
public void otelMetricsHeaders() {
Assume.assumeTrue(Profile.Feature.OPENTELEMETRY_METRICS.isAvailable());
// Otel Metrics is disabled
var nonRunningPicocli = pseudoLaunch("start-dev", "--features=opentelemetry-metrics", "--metrics-enabled=true", "--telemetry-metrics-enabled=false", "--telemetry-metrics-header-Authorization=Bearer");
assertError(nonRunningPicocli, "Unknown option:"); //for some reason, the wildcard options does not respect the isEnabled() when disabled
@ -1630,6 +1633,7 @@ public class PicocliTest extends AbstractConfigurationTest {
@Test
public void otelMetrics() {
Assume.assumeTrue(Profile.Feature.OPENTELEMETRY_METRICS.isAvailable());
// parent feature disabled
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--feature-opentelemetry=disabled", "--feature-opentelemetry-metrics=enabled");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
@ -1783,6 +1787,7 @@ public class PicocliTest extends AbstractConfigurationTest {
@Test
public void otelAll() {
Assume.assumeTrue(Profile.Feature.OPENTELEMETRY_METRICS.isAvailable());
// tracing
pseudoLaunch("start-dev", "--tracing-enabled=true");
assertConfig("tracing-enabled", "true");

View file

@ -50,6 +50,7 @@ import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.in;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
public class IgnoredArtifactsTest extends AbstractConfigurationTest {
@ -159,6 +160,7 @@ public class IgnoredArtifactsTest extends AbstractConfigurationTest {
@Test
public void otelMetrics(){
assumeTrue(Profile.Feature.OPENTELEMETRY_METRICS.isAvailable());
assertIgnoredArtifacts(IgnoredArtifacts.OTEL_METRICS, TelemetryOptions.TELEMETRY_METRICS_ENABLED);
}

View file

@ -2,6 +2,9 @@ package org.keycloak.quarkus.runtime.configuration;
import java.util.Map;
import org.keycloak.common.Profile;
import org.junit.Assume;
import org.junit.Test;
public class TelemetryConfigurationTest extends AbstractConfigurationTest {
@ -126,6 +129,7 @@ public class TelemetryConfigurationTest extends AbstractConfigurationTest {
@Test
public void metricsDefaults() {
Assume.assumeTrue(Profile.Feature.OPENTELEMETRY_METRICS.isAvailable());
initConfig();
assertConfig(Map.of(
@ -146,6 +150,7 @@ public class TelemetryConfigurationTest extends AbstractConfigurationTest {
@Test
public void metricsPriorities() {
Assume.assumeTrue(Profile.Feature.OPENTELEMETRY_METRICS.isAvailable());
ConfigArgsConfigSource.setCliArgs("--features=opentelemetry-metrics", "--metrics-enabled=true", "--telemetry-metrics-enabled=true", "--telemetry-metrics-endpoint=localhost:2000", "--telemetry-metrics-protocol=http/protobuf");
initConfig();
assertConfig(Map.of(

View file

@ -128,6 +128,12 @@
</exclusion>
</exclusions>
</dependency>
<!-- needed for FeatureCompatibilityMetadataProviderTest to detect OPENTELEMETRY_METRICS as available -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-opentelemetry</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>