Skip to content

Conversation

@ikhoon
Copy link
Contributor

@ikhoon ikhoon commented Dec 9, 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.

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 implmentations used for collecting
metrics.

Motifications:

- Introduced `RequestLogListener` API that can be attached to
  `RequestLog`.
  - `RequestLogAccess.addListener()` API was added and
    `DefaultRequestLog` implmemeted it.
  - The listener will be notified whenever a new property is set to
    `RequestLog`. If some properties have already been set, they 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 exists for them.
    `RequestLogProperty` should represent all states in
    `RequestLogListener`, I added the new completion properties.
- Simplied 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.
@coderabbitai
Copy link

coderabbitai bot commented Dec 9, 2025

Walkthrough

A listener-based event system replaces chained callbacks in request logging. New RequestLogListener interface enables external components to subscribe to property availability events. Core logging in DefaultRequestLog refactors propagation logic to notify listeners when properties become available. ObservationClient and ObservationService adapt to use the new pattern.

Changes

Cohort / File(s) Summary
Listener API Introduction
RequestLogListener.java, RequestLogAccess.java
Introduces new public RequestLogListener functional interface with onEvent(RequestLogProperty, RequestLog) method, annotated @UnstableApi. Adds corresponding addListener() method to RequestLogAccess interface for property availability subscriptions.
Core Logging Refactoring
DefaultRequestLog.java
Replaces chained per-property callbacks with unified listener mechanism via addListener() and internal maybeNotifyListeners(). Introduces REQUEST_COMPLETE, RESPONSE_COMPLETE, and ALL_COMPLETE completion flags. Reworks propagateRequestSideLog and propagateResponseSideLog to use listener-based event handling. Adds helper toProperties(int flags) for flag-to-property mapping.
Request Log Property Enum Updates
RequestLogProperty.java
Reorganizes and adds new properties: REQUEST_COMPLETE, REQUEST_END_TIME, RESPONSE_CAUSE, RESPONSE_COMPLETE, ALL_COMPLETE. Updates flag computations for FLAGS_REQUEST_COMPLETE and FLAGS_RESPONSE_COMPLETE. Excludes ALL_COMPLETE from RESPONSE_PROPERTIES filter. Reorders property constants for lifecycle consistency.
Observation Pattern Migration
ObservationClient.java, ObservationService.java
Replaces sequential log callbacks (via REQUEST_FIRST_BYTES_TRANSFERRED_TIME, RESPONSE_FIRST_BYTES_TRANSFERRED_TIME, ALL_COMPLETE) with single listener using switch-based property dispatch. Maintains identical WIRE_SEND/WIRE_RECEIVE emission semantics and observation lifecycle.
Test Coverage
DefaultRequestLogTest.java
Removes unused sanitizer BiFunction fields and simplifies event loop stubbing in addChild test.
New Test Suite
RequestLogListenerTest.java
Comprehensive test suite covering listener registration, immediate notification for already-available properties, concurrent listener notifications, exception isolation, deferred properties, and property sequencing (REQUEST_START_TIME, SESSION, HEADERS, FIRST_BYTES_TRANSFERRED_TIME, REQUEST/RESPONSE_COMPLETE, ALL_COMPLETE).

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Log as DefaultRequestLog
    participant Listener as RequestLogListener
    participant Observer as Observation Service
    
    App->>Log: addListener(listener)
    Note over Log: Register listener for<br/>property events
    
    Alt Listener Registration with Existing Properties
        Log->>Listener: onEvent(property, log)<br/>for each available property
        Listener-->>Log: ✓ callback returns
    End
    
    Note over Log: Async I/O Processing
    
    Log->>Log: REQUEST_FIRST_BYTES_TRANSFERRED_TIME<br/>becomes available
    Log->>Listener: onEvent(REQUEST_FIRST_BYTES_TRANSFERRED_TIME, log)
    Listener->>Observer: emit(WIRE_SEND)
    
    Log->>Log: RESPONSE_FIRST_BYTES_TRANSFERRED_TIME<br/>becomes available
    Log->>Listener: onEvent(RESPONSE_FIRST_BYTES_TRANSFERRED_TIME, log)
    Listener->>Observer: emit(WIRE_RECEIVE)
    
    Log->>Log: ALL_COMPLETE flag set<br/>(request + response complete)
    Log->>Listener: onEvent(ALL_COMPLETE, log)
    Listener->>Observer: store response
    Observer->>Observer: stop observation
    Listener-->>Log: ✓ callback returns
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Core logging refactoring: DefaultRequestLog undergoes substantial restructuring of callback propagation logic into listener-based model with new completion flag semantics; requires verification of event ordering and listener notification guarantees.
  • New public API surface: RequestLogListener interface and addListener() methods introduce new stable contract; review must confirm thread-safety, documentation accuracy, and immediate-notification behavior for retroactive listeners.
  • Property enum reorganization: RequestLogProperty constant reordering and addition of REQUEST_COMPLETE, RESPONSE_COMPLETE, ALL_COMPLETE flags with updated flag computations; verify bitmask logic and filter exclusions.
  • Observation pattern migration: Two independent observation services (client and server) migrate to new listener pattern; verify semantic equivalence of WIRE_SEND/WIRE_RECEIVE emission and lifecycle preservation.
  • Test coverage validation: Comprehensive new test suite (RequestLogListenerTest) with concurrent listeners, deferred properties, and exception isolation; spot-check edge cases around listener exception handling and property sequencing.

