Skip to content

Commit 3f1dfb3

Browse files
Add user principal tag in metrics (#2445)
* Added API change to enable tag * Added test * Added production readiness check
1 parent f0ee460 commit 3f1dfb3

File tree

8 files changed

+145
-2
lines changed

8 files changed

+145
-2
lines changed

runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.apache.polaris.service.context.TestRealmContextResolver;
4545
import org.apache.polaris.service.events.PolarisEventListener;
4646
import org.apache.polaris.service.events.TestPolarisEventListener;
47+
import org.apache.polaris.service.metrics.MetricsConfiguration;
4748
import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
4849
import org.eclipse.microprofile.config.Config;
4950
import org.eclipse.microprofile.config.ConfigValue;
@@ -113,6 +114,30 @@ public void warnOnFailedChecks(
113114
}
114115
}
115116

117+
@Produces
118+
public ProductionReadinessCheck checkUserPrincipalMetricTag(MetricsConfiguration config) {
119+
if (config.userPrincipalTag().enableInApiMetrics()) {
120+
return ProductionReadinessCheck.of(
121+
Error.of(
122+
"Metrics configuration includes user principal name and this could have security implications.",
123+
"polaris.metrics.user-principal-tag.enable-in-api-metrics"));
124+
}
125+
return ProductionReadinessCheck.OK;
126+
}
127+
128+
@Produces
129+
public ProductionReadinessCheck checkUserPrincipalAndRealmIdMetricTags(
130+
MetricsConfiguration config) {
131+
if (config.userPrincipalTag().enableInApiMetrics()
132+
&& config.realmIdTag().enableInApiMetrics()) {
133+
return ProductionReadinessCheck.of(
134+
Error.of(
135+
"Metrics configuration includes both user principal name and realm id in tags and this could have performance implications.",
136+
"polaris.metrics.user-principal-tag.enable-in-api-metrics"));
137+
}
138+
return ProductionReadinessCheck.OK;
139+
}
140+
116141
@Produces
117142
public ProductionReadinessCheck checkTokenBrokers(AuthenticationConfiguration configuration) {
118143
List<ProductionReadinessCheck.Error> errors = new ArrayList<>();

runtime/service/src/main/java/org/apache/polaris/service/metrics/MetricsConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public interface MetricsConfiguration {
3232
/** Configuration for the Realm ID metric tag. */
3333
RealmIdTag realmIdTag();
3434

35+
/** Configuration for the user principal metric tag. */
36+
UserPrincipalTag userPrincipalTag();
37+
3538
interface RealmIdTag {
3639

3740
/**
@@ -65,4 +68,16 @@ interface RealmIdTag {
6568
@Min(1)
6669
int httpMetricsMaxCardinality();
6770
}
71+
72+
interface UserPrincipalTag {
73+
74+
/**
75+
* Whether to include the User Principal tag in the API request metrics.
76+
*
77+
* <p>Beware that if the cardinality of this tag is too high, it can cause performance issues or
78+
* even crash the server.
79+
*/
80+
@WithDefault("false")
81+
boolean enableInApiMetrics();
82+
}
6883
}

runtime/service/src/main/java/org/apache/polaris/service/metrics/PolarisValueExpressionResolver.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import jakarta.annotation.Nonnull;
2424
import jakarta.enterprise.context.ApplicationScoped;
2525
import jakarta.inject.Inject;
26+
import jakarta.ws.rs.core.SecurityContext;
2627
import org.apache.polaris.core.context.RealmContext;
2728

2829
@ApplicationScoped
@@ -39,6 +40,13 @@ public String resolve(@Nonnull String expression, @Nullable Object parameter) {
3940
&& expression.equals("realmIdentifier")) {
4041
return realmContext.getRealmIdentifier();
4142
}
43+
44+
if (metricsConfiguration.userPrincipalTag().enableInApiMetrics()
45+
&& parameter instanceof SecurityContext securityContext
46+
&& expression.equals("userPrincipal")
47+
&& securityContext.getUserPrincipal() != null) {
48+
return securityContext.getUserPrincipal().getName();
49+
}
4250
return null;
4351
}
4452
}

