diff --git a/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java b/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java index 9f8ceb9a55..ca1373ea5f 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java @@ -44,6 +44,7 @@ import org.apache.polaris.service.context.TestRealmContextResolver; import org.apache.polaris.service.events.PolarisEventListener; import org.apache.polaris.service.events.TestPolarisEventListener; +import org.apache.polaris.service.metrics.MetricsConfiguration; import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigValue; @@ -113,6 +114,30 @@ public void warnOnFailedChecks( } } + @Produces + public ProductionReadinessCheck checkUserPrincipalMetricTag(MetricsConfiguration config) { + if (config.userPrincipalTag().enableInApiMetrics()) { + return ProductionReadinessCheck.of( + Error.of( + "Metrics configuration includes user principal name and this could have security implications.", + "polaris.metrics.user-principal-tag.enable-in-api-metrics")); + } + return ProductionReadinessCheck.OK; + } + + @Produces + public ProductionReadinessCheck checkUserPrincipalAndRealmIdMetricTags( + MetricsConfiguration config) { + if (config.userPrincipalTag().enableInApiMetrics() + && config.realmIdTag().enableInApiMetrics()) { + return ProductionReadinessCheck.of( + Error.of( + "Metrics configuration includes both user principal name and realm id in tags and this could have performance implications.", + "polaris.metrics.user-principal-tag.enable-in-api-metrics")); + } + return ProductionReadinessCheck.OK; + } + @Produces public ProductionReadinessCheck checkTokenBrokers(AuthenticationConfiguration configuration) { List errors = new ArrayList<>(); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/metrics/MetricsConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/metrics/MetricsConfiguration.java index 504895158a..8b8c0a2475 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/metrics/MetricsConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/metrics/MetricsConfiguration.java @@ -32,6 +32,9 @@ public interface MetricsConfiguration { /** Configuration for the Realm ID metric tag. */ RealmIdTag realmIdTag(); + /** Configuration for the user principal metric tag. */ + UserPrincipalTag userPrincipalTag(); + interface RealmIdTag { /** @@ -65,4 +68,16 @@ interface RealmIdTag { @Min(1) int httpMetricsMaxCardinality(); } + + interface UserPrincipalTag { + + /** + * Whether to include the User Principal tag in the API request metrics. + * + *

Beware that if the cardinality of this tag is too high, it can cause performance issues or + * even crash the server. + */ + @WithDefault("false") + boolean enableInApiMetrics(); + } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/metrics/PolarisValueExpressionResolver.java b/runtime/service/src/main/java/org/apache/polaris/service/metrics/PolarisValueExpressionResolver.java index 2c71427702..19b8df4801 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/metrics/PolarisValueExpressionResolver.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/metrics/PolarisValueExpressionResolver.java @@ -23,6 +23,7 @@ import jakarta.annotation.Nonnull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.core.SecurityContext; import org.apache.polaris.core.context.RealmContext; @ApplicationScoped @@ -39,6 +40,13 @@ public String resolve(@Nonnull String expression, @Nullable Object parameter) { && expression.equals("realmIdentifier")) { return realmContext.getRealmIdentifier(); } + + if (metricsConfiguration.userPrincipalTag().enableInApiMetrics() + && parameter instanceof SecurityContext securityContext + && expression.equals("userPrincipal") + && securityContext.getUserPrincipal() != null) { + return securityContext.getUserPrincipal().getName(); + } return null; } } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/metrics/MetricsTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/metrics/MetricsTestBase.java index 34722731e7..ebff8b97b0 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/metrics/MetricsTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/metrics/MetricsTestBase.java @@ -105,6 +105,11 @@ public void testMetricsEmittedOnSuccessfulRequest(String endpoint) { metricsConfiguration.realmIdTag().enableInApiMetrics() ? fixture.realm : ""), + Map.entry( + "principal", + metricsConfiguration.userPrincipalTag().enableInApiMetrics() + ? "root" + : ""), Map.entry( "class", "org.apache.polaris.service.admin.api.PolarisPrincipalsApi"), Map.entry("exception", "none"), @@ -156,6 +161,11 @@ public void testMetricsEmittedOnFailedRequest(String endpoint) { metricsConfiguration.realmIdTag().enableInApiMetrics() ? fixture.realm : ""), + Map.entry( + "principal", + metricsConfiguration.userPrincipalTag().enableInApiMetrics() + ? "root" + : ""), Map.entry( "class", "org.apache.polaris.service.admin.api.PolarisPrincipalsApi"), Map.entry("exception", "NotFoundException"), diff --git a/runtime/service/src/test/java/org/apache/polaris/service/metrics/RealmIdTagEnabledMetricsTest.java b/runtime/service/src/test/java/org/apache/polaris/service/metrics/RealmIdTagEnabledMetricsTest.java index 7235dccd18..86ddf1fb99 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/metrics/RealmIdTagEnabledMetricsTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/metrics/RealmIdTagEnabledMetricsTest.java @@ -39,7 +39,9 @@ public Map getConfigOverrides() { "polaris.metrics.realm-id-tag.enable-in-api-metrics", "true", "polaris.metrics.realm-id-tag.enable-in-http-metrics", - "true"); + "true", + "polaris.metrics.user-principal-tag.enable-in-api-metrics", + "false"); } } } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/metrics/UserPrincipalTagDisabledMetricsTest.java b/runtime/service/src/test/java/org/apache/polaris/service/metrics/UserPrincipalTagDisabledMetricsTest.java new file mode 100644 index 0000000000..9f45bb51c4 --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/metrics/UserPrincipalTagDisabledMetricsTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.polaris.service.metrics; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import java.util.Map; + +@QuarkusTest +@TestProfile(UserPrincipalTagDisabledMetricsTest.Profile.class) +public class UserPrincipalTagDisabledMetricsTest extends MetricsTestBase { + + public static class Profile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of( + "polaris.metrics.tags.environment", "prod", "polaris.realm-context.type", "test"); + } + } +} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/metrics/UserPrincipalTagEnabledMetricsTest.java b/runtime/service/src/test/java/org/apache/polaris/service/metrics/UserPrincipalTagEnabledMetricsTest.java new file mode 100644 index 0000000000..b7e706fea0 --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/metrics/UserPrincipalTagEnabledMetricsTest.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.polaris.service.metrics; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import java.util.Map; + +@QuarkusTest +@TestProfile(UserPrincipalTagEnabledMetricsTest.Profile.class) +public class UserPrincipalTagEnabledMetricsTest extends MetricsTestBase { + + public static class Profile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of( + "polaris.metrics.tags.environment", + "prod", + "polaris.metrics.user-principal-tag.enable-in-api-metrics", + "true", + "polaris.metrics.realm-id-tag.enable-in-api-metrics", + "false", + "polaris.metrics.realm-id-tag.enable-in-http-metrics", + "false"); + } + } +} diff --git a/server-templates/api.mustache b/server-templates/api.mustache index 8928bc356e..6d42e277f7 100644 --- a/server-templates/api.mustache +++ b/server-templates/api.mustache @@ -93,7 +93,7 @@ public class {{classname}} { {{#authMethods}}{{#isOAuth}}@RolesAllowed("**"){{/isOAuth}}{{/authMethods}}{{/hasAuthMethods}} @Timed("{{metricsPrefix}}.{{baseName}}.{{nickname}}") @Timeout - public Response {{nickname}}({{#isMultipart}}MultipartFormDataInput input,{{/isMultipart}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{^isMultipart}}{{>formParams}},{{/isMultipart}}{{#isMultipart}}{{^isFormParam}},{{/isFormParam}}{{/isMultipart}}{{/allParams}}@Context @MeterTag(key="realm_id",expression="realmIdentifier") RealmContext realmContext,@Context SecurityContext securityContext) { + public Response {{nickname}}({{#isMultipart}}MultipartFormDataInput input,{{/isMultipart}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{^isMultipart}}{{>formParams}},{{/isMultipart}}{{#isMultipart}}{{^isFormParam}},{{/isFormParam}}{{/isMultipart}}{{/allParams}}@Context @MeterTag(key="realm_id",expression="realmIdentifier") RealmContext realmContext,@Context @MeterTag(key="principal",expression="userPrincipal") SecurityContext securityContext) { {{! Don't log form or header params in case there are secrets, e.g., OAuth tokens }} LOGGER.atDebug().setMessage("Invoking {{baseName}} with params") .addKeyValue("operation", "{{nickname}}"){{#allParams}}{{^isHeaderParam}}{{^isFormParam}}