Skip to content

Conversation

@chickenchickenlove
Copy link
Contributor

@chickenchickenlove chickenchickenlove commented Nov 16, 2025

Motivation:

Because of #6372 .

Modifications:

  • Add new class ClientMetrics.
  • HttpClientFactory has ClientMetrics as its field.
  • HttpChannelPool has ClientMetrics as its field.
  • HttpChannelPool calls ClientMetrics whenever calling setPendingAcquisition(...) and removePendingAcquisition(...).
  • HttpSessionHandler calls ClientMetrics to increment count for active request and reserve to decrement count of active request.

Result:

/**
* Collects simple client-side metrics such as:
* <ul>
* <li>The number of pending requests per {@link SessionProtocol}</li>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) If the objective is to determine the optimal number of event loops depending on pending requests for each endpoint group/protocol, would it be enough to check the duration instead?

final EndpointGroup endpointGroup = ctx.endpointGroup();
ctx.log().whenComplete().thenAccept(log -> {
    final long pendingDuration = log.connectionTimings().pendingAcquisitionDurationNanos();
    final SessionProtocol sessionProtocol = log.sessionProtocol();
});

Copy link
Contributor Author

@chickenchickenlove chickenchickenlove Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrhee17 thanks for your comments!
Your comment also make sense to me!
However, IMHO, it may not be enough or enough. 😄
So I would like to vote for counting the number of pending requests.

The pendingAcquisitionDurationNanos seems to be set after request get channel.
In that case, pendingAcquisitionDurationNanos can be -1 or large value.

If the acquisition of a channel is delayed due to a sudden surge in requests, the value of pendingAcquisitionDurationNanos will continuously remain -1. Or, if the channel is acquired after a very long time, the value of pendingAcquisitionDurationNanos will change from -1 to a very large value. Therefore, the responsiveness of the operation that increases the EventLoop is likely to be degraded.

Additionally, when the Channel is Busy, we need to consider two states:

  1. The value is -1.
  2. The value is an enormously large number.

Consequently, the ClientMetrics object would need to contain code to account for this, which potentially implies that ClientMetrics is tightly coupled with the logic of ConnectionTimings.

For these reasons, I vote for counting the number of pending requests.

What do you think?

Also, @ikhoon , please give your opinion when you have time!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using pending duration is also a good idea. However, adding ClientMetrics would be worthwhile because:

  • ClientMetrics itself seems to provide useful information at the ClientFactory level.
  • Using ConnectionTimings inside maxNumEventLoopsFunction method doesn't look straightforward as it may require additional processing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see - I don't think I can meaningfully review this PR at this point since I'm not sure from the issue/PR description how ClientMetrics is expected to be used.

Do you envision ClientMetrics as a general metric collector that collects metrics on the overall Client (or ClientFactory)? What would be the relationship between this metric collector and other metrics that are already being exported?

Otherwise, it might help if the class name were more specific.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most APIs that are exposed at the request level will primarily be used for logging or collected by metric collectors such as Prometheus. Since they contain per-request details, it does not seem easy to use them directly to control the server’s runtime behavior.

ServerMetrics was first exposed as a Java API because it was difficult to obtain information about in-flight requests at runtime when implementing a custom graceful shutdown. Similarly, ClientMetrics will expose the information needed to control runtime behavior at the client or client-factory level. I expect these values to be exposed mostly as simple counters rather than histograms.

Otherwise, it might help if the class name were more specific.

I’m open to changing it if you think there’s a better name.

// EndpointGroup does not override equals() and hashCode().
// Call sites must use the same 'EndpointGroup' instance when invoking
// 'incrementActiveRequest(...)' and 'decrementActiveRequest'.
private final ConcurrentMap<EndpointGroup, LongAdder> activeRequestsPerEndpointGroup;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Couldn't we use Endpoint instead of EndpointGroup as the key to collect metrics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ikhoon nim, thanks for your comments.
Addressed it!

@chickenchickenlove
Copy link
Contributor Author

@ikhoon , @jrhee17
I made a new commit base on comments!
When you have time, please take another look. 🙇‍♂️

@ikhoon
Copy link
Contributor

ikhoon commented Dec 8, 2025

After discussing with other team members, we came to the conclusion that exposing client metrics in an event-listener form would be a better approach.

public interface ClientRequestLifecycleListener {

    /**
     * Called when a request is created and scheduled but not yet executed.
     */
    void onRequestPending(ClientRequestContext ctx);

    /**
     * Called when the client begins execution (connection acquired, headers sent).
     */
    void onRequestStart(ClientRequestContext ctx);

    /**
     * Called when the request is fully sent.
     */
    void onRequestSendComplete(ClientRequestContext ctx);

    /**
     * Called when the first response headers are received.
     */
    void onResponseHeaders(ClientRequestContext ctx, ResponseHeaders headers);

    /**
     * Called when the full response body is received successfully.
     */
    void onResponseComplete(ClientRequestContext ctx);

    /**
     * Called when a request is completed with either success or failure.
     */
    void onRequestComplete(ClientRequestContext ctx, @Nullable Throwable cause);
}

I’m thinking of allowing this listener to be configured at the ClientFactory level and integrating it into RequestLog for everything except onRequestPending().
And it might be good to provide a default implementation so that users can easily obtain information such as pendingRequests.

ClientRequestLifecycleListener.counting();

@chickenchickenlove
Copy link
Contributor Author

@ikhoon
Thanks for your comments.
Let me implement it.

@coderabbitai
Copy link

coderabbitai bot commented Dec 9, 2025

Walkthrough

Adds a ClientRequestLifecycleListener API and implementations (noop, counting), a RequestLog bridge and listener, threadsafe client request metrics, wires the listener through ClientFactoryBuilder → HttpClientFactory, invokes it in HttpClientDelegate, adds integration tests, and inserts a debug print in DefaultRequestLog.