runtime/service/src/test/java/org/apache/polaris/service/metrics/MetricsTestBase.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ public void testMetricsEmittedOnSuccessfulRequest(String endpoint) {
105105
metricsConfiguration.realmIdTag().enableInApiMetrics()
106106
? fixture.realm
107107
: ""),
108+
Map.entry(
109+
"principal",
110+
metricsConfiguration.userPrincipalTag().enableInApiMetrics()
111+
? "root"
112+
: ""),
108113
Map.entry(
109114
"class", "org.apache.polaris.service.admin.api.PolarisPrincipalsApi"),
110115
Map.entry("exception", "none"),
@@ -156,6 +161,11 @@ public void testMetricsEmittedOnFailedRequest(String endpoint) {
156161
metricsConfiguration.realmIdTag().enableInApiMetrics()
157162
? fixture.realm
158163
: ""),
164+
Map.entry(
165+
"principal",
166+
metricsConfiguration.userPrincipalTag().enableInApiMetrics()
167+
? "root"
168+
: ""),
159169
Map.entry(
160170
"class", "org.apache.polaris.service.admin.api.PolarisPrincipalsApi"),
161171
Map.entry("exception", "NotFoundException"),

runtime/service/src/test/java/org/apache/polaris/service/metrics/RealmIdTagEnabledMetricsTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ public Map<String, String> getConfigOverrides() {
3939
"polaris.metrics.realm-id-tag.enable-in-api-metrics",
4040
"true",
4141
"polaris.metrics.realm-id-tag.enable-in-http-metrics",
42-
"true");
42+
"true",
43+
"polaris.metrics.user-principal-tag.enable-in-api-metrics",
44+
"false");
4345
}
4446
}
4547
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.metrics;
20+
21+
import io.quarkus.test.junit.QuarkusTest;
22+
import io.quarkus.test.junit.QuarkusTestProfile;
23+
import io.quarkus.test.junit.TestProfile;
24+
import java.util.Map;
25+
26+
@QuarkusTest
27+
@TestProfile(UserPrincipalTagDisabledMetricsTest.Profile.class)
28+
public class UserPrincipalTagDisabledMetricsTest extends MetricsTestBase {
29+
30+
public static class Profile implements QuarkusTestProfile {
31+
32+
@Override
33+
public Map<String, String> getConfigOverrides() {
34+
return Map.of(
35+
"polaris.metrics.tags.environment", "prod", "polaris.realm-context.type", "test");
36+
}
37+
}
38+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.metrics;
20+
21+
import io.quarkus.test.junit.QuarkusTest;
22+
import io.quarkus.test.junit.QuarkusTestProfile;
23+
import io.quarkus.test.junit.TestProfile;
24+
import java.util.Map;
25+
26+
@QuarkusTest
27+
@TestProfile(UserPrincipalTagEnabledMetricsTest.Profile.class)
28+
public class UserPrincipalTagEnabledMetricsTest extends MetricsTestBase {
29+
30+
public static class Profile implements QuarkusTestProfile {
31+
32+
@Override
33+
public Map<String, String> getConfigOverrides() {
34+
return Map.of(
35+
"polaris.metrics.tags.environment",
36+
"prod",
37+
"polaris.metrics.user-principal-tag.enable-in-api-metrics",
38+
"true",
39+
"polaris.metrics.realm-id-tag.enable-in-api-metrics",
40+
"false",
41+
"polaris.metrics.realm-id-tag.enable-in-http-metrics",
42+
"false");
43+
}
44+
}
45+
}

server-templates/api.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public class {{classname}} {
9393
{{#authMethods}}{{#isOAuth}}@RolesAllowed("**"){{/isOAuth}}{{/authMethods}}{{/hasAuthMethods}}
9494
@Timed("{{metricsPrefix}}.{{baseName}}.{{nickname}}")
9595
@Timeout
96-
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) {
96+
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) {
9797
{{! Don't log form or header params in case there are secrets, e.g., OAuth tokens }}
9898
LOGGER.atDebug().setMessage("Invoking {{baseName}} with params")
9999
.addKeyValue("operation", "{{nickname}}"){{#allParams}}{{^isHeaderParam}}{{^isFormParam}}

0 commit comments

Comments
 (0)