Poem

🐰 A listener hops where callbacks once chained,
Property events flow in orderly strain.
From wire's first byte to completion's sweet call,
One unified path now observes it all!
With deferred properties and threads working well,
This logging's pure magic—no callbacks to quell!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Introduce RequestLogListener' is clear, concise, and directly reflects the main change—a new listener API for observing all RequestLog events, which is the central feature of this PR.
Description check ✅ Passed The description clearly explains the motivation (limitations of existing whenAvailable API), outlines all key modifications (RequestLogListener API, new completion properties, simplification of child log propagation), and states the result; it aligns well with the actual changeset.
✨ 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.

@ikhoon ikhoon added this to the 1.35.0 milestone Dec 9, 2025
@codecov
Copy link

codecov bot commented Dec 9, 2025

Codecov Report

❌ Patch coverage is 97.01493% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.23%. Comparing base (8150425) to head (4ea890e).
⚠️ Report is 278 commits behind head on main.

Files with missing lines Patch % Lines
...corp/armeria/common/logging/DefaultRequestLog.java 96.07% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6543      +/-   ##
============================================
- Coverage     74.46%   74.23%   -0.23%     
- Complexity    22234    23454    +1220     
============================================
  Files          1963     2105     +142     
  Lines         82437    87873    +5436     
  Branches      10764    11516     +752     
============================================
+ Hits          61385    65236    +3851     
- Misses        15918    17147    +1229     
- Partials       5134     5490     +356     

☔ 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.

@ikhoon ikhoon marked this pull request as ready for review December 9, 2025 12:46
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 (5)
core/src/test/java/com/linecorp/armeria/common/logging/RequestLogListenerTest.java (1)

119-131: Consider using partial() for clearer comparison.

The assertion ctx.log() returns RequestLogAccess, while the listener receives RequestLog. This works because DefaultRequestLog implements both interfaces, but could be clearer.

-        assertThat(receivedLogs.get(0)).isSameAs(ctx.log());
+        assertThat(receivedLogs.get(0)).isSameAs(ctx.log().partial());

Alternatively, if the intent is to verify the same underlying log instance, the current assertion is acceptable since they share the same reference.

core/src/main/java/com/linecorp/armeria/common/logging/RequestLogProperty.java (1)

140-144: Address the TODO comment or track it as a follow-up.

The TODO asks whether RESPONSE_LENGTH is actually used. Consider investigating this now or creating an issue to track.

Would you like me to search the codebase for usages of RESPONSE_LENGTH to determine if it's actively used?

#!/bin/bash
# Search for usages of RESPONSE_LENGTH property
rg -n --type=java 'RESPONSE_LENGTH' -C 3
core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java (3)

117-118: Consider adding @GuardedBy("lock") annotation.

The listeners field is accessed under the lock (as seen in addListener and maybeNotifyListeners). Adding the annotation would improve documentation consistency with other guarded fields like pendingFutures.

 @Nullable
+@GuardedBy("lock")
 private List<RequestLogListener> listeners;

655-692: Minor: responseTrailers is set twice.

The RESPONSE_TRAILERS case (line 679) and the RESPONSE_COMPLETE case (line 685) both call responseTrailers(log.responseTrailers()). While harmless (the setter is idempotent), the duplicate in RESPONSE_COMPLETE is unnecessary since RESPONSE_TRAILERS will always fire before RESPONSE_COMPLETE.

                 case RESPONSE_COMPLETE:
                     responseContent(log.responseContent(), log.rawResponseContent());
                     responseLength(log.responseLength());
                     responseContentPreview(log.responseContentPreview());
-                    responseTrailers(log.responseTrailers());
                     endResponse0(log.responseCause(), log.responseEndTimeNanos());
                     break;

