diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index e1601bafed7..5b42fd067d5 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -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 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 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 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, diff --git a/docs/guides/observability/telemetry.adoc b/docs/guides/observability/telemetry.adoc index 7c6e5721be7..778118bd9e5 100644 --- a/docs/guides/observability/telemetry.adoc +++ b/docs/guides/observability/telemetry.adoc @@ -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-*"/> + diff --git a/quarkus/deployment/pom.xml b/quarkus/deployment/pom.xml index 18eade9879f..eced1a0f3c9 100644 --- a/quarkus/deployment/pom.xml +++ b/quarkus/deployment/pom.xml @@ -276,11 +276,6 @@ io.quarkus quarkus-opentelemetry-deployment - - io.quarkus - quarkus-micrometer-opentelemetry-deployment - - io.quarkus quarkus-junit5-internal @@ -338,4 +333,21 @@ + + + community + + + !product + + + + + io.quarkus + quarkus-micrometer-opentelemetry-deployment + + + + + diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index 32fabfe3953..f19ba05a490 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -125,11 +125,6 @@ io.opentelemetry.instrumentation opentelemetry-apache-httpclient-4.3 - - io.quarkus - quarkus-micrometer-opentelemetry - - com.apicatalog titanium-json-ld @@ -860,5 +855,19 @@ + + community + + + !product + + + + + io.quarkus + quarkus-micrometer-opentelemetry + + + diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifacts.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifacts.java index 65e7c3e97a7..497186a437b 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifacts.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifacts.java @@ -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 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(); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TelemetryPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TelemetryPropertyMappers.java index 737e1e9f72a..57e7ebb5ace 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TelemetryPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TelemetryPropertyMappers.java @@ -58,6 +58,7 @@ public class TelemetryPropertyMappers implements PropertyMapperGrouping{ @Override public List> 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("") .isMasked(true) // it may contain sensitive information @@ -164,6 +165,10 @@ public class TelemetryPropertyMappers implements PropertyMapperGrouping{ ); } + private static Option toHidden(Option 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(); diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java index ccec4f93aab..2487f01cded 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java @@ -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"); diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifactsTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifactsTest.java index f9b722e5471..8f84d4f7973 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifactsTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/IgnoredArtifactsTest.java @@ -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); } diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/TelemetryConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/TelemetryConfigurationTest.java index 3fcd53db4e2..972bd54dc84 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/TelemetryConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/TelemetryConfigurationTest.java @@ -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( diff --git a/services/pom.xml b/services/pom.xml index 3336ce6bac7..29ab9c651ab 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -128,6 +128,12 @@ + + + io.quarkus + quarkus-micrometer-opentelemetry + test + org.apache.httpcomponents httpclient