Skip to content

Commit 927bb2e

Browse files
authored
Add metrics to Athenz{Service,Client} (#6517)
Motivation: `AthenzService` and `AthenzClient` do not expose metrics that are helpful to monitor. Modifications: - `ZtsBaseClient`) - The metrics for the underline token fetch client could be enabled by using `ZtsBaseClientBuilder.enableMetrics(MeterRegistory,MeterIdPrefixFunction)` - `AthenzClient` decorator) - Added `AthenzClientBuilder` to fluently configure various options. - `meterIdPrefix` can be configured through the builder; the default is `armeria.client.athenz`. - The results of token-fetch operations are tracked with timers. - `AthenzService` decorator) - The results of access check is recorded in two timers. - `meterIdPrefix` can be configured through the builder; the default is `armeria.server.athenz`. Result: - You can monitor Athenz’s authentication results and execution time using the metrics below. - `armeria.server.athenz{result=(allowed|denied), resource=<athenz-resource>, action=<athenz-action>}` - `armeria.client.athenz{result=(success|failure), domain=<athenz-domain>, roles=<athenz-roles> type=<token-type>}` - Closes #6423
1 parent 2a34ce5 commit 927bb2e

File tree

12 files changed

+550
-50
lines changed

12 files changed

+550
-50
lines changed

athenz/src/main/java/com/linecorp/armeria/client/athenz/AthenzClient.java

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,12 @@
1616

1717
package com.linecorp.armeria.client.athenz;
1818

19-
import static java.util.Objects.requireNonNull;
20-
2119
import java.time.Duration;
2220
import java.util.List;
2321
import java.util.concurrent.CompletableFuture;
22+
import java.util.concurrent.TimeUnit;
2423
import java.util.function.Function;
2524

26-
import com.google.common.collect.ImmutableList;
27-
2825
import com.linecorp.armeria.client.ClientRequestContext;
2926
import com.linecorp.armeria.client.HttpClient;
3027
import com.linecorp.armeria.client.SimpleDecoratingHttpClient;
@@ -33,8 +30,13 @@
3330
import com.linecorp.armeria.common.RequestHeadersBuilder;
3431
import com.linecorp.armeria.common.annotation.UnstableApi;
3532
import com.linecorp.armeria.common.athenz.TokenType;
33+
import com.linecorp.armeria.common.metric.MeterIdPrefix;
34+
import com.linecorp.armeria.common.metric.MoreMeters;
3635
import com.linecorp.armeria.common.util.Exceptions;
3736