1939-1945: notifyListener is called without holding the lock.

The notifyListener method's comment states "The lock should be held by the caller" (line 1510), but CompleteRequestLog.addListener calls it without acquiring the lock. While this is safe because the log is already complete (immutable state), the inconsistency could confuse maintainers.

Consider either:

  1. Updating the comment in notifyListener to clarify that the lock is only required for incomplete logs, or
  2. Using a separate helper for the complete case.
📜 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 d8f0879 and 4ea890e.

📒 Files selected for processing (8)
  • core/src/main/java/com/linecorp/armeria/client/observation/ObservationClient.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java (14 hunks)
  • core/src/main/java/com/linecorp/armeria/common/logging/RequestLogAccess.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/common/logging/RequestLogListener.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/common/logging/RequestLogProperty.java (5 hunks)
  • core/src/main/java/com/linecorp/armeria/server/observation/ObservationService.java (1 hunks)
  • core/src/test/java/com/linecorp/armeria/common/logging/DefaultRequestLogTest.java (0 hunks)
  • core/src/test/java/com/linecorp/armeria/common/logging/RequestLogListenerTest.java (1 hunks)
💤 Files with no reviewable changes (1)
  • core/src/test/java/com/linecorp/armeria/common/logging/DefaultRequestLogTest.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/server/observation/ObservationService.java
  • core/src/main/java/com/linecorp/armeria/common/logging/RequestLogAccess.java
  • core/src/main/java/com/linecorp/armeria/client/observation/ObservationClient.java
  • core/src/main/java/com/linecorp/armeria/common/logging/RequestLogListener.java
  • core/src/test/java/com/linecorp/armeria/common/logging/RequestLogListenerTest.java
  • core/src/main/java/com/linecorp/armeria/common/logging/RequestLogProperty.java
  • core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java
🧬 Code graph analysis (1)
core/src/main/java/com/linecorp/armeria/common/logging/RequestLogListener.java (2)
core/src/main/java/com/linecorp/armeria/client/observation/ObservationClient.java (1)
  • UnstableApi (72-162)
core/src/main/java/com/linecorp/armeria/server/observation/ObservationService.java (1)
  • UnstableApi (72-162)
⏰ 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 (21)
core/src/main/java/com/linecorp/armeria/server/observation/ObservationService.java (1)

142-160: Clean refactor to listener-based observation.

The switch-based listener pattern is cleaner than chained callbacks. The asymmetric null check (responseFirstBytesTransferredTimeNanos() check at line 148 but not for request) appears intentional since the property availability for REQUEST_FIRST_BYTES_TRANSFERRED_TIME implies the event occurred, whereas response bytes might not be transferred in all cases.

core/src/main/java/com/linecorp/armeria/common/logging/RequestLogAccess.java (1)

233-238: LGTM! New API method properly annotated.

The addListener method is correctly annotated with @UnstableApi as required for new public APIs. The Javadoc clearly describes when the listener will be invoked.

core/src/main/java/com/linecorp/armeria/common/logging/RequestLogListener.java (1)

21-39: Well-designed listener interface with clear documentation.

The interface is properly annotated with @UnstableApi and @FunctionalInterface. The Javadoc appropriately warns users about I/O thread invocation and documents the immediate notification behavior for already-available properties. This enables clean event-driven integrations as intended by the PR.

core/src/main/java/com/linecorp/armeria/client/observation/ObservationClient.java (1)

140-160: Consistent listener-based observation matching the server-side pattern.

The implementation correctly mirrors ObservationService with appropriate event mappings for client-side (WIRE_SEND for request, WIRE_RECEIVE for response). The retained TODO comment about ClientConnectionTimings (lines 151-152) is acknowledged.

core/src/test/java/com/linecorp/armeria/common/logging/RequestLogListenerTest.java (3)

54-84: Comprehensive test for listener notification ordering.

The test effectively validates the property notification sequence from request start through response completion, including the ALL_COMPLETE property. Good coverage of the listener lifecycle.


151-171: Good exception isolation test.

This test is important to verify that a misbehaving listener doesn't break other listeners. It validates a key resilience property of the listener mechanism.


173-206: Thorough deferred properties test.

This test covers the complex deferred content scenario well, verifying that REQUEST_COMPLETE, RESPONSE_COMPLETE, and ALL_COMPLETE are only notified after deferred properties are set.

core/src/main/java/com/linecorp/armeria/common/logging/RequestLogProperty.java (5)

66-69: LGTM!

The new REQUEST_FIRST_BYTES_TRANSFERRED_TIME property is correctly defined as a request property and properly documented with its corresponding accessor method.


