Skip to content

Add WebSocket client metrics#4118

Open
LivingLikeKrillin wants to merge 13 commits intoreactor:mainfrom
LivingLikeKrillin:feature/websocket-client-metrics
Open

Add WebSocket client metrics#4118
LivingLikeKrillin wants to merge 13 commits intoreactor:mainfrom
LivingLikeKrillin:feature/websocket-client-metrics

Conversation

@LivingLikeKrillin
Copy link
Copy Markdown

Summary

Add metrics support for WebSocket clients, including handshake time,
data received/sent, connection duration, and error counting.

  • Add constants, meter/observation documentation, and recorder interfaces
    with Micrometer-based implementation
  • Add channel handlers that replace the HTTP metrics handler on WebSocket
    upgrade and track connection duration via handler lifecycle
  • Exclude control frames (Close, Ping, Pong) from data metrics

Test plan

  • Handshake time timer
  • Data received time and distribution summary
  • Data sent time and distribution summary
  • Connection duration timer
  • Multiple connections aggregation
  • URI tag value normalization function
  • Handshake failure scenario
  • HTTP/1.1 and H2 protocol combinations

Related to #3820

@LivingLikeKrillin LivingLikeKrillin force-pushed the feature/websocket-client-metrics branch from d16260f to 49cdeda Compare February 28, 2026 15:19
@violetagg violetagg linked an issue Mar 4, 2026 that may be closed by this pull request
@violetagg
Copy link
Copy Markdown
Member

@LivingLikeKrillin On most classes you have @author raccoonback is that the correct author? Also the copyright is 2025 while it should be 2026, also change the @since 1.3.2 to 1.3.5

Add WEBSOCKET_CLIENT_PREFIX, HANDSHAKE_TIME, and CONNECTION_DURATION
constants, meter/observation documentation enums, and recorder
interfaces with Micrometer-based implementation.

Related to reactor#3820

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
Replace the HTTP metrics handler with a WebSocket-aware handler on
upgrade. Connection duration is recorded when the handler is removed
from the pipeline. Control frames (Close, Ping, Pong) are excluded
from data metrics.

Related to reactor#3820

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
Test handshake time, data received/sent, connection duration,
handshake failure, and protocol combinations (HTTP/1.1 and H2).

Related to reactor#3820

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
@LivingLikeKrillin LivingLikeKrillin force-pushed the feature/websocket-client-metrics branch from 49cdeda to dcbecaa Compare March 9, 2026 09:27
Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
@violetagg violetagg added the type/enhancement A general enhancement label Mar 9, 2026
@violetagg violetagg modified the milestones: 1.3.4, 1.3.5 Mar 9, 2026
Copy link
Copy Markdown
Member

@violetagg violetagg left a comment

Choose a reason for hiding this comment

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

Thanks for the PR!
I started with the review. Can you please resolve the issues that I've identified till now?

Comment thread reactor-netty-core/src/main/java/reactor/netty/Metrics.java Outdated
@LivingLikeKrillin LivingLikeKrillin force-pushed the feature/websocket-client-metrics branch from 7161609 to 10db602 Compare March 24, 2026 00:48
Copy link
Copy Markdown
Member

@violetagg violetagg left a comment

Choose a reason for hiding this comment

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

I have proposal for moving handshake timer to the abstract class so that it is available for all handlers. What do you think?

@violetagg
Copy link
Copy Markdown
Member

Please also ensure you have Signed-off-by so that we have a happy DCO check

- Compute path and contextView eagerly in swapMetricsHandler()
  instead of lazy initialization via initMetrics()
- Change extractProcessedDataFromBuffer parameter type to WebSocketFrame
- Use GET for HTTP/1.1 and CONNECT for HTTP/2 via wsHttpMethod()
- Restore missing @author raccoonback in WebsocketClientOperations

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
- Remove @nullable from path and contextView fields and parameters,
  since they are eagerly initialized in swapMetricsHandler()
- Remove dead code: null guards, ternary fallbacks, and else branches
  in ContextAwareWebSocketClientMetricsHandler that are unreachable
  after the @nullable removal
- Remove unused copy constructors from all handler classes
- Move resolvePath() from AbstractWebSocketClientMetricsHandler to
  WebsocketClientOperations where it is actually called
- Remove unused URI parameter from swapMetricsHandler()
- Remove unused status parameter from recordHandshakeFailure() and
  hardcode "ERROR" since no other value is used
- Rename metric namespace from reactor.netty.http.client.websocket
  to reactor.netty.websocket.client

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
Accumulate data across fragmented WebSocket frames and record
metrics only when isFinalFragment() is true, instead of recording
per individual frame.

- Set dataSentTime/dataReceivedTime only on the first fragment
- Accumulate dataSent/dataReceived across continuation frames
- Record and reset counters on final fragment
- Use synchronous recording in write() since reactor-netty's
  SendManyInner does not support ChannelPromise.addListener()

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
- testWebSocketFragmentedDataSentMetrics: verify that sending
  3 fragments records DATA_SENT once with accumulated total
- testWebSocketFragmentedDataReceivedMetrics: verify that receiving
  2 fragments records DATA_RECEIVED once with accumulated total