Changes

Cohort / File(s) Summary
Lifecycle Interface
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java
New public interface declaring client request lifecycle callbacks, asRequestLogListener() bridge, and static helpers counting() and noop().
No-op Implementation
core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java
Singleton no-op implementation returning a no-op RequestLogListener.
Metrics Implementation
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java
New counting implementation tracking pending per SessionProtocol and active per Endpoint using thread-safe counters and per-request state; exposes metric accessors.
RequestLog Bridge
core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java
New RequestLogListener mapping RequestLogProperty events to ClientRequestLifecycleListener callbacks.
Factory Builder & Factory
core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java, core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java
Added setter/field for ClientRequestLifecycleListener on builder; HttpClientFactory constructors/field/getter expanded to accept/store the listener and default to noop.
Client Execution Hook
core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java
Calls clientRequestLifecycleListener.onRequestPending(ctx) and attaches clientRequestLifecycleListener.asRequestLogListener() to the request log during execute flow.
Debug Print Added
core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java
Added a System.out.println that prints newly added log properties in maybeNotifyListeners.
Integration Tests
core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java
New tests covering normal lifecycle, early cancellation, and failed-connect scenarios validating pending/active metric transitions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant ClientFactoryBuilder
  participant HttpClientFactory
  participant HttpClientDelegate
  participant RequestLog
  participant ClientRequestLifecycleListener

  Client->>ClientFactoryBuilder: clientRequestLifecycleListener(listener)
  ClientFactoryBuilder->>HttpClientFactory: build(..., listener)
  Client->>HttpClientDelegate: execute(request, ctx)
  HttpClientDelegate->>ClientRequestLifecycleListener: onRequestPending(ctx)
  HttpClientDelegate->>RequestLog: addListener(listener.asRequestLogListener())
  Note right of RequestLog: RequestLog emits properties over time
  RequestLog->>ClientRequestLifecycleListener: onRequestStart(ctx)
  RequestLog->>ClientRequestLifecycleListener: onRequestSendComplete(ctx)
  RequestLog->>ClientRequestLifecycleListener: onResponseHeaders(ctx, headers)
  RequestLog->>ClientRequestLifecycleListener: onResponseComplete(ctx)
  RequestLog->>ClientRequestLifecycleListener: onRequestComplete(ctx, cause)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect concurrency and per-request State handling in ClientRequestMetrics.
  • Verify safe attachment/removal of RequestLogListener in HttpClientDelegate to avoid leaks.
  • Confirm whether the System.out.println in DefaultRequestLog is intentional and should be removed.

Possibly related PRs

Suggested labels

new feature

Suggested reviewers

  • ikhoon
  • trustin
  • minwoox

Poem

I hop through queues with whiskers bright,
Counting hops from morning to night,
Adders click and endpoints sing,
I tally requests on every spring,
This rabbit keeps the client metrics right. 🐇

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes a debug print statement in DefaultRequestLog.maybeNotifyListeners that appears unrelated to the core metrics exposure objectives. Remove the System.out.println debug statement from DefaultRequestLog.maybeNotifyListeners as it is not part of the metrics exposure feature.
Docstring Coverage ⚠️ Warning Docstring coverage is 36.54% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the PR: exposing client request metrics via a listener-based API.
Description check ✅ Passed The description mentions key modifications related to client request lifecycle metrics, though it references outdated class names compared to actual implementation.
Linked Issues check ✅ Passed The PR implements the core requirement from #6372 to expose client request metrics, but uses a listener-based architecture rather than the originally proposed direct API.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (1)

88-94: Fix method naming for consistency.

The method name pendingRequest() should be pendingRequests() (plural) to match the naming pattern of activeRequests() and align with conventions for aggregate query methods.

Apply this diff:

-    public long pendingRequest() {
+    public long pendingRequests() {
         long sum = 0L;
         for (LongAdder adder : pendingRequestsPerProtocol.values()) {
             sum += adder.sum();
         }
         return sum;
     }
core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java (1)

345-346: Fix formatting for consistency.

The getter method formatting doesn't match the style of other methods in this file. It should be properly formatted with the return statement on a new line.

Apply this diff:

-    
-    ClientRequestLifecycleListener clientRequestLifecycleListener() {return clientRequestLifecycleListener;}
+
+    ClientRequestLifecycleListener clientRequestLifecycleListener() {
+        return clientRequestLifecycleListener;
+    }
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java (1)

8-45: LGTM with optional documentation suggestions.

The interface design is clean and follows good practices with the factory method pattern. The lifecycle callbacks provide comprehensive coverage of the request lifecycle.

Consider enhancing the interface-level javadoc to document:

  1. The order in which callbacks are invoked
  2. Threading guarantees (which thread calls each method)
  3. Exception handling behavior (what happens if a callback throws)
  4. Whether callbacks are optional or all must be implemented

Example addition to interface javadoc:

/**
 * Listener interface for client request lifecycle events.
 * 
 * <p>Callbacks are invoked in the following order:
 * <ol>
 *   <li>{@link #onRequestPending(ClientRequestContext)} - on the caller thread</li>
 *   <li>{@link #onRequestStart(ClientRequestContext)} - on the event loop</li>
 *   <li>{@link #onRequestSendComplete(ClientRequestContext)} - on the event loop</li>
 *   <li>{@link #onResponseHeaders(ClientRequestContext, ResponseHeaders)} - on the event loop</li>
 *   <li>{@link #onResponseComplete(ClientRequestContext)} - on the event loop</li>
 *   <li>{@link #onRequestComplete(ClientRequestContext, Throwable)} - on the event loop</li>
 * </ol>
 * 
 * <p>Implementations should not throw exceptions. If an exception is thrown,
 * it will be logged but will not affect request processing.
 */
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b06341 and d309aa3.

📒 Files selected for processing (4)
  • core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java (4 hunks)
  • core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java (4 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java (1)
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (1)
  • ClientRequestMetrics (14-96)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: site
  • GitHub Check: lint
  • GitHub Check: flaky-tests
  • GitHub Check: Summary
🔇 Additional comments (1)
core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java (1)

139-140: Verify default metrics collection behavior is intentional.

Every ClientFactory will have metrics tracking enabled by default via the automatically instantiated ClientRequestMetrics. While the overhead is likely minimal due to LongAdder, you may want to verify:

  1. Whether this default behavior is documented for users
  2. Whether a no-op implementation should be available for users who want to opt out of metrics collection

Consider documenting this default behavior in the class-level javadoc or providing a static factory method like ClientRequestLifecycleListener.noop() for users who want to disable metrics tracking.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java (2)

11-39: Consider enhancing lifecycle method documentation.

The current documentation is concise but could benefit from additional details:

  • Expected callback ordering (e.g., onRequestPendingonRequestStart → ... → onRequestComplete)
  • Relationship between onResponseComplete (success only) and onRequestComplete (always called, success or failure)
  • Threading guarantees for implementations
  • Exception handling behavior

These clarifications would help implementers understand the contract more clearly.


49-82: Add private constructor to prevent external instantiation.

The singleton pattern should include an explicit private constructor to prevent external instantiation of NoopClientRequestLifecycleListener.

Apply this diff to add a private constructor:

 class NoopClientRequestLifecycleListener implements ClientRequestLifecycleListener {
     
     private static final NoopClientRequestLifecycleListener INSTANCE = new NoopClientRequestLifecycleListener();
+    
+    private NoopClientRequestLifecycleListener() {
+    }
     
     @Override
     public void onRequestPending(ClientRequestContext ctx) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d309aa3 and af7797a.

📒 Files selected for processing (2)
  • core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java (4 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: flaky-tests
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: site
  • GitHub Check: lint
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: Summary
🔇 Additional comments (2)
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java (2)

8-9: LGTM!

The @UnstableApi annotation is appropriate for this new lifecycle listener interface.


41-43: Verify ClientRequestMetrics exists and implements this interface.

The counting() method returns ClientRequestMetrics, which according to the PR description should implement ClientRequestLifecycleListener. Ensure this class exists and properly implements all lifecycle methods.

#!/bin/bash
# Verify ClientRequestMetrics exists and implements ClientRequestLifecycleListener

# Find the ClientRequestMetrics class file
fd -t f "ClientRequestMetrics.java"

# Check if it implements ClientRequestLifecycleListener
ast-grep --pattern $'class ClientRequestMetrics implements $$$ClientRequestLifecycleListener$$$ {
  $$$
}'

@chickenchickenlove
Copy link
Contributor Author

chickenchickenlove commented Dec 9, 2025

@ikhoon
I made new commits based on your comments to make sure we're on the same page.
Once #6543 is merged, I think we can make this PR more specific.
What do you think?

ikhoon added a commit that referenced this pull request Dec 11, 2025
Motivation:

A single `RequestLogProperty` can be received through
`RequestLog.whenAvailable()`, but it is not straightforward to receive
all events published by `RequestLog` using `.whenAvailable()`.

To address the limitation, I propose introducing an interface that
listens to all events by adding it to `RequestLog`. This will also
simplify the integration of other implementations used for collecting
metrics. Related: #6502

Modifications:

- Introduced `RequestLogListener` API that can be attached to
`RequestLog`.
- `RequestLogAccess.addListener()` API was added and `DefaultRequestLog`
implemented it.
- The listener will be notified whenever a new property is set to
`RequestLog`. If some properties have already been set, it will notified
of them immediately.
- Add `REQUEST_COMPLETE`, `RESPONSE_COMPLETE` and `ALL_COMPLETE` to
`RequestLogProperty`.
- Previously, there were APIs such as `whenRequestComplete()` and
`whenComplete()` that computed and signaled request or response
completion and but no explicit properties exist for them.
`RequestLogProperty` should represent all states in
`RequestLogListener`, I added the new completion properties.
- Simplified child log propagation in `DefaultRequestLog` and the
`Observation{Service,Client}` implementations by using
`RequestLogListener`.

Result:

You can now use `RequestLogListener` to observe all `RequestLog` events.
@ikhoon
Copy link
Contributor

ikhoon commented Dec 11, 2025

You may want to use RequestLogListener added in #6543 to implement ClientRequestLifecycleListener

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java (1)

161-168: Clarify test assertion comment.

The logic is correct, but the comment "it will never reach here" is slightly misleading. The await() call does execute but times out as expected (since the endpoint is never invoked due to early cancellation). Consider clarifying:

             try {
-                // because of early cancel,
-                // it will never reach here.
+                // Because of early cancel, the endpoint should never be invoked,
+                // so the latch will timeout.
                 SCENARIO2_NEVER_CALLED.await(1, TimeUnit.SECONDS);
             } catch (Exception e) {
                 assertInstanceOf(InterruptedException.class, e);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af7797a and 606bf40.

📒 Files selected for processing (7)
  • core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java (4 hunks)
  • core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java (6 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java (1 hunks)
  • core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java
  • core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java
  • core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java
  • core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java
🧬 Code graph analysis (1)
core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java (3)
core/src/main/java/com/linecorp/armeria/client/UnprocessedRequestException.java (1)
  • UnprocessedRequestException (32-70)
sangria/sangria_2.13/src/main/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlService.scala (1)
  • builder (156-161)
sangria/sangria_2.13/src/main/scala/com/linecorp/armeria/server/sangria/SangriaGraphqlServiceBuilder.scala (1)
  • build (146-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: flaky-tests
  • GitHub Check: site
  • GitHub Check: lint
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: Summary
🔇 Additional comments (5)
core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java (1)

102-106: LGTM! Well-positioned lifecycle integration.

The lifecycle listener hooks are correctly placed:

  • onRequestPending is invoked early, before connection acquisition
  • The RequestLogListener bridge is attached to capture subsequent lifecycle events

This implementation aligns with the event-listener approach discussed in the PR comments.

core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java (2)

94-134: LGTM! Comprehensive test coverage.

The test properly validates the complete lifecycle of concurrent requests, including:

  • Pending state before connection acquisition
  • Active state during request processing
  • Proper cleanup after completion

The use of CountDownLatch for synchronization is appropriate.


172-201: LGTM! Proper failure scenario coverage.

The test correctly validates metrics behavior during connection failures, ensuring:

  • Pending requests are tracked during the failed connection attempt
  • Proper exception handling (UnprocessedRequestException)
  • Complete cleanup of metrics after failure
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (2)

56-72: LGTM! Correct event triggers for metrics.

The choice to use REQUEST_FIRST_BYTES_TRANSFERRED_TIME as the trigger for transitioning from pending to active is correct. This accurately reflects when a request starts sending data (has acquired a connection) rather than just being scheduled.


95-196: LGTM! Thread-safe and defensive implementation.

The metrics tracking implementation is well-designed:

  • Uses AtomicBoolean flags in State to prevent double-counting
  • Thread-safe data structures (LongAdder, ConcurrentHashMap, EnumMap)
  • Defensive null checks for endpoint (which can be null during early failures)
  • Clean separation between pending (per-protocol) and active (per-endpoint) metrics

The accessor methods correctly aggregate counters across all tracked entities.

* completed. Tracked per {@link Endpoint}.</li>
* </ul>
*/
public class ClientRequestMetrics implements ClientRequestLifecycleListener {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add @UnstableApi annotation to the class.

Per the coding guidelines, all newly added public classes must have the @UnstableApi annotation unless they are in .internal or .testing packages, or in test source sets. This class is public and in the main source set, so it requires the annotation.

As per coding guidelines, apply this diff:

+import com.linecorp.armeria.common.annotation.UnstableApi;
+
 /**
  * A {@link ClientRequestLifecycleListener} that collects the number of pending and active requests.
  *
  * <p>This class tracks:
  * <ul>
  * <li><b>Pending requests:</b> Requests that have been created but have not yet started execution
  * (e.g., waiting for a connection). Tracked per {@link SessionProtocol}.</li>
  * <li><b>Active requests:</b> Requests that have started execution (headers sent) but have not yet
  * completed. Tracked per {@link Endpoint}.</li>
  * </ul>
  */
+@UnstableApi
 public class ClientRequestMetrics implements ClientRequestLifecycleListener {
🤖 Prompt for AI Agents
In
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java
around line 45, the new public class lacks the required @UnstableApi annotation;
add the import for com.linecorp.armeria.common.annotation.UnstableApi (or adjust
existing imports) and annotate the class with @UnstableApi directly above the
class declaration so the public class complies with the coding guideline for
newly added public API.

@codecov
Copy link

codecov bot commented Dec 13, 2025

Codecov Report

❌ Patch coverage is 75.21368% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.25%. Comparing base (8150425) to head (bee21ea).
⚠️ Report is 281 commits behind head on main.

Files with missing lines Patch % Lines
...rp/armeria/client/metric/ClientRequestMetrics.java 72.46% 9 Missing and 10 partials ⚠️
...ent/metric/NoopClientRequestLifecycleListener.java 54.54% 5 Missing ⚠️
...com/linecorp/armeria/client/HttpClientFactory.java 57.14% 3 Missing ⚠️
...lient/logging/DefaultClientRequestLogListener.java 88.88% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6502      +/-   ##
============================================
- Coverage     74.46%   74.25%   -0.22%     
- Complexity    22234    23573    +1339     
============================================
  Files          1963     2120     +157     
  Lines         82437    88359    +5922     
  Branches      10764    11586     +822     
============================================
+ Hits          61385    65608    +4223     
- Misses        15918    17209    +1291     
- Partials       5134     5542     +408     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 606bf40 and 002b1ea.

📒 Files selected for processing (1)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: lint
  • GitHub Check: site
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: flaky-tests
  • GitHub Check: Summary
🔇 Additional comments (5)
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (5)

47-54: LGTM: Well-chosen data structures for concurrent metrics.

The combination of ConcurrentHashMap for dynamic endpoints, EnumMap for fixed protocols, and LongAdder for high-contention counters is appropriate and efficient for thread-safe metric tracking.


56-72: LGTM: Clean integration with RequestLog system.

The bridge listener correctly maps log properties to lifecycle methods. The idempotent design (via AtomicBoolean guards in the lifecycle methods) ensures correctness even if events are delivered multiple times.


77-81: LGTM: Proper initialization of protocol metrics.

Pre-populating the EnumMap with all SessionProtocol values ensures consistent behavior and avoids runtime null checks.


83-151: LGTM: Lifecycle methods correctly handle all request states.

The implementation correctly tracks pending→active→complete transitions with proper edge-case handling:

  • AtomicBoolean guards prevent double-counting.
  • Early termination (cancelled before start) is handled correctly in onRequestComplete.
  • Null-safety checks are defensive and appropriate.

198-212: LGTM: Correct per-context state management.

The lazy initialization pattern and use of context attributes ensures each request has its own isolated State instance, preventing cross-request interference.

Comment on lines +190 to +196
public long pendingRequest() {
long sum = 0L;
for (LongAdder adder : pendingRequestsPerProtocol.values()) {
sum += adder.sum();
}
return sum;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix method naming: use plural form pendingRequests().

The method name pendingRequest() is inconsistent with:

  • The naming convention used in other methods (activeRequests(), pendingRequestsPerProtocol()).
  • The PR objectives which explicitly specify pendingRequests() (plural).

Apply this diff to fix the naming:

     /**
      * Returns the total number of pending requests across all protocols.
      */
-    public long pendingRequest() {
+    public long pendingRequests() {
         long sum = 0L;
         for (LongAdder adder : pendingRequestsPerProtocol.values()) {
             sum += adder.sum();
         }
         return sum;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public long pendingRequest() {
long sum = 0L;
for (LongAdder adder : pendingRequestsPerProtocol.values()) {
sum += adder.sum();
}
return sum;
}
/**
* Returns the total number of pending requests across all protocols.
*/
public long pendingRequests() {
long sum = 0L;
for (LongAdder adder : pendingRequestsPerProtocol.values()) {
sum += adder.sum();
}
return sum;
}
🤖 Prompt for AI Agents
In
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java
around lines 190 to 196, rename the method from pendingRequest() to
pendingRequests() to match existing naming conventions and PR requirements;
update the method signature and any internal references or callers to use
pendingRequests(), keeping the implementation identical (summing
pendingRequestsPerProtocol values), and ensure visibility and javadoc (if
present) are updated to reflect the new plural name.

@chickenchickenlove
Copy link
Contributor Author

@ikhoon @jrhee17
I made new commits based on your comments!
When you have time, please take a look 🙇‍♂️

@ikhoon ikhoon removed the request for review from jrhee17 December 15, 2025 02:10
* defined in this interface. The returned listener is registered to the {@link ClientRequestContext}
* automatically when the request starts.
*/
RequestLogListener asRequestLogListener();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove this from public API but convert this to RequestLogListener internally? Users are only interested in implementing the callbacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone might want to define and use their own RequestLogListener.
For now, I revised it in a way that provides a default method so users don’t have to worry about it as much.
What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need such a extension. What do you think of considering it later if necessary?

/**
* A {@link ClientRequestLifecycleListener} that does nothing.
*/
class NoopClientRequestLifecycleListener implements ClientRequestLifecycleListener {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this internal implementation to the top level class? NoopClientRequestLifecycleListener will be exposed to users.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed it!

final Endpoint endpoint = ctx.endpoint();
if (endpoint != null) {
activeRequestsPerEndpoint
.computeIfAbsent(endpoint, e -> new LongAdder())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to remove the cached Endpoint from activeRequestsPerEndpoint if the Endpoint is no longer used. Otherwise, it could be a memory leak.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, Addressed It.
FYI, I replace computeIfAbsent(...) to compute(...) to ensure atomic behavior during removing, adding adder.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (2)

44-44: Add @UnstableApi annotation to the class.

This issue has already been flagged in previous review comments.


173-179: Fix method naming: use plural form pendingRequests().

This issue has already been flagged in previous review comments.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 002b1ea and 3f8df1e.

📒 Files selected for processing (4)
  • core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java
  • core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java
  • core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (3)
core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java (1)

46-72: LGTM!

The event-to-lifecycle mapping logic is correct. The defensive type check ensures the listener only processes client requests, and each property is correctly mapped to its corresponding lifecycle callback.

core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java (1)

114-139: Memory management correctly implemented.

The logic properly addresses the memory leak concern raised in previous comments. Line 136 returns null when the active request count reaches zero, which causes ConcurrentHashMap.compute() to remove the endpoint entry from the map. This ensures unused endpoints are cleaned up and prevents unbounded memory growth.

core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java (1)

29-77: LGTM!

The singleton pattern is correctly implemented with proper encapsulation. All lifecycle methods are appropriately no-op, and the asRequestLogListener() method correctly returns a no-op RequestLogListener lambda, making this an efficient no-overhead implementation when lifecycle tracking is not needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java (2)

35-41: Improve Javadoc clarity.

The Javadoc is redundant. Consider making it more concise and informative.

Apply this diff:

 /**
- * Returns NoopClientRequestLifecycleListener.
- * @return Returns NoopClientRequestLifecycleListener.
+ * Returns the singleton instance.
+ * @return the singleton instance
  */
 public static NoopClientRequestLifecycleListener getInstance() {

31-33: Simplify lambda syntax.

The extra parentheses around the lambda parameters are unnecessary and unconventional.

Apply this diff:

 private static final NoopClientRequestLifecycleListener INSTANCE =
         new NoopClientRequestLifecycleListener();
-private final RequestLogListener requestLogListener = ((property, log) -> {});
+private final RequestLogListener requestLogListener = (property, log) -> {};
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8df1e and c6ffdc9.

📒 Files selected for processing (2)
  • core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java
  • core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java
🧬 Code graph analysis (2)
core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java (1)
core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java (1)
  • UnstableApi (34-75)
core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java (1)
core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java (1)
  • UnstableApi (28-80)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (2)
core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java (1)

48-74: LGTM!

The event-to-lifecycle mapping is correct and the defensive type check ensures safe execution. The switch statement properly handles all relevant RequestLogProperty values and delegates to the appropriate lifecycle methods.

core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java (1)

45-79: LGTM!

The no-op implementations are correct and thread-safe. The singleton pattern is properly implemented, and all lifecycle methods correctly do nothing as intended.

Comment on lines +44 to +46
public DefaultClientRequestLogListener(ClientRequestLifecycleListener lifecycleListener) {
this.lifecycleListener = lifecycleListener;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add null validation for the constructor parameter.

The constructor should validate that lifecycleListener is not null to prevent potential NPE when onEvent is called. Consider using Objects.requireNonNull() to fail fast with a clear error message.

Apply this diff to add validation:

+import static java.util.Objects.requireNonNull;
+
 /**
  * Creates a new instance with the specified {@link ClientRequestLifecycleListener}.
  *
  * @param lifecycleListener the listener to which the {@link RequestLog} events will be delegated
  */
 public DefaultClientRequestLogListener(ClientRequestLifecycleListener lifecycleListener) {
-    this.lifecycleListener = lifecycleListener;
+    this.lifecycleListener = requireNonNull(lifecycleListener, "lifecycleListener");
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java
around lines 44 to 46, the constructor does not validate the lifecycleListener
parameter; add a null check using Objects.requireNonNull(lifecycleListener,
"lifecycleListener") (or equivalent) so the constructor fails fast with a clear
message if null is passed, assigning the validated value to
this.lifecycleListener.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6ffdc9 and 5243403.

📒 Files selected for processing (3)
  • core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java (4 hunks)
  • core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java
  • core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: flaky-tests
  • GitHub Check: site
  • GitHub Check: lint
  • GitHub Check: Summary
🔇 Additional comments (2)
core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java (2)

139-140: LGTM!

The default noop() implementation is appropriate for an optional listener, ensuring clients can omit configuration without null checks.


1148-1148: LGTM!

The listener is correctly wired into the HttpClientFactory, enabling lifecycle observation for client requests.

Comment on lines +267 to +280
/**
* Sets the {@link ClientRequestLifecycleListener} that listens to the lifecycle events of
* client requests created by the {@link ClientFactory}.
*
* <p>If not set, {@link ClientRequestLifecycleListener#noop()} is used by default.
*
* @param listener the listener to be notified of client request lifecycle events. Must not be {@code null}.
* @return {@code this} to support method chaining.
*/
public ClientFactoryBuilder clientRequestLifecycleListener(ClientRequestLifecycleListener listener) {
requireNonNull(listener, "listener");
clientRequestLifecycleListener = listener;
return this;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add @UnstableApi annotation to the new public method.

This new public API method should be annotated with @UnstableApi to signal that the API is still evolving. As per coding guidelines, newly added public methods require this annotation unless the class itself is already annotated or the method is in an internal/testing package.

🔎 Proposed fix
     /**
      * Sets the {@link ClientRequestLifecycleListener} that listens to the lifecycle events of
      * client requests created by the {@link ClientFactory}.
      *
      * <p>If not set, {@link ClientRequestLifecycleListener#noop()} is used by default.
      *
      * @param listener the listener to be notified of client request lifecycle events. Must not be {@code null}.
      * @return {@code this} to support method chaining.
      */
+    @UnstableApi
     public ClientFactoryBuilder clientRequestLifecycleListener(ClientRequestLifecycleListener listener) {
         requireNonNull(listener, "listener");
         clientRequestLifecycleListener = listener;
         return this;
     }

Based on coding guidelines, new public API methods require @UnstableApi annotation.

🤖 Prompt for AI Agents
In core/src/main/java/com/linecorp/armeria/client/ClientFactoryBuilder.java
around lines 267 to 280, the newly added public method
clientRequestLifecycleListener(...) is missing the @UnstableApi annotation
required for new public APIs; add the
@com.linecorp.armeria.common.annotation.UnstableApi annotation (or the correct
import) to the method declaration, ensure the import statement for UnstableApi
is present/updated, and keep the existing null check and return behavior
unchanged.

Comment on lines +107 to +111
factory.clientRequestLifecycleListener().onRequestPending(ctx);
ctx.log().addListener(
factory.clientRequestLifecycleListener()
.asRequestLogListener()
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Cache the lifecycle listener to avoid redundant method calls.

The listener is retrieved twice from the factory on the hot path. Store it in a local variable for efficiency.

🔎 Proposed fix
+        final ClientRequestLifecycleListener lifecycleListener = 
+                factory.clientRequestLifecycleListener();
-        factory.clientRequestLifecycleListener().onRequestPending(ctx);
+        lifecycleListener.onRequestPending(ctx);
         ctx.log().addListener(
-                factory.clientRequestLifecycleListener()
-                        .asRequestLogListener()
+                lifecycleListener.asRequestLogListener()
         );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
factory.clientRequestLifecycleListener().onRequestPending(ctx);
ctx.log().addListener(
factory.clientRequestLifecycleListener()
.asRequestLogListener()
);
final ClientRequestLifecycleListener lifecycleListener =
factory.clientRequestLifecycleListener();
lifecycleListener.onRequestPending(ctx);
ctx.log().addListener(
lifecycleListener.asRequestLogListener()
);
🤖 Prompt for AI Agents
In core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java around
lines 107 to 111, the call factory.clientRequestLifecycleListener() is invoked
twice on the hot path; cache it in a local variable (e.g.,
ClientRequestLifecycleListener listener =
factory.clientRequestLifecycleListener()) and then call
listener.onRequestPending(ctx) and
ctx.log().addListener(listener.asRequestLogListener()) to avoid redundant method
calls while preserving behavior.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find ClientRequestLifecycleListener interface
fd -t f -e java -p . | xargs rg -l "interface ClientRequestLifecycleListener" 2>/dev/null | head -5

# Find HttpClientDelegate.java
fd -t f -name "HttpClientDelegate.java" 2>/dev/null

# Find any classes with "ClientRequestLifecycle" in their name
fd -t f -e java -p . | xargs rg -l "ClientRequestLifecycle" 2>/dev/null

Repository: line/armeria

Length of output: 741


🏁 Script executed:

# Read ClientRequestLifecycleListener.java
echo "=== ClientRequestLifecycleListener.java ==="
head -30 core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java

echo -e "\n=== DefaultClientRequestLogListener.java ==="
head -30 core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java

echo -e "\n=== ClientRequestMetrics.java ==="
head -30 core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java

echo -e "\n=== NoopClientRequestLifecycleListener.java ==="
head -30 core/src/main/java/com/linecorp/armeria/client/metric/NoopClientRequestLifecycleListener.java

# Check HttpClientDelegate.java location and lines around 107-111
echo -e "\n=== HttpClientDelegate.java (around lines 100-115) ==="
sed -n '100,115p' core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java

Repository: line/armeria

Length of output: 5927


🏁 Script executed:

# Get the full ClientRequestLifecycleListener interface declaration
echo "=== ClientRequestLifecycleListener interface ==="
sed -n '1,50p' core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java | tail -25

# Get the full DefaultClientRequestLogListener class declaration
echo -e "\n=== DefaultClientRequestLogListener class declaration ==="
sed -n '1,65p' core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java | tail -25

# Get the asRequestLogListener method signature
echo -e "\n=== asRequestLogListener method in ClientRequestLifecycleListener ==="
rg -A5 "asRequestLogListener" core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java

# Check ClientRequestMetrics for @UnstableApi
echo -e "\n=== ClientRequestMetrics @UnstableApi check ==="
rg "@UnstableApi|^public class ClientRequestMetrics" core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java | head -5

Repository: line/armeria

Length of output: 3016


🏁 Script executed:

# Check ClientRequestMetrics full class declaration with annotations
echo "=== ClientRequestMetrics full header ==="
sed -n '1,45p' core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java

# Also check if there are any other public classes/methods in ClientRequestLifecycleListener
echo -e "\n=== Full ClientRequestLifecycleListener interface ==="
wc -l core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java

# Get full interface
cat core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java | head -150

Repository: line/armeria

Length of output: 6333


🏁 Script executed:

# Check if ClientRequestMetrics has @UnstableApi annotation
echo "=== ClientRequestMetrics class with preceding annotations ==="
rg -B2 "^public class ClientRequestMetrics" core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java

# Also verify DefaultClientRequestLogListener has @UnstableApi
echo -e "\n=== DefaultClientRequestLogListener with annotations ==="
rg -B2 "^public class DefaultClientRequestLogListener" core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java

# Check if DefaultClientRequestLogListener is in logging package (not .internal)
echo -e "\n=== Package declarations ==="
rg "^package" core/src/main/java/com/linecorp/armeria/client/logging/DefaultClientRequestLogListener.java
rg "^package" core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java
rg "^package" core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestLifecycleListener.java

Repository: line/armeria

Length of output: 523


🏁 Script executed:

# Get the full ClientRequestMetrics class definition to confirm it's missing @UnstableApi
echo "=== ClientRequestMetrics lines 30-50 ==="
sed -n '30,50p' core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java

# Double-check by looking at all annotations before the class
echo -e "\n=== ClientRequestMetrics all lines before class declaration ==="
grep -n "@\|^public class" core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java | head -10

Repository: line/armeria

Length of output: 1488


Add @UnstableApi annotation to ClientRequestMetrics.

The ClientRequestMetrics class at core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java is missing the @UnstableApi annotation. Per coding guidelines, all newly added public classes must be annotated with @UnstableApi unless they are located in .internal or .testing packages or test source sets. The related classes ClientRequestLifecycleListener, DefaultClientRequestLogListener, and NoopClientRequestLifecycleListener are correctly annotated.

🤖 Prompt for AI Agents
In
core/src/main/java/com/linecorp/armeria/client/metric/ClientRequestMetrics.java
around lines 1 to 999 (the class declaration), the public class
ClientRequestMetrics is missing the required @UnstableApi annotation; add the
annotation above the class declaration, import
com.linecorp.armeria.common.annotation.UnstableApi (or the correct package used
in the project), and run a quick compile to ensure there are no import or symbol
issues. Ensure you do not annotate classes in .internal or .testing packages,
but since this is a public non-internal class, the annotation must be applied.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5243403 and 37dee31.

📒 Files selected for processing (1)
  • core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: lint
  • GitHub Check: flaky-tests
  • GitHub Check: site
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: Summary
🔇 Additional comments (4)
core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java (4)

41-41: LGTM!

Import is correct and appropriately placed.


127-127: LGTM!

Field declaration follows proper conventions with appropriate visibility and immutability.


129-133: LGTM!

Constructor delegation with noop() default maintains backward compatibility while enabling the new listener functionality.


319-321: LGTM!

Package-private getter is appropriately scoped for internal use within the client framework.

Comment on lines +135 to +137
HttpClientFactory(ClientFactoryOptions options, boolean autoCloseConnectionPoolListener,
ClientTlsSpec baseClientTlsSpec,
ClientRequestLifecycleListener clientRequestLifecycleListener) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add null validation for the listener parameter.

The clientRequestLifecycleListener parameter should be validated to prevent potential NullPointerException. Other parts of this class use requireNonNull for parameter validation (e.g., line 362, line 510).

🔎 Proposed fix
 HttpClientFactory(ClientFactoryOptions options, boolean autoCloseConnectionPoolListener,
                   ClientTlsSpec baseClientTlsSpec,
                   ClientRequestLifecycleListener clientRequestLifecycleListener) {
+    this.clientRequestLifecycleListener = requireNonNull(clientRequestLifecycleListener,
+                                                          "clientRequestLifecycleListener");
     workerGroup = options.workerGroup();

And update line 211:

     this.options = options;
-    this.clientRequestLifecycleListener = clientRequestLifecycleListener;

Also applies to: 211-211

🤖 Prompt for AI Agents
In core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java around
lines 135-137 (and also at line 211), the clientRequestLifecycleListener
parameter is not being null-checked; add a requireNonNull validation for
clientRequestLifecycleListener in the constructor (and the other affected
constructor/factory method at line 211) so the parameter is validated like the
other parameters, throwing a clear NPE message if null.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java (1)

144-160: Consider a more deterministic delay mechanism.

While Thread.sleep(3000) effectively simulates a slow connection for testing early cancellation, it makes the test slower and potentially brittle. Consider using a more controllable synchronization mechanism (e.g., a latch that the test explicitly releases) to gain finer control over timing and reduce test execution time.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37dee31 and 3c173a0.

📒 Files selected for processing (1)
  • core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: site
  • GitHub Check: flaky-tests
  • GitHub Check: lint
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: Summary
🔇 Additional comments (1)
core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java (1)

48-96: Server setup looks well-structured.

The use of CountDownLatches to synchronize client and server behavior is appropriate for integration testing, and the server handlers correctly use blockingTaskExecutor for blocking operations.

Comment on lines +115 to +116
assertEquals(2L, metrics.pendingRequest());
assertEquals(0L, metrics.activeRequests());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential race condition in metrics assertions.

These assertions check metrics immediately after initiating requests without synchronization. Depending on thread scheduling and network latency, the requests might transition from pending to active before these assertions execute, potentially causing test flakiness.

🔎 Suggested fix using Awaitility
-            assertEquals(2L, metrics.pendingRequest());
-            assertEquals(0L, metrics.activeRequests());
+            await().pollInSameThread().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
+                assertEquals(2L, metrics.pendingRequest());
+                assertEquals(0L, metrics.activeRequests());
+            });

Alternatively, if the intent is to verify the pending state exists even momentarily, consider adding a brief synchronization point (e.g., a short polling interval) to confirm metrics stabilize at the expected values before proceeding.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java
around lines 115-116, the assertions assertEquals(2L, metrics.pendingRequest())
and assertEquals(0L, metrics.activeRequests()) can race with request state
transitions; replace the immediate assertions with a short wait/poll (e.g.,
Awaitility or a loop with timeout) that repeatedly checks metrics until
pendingRequest() == 2 and activeRequests() == 0 or a timeout occurs, to ensure
the metrics stabilize before asserting and avoid flakiness.

Comment on lines +209 to +210
assertEquals(1L, metrics.pendingRequest());
assertEquals(0L, metrics.activeRequests());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential race condition in metrics assertions.

Similar to the first test, these assertions check metrics immediately after initiating a request to an invalid host. The connection failure might occur very quickly (especially with 255.255.255.255, which is typically unreachable), causing the request to transition through its lifecycle before these assertions execute.

🔎 Suggested fix using Awaitility
-            assertEquals(1L, metrics.pendingRequest());
-            assertEquals(0L, metrics.activeRequests());
+            await().pollInSameThread().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
+                assertEquals(1L, metrics.pendingRequest());
+                assertEquals(0L, metrics.activeRequests());
+            });

This ensures the test captures the pending state even if the failure happens quickly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assertEquals(1L, metrics.pendingRequest());
assertEquals(0L, metrics.activeRequests());
await().pollInSameThread().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
assertEquals(1L, metrics.pendingRequest());
assertEquals(0L, metrics.activeRequests());
});
🤖 Prompt for AI Agents
In
core/src/test/java/com/linecorp/armeria/client/metric/ClientRequestMetricsIntegrationTest.java
around lines 209-210, the assertions check metrics immediately and may race with
a fast connection failure; replace the direct assertEquals calls with an
Awaitility wait that repeatedly checks until metrics.pendingRequest() == 1 and
metrics.activeRequests() == 0 (with a sensible timeout and poll interval) so the
test reliably observes the pending state even if the failure happens quickly.

@chickenchickenlove
Copy link
Contributor Author

Hi, @ikhoon , @jrhee17 !
Gently ping,
When you have time, could you take another look..?

@ikhoon ikhoon removed request for ikhoon and jrhee17 December 29, 2025 06:36
@Override
public void onRequestPending(ClientRequestContext ctx) {
final State s = state(ctx);
if (s.pendingCounted.compareAndSet(false, true)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Why do we need this guard? Is this method invoked more than once?

}

@Override
public void onEvent(RequestLogProperty property, RequestLog log) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we create one RequestLogListener that invokes all registered lifecycleListener?

public void onEvent(RequestLogProperty property, RequestLog log) {
    ...
    switch (property) {
        case REQUEST_FIRST_BYTES_TRANSFERRED_TIME:
            for (var lifecycleListener: lifecycleListeners) {
                lifecycleListener.onRequestStart(ctx);
            }
            break;
}

Otherwise, orElse() could be added to compose two ClientRequestLifecycleListener easily.

* @param listener the listener to be notified of client request lifecycle events. Must not be {@code null}.
* @return {@code this} to support method chaining.
*/
public ClientFactoryBuilder clientRequestLifecycleListener(ClientRequestLifecycleListener listener) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For simplicity.

Suggested change
public ClientFactoryBuilder clientRequestLifecycleListener(ClientRequestLifecycleListener listener) {
public ClientFactoryBuilder requestLifecycleListener(ClientRequestLifecycleListener listener) {

*/
public ClientFactoryBuilder clientRequestLifecycleListener(ClientRequestLifecycleListener listener) {
requireNonNull(listener, "listener");
clientRequestLifecycleListener = listener;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow multiple ClientRequestLifecycleListeners?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose client request metrics

3 participants