96-104: LGTM!

The REQUEST_END_TIME and REQUEST_COMPLETE properties are correctly defined. REQUEST_COMPLETE serves as a synthetic completion marker for the new listener-based model.


108-112: LGTM!

Good design decision to notify RESPONSE_CAUSE before other response properties. The comment clearly explains this enables early propagation for retry logic and circuit breakers.


151-165: LGTM!

The completion properties (RESPONSE_END_TIME, RESPONSE_COMPLETE, ALL_COMPLETE) are well-defined and documented. ALL_COMPLETE correctly serves as the union of request and response completion states.


172-188: LGTM!

The flag calculations correctly exclude the synthetic *_COMPLETE properties from their respective flag sets. This avoids circular dependencies—FLAGS_REQUEST_COMPLETE represents the properties that must be set before REQUEST_COMPLETE can be derived, not including REQUEST_COMPLETE itself.

core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java (9)

39-41: LGTM!

Standard SLF4J logger initialization for listener error handling.


20-25: LGTM!

Clean static imports for the new completion flags used throughout the class.


192-203: LGTM!

The completion checks now use the dedicated ALL_COMPLETE and REQUEST_COMPLETE properties, which aligns with the new listener-based model.


268-276: LGTM!

The toProperties helper correctly maps a bitmask to the corresponding list of RequestLogProperty values. Acceptable overhead given the small enum size.


288-295: LGTM!

Both methods correctly delegate to the new completion property flags.


426-457: Listener callbacks are invoked while holding the lock—verify this is acceptable.

Invoking maybeNotifyListeners (line 448) while holding lock means listeners execute synchronously within the critical section. If a listener performs a slow operation or attempts to interact with this RequestLog in complex ways, it could increase lock contention. While ReentrantShortLock allows re-entry, long-held locks can degrade throughput in high-concurrency scenarios.

Consider whether listener notification should be deferred outside the lock by collecting the notifications inside the lock and dispatching them afterward (similar to how completeSatisfiedFutures is handled via event loop). If this is intentional for ordering guarantees, a brief comment explaining the design choice would help future maintainers.


588-635: LGTM!

Clean refactoring to listener-based propagation. The switch covers all relevant request properties, and the comment at lines 627-628 correctly explains why requestCause is not propagated to avoid marking retried requests as failed.


1101-1101: LGTM!

Correctly uses FLAGS_REQUEST_COMPLETE for checking request completion prerequisites.


1490-1529: LGTM!

The listener mechanism is well-implemented:

  • Proper null-check and lazy initialization
  • Immediate notification of already-available properties on registration
  • Exceptions are caught and logged to prevent one faulty listener from affecting others
  • Property-first iteration order in maybeNotifyListeners ensures consistent global ordering

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Left a small concern on notifying listeners/futures within the same lock as updating flags. The rest looks good to me

*/
REQUEST_START_TIME(true),

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Note) Understood the reason for the reorder is to ensure properties are notified in the correct order

}
break;
this.flags = newFlags;
maybeNotifyListeners(oldFlags, newFlags);
Copy link
Contributor

@jrhee17 jrhee17 Dec 10, 2025

Choose a reason for hiding this comment

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

I'm unsure if locking flags and listeners is safe since flag updates may be blocked due to notifying futures, and flag updates are almost always done on event loops.

What do you think of isolating locks for each listener instead? This will incur additional memory, so I'm unsure which is a better approach, but leaving this as food for thought.

e.g.

public class CachingRequestLogListener implements RequestLogListener {

    private final RequestLogListener delegate;
    int notifiedFlags;
    private final ReentrantLock lock = new ReentrantLock();

    CachingRequestLogListener(RequestLogListener delegate) {
        this.delegate = delegate;
    }

    @Override
    public void onEvent(RequestLogProperty property, RequestLog log) {
        lock.lock();
        try {
            if ((notifiedFlags & property.flag()) != 0) {
                return;
            }
            notifiedFlags |= property.flag();
        } finally {
            lock.unlock();
        }
        delegate.onEvent(property, log);
    }
}

Comment on lines +185 to +186
FLAGS_REQUEST_COMPLETE = flags(REQUEST_PROPERTIES) & ~REQUEST_COMPLETE.flag();
FLAGS_RESPONSE_COMPLETE = flags(RESPONSE_PROPERTIES) & ~RESPONSE_COMPLETE.flag();
Copy link
Contributor

Choose a reason for hiding this comment

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

Understood *COMPLETE flags should not be updated directly using updateFlags now, but are computed based on other flags.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants