Skip to content

Commit 9062ebf

Browse files
GH-6372: Expose client request metrics.
1 parent c372307 commit 9062ebf

File tree

4 files changed

+239
-3
lines changed

4 files changed

+239
-3
lines changed

core/src/main/java/com/linecorp/armeria/client/HttpChannelPool.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
import com.google.common.collect.ImmutableSet;
4141

42+
import com.linecorp.armeria.client.metric.ClientMetrics;
4243
import com.linecorp.armeria.client.proxy.ConnectProxyConfig;
4344
import com.linecorp.armeria.client.proxy.HAProxyConfig;
4445
import com.linecorp.armeria.client.proxy.ProxyConfig;
@@ -99,14 +100,17 @@ final class HttpChannelPool implements AsyncCloseable {
99100
// Fields for creating a new connection:
100101
private final Bootstraps bootstraps;
101102
private final int connectTimeoutMillis;
103+
private final ClientMetrics clientMetrics;
102104

103105
HttpChannelPool(HttpClientFactory clientFactory, EventLoop eventLoop,
104106
SslContext sslCtxHttp1Or2, SslContext sslCtxHttp1Only,
105107
@Nullable SslContextFactory sslContextFactory,
106-
ConnectionPoolListener listener) {
108+
ConnectionPoolListener listener,
109+
ClientMetrics clientMetrics) {
107110
this.clientFactory = clientFactory;
108111
this.eventLoop = eventLoop;
109112
this.listener = listener;
113+
this.clientMetrics = clientMetrics;
110114

111115
pool = newEnumMap(ImmutableSet.of(SessionProtocol.H1, SessionProtocol.H1C,
112116
SessionProtocol.H2, SessionProtocol.H2C));
@@ -211,10 +215,12 @@ private ChannelAcquisitionFuture getPendingAcquisition(SessionProtocol desiredPr
211215

212216
private void setPendingAcquisition(SessionProtocol desiredProtocol, PoolKey key,
213217
ChannelAcquisitionFuture future) {
218+
clientMetrics.incrementPendingRequest(desiredProtocol);
214219
pendingAcquisitions[desiredProtocol.ordinal()].put(key, future);
215220
}
216221

217222
private void removePendingAcquisition(SessionProtocol desiredProtocol, PoolKey key) {
223+
clientMetrics.decrementPendingRequest(desiredProtocol);
218224
pendingAcquisitions[desiredProtocol.ordinal()].remove(key);
219225
}
220226

core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.google.common.collect.MapMaker;
4040

4141
import com.linecorp.armeria.client.endpoint.EndpointGroup;
42+
import com.linecorp.armeria.client.metric.ClientMetrics;
4243
import com.linecorp.armeria.client.proxy.ProxyConfigSelector;
4344
import com.linecorp.armeria.client.redirect.RedirectConfig;
4445
import com.linecorp.armeria.common.Http1HeaderNaming;
@@ -142,6 +143,7 @@ private static void setupTlsMetrics(List<X509Certificate> certificates, MeterReg
142143
private final ClientFactoryOptions options;
143144
private final boolean autoCloseConnectionPoolListener;
144145
private final AsyncCloseableSupport closeable = AsyncCloseableSupport.of(this::closeAsync);
146+
private final ClientMetrics clientMetrics = new ClientMetrics();
145147

146148
HttpClientFactory(ClientFactoryOptions options, boolean autoCloseConnectionPoolListener) {
147149
workerGroup = options.workerGroup();
@@ -543,7 +545,13 @@ HttpChannelPool pool(EventLoop eventLoop) {
543545
e -> new HttpChannelPool(this, eventLoop,
544546
sslCtxHttp1Or2, sslCtxHttp1Only,
545547
sslContextFactory,
546-
connectionPoolListener()));
548+
connectionPoolListener(),
549+
clientMetrics)
550+
);
551+
}
552+
553+
ClientMetrics clientMetrics() {
554+
return this.clientMetrics;
547555
}
548556

549557
@VisibleForTesting

core/src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.slf4j.LoggerFactory;
3535

3636
import com.linecorp.armeria.client.HttpChannelPool.PoolKey;
37+
import com.linecorp.armeria.client.endpoint.EndpointGroup;
38+
import com.linecorp.armeria.client.metric.ClientMetrics;
3739
import com.linecorp.armeria.client.proxy.ProxyType;
3840
import com.linecorp.armeria.common.AggregationOptions;
3941
import com.linecorp.armeria.common.ClosedSessionException;
@@ -240,8 +242,12 @@ public void invoke(PooledChannel pooledChannel, ClientRequestContext ctx,
240242
pooledChannel.release();
241243
return;
242244
}
243-
244245
final long writeTimeoutMillis = ctx.writeTimeoutMillis();
246+
final EndpointGroup endpointGroup = ctx.endpointGroup();
247+
final ClientMetrics clientMetrics = clientFactory.clientMetrics();
248+
clientMetrics.incrementActiveRequest(endpointGroup);
249+
ctx.log().whenComplete()
250+
.whenComplete((unusedLog, unusedCause) -> clientMetrics.decrementActiveRequest(endpointGroup));
245251

246252
assert protocol != null;
247253
assert requestEncoder != null;
@@ -258,6 +264,7 @@ public void invoke(PooledChannel pooledChannel, ClientRequestContext ctx,
258264
: CompletableFuture.allOf(req.whenComplete(), res.whenComplete());
259265
completionFuture.handle((ret, cause) -> {
260266
assert responseDecoder != null;
267+
// TODO: HTTP/1.1 계열이면 여기서 active -1
261268
if (isAcquirable(responseDecoder.keepAliveHandler())) {
262269
pooledChannel.release();
263270
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* LINE 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.metric;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.concurrent.ConcurrentMap;
23+
import java.util.concurrent.atomic.LongAdder;
24+
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import com.linecorp.armeria.client.endpoint.EndpointGroup;
29+
import com.linecorp.armeria.common.SessionProtocol;
30+
import com.linecorp.armeria.common.annotation.Nullable;
31+
32+
/**
33+
* Collects simple client-side metrics such as:
34+
* <ul>
35+
* <li>The number of pending requests per {@link SessionProtocol}</li>
36+
* <li>The number of active requests per {@link EndpointGroup}</li>
37+
* </ul>
38+
* This class is intended to be used as an in-memory counter.
39+
*/
40+
public class ClientMetrics {
41+
42+
private static final Logger logger = LoggerFactory.getLogger(ClientMetrics.class);
43+
44+
private final LongAdder httpsPendingRequest;
45+
private final LongAdder httpPendingRequest;
46+
private final LongAdder http1PendingRequest;
47+
private final LongAdder http2PendingRequest;
48+
private final LongAdder proxyPendingRequest;
49+
50+
// EndpointGroup does not override equals() and hashCode().
51+
// Call sites must use the same 'EndpointGroup' instance when invoking
52+
// 'incrementActiveRequest(...)' and 'decrementActiveRequest'.
53+
private final ConcurrentMap<EndpointGroup, LongAdder> activeRequestsPerEndpointGroup;
54+
55+
/**
56+
* Creates a new instance with all counters initialized to zero.
57+
*/
58+
public ClientMetrics() {
59+
this.activeRequestsPerEndpointGroup = new ConcurrentHashMap<>();
60+
this.httpsPendingRequest = new LongAdder();
61+
this.httpPendingRequest = new LongAdder();
62+
this.http1PendingRequest = new LongAdder();
63+
this.http2PendingRequest = new LongAdder();
64+
this.proxyPendingRequest = new LongAdder();
65+
}
66+
67+
/**
68+
* Increments the number of active requests for the
69+
* specified {@link EndpointGroup}.
70+
* @param endpointGroup the endpoint group.
71+
*/
72+
public void incrementActiveRequest(EndpointGroup endpointGroup) {
73+
if (endpointGroup == null) {
74+
return;
75+
}
76+
this.activeRequestsPerEndpointGroup
77+
.computeIfAbsent(endpointGroup, unusedKey -> new LongAdder())
78+
.increment();
79+
}
80+
81+
/**
82+
* Decrements the number of active requests for the
83+
* specified {@link EndpointGroup}. If the counter for the
84+
* {@code endpointGroup} becomes zero after decrement,
85+
* the entry is removed from {@code activeRequestsPerEndpointGroup}.
86+
* @param endpointGroup the endpoint group.
87+
*/
88+
public void decrementActiveRequest(EndpointGroup endpointGroup) {
89+
if (endpointGroup == null) {
90+
return;
91+
}
92+
93+
activeRequestsPerEndpointGroup.computeIfPresent(endpointGroup, (key, counter) -> {
94+
counter.decrement();
95+
final long currentCount = counter.sum();
96+
assert currentCount >= 0;
97+
return currentCount == 0 ? null : counter;
98+
});
99+
}
100+
101+
/**
102+
* Decrements the number of pending requests for the given {@link SessionProtocol}.
103+
* @param desiredProtocol the desired protocol.
104+
*/
105+
public void decrementPendingRequest(SessionProtocol desiredProtocol) {
106+
final LongAdder counter = counter(desiredProtocol);
107+
if (counter != null) {
108+
counter.decrement();
109+
assert counter.sum() >= 0;
110+
}
111+
}
112+
113+
/**
114+
* Increments the number of pending requests for the given {@link SessionProtocol}.
115+
* @param desiredProtocol the desired protocol.
116+
*/
117+
public void incrementPendingRequest(SessionProtocol desiredProtocol) {
118+
final LongAdder counter = counter(desiredProtocol);
119+
if (counter != null) {
120+
counter.increment();
121+
}
122+
}
123+
124+
/**
125+
* Returns the total number of pending requests across all supported protocols.
126+
* @return the total number of pending requests
127+
*/
128+
public long pendingRequests() {
129+
return httpPendingRequests() +
130+
httpsPendingRequests() +
131+
http1PendingRequests() +
132+
http2PendingRequests() +
133+
proxyPendingRequests();
134+
}
135+
136+
/**
137+
* Returns the count of http pending requests.
138+
* @return the count of http pending requests.
139+
*/
140+
public long httpPendingRequests() {
141+
return httpPendingRequest.sum();
142+
}
143+
144+
/**
145+
* Returns the count of https pending requests.
146+
* @return the count of https pending requests.
147+
*/
148+
public long httpsPendingRequests() {
149+
return httpsPendingRequest.sum();
150+
}
151+
152+
/**
153+
* Returns the count of http1 pending requests.
154+
* @return the count of http1 pending requests.
155+
*/
156+
public long http1PendingRequests() {
157+
return http1PendingRequest.sum();
158+
}
159+
160+
/**
161+
* Returns the count of http2 pending requests.
162+
* @return the count of http2 pending requests.
163+
*/
164+
public long http2PendingRequests() {
165+
return http2PendingRequest.sum();
166+
}
167+
168+
/**
169+
* Returns the count of proxy pending requests.
170+
* @return the count of proxy pending requests.
171+
*/
172+
public long proxyPendingRequests() {
173+
return proxyPendingRequest.sum();
174+
}
175+
176+
/**
177+
* Returns a snapshot of the number of active requests per {@link EndpointGroup}.
178+
* @return a map whose key is an {@link EndpointGroup} and whose value is the number of
179+
* active requests currently associated with it
180+
*/
181+
public Map<EndpointGroup, Long> activeRequestPerEndpointGroup() {
182+
final Map<EndpointGroup, Long> result = new HashMap<>();
183+
for (Map.Entry<EndpointGroup, LongAdder> entry : activeRequestsPerEndpointGroup.entrySet()) {
184+
final EndpointGroup key = entry.getKey();
185+
final LongAdder value = entry.getValue();
186+
result.put(key, value.sum());
187+
}
188+
189+
return result;
190+
}
191+
192+
@Nullable
193+
private LongAdder counter(SessionProtocol desiredProtocol) {
194+
switch (desiredProtocol) {
195+
case HTTPS:
196+
return httpsPendingRequest;
197+
case HTTP:
198+
return httpPendingRequest;
199+
case H1:
200+
return http1PendingRequest;
201+
case H1C:
202+
return http1PendingRequest;
203+
case H2:
204+
return http2PendingRequest;
205+
case H2C:
206+
return http2PendingRequest;
207+
case PROXY:
208+
return proxyPendingRequest;
209+
default:
210+
// To prevent log explosion in production environment.
211+
logger.debug("Unexpected SessionProtocol: {}", desiredProtocol);
212+
return null;
213+
}
214+
}
215+
}

0 commit comments

Comments
 (0)