- Add /ws-fragment server route that sends fragmented frames

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
@violetagg violetagg modified the milestones: 1.3.5, 1.3.6 Apr 3, 2026
@LivingLikeKrillin LivingLikeKrillin force-pushed the feature/websocket-client-metrics branch from 10db602 to 06045d4 Compare April 4, 2026 09:44
- Move handshake timer (handshakeStartTime, startHandshake,
  recordHandshakeComplete, recordHandshakeFailure) from
  MicrometerWebSocketClientMetricsHandler to
  AbstractWebSocketClientMetricsHandler so all handler variants
  can record handshake time
- Override handshake methods in ContextAwareWebSocketClientMetricsHandler
  to pass contextView to the recorder
- Record write metrics via promise.addListener() to ensure metrics
  are captured only after successful write; use promise.unvoid()
  to handle VoidChannelPromise
- Remove unused parameters: ctx from recordConnectionClosed and
  recordException, channel from recordRead
- Add DefaultWebSocketClientMetricsRecorder to wrap plain
  HttpClientMetricsRecorder for custom recorder support
- Restructure swapMetricsHandler with recorder type checking and
  wrapper pattern for non-WebSocket-aware recorders
- Change micrometerWsHandler type from
  MicrometerWebSocketClientMetricsHandler to
  AbstractWebSocketClientMetricsHandler
- Make path and contextView fields final in
  AbstractWebSocketClientMetricsHandler
- Fix FQN usage: import Channel in ContextAwareWebSocketClientMetricsHandler

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
@LivingLikeKrillin LivingLikeKrillin force-pushed the feature/websocket-client-metrics branch from 06045d4 to 7dfb184 Compare April 15, 2026 04:46
@LivingLikeKrillin
Copy link
Copy Markdown
Author

Hi @violetagg, round 3 changes are pushed. Here's what was addressed:

  • Handshake timer moved to AbstractWebSocketClientMetricsHandler
    handshakeStartTime, startHandshake, recordHandshakeComplete, and
    recordHandshakeFailure now live in the abstract class.
  • MicrometerWebSocketClientMetricsHandler now calls
    super.startHandshake(channel) and keeps only the Micrometer Observation
    setup on top. recordHandshakeComplete/recordHandshakeFailure are
    overridden to stop the Observation instead of invoking the recorder, as the
    Observation replaces recorder-based handshake time recording.
  • ContextAwareWebSocketClientMetricsHandler overrides
    recordHandshakeComplete and recordHandshakeFailure to pass contextView
    to the recorder.
  • micrometerWsHandler field type in WebsocketClientOperations
    changed to AbstractWebSocketClientMetricsHandler, since the handshake
    timer is now available for all variants.
  • Micrometer branch in swapMetricsHandler simplified to only
    construct the new handler, matching your suggestion.
  • DefaultWebSocketClientMetricsRecorder wrapper added to wrap a plain
    HttpClientMetricsRecorder when the user supplies a non-WebSocket-aware
    recorder, mirroring the context-aware pattern.
  • Unused parameters removed from AbstractWebSocketClientMetricsHandler
    ctx from recordConnectionClosed and recordException, and channel
    from recordRead, as suggested.
  • path and contextView fields in
    AbstractWebSocketClientMetricsHandler are now final.

The DCO check is green. Could you take another look when you have a moment?

Comment thread reactor-netty-core/src/main/java/reactor/netty/Metrics.java Outdated
Co-authored-by: Violeta Georgieva <696661+violetagg@users.noreply.github.com>
Signed-off-by: Violeta Georgieva <696661+violetagg@users.noreply.github.com>
Co-authored-by: Violeta Georgieva <696661+violetagg@users.noreply.github.com>
Signed-off-by: Violeta Georgieva <696661+violetagg@users.noreply.github.com>
violetagg and others added 2 commits April 16, 2026 15:01
Signed-off-by: Violeta Georgieva <696661+violetagg@users.noreply.github.com>
Register the new ChannelHandler implementations introduced by the
WebSocket client metrics feature in the GraalVM native-image
reflect-config so that NativeConfigTest passes:

- AbstractWebSocketClientMetricsHandler
- ContextAwareWebSocketClientMetricsHandler
- MicrometerWebSocketClientMetricsHandler
- WebSocketClientMetricsHandler

Signed-off-by: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com>
@LivingLikeKrillin
Copy link
Copy Markdown
Author

LivingLikeKrillin commented Apr 24, 2026

Thanks for picking up the copyright and unused-import cleanup directly.

The remaining CI failure was NativeConfigTest.testChannelHandler() — the new WebSocket ChannelHandler classes weren't registered in reflect-config.json for GraalVM native image. Pushed b48178d8 to add them:

  • AbstractWebSocketClientMetricsHandler
  • ContextAwareWebSocketClientMetricsHandler
  • MicrometerWebSocketClientMetricsHandler
  • WebSocketClientMetricsHandler

Verified locally by running the same commands CI does:

  • ./gradlew clean spotlessCheck -PspotlessFrom=origin/main — PASS
  • ./gradlew clean japicmp — PASS
  • ./gradlew :reactor-netty-http:check -PforceTransport=nio (with the Java 8 flags) — PASS, no flakes

Ready for another CI run.

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

Labels

type/enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for WebSocket client metrics

3 participants