37+
import io.micrometer.core.instrument.MeterRegistry;
38+
import io.micrometer.core.instrument.Timer;
39+
3840
/**
3941
* An {@link HttpClient} that adds an Athenz token to the request headers.
4042
* {@link TokenType#ACCESS_TOKEN} and {@link TokenType#YAHOO_ROLE_TOKEN} are supported.
@@ -53,18 +55,34 @@
5355
* .keyPair("/var/lib/athenz/service.key.pem", "/var/lib/athenz/service.cert.pem")
5456
* .build();
5557
*
58+
* // Using builder
59+
* WebClient
60+
* .builder()
61+
* .decorator(AthenzClient.builder(ztsBaseClient)
62+
* .domainName("my-domain")
63+
* .tokenType(TokenType.ROLE_TOKEN)
64+
* .newDecorator())
65+
* ...
66+
* .build();
67+
*
68+
* // Or using static factory method
5669
* WebClient
5770
* .builder()
5871
* .decorator(AthenzClient.newDecorator(ztsBaseClient, "my-domain",
59-
* TokenType.ROLE_TOKEN)
72+
* TokenType.ROLE_TOKEN))
6073
* ...
6174
* .build();
6275
* }</pre>
6376
*/
6477
@UnstableApi
6578
public final class AthenzClient extends SimpleDecoratingHttpClient {
6679

67-
private static final Duration DEFAULT_REFRESH_BEFORE = Duration.ofMinutes(10);
80+
/**
81+
* Returns a new {@link AthenzClientBuilder} with the specified {@link ZtsBaseClient}.
82+
*/
83+
public static AthenzClientBuilder builder(ZtsBaseClient ztsBaseClient) {
84+
return new AthenzClientBuilder(ztsBaseClient);
85+
}
6886

6987
/**
7088
* Returns a new {@link HttpClient} decorator that obtains an Athenz token for the specified domain and
@@ -74,9 +92,12 @@ public final class AthenzClient extends SimpleDecoratingHttpClient {
7492
* @param domainName the Athenz domain name
7593
* @param tokenType the type of Athenz token to obtain
7694
*/
77-
public static Function<HttpClient, AthenzClient> newDecorator(ZtsBaseClient ztsBaseClient,
78-
String domainName, TokenType tokenType) {
79-
return newDecorator(ztsBaseClient, domainName, ImmutableList.of(), tokenType);
95+
public static Function<? super HttpClient, AthenzClient> newDecorator(
96+
ZtsBaseClient ztsBaseClient, String domainName, TokenType tokenType) {
97+
return builder(ztsBaseClient)
98+
.domainName(domainName)
99+
.tokenType(tokenType)
100+
.newDecorator();
80101
}
81102

82103
/**
@@ -88,10 +109,13 @@ public static Function<HttpClient, AthenzClient> newDecorator(ZtsBaseClient ztsB
88109
* @param roleName the Athenz role name
89110
* @param tokenType the type of Athenz token to obtain
90111
*/
91-
public static Function<HttpClient, AthenzClient> newDecorator(ZtsBaseClient ztsBaseClient,
92-
String domainName, String roleName,
93-
TokenType tokenType) {
94-
return newDecorator(ztsBaseClient, domainName, ImmutableList.of(roleName), tokenType);
112+
public static Function<? super HttpClient, AthenzClient> newDecorator(
113+
ZtsBaseClient ztsBaseClient, String domainName, String roleName, TokenType tokenType) {
114+
return builder(ztsBaseClient)
115+
.domainName(domainName)
116+
.roleNames(roleName)
117+
.tokenType(tokenType)
118+
.newDecorator();
95119
}
96120

97121
/**
@@ -103,10 +127,13 @@ public static Function<HttpClient, AthenzClient> newDecorator(ZtsBaseClient ztsB
103127
* @param roleNames the list of Athenz role names
104128
* @param tokenType the type of Athenz token to obtain
105129
*/
106-
public static Function<HttpClient, AthenzClient> newDecorator(ZtsBaseClient ztsBaseClient,
107-
String domainName, List<String> roleNames,
108-
TokenType tokenType) {
109-
return newDecorator(ztsBaseClient, domainName, roleNames, tokenType, DEFAULT_REFRESH_BEFORE);
130+
public static Function<? super HttpClient, AthenzClient> newDecorator(
131+
ZtsBaseClient ztsBaseClient, String domainName, List<String> roleNames, TokenType tokenType) {
132+
return builder(ztsBaseClient)
133+
.domainName(domainName)
134+
.roleNames(roleNames)
135+
.tokenType(tokenType)
136+
.newDecorator();
110137
}
111138

112139
/**
@@ -119,26 +146,40 @@ public static Function<HttpClient, AthenzClient> newDecorator(ZtsBaseClient ztsB
119146
* @param tokenType the type of Athenz token to obtain
120147
* @param refreshBefore the duration before the token expires to refresh it
121148
*/
122-
public static Function<HttpClient, AthenzClient> newDecorator(ZtsBaseClient ztsBaseClient,
123-
String domainName, List<String> roleNames,
124-
TokenType tokenType, Duration refreshBefore) {
125-
requireNonNull(ztsBaseClient, "ztsBaseClient");
126-
requireNonNull(domainName, "domainName");
127-
requireNonNull(roleNames, "roleNames");
128-
final ImmutableList<String> roleNames0 = ImmutableList.copyOf(roleNames);
129-
requireNonNull(tokenType, "tokenType");
130-
requireNonNull(refreshBefore, "refreshBefore");
131-
return delegate -> new AthenzClient(delegate, ztsBaseClient, domainName, roleNames0,
132-
tokenType, refreshBefore);
149+
public static Function<? super HttpClient, AthenzClient> newDecorator(
150+
ZtsBaseClient ztsBaseClient, String domainName, List<String> roleNames,
151+
TokenType tokenType, Duration refreshBefore) {
152+
return builder(ztsBaseClient)
153+
.domainName(domainName)
154+
.roleNames(roleNames)
155+
.tokenType(tokenType)
156+
.refreshBefore(refreshBefore)
157+
.newDecorator();
133158
}
134159

135160
private final TokenType tokenType;
136161
private final TokenClient tokenClient;
162+
private final Timer successTimer;
163+
private final Timer failureTimer;
137164

138-
private AthenzClient(HttpClient delegate, ZtsBaseClient ztsBaseClient, String domainName,
139-
List<String> roleNames, TokenType tokenType, Duration refreshBefore) {
165+
AthenzClient(HttpClient delegate, ZtsBaseClient ztsBaseClient, String domainName,
166+
List<String> roleNames, TokenType tokenType, Duration refreshBefore,
167+
MeterIdPrefix meterIdPrefix) {
140168
super(delegate);
141169
this.tokenType = tokenType;
170+
final MeterRegistry meterRegistry = ztsBaseClient.clientFactory().meterRegistry();
171+
final String prefix = meterIdPrefix.name("token.fetch");
172+
successTimer = MoreMeters.newTimer(meterRegistry, prefix,
173+
meterIdPrefix.tags("result", "success",
174+
"domain", domainName,
175+
"roles", String.join(",", roleNames),
176+
"type", tokenType.name()));
177+
failureTimer = MoreMeters.newTimer(meterRegistry, prefix,
178+
meterIdPrefix.tags("result", "failure",
179+
"domain", domainName,
180+
"roles", String.join(",", roleNames),
181+
"type", tokenType.name()));
182+
142183
if (tokenType.isRoleToken()) {
143184
tokenClient = new RoleTokenClient(ztsBaseClient, domainName, roleNames, refreshBefore);
144185
} else {
@@ -148,7 +189,16 @@ private AthenzClient(HttpClient delegate, ZtsBaseClient ztsBaseClient, String do
148189

149190
@Override
150191
public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Exception {
151-
final CompletableFuture<HttpResponse> future = tokenClient.getToken().thenApply(token -> {
192+
final long startNanos = System.nanoTime();
193+
194+
final CompletableFuture<HttpResponse> future = tokenClient.getToken().handle((token, cause) -> {
195+
final long elapsedNanos = System.nanoTime() - startNanos;
196+
if (cause != null) {
197+
failureTimer.record(elapsedNanos, TimeUnit.NANOSECONDS);
198+
return Exceptions.throwUnsafely(cause);
199+
}
200+
201+
successTimer.record(elapsedNanos, TimeUnit.NANOSECONDS);
152202
final HttpRequest newReq = req.mapHeaders(headers -> {
153203
final RequestHeadersBuilder builder = headers.toBuilder();
154204
String token0 = token;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2025 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.armeria.client.athenz;
18+
19+
import static com.google.common.base.Preconditions.checkState;
20+
import static java.util.Objects.requireNonNull;
21+
22+
import java.time.Duration;
23+
import java.util.List;
24+
import java.util.function.Function;
25+
26+
import com.google.common.collect.ImmutableList;
27+
28+
import com.linecorp.armeria.client.HttpClient;
29+
import com.linecorp.armeria.common.annotation.Nullable;
30+
import com.linecorp.armeria.common.annotation.UnstableApi;
31+
import com.linecorp.armeria.common.athenz.TokenType;
32+
import com.linecorp.armeria.common.metric.MeterIdPrefix;
33+
34+
/**
35+
* A builder for creating an {@link AthenzClient} decorator.
36+
*
37+
* <p>Example:
38+
* <pre>{@code
39+
* ZtsBaseClient ztsBaseClient = ...;
40+
*
41+
* AthenzClient.builder(ztsBaseClient)
42+
* .domainName("my-domain")
43+
* .roleNames("my-role")
44+
* .tokenType(TokenType.ACCESS_TOKEN)
45+
* .refreshBefore(Duration.ofMinutes(5))
46+
* .newDecorator();
47+
* }</pre>
48+
*/
49+
@UnstableApi
50+
public final class AthenzClientBuilder {
51+
52+
private static final Duration DEFAULT_REFRESH_BEFORE = Duration.ofMinutes(10);
53+
private static final MeterIdPrefix DEFAULT_METER_ID_PREFIX =
54+
new MeterIdPrefix("armeria.client.athenz");
55+
56+
private final ZtsBaseClient ztsBaseClient;
57+
@Nullable
58+
private String domainName;
59+
private final ImmutableList.Builder<String> roleNamesBuilder = ImmutableList.builder();
60+
private TokenType tokenType = TokenType.ACCESS_TOKEN;
61+
private Duration refreshBefore = DEFAULT_REFRESH_BEFORE;
62+
private MeterIdPrefix meterIdPrefix = DEFAULT_METER_ID_PREFIX;
63+
64+
AthenzClientBuilder(ZtsBaseClient ztsBaseClient) {
65+
this.ztsBaseClient = requireNonNull(ztsBaseClient, "ztsBaseClient");
66+
}
67+
68+
/**
69+
* Sets the Athenz domain name.
70+
* The domain name must be set before calling {@link #newDecorator()}.
71+
*/
72+
public AthenzClientBuilder domainName(String domainName) {
73+
this.domainName = requireNonNull(domainName, "domainName");
74+
return this;
75+
}
76+
77+
/**
78+
* Adds Athenz role names.
79+
*/
80+
public AthenzClientBuilder roleNames(String... roleNames) {
81+
requireNonNull(roleNames, "roleNames");
82+
roleNamesBuilder.add(roleNames);
83+
return this;
84+
}
85+
86+
/**
87+
* Adds Athenz role names.
88+
*/
89+
public AthenzClientBuilder roleNames(Iterable<String> roleNames) {
90+
requireNonNull(roleNames, "roleNames");
91+
roleNamesBuilder.addAll(roleNames);
92+
return this;
93+
}
94+
95+
/**
96+
* Sets the type of Athenz token to obtain.
97+
* If not set, the default is {@link TokenType#ACCESS_TOKEN}.
98+
*/
99+
public AthenzClientBuilder tokenType(TokenType tokenType) {
100+
this.tokenType = requireNonNull(tokenType, "tokenType");
101+
return this;
102+
}
103+
104+
/**
105+
* Sets the duration before the token expires to refresh it.
106+
* If not set, the default is 10 minutes.
107+
*/
108+
public AthenzClientBuilder refreshBefore(Duration refreshBefore) {
109+
requireNonNull(refreshBefore, "refreshBefore");
110+
checkState(!refreshBefore.isNegative(), "refreshBefore: %s (expected: >= 0)", refreshBefore);
111+
this.refreshBefore = refreshBefore;
112+
return this;
113+
}
114+
115+
/**
116+
* Sets the prefix to use when naming the metrics for this client.
117+
* If not set, the default is {@code "armeria.client.athenz"}.
118+
*/
119+
public AthenzClientBuilder meterIdPrefix(MeterIdPrefix meterIdPrefix) {
120+
this.meterIdPrefix = requireNonNull(meterIdPrefix, "meterIdPrefix");
121+
return this;
122+
}
123+
124+
/**
125+
* Returns a new {@link HttpClient} decorator configured with the settings in this builder.
126+
*/
127+
public Function<? super HttpClient, AthenzClient> newDecorator() {
128+
final String domainName = this.domainName;
129+
checkState(domainName != null, "domainName is not set");
130+
131+
final List<String> roleNames = roleNamesBuilder.build();
132+
return delegate -> new AthenzClient(delegate, ztsBaseClient, domainName, roleNames,
133+
tokenType, refreshBefore, meterIdPrefix);
134+
}
135+
}
136+

athenz/src/main/java/com/linecorp/armeria/client/athenz/ZtsBaseClientBuilder.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,17 @@
3535
import com.linecorp.armeria.client.ClientFactoryBuilder;
3636
import com.linecorp.armeria.client.WebClient;
3737
import com.linecorp.armeria.client.WebClientBuilder;
38+
import com.linecorp.armeria.client.metric.MetricCollectingClient;
3839
import com.linecorp.armeria.client.proxy.ConnectProxyConfig;
3940
import com.linecorp.armeria.client.proxy.ProxyConfig;
4041
import com.linecorp.armeria.common.TlsKeyPair;
4142
import com.linecorp.armeria.common.annotation.Nullable;
4243
import com.linecorp.armeria.common.annotation.UnstableApi;
44+
import com.linecorp.armeria.common.metric.MeterIdPrefixFunction;
4345
import com.linecorp.armeria.internal.common.util.CertificateUtil;
4446

47+
import io.micrometer.core.instrument.MeterRegistry;
48+
4549
/**
4650
* A builder for creating a {@link ZtsBaseClient} instance.
4751
*/
@@ -187,6 +191,19 @@ public ZtsBaseClientBuilder configureWebClient(Consumer<? super WebClientBuilder
187191
return this;
188192
}
189193

194+
/**
195+
* Enable metrics collection for the ZTS client using the specified {@link MeterRegistry} and
196+
* {@link MeterIdPrefixFunction}.
197+
*/
198+
public ZtsBaseClientBuilder enableMetrics(MeterRegistry meterRegistry,
199+
MeterIdPrefixFunction meterIdPrefixFunction) {
200+
requireNonNull(meterRegistry, "meterRegistry");
201+
requireNonNull(meterIdPrefixFunction, "meterIdPrefixFunction");
202+
configureClientFactory(fb -> fb.meterRegistry(meterRegistry));
203+
configureWebClient(cb -> cb.decorator(MetricCollectingClient.newDecorator(meterIdPrefixFunction)));
204+
return this;
205+
}
206+
190207
/**
191208
* Builds a new {@link ZtsBaseClient} instance with the configured settings.
192209
*/

0 commit comments

Comments
 (0)