001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.net.InetSocketAddress;
025import java.time.Duration;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Optional;
032import java.util.concurrent.atomic.AtomicLong;
033import java.util.concurrent.atomic.LongAdder;
034import java.util.function.Predicate;
035
036import static java.util.Objects.requireNonNull;
037
038/**
039 * Contract for collecting operational metrics from Soklet.
040 * <p>
041 * Soklet's standard implementation, available via {@link #defaultInstance()}, supports detailed histogram collection,
042 * connection accept/reject counters, immutable snapshots (via {@link #snapshot()}), and provides Prometheus
043 * (text format v0.0.4) / OpenMetrics (1.0) export helpers for convenience.
044 * To disable metrics collection without a custom implementation, use {@link #disabledInstance()}.
045 * <p>
046 * If you prefer OpenTelemetry, Micrometer, or another metrics system for monitoring, you might choose to create your own
047 * implementation of this interface.
048 * <p>
049 * Example configuration:
050 * <pre><code>
051 * SokletConfig config = SokletConfig.withHttpServer(HttpServer.fromPort(8080))
052 *   // This is already the default; specifying it here is optional
053 *   .metricsCollector(MetricsCollector.defaultInstance())
054 *   .build();
055 * </code></pre>
056 * <p>
057 * To disable metrics collection entirely, specify Soklet's no-op implementation:
058 * <pre><code>
059 * SokletConfig config = SokletConfig.withHttpServer(HttpServer.fromPort(8080))
060 *   // Use this instead of null to disable metrics collection
061 *   .metricsCollector(MetricsCollector.disabledInstance())
062 *   .build();
063 * </code></pre>
064 * <p>
065 * <p>All methods must be:
066 * <ul>
067 *   <li><strong>Thread-safe</strong> — called concurrently from multiple request threads</li>
068 *   <li><strong>Non-blocking</strong> — should not perform I/O or acquire locks that might contend</li>
069 *   <li><strong>Failure-tolerant</strong> — exceptions are caught and logged, never break request handling</li>
070 * </ul>
071 * <p>
072 * Example usage:
073 * <pre><code>
074 * {@literal @}GET("/metrics")
075 * public MarshaledResponse getMetrics(@NonNull MetricsCollector metricsCollector) {
076 *   SnapshotTextOptions options = SnapshotTextOptions
077 *     .fromMetricsFormat(MetricsFormat.PROMETHEUS);
078 *
079 *   String body = metricsCollector.snapshotText(options).orElse(null);
080 *
081 *   if (body == null)
082 *     return MarshaledResponse.fromStatusCode(204);
083 *
084 *   return MarshaledResponse.withStatusCode(200)
085 *     .headers(Map.of("Content-Type", Set.of("text/plain; charset=UTF-8")))
086 *     .body(body.getBytes(StandardCharsets.UTF_8))
087 *     .build();
088 * }
089 * </code></pre>
090 * <p>
091 * See <a href="https://www.soklet.com/docs/metrics-collection">https://www.soklet.com/docs/metrics-collection</a> for detailed documentation.
092 *
093 * @author <a href="https://www.revetkn.com">Mark Allen</a>
094 */
095@ThreadSafe
096public interface MetricsCollector {
097        /**
098         * Called when a server is about to accept a new TCP connection.
099         *
100         * @param serverType    the server type that is accepting the connection
101         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
102         */
103        default void willAcceptConnection(@NonNull ServerType serverType,
104                                                                                                                                                @Nullable InetSocketAddress remoteAddress) {
105                // No-op by default
106        }
107
108        /**
109         * Called after a server accepts a new TCP connection.
110         *
111         * @param serverType    the server type that accepted the connection
112         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
113         */
114        default void didAcceptConnection(@NonNull ServerType serverType,
115                                                                                                                                         @Nullable InetSocketAddress remoteAddress) {
116                // No-op by default
117        }
118
119        /**
120         * Called after a server fails to accept a new TCP connection.
121         *
122         * @param serverType    the server type that failed to accept the connection
123         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
124         * @param reason        the failure reason
125         * @param throwable     an optional underlying cause, or {@code null} if not applicable
126         */
127        default void didFailToAcceptConnection(@NonNull ServerType serverType,
128                                                                                                                                                                 @Nullable InetSocketAddress remoteAddress,
129                                                                                                                                                                 @NonNull ConnectionRejectionReason reason,
130                                                                                                                                                                 @Nullable Throwable throwable) {
131                // No-op by default
132        }
133
134        /**
135         * Called when a request is about to be accepted for application-level handling.
136         *
137         * @param serverType    the server type that received the request
138         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
139         * @param requestTarget the raw request target (path + query) if known, or {@code null} if unavailable
140         */
141        default void willAcceptRequest(@NonNull ServerType serverType,
142                                                                                                                                 @Nullable InetSocketAddress remoteAddress,
143                                                                                                                                 @Nullable String requestTarget) {
144                // No-op by default
145        }
146
147        /**
148         * Called after a request is accepted for application-level handling.
149         *
150         * @param serverType    the server type that received the request
151         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
152         * @param requestTarget the raw request target (path + query) if known, or {@code null} if unavailable
153         */
154        default void didAcceptRequest(@NonNull ServerType serverType,
155                                                                                                                                @Nullable InetSocketAddress remoteAddress,
156                                                                                                                                @Nullable String requestTarget) {
157                // No-op by default
158        }
159
160        /**
161         * Called when a request fails to be accepted before application-level handling begins.
162         *
163         * @param serverType    the server type that received the request
164         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
165         * @param requestTarget the raw request target (path + query) if known, or {@code null} if unavailable
166         * @param reason        the rejection reason
167         * @param throwable     an optional underlying cause, or {@code null} if not applicable
168         */
169        default void didFailToAcceptRequest(@NonNull ServerType serverType,
170                                                                                                                                                        @Nullable InetSocketAddress remoteAddress,
171                                                                                                                                                        @Nullable String requestTarget,
172                                                                                                                                                        @NonNull RequestRejectionReason reason,
173                                                                                                                                                        @Nullable Throwable throwable) {
174                // No-op by default
175        }
176
177        /**
178         * Called when Soklet is about to read or parse a request into a valid {@link Request}.
179         *
180         * @param serverType    the server type that received the request
181         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
182         * @param requestTarget the raw request target (path + query) if known, or {@code null} if unavailable
183         */
184        default void willReadRequest(@NonNull ServerType serverType,
185                                                                                                                         @Nullable InetSocketAddress remoteAddress,
186                                                                                                                         @Nullable String requestTarget) {
187                // No-op by default
188        }
189
190        /**
191         * Called when a request was successfully read or parsed into a valid {@link Request}.
192         *
193         * @param serverType    the server type that received the request
194         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
195         * @param requestTarget the raw request target (path + query) if known, or {@code null} if unavailable
196         */
197        default void didReadRequest(@NonNull ServerType serverType,
198                                                                                                                        @Nullable InetSocketAddress remoteAddress,
199                                                                                                                        @Nullable String requestTarget) {
200                // No-op by default
201        }
202
203        /**
204         * Called when a request could not be read or parsed into a valid {@link Request}.
205         *
206         * @param serverType    the server type that received the request
207         * @param remoteAddress the best-effort remote address, or {@code null} if unavailable
208         * @param requestTarget the raw request target (path + query) if known, or {@code null} if unavailable
209         * @param reason        the failure reason
210         * @param throwable     an optional underlying cause, or {@code null} if not applicable
211         */
212        default void didFailToReadRequest(@NonNull ServerType serverType,
213                                                                                                                                                @Nullable InetSocketAddress remoteAddress,
214                                                                                                                                                @Nullable String requestTarget,
215                                                                                                                                                @NonNull RequestReadFailureReason reason,
216                                                                                                                                                @Nullable Throwable throwable) {
217                // No-op by default
218        }
219
220        /**
221         * Called as soon as a request is received and a <em>Resource Method</em> has been resolved to handle it.
222         *
223         * @param serverType the server type that received the request
224         */
225        default void didStartRequestHandling(@NonNull ServerType serverType,
226                                                                                                                                                         @NonNull Request request,
227                                                                                                                                                         @Nullable ResourceMethod resourceMethod) {
228                // No-op by default
229        }
230
231        /**
232         * Called after a request finishes processing.
233         */
234        default void didFinishRequestHandling(@NonNull ServerType serverType,
235                                                                                                                                                                @NonNull Request request,
236                                                                                                                                                                @Nullable ResourceMethod resourceMethod,
237                                                                                                                                                                @NonNull MarshaledResponse marshaledResponse,
238                                                                                                                                                                @NonNull Duration duration,
239                                                                                                                                                                @NonNull List<@NonNull Throwable> throwables) {
240                // No-op by default
241        }
242
243        /**
244         * Called before response data is written.
245         */
246        default void willWriteResponse(@NonNull ServerType serverType,
247                                                                                                                                 @NonNull Request request,
248                                                                                                                                 @Nullable ResourceMethod resourceMethod,
249                                                                                                                                 @NonNull MarshaledResponse marshaledResponse) {
250                // No-op by default
251        }
252
253        /**
254         * Called after response data is written.
255         */
256        default void didWriteResponse(@NonNull ServerType serverType,
257                                                                                                                                @NonNull Request request,
258                                                                                                                                @Nullable ResourceMethod resourceMethod,
259                                                                                                                                @NonNull MarshaledResponse marshaledResponse,
260                                                                                                                                @NonNull Duration responseWriteDuration) {
261                // No-op by default
262        }
263
264        /**
265         * Called after response data fails to write.
266         */
267        default void didFailToWriteResponse(@NonNull ServerType serverType,
268                                                                                                                                                        @NonNull Request request,
269                                                                                                                                                        @Nullable ResourceMethod resourceMethod,
270                                                                                                                                                        @NonNull MarshaledResponse marshaledResponse,
271                                                                                                                                                        @NonNull Duration responseWriteDuration,
272                                                                                                                                                        @NonNull Throwable throwable) {
273                // No-op by default
274        }
275
276        /**
277         * Called after an MCP session is durably created.
278         */
279        default void didCreateMcpSession(@NonNull Request request,
280                                                                                                                                         @NonNull Class<? extends McpEndpoint> endpointClass,
281                                                                                                                                         @NonNull String sessionId) {
282                // No-op by default
283        }
284
285        /**
286         * Called after an MCP session is terminated.
287         */
288        default void didTerminateMcpSession(@NonNull Class<? extends McpEndpoint> endpointClass,
289                                                                                                                                                        @NonNull String sessionId,
290                                                                                                                                                        @NonNull Duration sessionDuration,
291                                                                                                                                                        @NonNull McpSessionTerminationReason terminationReason,
292                                                                                                                                                        @Nullable Throwable throwable) {
293                // No-op by default
294        }
295
296        /**
297         * Called after a valid MCP JSON-RPC request begins handling.
298         */
299        default void didStartMcpRequestHandling(@NonNull Request request,
300                                                                                                                                                                        @NonNull Class<? extends McpEndpoint> endpointClass,
301                                                                                                                                                                        @Nullable String sessionId,
302                                                                                                                                                                        @NonNull String jsonRpcMethod,
303                                                                                                                                                                        @Nullable McpJsonRpcRequestId jsonRpcRequestId) {
304                // No-op by default
305        }
306
307        /**
308         * Called after MCP JSON-RPC request handling finishes.
309         */
310        default void didFinishMcpRequestHandling(@NonNull Request request,
311                                                                                                                                                                         @NonNull Class<? extends McpEndpoint> endpointClass,
312                                                                                                                                                                         @Nullable String sessionId,
313                                                                                                                                                                         @NonNull String jsonRpcMethod,
314                                                                                                                                                                         @Nullable McpJsonRpcRequestId jsonRpcRequestId,
315                                                                                                                                                                         @NonNull McpRequestOutcome requestOutcome,
316                                                                                                                                                                         @Nullable McpJsonRpcError jsonRpcError,
317                                                                                                                                                                         @NonNull Duration duration,
318                                                                                                                                                                         @NonNull List<@NonNull Throwable> throwables) {
319                // No-op by default
320        }
321
322        /**
323         * Called after an MCP GET stream is established.
324         */
325        default void didEstablishMcpSseStream(@NonNull Request request,
326                                                                                                                                                                                                                @NonNull Class<? extends McpEndpoint> endpointClass,
327                                                                                                                                                                                                                @NonNull String sessionId) {
328                // No-op by default
329        }
330
331        /**
332         * Called after an MCP GET stream is terminated.
333         */
334        default void didTerminateMcpSseStream(@NonNull Request request,
335                                                                                                                                                                                                                @NonNull Class<? extends McpEndpoint> endpointClass,
336                                                                                                                                                                                                                @NonNull String sessionId,
337                                                                                                                                                                                                                @NonNull Duration connectionDuration,
338                                                                                                                                                                                                                @NonNull McpStreamTerminationReason terminationReason,
339                                                                                                                                                                                                                @Nullable Throwable throwable) {
340                // No-op by default
341        }
342
343        /**
344         * Called before an SSE connection is established.
345         */
346        default void willEstablishSseConnection(@NonNull Request request,
347                                                                                                                                                                                                                        @Nullable ResourceMethod resourceMethod) {
348                // No-op by default
349        }
350
351        /**
352         * Called after an SSE connection is established.
353         */
354        default void didEstablishSseConnection(@NonNull SseConnection sseConnection) {
355                // No-op by default
356        }
357
358        /**
359         * Called if an SSE connection fails to establish.
360         *
361         * @param reason    the handshake failure reason
362         * @param throwable an optional underlying cause, or {@code null} if not applicable
363         */
364        default void didFailToEstablishSseConnection(@NonNull Request request,
365                                                                                                                                                                                                                                         @Nullable ResourceMethod resourceMethod,
366                                                                                                                                                                                                                                         SseConnection.@NonNull HandshakeFailureReason reason,
367                                                                                                                                                                                                                                         @Nullable Throwable throwable) {
368                // No-op by default
369        }
370
371        /**
372         * Called before an SSE connection is terminated.
373         */
374        default void willTerminateSseConnection(@NonNull SseConnection sseConnection,
375                                                                                                                                                                                                                        SseConnection.@NonNull TerminationReason terminationReason,
376                                                                                                                                                                                                                        @Nullable Throwable throwable) {
377                // No-op by default
378        }
379
380        /**
381         * Called after an SSE connection is terminated.
382         */
383        default void didTerminateSseConnection(@NonNull SseConnection sseConnection,
384                                                                                                                                                                                                                 @NonNull Duration connectionDuration,
385                                                                                                                                                                                                                 SseConnection.@NonNull TerminationReason terminationReason,
386                                                                                                                                                                                                                 @Nullable Throwable throwable) {
387                // No-op by default
388        }
389
390        /**
391         * Called before an SSE event is written.
392         */
393        default void willWriteSseEvent(@NonNull SseConnection sseConnection,
394                                                                                                                                                                @NonNull SseEvent sseEvent) {
395                // No-op by default
396        }
397
398        /**
399         * Called after an SSE event is written.
400         *
401         * @param sseConnection the connection the event was written to
402         * @param sseEvent           the event that was written
403         * @param writeDuration             how long it took to write the event
404         * @param deliveryLag               elapsed time between enqueue and write start, or {@code null} if unknown
405         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
406         * @param queueDepth                number of queued elements remaining at write time, or {@code null} if unknown
407         */
408        default void didWriteSseEvent(@NonNull SseConnection sseConnection,
409                                                                                                                                                         @NonNull SseEvent sseEvent,
410                                                                                                                                                         @NonNull Duration writeDuration,
411                                                                                                                                                         @Nullable Duration deliveryLag,
412                                                                                                                                                         @Nullable Integer payloadBytes,
413                                                                                                                                                         @Nullable Integer queueDepth) {
414                // No-op by default
415        }
416
417        /**
418         * Called after an SSE event fails to write.
419         *
420         * @param sseConnection the connection the event was written to
421         * @param sseEvent           the event that was written
422         * @param writeDuration             how long it took to attempt the write
423         * @param throwable                 the failure cause
424         * @param deliveryLag               elapsed time between enqueue and write start, or {@code null} if unknown
425         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
426         * @param queueDepth                number of queued elements remaining at write time, or {@code null} if unknown
427         */
428        default void didFailToWriteSseEvent(@NonNull SseConnection sseConnection,
429                                                                                                                                                                                 @NonNull SseEvent sseEvent,
430                                                                                                                                                                                 @NonNull Duration writeDuration,
431                                                                                                                                                                                 @NonNull Throwable throwable,
432                                                                                                                                                                                 @Nullable Duration deliveryLag,
433                                                                                                                                                                                 @Nullable Integer payloadBytes,
434                                                                                                                                                                                 @Nullable Integer queueDepth) {
435                // No-op by default
436        }
437
438        /**
439         * Called before an SSE comment is written.
440         */
441        default void willWriteSseComment(@NonNull SseConnection sseConnection,
442                                                                                                                                                                                         @NonNull SseComment sseComment) {
443                // No-op by default
444        }
445
446        /**
447         * Called after an SSE comment is written.
448         *
449         * @param sseConnection the connection the comment was written to
450         * @param sseComment    the comment that was written
451         * @param writeDuration             how long it took to write the comment
452         * @param deliveryLag               elapsed time between enqueue and write start, or {@code null} if unknown
453         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
454         * @param queueDepth                number of queued elements remaining at write time, or {@code null} if unknown
455         */
456        default void didWriteSseComment(@NonNull SseConnection sseConnection,
457                                                                                                                                                                                        @NonNull SseComment sseComment,
458                                                                                                                                                                                        @NonNull Duration writeDuration,
459                                                                                                                                                                                        @Nullable Duration deliveryLag,
460                                                                                                                                                                                        @Nullable Integer payloadBytes,
461                                                                                                                                                                                        @Nullable Integer queueDepth) {
462                // No-op by default
463        }
464
465        /**
466         * Called after an SSE comment fails to write.
467         *
468         * @param sseConnection the connection the comment was written to
469         * @param sseComment    the comment that was written
470         * @param writeDuration             how long it took to attempt the write
471         * @param throwable                 the failure cause
472         * @param deliveryLag               elapsed time between enqueue and write start, or {@code null} if unknown
473         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
474         * @param queueDepth                number of queued elements remaining at write time, or {@code null} if unknown
475         */
476        default void didFailToWriteSseComment(@NonNull SseConnection sseConnection,
477                                                                                                                                                                                                                @NonNull SseComment sseComment,
478                                                                                                                                                                                                                @NonNull Duration writeDuration,
479                                                                                                                                                                                                                @NonNull Throwable throwable,
480                                                                                                                                                                                                                @Nullable Duration deliveryLag,
481                                                                                                                                                                                                                @Nullable Integer payloadBytes,
482                                                                                                                                                                                                                @Nullable Integer queueDepth) {
483                // No-op by default
484        }
485
486        /**
487         * Called after an SSE event is dropped before it can be enqueued for delivery.
488         *
489         * @param sseConnection the connection the event was targeting
490         * @param sseEvent           the event that was dropped
491         * @param reason                    the drop reason
492         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
493         * @param queueDepth                number of queued elements at drop time, or {@code null} if unknown
494         */
495        default void didDropSseEvent(@NonNull SseConnection sseConnection,
496                                                                                                                                                        @NonNull SseEvent sseEvent,
497                                                                                                                                                        @NonNull SseEventDropReason reason,
498                                                                                                                                                        @Nullable Integer payloadBytes,
499                                                                                                                                                        @Nullable Integer queueDepth) {
500                // No-op by default
501        }
502
503        /**
504         * Called after an SSE comment is dropped before it can be enqueued for delivery.
505         *
506         * @param sseConnection the connection the comment was targeting
507         * @param sseComment    the comment that was dropped
508         * @param reason                    the drop reason
509         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
510         * @param queueDepth                number of queued elements at drop time, or {@code null} if unknown
511         */
512        default void didDropSseComment(@NonNull SseConnection sseConnection,
513                                                                                                                                                                                 @NonNull SseComment sseComment,
514                                                                                                                                                                                 @NonNull SseEventDropReason reason,
515                                                                                                                                                                                 @Nullable Integer payloadBytes,
516                                                                                                                                                                                 @Nullable Integer queueDepth) {
517                // No-op by default
518        }
519
520        /**
521         * Called after a broadcast attempt for a Server-Sent Event payload.
522         *
523         * @param route     the route declaration that was broadcast to
524         * @param attempted number of connections targeted
525         * @param enqueued  number of connections for which enqueue succeeded
526         * @param dropped   number of connections for which enqueue failed
527         */
528        default void didBroadcastSseEvent(@NonNull ResourcePathDeclaration route,
529                                                                                                                                                                         int attempted,
530                                                                                                                                                                         int enqueued,
531                                                                                                                                                                         int dropped) {
532                // No-op by default
533        }
534
535        /**
536         * Called after a broadcast attempt for a Server-Sent Event comment payload.
537         *
538         * @param route       the route declaration that was broadcast to
539         * @param commentType the comment type
540         * @param attempted   number of connections targeted
541         * @param enqueued    number of connections for which enqueue succeeded
542         * @param dropped     number of connections for which enqueue failed
543         */
544        default void didBroadcastSseComment(@NonNull ResourcePathDeclaration route,
545                                                                                                                                                                                                        SseComment.@NonNull CommentType commentType,
546                                                                                                                                                                                                        int attempted,
547                                                                                                                                                                                                        int enqueued,
548                                                                                                                                                                                                        int dropped) {
549                // No-op by default
550        }
551
552        /**
553         * Returns a snapshot of metrics collected so far, if supported.
554         *
555         * @return an optional metrics snapshot
556         */
557        @NonNull
558        default Optional<Snapshot> snapshot() {
559                return Optional.empty();
560        }
561
562        /**
563         * Returns a text snapshot of metrics collected so far, if supported.
564         * <p>
565         * The default collector supports Prometheus (text format v0.0.4) and OpenMetrics (1.0) text exposition formats.
566         *
567         * @param options the snapshot rendering options
568         * @return a textual metrics snapshot, or {@link Optional#empty()} if unsupported
569         */
570        @NonNull
571        default Optional<String> snapshotText(@NonNull SnapshotTextOptions options) {
572                requireNonNull(options);
573                return Optional.empty();
574        }
575
576        /**
577         * Resets any in-memory metrics state, if supported.
578         */
579        default void reset() {
580                // No-op by default
581        }
582
583        /**
584         * Text format to use for {@link #snapshotText(SnapshotTextOptions)}.
585         * <p>
586         * This controls serialization only; the underlying metrics data is unchanged.
587         */
588        enum MetricsFormat {
589                /**
590                 * Prometheus text exposition format (v0.0.4).
591                 */
592                PROMETHEUS,
593                /**
594                 * OpenMetrics text exposition format (1.0), including the {@code # EOF} trailer.
595                 */
596                OPEN_METRICS_1_0
597        }
598
599        /**
600         * Options for rendering a textual metrics snapshot.
601         * <p>
602         * Use {@link #withMetricsFormat(MetricsFormat)} to obtain a builder and customize output.
603         * <p>
604         * Key options:
605         * <ul>
606         *   <li>{@code metricFilter} allows per-sample filtering by name and labels</li>
607         *   <li>{@code histogramFormat} controls bucket vs count/sum output</li>
608         *   <li>{@code includeZeroBuckets} drops empty bucket samples when false</li>
609         * </ul>
610         */
611        @ThreadSafe
612        final class SnapshotTextOptions {
613                @NonNull
614                private final MetricsFormat metricsFormat;
615                @Nullable
616                private final Predicate<MetricSample> metricFilter;
617                @NonNull
618                private final HistogramFormat histogramFormat;
619                @NonNull
620                private final Boolean includeZeroBuckets;
621
622                private SnapshotTextOptions(@NonNull Builder builder) {
623                        requireNonNull(builder);
624
625                        this.metricsFormat = requireNonNull(builder.metricsFormat);
626                        this.metricFilter = builder.metricFilter;
627                        this.histogramFormat = requireNonNull(builder.histogramFormat);
628                        this.includeZeroBuckets = builder.includeZeroBuckets == null ? true : builder.includeZeroBuckets;
629                }
630
631                /**
632                 * Begins building options with the specified format.
633                 *
634                 * @param metricsFormat the text exposition format
635                 * @return a builder seeded with the format
636                 */
637                @NonNull
638                public static Builder withMetricsFormat(@NonNull MetricsFormat metricsFormat) {
639                        return new Builder(metricsFormat);
640                }
641
642                /**
643                 * Creates options with the specified format and defaults for all other fields.
644                 *
645                 * @param metricsFormat the text exposition format
646                 * @return a {@link SnapshotTextOptions} instance
647                 */
648                @NonNull
649                public static SnapshotTextOptions fromMetricsFormat(@NonNull MetricsFormat metricsFormat) {
650                        return withMetricsFormat(metricsFormat).build();
651                }
652
653                /**
654                 * The text exposition format to emit.
655                 *
656                 * @return the metrics format
657                 */
658                @NonNull
659                public MetricsFormat getMetricsFormat() {
660                        return this.metricsFormat;
661                }
662
663                /**
664                 * Optional filter for rendered samples.
665                 *
666                 * @return the filter, if present
667                 */
668                @NonNull
669                public Optional<Predicate<MetricSample>> getMetricFilter() {
670                        return Optional.ofNullable(this.metricFilter);
671                }
672
673                /**
674                 * The histogram rendering strategy.
675                 *
676                 * @return the histogram format
677                 */
678                @NonNull
679                public HistogramFormat getHistogramFormat() {
680                        return this.histogramFormat;
681                }
682
683                /**
684                 * Whether zero-count buckets should be emitted.
685                 *
686                 * @return {@code true} if zero-count buckets are included
687                 */
688                @NonNull
689                public Boolean getIncludeZeroBuckets() {
690                        return this.includeZeroBuckets;
691                }
692
693                /**
694                 * Supported histogram rendering strategies.
695                 */
696                public enum HistogramFormat {
697                        /**
698                         * Emit full histogram series (buckets, count, and sum).
699                         */
700                        FULL_BUCKETS,
701                        /**
702                         * Emit only {@code _count} and {@code _sum} samples (omit buckets).
703                         */
704                        COUNT_SUM_ONLY,
705                        /**
706                         * Suppress histogram output entirely.
707                         */
708                        NONE
709                }
710
711                /**
712                 * A single text-format sample with its label set.
713                 * <p>
714                 * Filters receive these instances for each rendered sample. For histogram buckets,
715                 * the sample name includes {@code _bucket} and the labels include {@code le}.
716                 * Label maps are immutable and preserve insertion order.
717                 */
718                public static final class MetricSample {
719                        @NonNull
720                        private final String name;
721                        @NonNull
722                        private final Map<@NonNull String, @NonNull String> labels;
723
724                        /**
725                         * Creates a metrics sample definition.
726                         *
727                         * @param name   the sample name (e.g. {@code soklet_http_request_duration_nanos_bucket})
728                         * @param labels the sample labels
729                         */
730                        public MetricSample(@NonNull String name,
731                                                                                                        @NonNull Map<@NonNull String, @NonNull String> labels) {
732                                this.name = requireNonNull(name);
733                                this.labels = Collections.unmodifiableMap(new LinkedHashMap<>(requireNonNull(labels)));
734                        }
735
736                        /**
737                         * The name for this sample.
738                         *
739                         * @return the sample name
740                         */
741                        @NonNull
742                        public String getName() {
743                                return this.name;
744                        }
745
746                        /**
747                         * The label set for this sample.
748                         *
749                         * @return immutable labels
750                         */
751                        @NonNull
752                        public Map<@NonNull String, @NonNull String> getLabels() {
753                                return this.labels;
754                        }
755                }
756
757                /**
758                 * Builder for {@link SnapshotTextOptions}.
759                 * <p>
760                 * Defaults are {@link HistogramFormat#FULL_BUCKETS} and {@code includeZeroBuckets=true}.
761                 */
762                @ThreadSafe
763                public static final class Builder {
764                        @NonNull
765                        private final MetricsFormat metricsFormat;
766                        @Nullable
767                        private Predicate<MetricSample> metricFilter;
768                        @NonNull
769                        private HistogramFormat histogramFormat;
770                        @Nullable
771                        private Boolean includeZeroBuckets;
772
773                        private Builder(@NonNull MetricsFormat metricsFormat) {
774                                this.metricsFormat = requireNonNull(metricsFormat);
775                                this.histogramFormat = HistogramFormat.FULL_BUCKETS;
776                                this.includeZeroBuckets = true;
777                        }
778
779                        /**
780                         * Sets an optional per-sample filter.
781                         *
782                         * @param metricFilter the filter to apply, or {@code null} to disable filtering
783                         * @return this builder
784                         */
785                        @NonNull
786                        public Builder metricFilter(@Nullable Predicate<MetricSample> metricFilter) {
787                                this.metricFilter = metricFilter;
788                                return this;
789                        }
790
791                        /**
792                         * Sets how histograms are rendered in the text snapshot.
793                         *
794                         * @param histogramFormat the histogram format
795                         * @return this builder
796                         */
797                        @NonNull
798                        public Builder histogramFormat(@NonNull HistogramFormat histogramFormat) {
799                                this.histogramFormat = requireNonNull(histogramFormat);
800                                return this;
801                        }
802
803                        /**
804                         * Controls whether zero-count buckets are emitted.
805                         *
806                         * @param includeZeroBuckets {@code true} to include zero-count buckets, {@code false} to omit them
807                         * @return this builder
808                         */
809                        @NonNull
810                        public Builder includeZeroBuckets(@Nullable Boolean includeZeroBuckets) {
811                                this.includeZeroBuckets = includeZeroBuckets;
812                                return this;
813                        }
814
815                        /**
816                         * Builds a {@link SnapshotTextOptions} instance.
817                         *
818                         * @return the built options
819                         */
820                        @NonNull
821                        public SnapshotTextOptions build() {
822                                return new SnapshotTextOptions(this);
823                        }
824                }
825        }
826
827        /**
828         * Immutable snapshot of collected metrics.
829         * <p>
830         * Durations are in nanoseconds, sizes are in bytes, and queue depths are raw counts.
831         * Histogram values are captured as {@link HistogramSnapshot} instances.
832         * Connection counts report total accepted/rejected connections for the HTTP and SSE servers.
833         * Request read failures and request rejections are reported separately for HTTP and SSE traffic.
834         * Instances are typically produced by {@link MetricsCollector#snapshot()} but can also be built
835         * manually via {@link #builder()}.
836         *
837         * @author <a href="https://www.revetkn.com">Mark Allen</a>
838         */
839        @ThreadSafe
840        final class Snapshot {
841                @NonNull
842                private final Long activeRequests;
843                @NonNull
844                private final Long activeSseConnections;
845                @NonNull
846                private final Long activeMcpSessions;
847                @NonNull
848                private final Long activeMcpStreams;
849                @NonNull
850                private final Long httpConnectionsAccepted;
851                @NonNull
852                private final Long httpConnectionsRejected;
853                @NonNull
854                private final Long sseConnectionsAccepted;
855                @NonNull
856                private final Long sseConnectionsRejected;
857                @NonNull
858                private final Long mcpConnectionsAccepted;
859                @NonNull
860                private final Long mcpConnectionsRejected;
861                @NonNull
862                private final Map<@NonNull RequestReadFailureKey, @NonNull Long> httpRequestReadFailures;
863                @NonNull
864                private final Map<@NonNull RequestRejectionKey, @NonNull Long> httpRequestRejections;
865                @NonNull
866                private final Map<@NonNull RequestReadFailureKey, @NonNull Long> sseRequestReadFailures;
867                @NonNull
868                private final Map<@NonNull RequestRejectionKey, @NonNull Long> sseRequestRejections;
869                @NonNull
870                private final Map<@NonNull RequestReadFailureKey, @NonNull Long> mcpRequestReadFailures;
871                @NonNull
872                private final Map<@NonNull RequestRejectionKey, @NonNull Long> mcpRequestRejections;
873                @NonNull
874                private final Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpRequestDurations;
875                @NonNull
876                private final Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpHandlerDurations;
877                @NonNull
878                private final Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpTimeToFirstByte;
879                @NonNull
880                private final Map<@NonNull HttpServerRouteKey, @NonNull HistogramSnapshot> httpRequestBodyBytes;
881                @NonNull
882                private final Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpResponseBodyBytes;
883                @NonNull
884                private final Map<@NonNull SseEventRouteKey, @NonNull Long> sseHandshakesAccepted;
885                @NonNull
886                private final Map<@NonNull SseEventRouteHandshakeFailureKey, @NonNull Long> sseHandshakesRejected;
887                @NonNull
888                private final Map<@NonNull SseEventRouteEnqueueOutcomeKey, @NonNull Long> sseEventEnqueueOutcomes;
889                @NonNull
890                private final Map<@NonNull SseCommentRouteEnqueueOutcomeKey, @NonNull Long> sseCommentEnqueueOutcomes;
891                @NonNull
892                private final Map<@NonNull SseEventRouteDropKey, @NonNull Long> sseEventDrops;
893                @NonNull
894                private final Map<@NonNull SseCommentRouteDropKey, @NonNull Long> sseCommentDrops;
895                @NonNull
896                private final Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseTimeToFirstEvent;
897                @NonNull
898                private final Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventWriteDurations;
899                @NonNull
900                private final Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventDeliveryLag;
901                @NonNull
902                private final Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventSizes;
903                @NonNull
904                private final Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseQueueDepth;
905                @NonNull
906                private final Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentDeliveryLag;
907                @NonNull
908                private final Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentSizes;
909                @NonNull
910                private final Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentQueueDepth;
911                @NonNull
912                private final Map<@NonNull SseEventRouteTerminationKey, @NonNull HistogramSnapshot> sseConnectionDurations;
913                @NonNull
914                private final Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull Long> mcpRequests;
915                @NonNull
916                private final Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull HistogramSnapshot> mcpRequestDurations;
917                @NonNull
918                private final Map<@NonNull McpEndpointSessionTerminationKey, @NonNull HistogramSnapshot> mcpSessionDurations;
919                @NonNull
920                private final Map<@NonNull McpEndpointStreamTerminationKey, @NonNull HistogramSnapshot> mcpStreamDurations;
921
922                /**
923                 * Acquires an "empty" builder for {@link Snapshot} instances.
924                 *
925                 * @return the builder
926                 */
927                @NonNull
928                public static Builder builder() {
929                        return new Builder();
930                }
931
932                private Snapshot(@NonNull Builder builder) {
933                        requireNonNull(builder);
934
935                        this.activeRequests = requireNonNull(builder.activeRequests);
936                        this.activeSseConnections = requireNonNull(builder.activeSseConnections);
937                        this.activeMcpSessions = requireNonNull(builder.activeMcpSessions);
938                        this.activeMcpStreams = requireNonNull(builder.activeMcpStreams);
939                        this.httpConnectionsAccepted = requireNonNull(builder.httpConnectionsAccepted);
940                        this.httpConnectionsRejected = requireNonNull(builder.httpConnectionsRejected);
941                        this.sseConnectionsAccepted = requireNonNull(builder.sseConnectionsAccepted);
942                        this.sseConnectionsRejected = requireNonNull(builder.sseConnectionsRejected);
943                        this.mcpConnectionsAccepted = requireNonNull(builder.mcpConnectionsAccepted);
944                        this.mcpConnectionsRejected = requireNonNull(builder.mcpConnectionsRejected);
945                        this.httpRequestReadFailures = copyOrEmpty(builder.httpRequestReadFailures);
946                        this.httpRequestRejections = copyOrEmpty(builder.httpRequestRejections);
947                        this.sseRequestReadFailures = copyOrEmpty(builder.sseRequestReadFailures);
948                        this.sseRequestRejections = copyOrEmpty(builder.sseRequestRejections);
949                        this.mcpRequestReadFailures = copyOrEmpty(builder.mcpRequestReadFailures);
950                        this.mcpRequestRejections = copyOrEmpty(builder.mcpRequestRejections);
951                        this.httpRequestDurations = copyOrEmpty(builder.httpRequestDurations);
952                        this.httpHandlerDurations = copyOrEmpty(builder.httpHandlerDurations);
953                        this.httpTimeToFirstByte = copyOrEmpty(builder.httpTimeToFirstByte);
954                        this.httpRequestBodyBytes = copyOrEmpty(builder.httpRequestBodyBytes);
955                        this.httpResponseBodyBytes = copyOrEmpty(builder.httpResponseBodyBytes);
956                        this.sseHandshakesAccepted = copyOrEmpty(builder.sseHandshakesAccepted);
957                        this.sseHandshakesRejected = copyOrEmpty(builder.sseHandshakesRejected);
958                        this.sseEventEnqueueOutcomes = copyOrEmpty(builder.sseEventEnqueueOutcomes);
959                        this.sseCommentEnqueueOutcomes = copyOrEmpty(builder.sseCommentEnqueueOutcomes);
960                        this.sseEventDrops = copyOrEmpty(builder.sseEventDrops);
961                        this.sseCommentDrops = copyOrEmpty(builder.sseCommentDrops);
962                        this.sseTimeToFirstEvent = copyOrEmpty(builder.sseTimeToFirstEvent);
963                        this.sseEventWriteDurations = copyOrEmpty(builder.sseEventWriteDurations);
964                        this.sseEventDeliveryLag = copyOrEmpty(builder.sseEventDeliveryLag);
965                        this.sseEventSizes = copyOrEmpty(builder.sseEventSizes);
966                        this.sseQueueDepth = copyOrEmpty(builder.sseQueueDepth);
967                        this.sseCommentDeliveryLag = copyOrEmpty(builder.sseCommentDeliveryLag);
968                        this.sseCommentSizes = copyOrEmpty(builder.sseCommentSizes);
969                        this.sseCommentQueueDepth = copyOrEmpty(builder.sseCommentQueueDepth);
970                        this.sseConnectionDurations = copyOrEmpty(builder.sseConnectionDurations);
971                        this.mcpRequests = copyOrEmpty(builder.mcpRequests);
972                        this.mcpRequestDurations = copyOrEmpty(builder.mcpRequestDurations);
973                        this.mcpSessionDurations = copyOrEmpty(builder.mcpSessionDurations);
974                        this.mcpStreamDurations = copyOrEmpty(builder.mcpStreamDurations);
975                }
976
977                /**
978                 * Returns the number of active HTTP requests.
979                 *
980                 * @return the active HTTP request count
981                 */
982                @NonNull
983                public Long getActiveRequests() {
984                        return this.activeRequests;
985                }
986
987                /**
988                 * Returns the number of active server-sent event connections.
989                 *
990                 * @return the active SSE connection count
991                 */
992                @NonNull
993                public Long getActiveSseConnections() {
994                        return this.activeSseConnections;
995                }
996
997                /**
998                 * Returns the number of active MCP sessions.
999                 *
1000                 * @return the active MCP session count
1001                 */
1002                @NonNull
1003                public Long getActiveMcpSessions() {
1004                        return this.activeMcpSessions;
1005                }
1006
1007                /**
1008                 * Returns the number of active MCP streams.
1009                 *
1010                 * @return the active MCP stream count
1011                 */
1012                @NonNull
1013                public Long getActiveMcpStreams() {
1014                        return this.activeMcpStreams;
1015                }
1016
1017                /**
1018                 * Returns the total number of accepted HTTP connections.
1019                 *
1020                 * @return total accepted HTTP connections
1021                 */
1022                @NonNull
1023                public Long getHttpConnectionsAccepted() {
1024                        return this.httpConnectionsAccepted;
1025                }
1026
1027                /**
1028                 * Returns the total number of rejected HTTP connections.
1029                 *
1030                 * @return total rejected HTTP connections
1031                 */
1032                @NonNull
1033                public Long getHttpConnectionsRejected() {
1034                        return this.httpConnectionsRejected;
1035                }
1036
1037                /**
1038                 * Returns the total number of accepted SSE connections.
1039                 *
1040                 * @return total accepted SSE connections
1041                 */
1042                @NonNull
1043                public Long getSseConnectionsAccepted() {
1044                        return this.sseConnectionsAccepted;
1045                }
1046
1047                /**
1048                 * Returns the total number of rejected SSE connections.
1049                 *
1050                 * @return total rejected SSE connections
1051                 */
1052                @NonNull
1053                public Long getSseConnectionsRejected() {
1054                        return this.sseConnectionsRejected;
1055                }
1056
1057                /**
1058                 * Returns the total number of accepted MCP connections.
1059                 *
1060                 * @return total accepted MCP connections
1061                 */
1062                @NonNull
1063                public Long getMcpConnectionsAccepted() {
1064                        return this.mcpConnectionsAccepted;
1065                }
1066
1067                /**
1068                 * Returns the total number of rejected MCP connections.
1069                 *
1070                 * @return total rejected MCP connections
1071                 */
1072                @NonNull
1073                public Long getMcpConnectionsRejected() {
1074                        return this.mcpConnectionsRejected;
1075                }
1076
1077                /**
1078                 * Returns HTTP request read failure counters keyed by failure reason.
1079                 *
1080                 * @return HTTP request read failure counters
1081                 */
1082                @NonNull
1083                public Map<@NonNull RequestReadFailureKey, @NonNull Long> getHttpRequestReadFailures() {
1084                        return this.httpRequestReadFailures;
1085                }
1086
1087                /**
1088                 * Returns HTTP request rejection counters keyed by rejection reason.
1089                 *
1090                 * @return HTTP request rejection counters
1091                 */
1092                @NonNull
1093                public Map<@NonNull RequestRejectionKey, @NonNull Long> getHttpRequestRejections() {
1094                        return this.httpRequestRejections;
1095                }
1096
1097                /**
1098                 * Returns SSE request read failure counters keyed by failure reason.
1099                 *
1100                 * @return SSE request read failure counters
1101                 */
1102                @NonNull
1103                public Map<@NonNull RequestReadFailureKey, @NonNull Long> getSseRequestReadFailures() {
1104                        return this.sseRequestReadFailures;
1105                }
1106
1107                /**
1108                 * Returns SSE request rejection counters keyed by rejection reason.
1109                 *
1110                 * @return SSE request rejection counters
1111                 */
1112                @NonNull
1113                public Map<@NonNull RequestRejectionKey, @NonNull Long> getSseRequestRejections() {
1114                        return this.sseRequestRejections;
1115                }
1116
1117                /**
1118                 * Returns MCP request read failure counters keyed by failure reason.
1119                 *
1120                 * @return MCP request read failure counters
1121                 */
1122                @NonNull
1123                public Map<@NonNull RequestReadFailureKey, @NonNull Long> getMcpRequestReadFailures() {
1124                        return this.mcpRequestReadFailures;
1125                }
1126
1127                /**
1128                 * Returns MCP request rejection counters keyed by rejection reason.
1129                 *
1130                 * @return MCP request rejection counters
1131                 */
1132                @NonNull
1133                public Map<@NonNull RequestRejectionKey, @NonNull Long> getMcpRequestRejections() {
1134                        return this.mcpRequestRejections;
1135                }
1136
1137                /**
1138                 * Returns HTTP request duration histograms keyed by server route and status class.
1139                 *
1140                 * @return HTTP request duration histograms
1141                 */
1142                @NonNull
1143                public Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpRequestDurations() {
1144                        return this.httpRequestDurations;
1145                }
1146
1147                /**
1148                 * Returns HTTP handler duration histograms keyed by server route and status class.
1149                 *
1150                 * @return HTTP handler duration histograms
1151                 */
1152                @NonNull
1153                public Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpHandlerDurations() {
1154                        return this.httpHandlerDurations;
1155                }
1156
1157                /**
1158                 * Returns HTTP time-to-first-byte histograms keyed by server route and status class.
1159                 *
1160                 * @return HTTP time-to-first-byte histograms
1161                 */
1162                @NonNull
1163                public Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpTimeToFirstByte() {
1164                        return this.httpTimeToFirstByte;
1165                }
1166
1167                /**
1168                 * Returns HTTP request body size histograms keyed by server route.
1169                 *
1170                 * @return HTTP request body size histograms
1171                 */
1172                @NonNull
1173                public Map<@NonNull HttpServerRouteKey, @NonNull HistogramSnapshot> getHttpRequestBodyBytes() {
1174                        return this.httpRequestBodyBytes;
1175                }
1176
1177                /**
1178                 * Returns HTTP response body size histograms keyed by server route and status class.
1179                 *
1180                 * @return HTTP response body size histograms
1181                 */
1182                @NonNull
1183                public Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpResponseBodyBytes() {
1184                        return this.httpResponseBodyBytes;
1185                }
1186
1187                /**
1188                 * Returns SSE handshake acceptance counters keyed by route.
1189                 *
1190                 * @return SSE handshake acceptance counters
1191                 */
1192                @NonNull
1193                public Map<@NonNull SseEventRouteKey, @NonNull Long> getSseHandshakesAccepted() {
1194                        return this.sseHandshakesAccepted;
1195                }
1196
1197                /**
1198                 * Returns SSE handshake rejection counters keyed by route and failure reason.
1199                 *
1200                 * @return SSE handshake rejection counters
1201                 */
1202                @NonNull
1203                public Map<@NonNull SseEventRouteHandshakeFailureKey, @NonNull Long> getSseHandshakesRejected() {
1204                        return this.sseHandshakesRejected;
1205                }
1206
1207                /**
1208                 * Returns SSE event enqueue outcome counters keyed by route and outcome.
1209                 *
1210                 * @return SSE event enqueue outcome counters
1211                 */
1212                @NonNull
1213                public Map<@NonNull SseEventRouteEnqueueOutcomeKey, @NonNull Long> getSseEventEnqueueOutcomes() {
1214                        return this.sseEventEnqueueOutcomes;
1215                }
1216
1217                /**
1218                 * Returns SSE comment enqueue outcome counters keyed by route, comment type, and outcome.
1219                 *
1220                 * @return SSE comment enqueue outcome counters
1221                 */
1222                @NonNull
1223                public Map<@NonNull SseCommentRouteEnqueueOutcomeKey, @NonNull Long> getSseCommentEnqueueOutcomes() {
1224                        return this.sseCommentEnqueueOutcomes;
1225                }
1226
1227                /**
1228                 * Returns SSE event drop counters keyed by route and drop reason.
1229                 *
1230                 * @return SSE event drop counters
1231                 */
1232                @NonNull
1233                public Map<@NonNull SseEventRouteDropKey, @NonNull Long> getSseEventDrops() {
1234                        return this.sseEventDrops;
1235                }
1236
1237                /**
1238                 * Returns SSE comment drop counters keyed by route, comment type, and drop reason.
1239                 *
1240                 * @return SSE comment drop counters
1241                 */
1242                @NonNull
1243                public Map<@NonNull SseCommentRouteDropKey, @NonNull Long> getSseCommentDrops() {
1244                        return this.sseCommentDrops;
1245                }
1246
1247                /**
1248                 * Returns SSE time-to-first-event histograms keyed by route.
1249                 *
1250                 * @return SSE time-to-first-event histograms
1251                 */
1252                @NonNull
1253                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseTimeToFirstEvent() {
1254                        return this.sseTimeToFirstEvent;
1255                }
1256
1257                /**
1258                 * Returns SSE event write duration histograms keyed by route.
1259                 *
1260                 * @return SSE event write duration histograms
1261                 */
1262                @NonNull
1263                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseEventWriteDurations() {
1264                        return this.sseEventWriteDurations;
1265                }
1266
1267                /**
1268                 * Returns SSE event delivery lag histograms keyed by route.
1269                 *
1270                 * @return SSE event delivery lag histograms
1271                 */
1272                @NonNull
1273                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseEventDeliveryLag() {
1274                        return this.sseEventDeliveryLag;
1275                }
1276
1277                /**
1278                 * Returns SSE event size histograms keyed by route.
1279                 *
1280                 * @return SSE event size histograms
1281                 */
1282                @NonNull
1283                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseEventSizes() {
1284                        return this.sseEventSizes;
1285                }
1286
1287                /**
1288                 * Returns SSE queue depth histograms keyed by route.
1289                 *
1290                 * @return SSE queue depth histograms
1291                 */
1292                @NonNull
1293                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseQueueDepth() {
1294                        return this.sseQueueDepth;
1295                }
1296
1297                /**
1298                 * Returns SSE comment delivery lag histograms keyed by route and comment type.
1299                 *
1300                 * @return SSE comment delivery lag histograms
1301                 */
1302                @NonNull
1303                public Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentDeliveryLag() {
1304                        return this.sseCommentDeliveryLag;
1305                }
1306
1307                /**
1308                 * Returns SSE comment size histograms keyed by route and comment type.
1309                 *
1310                 * @return SSE comment size histograms
1311                 */
1312                @NonNull
1313                public Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentSizes() {
1314                        return this.sseCommentSizes;
1315                }
1316
1317                /**
1318                 * Returns SSE comment queue depth histograms keyed by route and comment type.
1319                 *
1320                 * @return SSE comment queue depth histograms
1321                 */
1322                @NonNull
1323                public Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentQueueDepth() {
1324                        return this.sseCommentQueueDepth;
1325                }
1326
1327                /**
1328                 * Returns SSE connection duration histograms keyed by route and termination reason.
1329                 *
1330                 * @return SSE connection duration histograms
1331                 */
1332                @NonNull
1333                public Map<@NonNull SseEventRouteTerminationKey, @NonNull HistogramSnapshot> getSseConnectionDurations() {
1334                        return this.sseConnectionDurations;
1335                }
1336
1337                /**
1338                 * Returns MCP request outcome counters keyed by endpoint, JSON-RPC method, and outcome.
1339                 *
1340                 * @return MCP request outcome counters
1341                 */
1342                @NonNull
1343                public Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull Long> getMcpRequests() {
1344                        return this.mcpRequests;
1345                }
1346
1347                /**
1348                 * Returns MCP request duration histograms keyed by endpoint, JSON-RPC method, and outcome.
1349                 *
1350                 * @return MCP request duration histograms
1351                 */
1352                @NonNull
1353                public Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull HistogramSnapshot> getMcpRequestDurations() {
1354                        return this.mcpRequestDurations;
1355                }
1356
1357                /**
1358                 * Returns MCP session duration histograms keyed by endpoint and termination reason.
1359                 *
1360                 * @return MCP session duration histograms
1361                 */
1362                @NonNull
1363                public Map<@NonNull McpEndpointSessionTerminationKey, @NonNull HistogramSnapshot> getMcpSessionDurations() {
1364                        return this.mcpSessionDurations;
1365                }
1366
1367                /**
1368                 * Returns MCP stream duration histograms keyed by endpoint and termination reason.
1369                 *
1370                 * @return MCP stream duration histograms
1371                 */
1372                @NonNull
1373                public Map<@NonNull McpEndpointStreamTerminationKey, @NonNull HistogramSnapshot> getMcpStreamDurations() {
1374                        return this.mcpStreamDurations;
1375                }
1376
1377                @NonNull
1378                private static <K, V> Map<K, V> copyOrEmpty(@Nullable Map<K, V> map) {
1379                        return map == null ? Map.of() : Map.copyOf(map);
1380                }
1381
1382                /**
1383                 * Builder used to construct instances of {@link Snapshot}.
1384                 * <p>
1385                 * This class is intended for use by a single thread.
1386                 *
1387                 * @author <a href="https://www.revetkn.com">Mark Allen</a>
1388                 */
1389                @NotThreadSafe
1390                public static final class Builder {
1391                        @NonNull
1392                        private Long activeRequests;
1393                        @NonNull
1394                        private Long activeSseConnections;
1395                        @NonNull
1396                        private Long activeMcpSessions;
1397                        @NonNull
1398                        private Long activeMcpStreams;
1399                        @NonNull
1400                        private Long httpConnectionsAccepted;
1401                        @NonNull
1402                        private Long httpConnectionsRejected;
1403                        @NonNull
1404                        private Long sseConnectionsAccepted;
1405                        @NonNull
1406                        private Long sseConnectionsRejected;
1407                        @NonNull
1408                        private Long mcpConnectionsAccepted;
1409                        @NonNull
1410                        private Long mcpConnectionsRejected;
1411                        @Nullable
1412                        private Map<@NonNull RequestReadFailureKey, @NonNull Long> httpRequestReadFailures;
1413                        @Nullable
1414                        private Map<@NonNull RequestRejectionKey, @NonNull Long> httpRequestRejections;
1415                        @Nullable
1416                        private Map<@NonNull RequestReadFailureKey, @NonNull Long> sseRequestReadFailures;
1417                        @Nullable
1418                        private Map<@NonNull RequestRejectionKey, @NonNull Long> sseRequestRejections;
1419                        @Nullable
1420                        private Map<@NonNull RequestReadFailureKey, @NonNull Long> mcpRequestReadFailures;
1421                        @Nullable
1422                        private Map<@NonNull RequestRejectionKey, @NonNull Long> mcpRequestRejections;
1423                        @Nullable
1424                        private Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpRequestDurations;
1425                        @Nullable
1426                        private Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpHandlerDurations;
1427                        @Nullable
1428                        private Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpTimeToFirstByte;
1429                        @Nullable
1430                        private Map<@NonNull HttpServerRouteKey, @NonNull HistogramSnapshot> httpRequestBodyBytes;
1431                        @Nullable
1432                        private Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpResponseBodyBytes;
1433                        @Nullable
1434                        private Map<@NonNull SseEventRouteKey, @NonNull Long> sseHandshakesAccepted;
1435                        @Nullable
1436                        private Map<@NonNull SseEventRouteHandshakeFailureKey, @NonNull Long> sseHandshakesRejected;
1437                        @Nullable
1438                        private Map<@NonNull SseEventRouteEnqueueOutcomeKey, @NonNull Long> sseEventEnqueueOutcomes;
1439                        @Nullable
1440                        private Map<@NonNull SseCommentRouteEnqueueOutcomeKey, @NonNull Long> sseCommentEnqueueOutcomes;
1441                        @Nullable
1442                        private Map<@NonNull SseEventRouteDropKey, @NonNull Long> sseEventDrops;
1443                        @Nullable
1444                        private Map<@NonNull SseCommentRouteDropKey, @NonNull Long> sseCommentDrops;
1445                        @Nullable
1446                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseTimeToFirstEvent;
1447                        @Nullable
1448                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventWriteDurations;
1449                        @Nullable
1450                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventDeliveryLag;
1451                        @Nullable
1452                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventSizes;
1453                        @Nullable
1454                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseQueueDepth;
1455                        @Nullable
1456                        private Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentDeliveryLag;
1457                        @Nullable
1458                        private Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentSizes;
1459                        @Nullable
1460                        private Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentQueueDepth;
1461                        @Nullable
1462                        private Map<@NonNull SseEventRouteTerminationKey, @NonNull HistogramSnapshot> sseConnectionDurations;
1463                        @Nullable
1464                        private Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull Long> mcpRequests;
1465                        @Nullable
1466                        private Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull HistogramSnapshot> mcpRequestDurations;
1467                        @Nullable
1468                        private Map<@NonNull McpEndpointSessionTerminationKey, @NonNull HistogramSnapshot> mcpSessionDurations;
1469                        @Nullable
1470                        private Map<@NonNull McpEndpointStreamTerminationKey, @NonNull HistogramSnapshot> mcpStreamDurations;
1471
1472                        private Builder() {
1473                                this.activeRequests = 0L;
1474                                this.activeSseConnections = 0L;
1475                                this.activeMcpSessions = 0L;
1476                                this.activeMcpStreams = 0L;
1477                                this.httpConnectionsAccepted = 0L;
1478                                this.httpConnectionsRejected = 0L;
1479                                this.sseConnectionsAccepted = 0L;
1480                                this.sseConnectionsRejected = 0L;
1481                                this.mcpConnectionsAccepted = 0L;
1482                                this.mcpConnectionsRejected = 0L;
1483                        }
1484
1485                        /**
1486                         * Sets the active HTTP request count.
1487                         *
1488                         * @param activeRequests the active HTTP request count
1489                         * @return this builder
1490                         */
1491                        @NonNull
1492                        public Builder activeRequests(@NonNull Long activeRequests) {
1493                                this.activeRequests = requireNonNull(activeRequests);
1494                                return this;
1495                        }
1496
1497                        /**
1498                         * Sets the active server-sent event connection count.
1499                         *
1500                         * @param activeSseConnections the active SSE connection count
1501                         * @return this builder
1502                         */
1503                        @NonNull
1504                        public Builder activeSseConnections(@NonNull Long activeSseConnections) {
1505                                this.activeSseConnections = requireNonNull(activeSseConnections);
1506                                return this;
1507                        }
1508
1509                        /**
1510                         * Sets the active MCP session count.
1511                         *
1512                         * @param activeMcpSessions the active MCP session count
1513                         * @return this builder
1514                         */
1515                        @NonNull
1516                        public Builder activeMcpSessions(@NonNull Long activeMcpSessions) {
1517                                this.activeMcpSessions = requireNonNull(activeMcpSessions);
1518                                return this;
1519                        }
1520
1521                        /**
1522                         * Sets the active MCP stream count.
1523                         *
1524                         * @param activeMcpStreams the active MCP stream count
1525                         * @return this builder
1526                         */
1527                        @NonNull
1528                        public Builder activeMcpStreams(@NonNull Long activeMcpStreams) {
1529                                this.activeMcpStreams = requireNonNull(activeMcpStreams);
1530                                return this;
1531                        }
1532
1533                        /**
1534                         * Sets the total number of accepted HTTP connections.
1535                         *
1536                         * @param httpConnectionsAccepted total accepted HTTP connections
1537                         * @return this builder
1538                         */
1539                        @NonNull
1540                        public Builder httpConnectionsAccepted(@NonNull Long httpConnectionsAccepted) {
1541                                this.httpConnectionsAccepted = requireNonNull(httpConnectionsAccepted);
1542                                return this;
1543                        }
1544
1545                        /**
1546                         * Sets the total number of rejected HTTP connections.
1547                         *
1548                         * @param httpConnectionsRejected total rejected HTTP connections
1549                         * @return this builder
1550                         */
1551                        @NonNull
1552                        public Builder httpConnectionsRejected(@NonNull Long httpConnectionsRejected) {
1553                                this.httpConnectionsRejected = requireNonNull(httpConnectionsRejected);
1554                                return this;
1555                        }
1556
1557                        /**
1558                         * Sets the total number of accepted SSE connections.
1559                         *
1560                         * @param sseConnectionsAccepted total accepted SSE connections
1561                         * @return this builder
1562                         */
1563                        @NonNull
1564                        public Builder sseConnectionsAccepted(@NonNull Long sseConnectionsAccepted) {
1565                                this.sseConnectionsAccepted = requireNonNull(sseConnectionsAccepted);
1566                                return this;
1567                        }
1568
1569                        /**
1570                         * Sets the total number of rejected SSE connections.
1571                         *
1572                         * @param sseConnectionsRejected total rejected SSE connections
1573                         * @return this builder
1574                         */
1575                        @NonNull
1576                        public Builder sseConnectionsRejected(@NonNull Long sseConnectionsRejected) {
1577                                this.sseConnectionsRejected = requireNonNull(sseConnectionsRejected);
1578                                return this;
1579                        }
1580
1581                        /**
1582                         * Sets the total number of accepted MCP connections.
1583                         *
1584                         * @param mcpConnectionsAccepted total accepted MCP connections
1585                         * @return this builder
1586                         */
1587                        @NonNull
1588                        public Builder mcpConnectionsAccepted(@NonNull Long mcpConnectionsAccepted) {
1589                                this.mcpConnectionsAccepted = requireNonNull(mcpConnectionsAccepted);
1590                                return this;
1591                        }
1592
1593                        /**
1594                         * Sets the total number of rejected MCP connections.
1595                         *
1596                         * @param mcpConnectionsRejected total rejected MCP connections
1597                         * @return this builder
1598                         */
1599                        @NonNull
1600                        public Builder mcpConnectionsRejected(@NonNull Long mcpConnectionsRejected) {
1601                                this.mcpConnectionsRejected = requireNonNull(mcpConnectionsRejected);
1602                                return this;
1603                        }
1604
1605                        /**
1606                         * Sets HTTP request read failure counters keyed by failure reason.
1607                         *
1608                         * @param httpRequestReadFailures the HTTP request read failure counters
1609                         * @return this builder
1610                         */
1611                        @NonNull
1612                        public Builder httpRequestReadFailures(
1613                                        @Nullable Map<@NonNull RequestReadFailureKey, @NonNull Long> httpRequestReadFailures) {
1614                                this.httpRequestReadFailures = httpRequestReadFailures;
1615                                return this;
1616                        }
1617
1618                        /**
1619                         * Sets HTTP request rejection counters keyed by rejection reason.
1620                         *
1621                         * @param httpRequestRejections the HTTP request rejection counters
1622                         * @return this builder
1623                         */
1624                        @NonNull
1625                        public Builder httpRequestRejections(
1626                                        @Nullable Map<@NonNull RequestRejectionKey, @NonNull Long> httpRequestRejections) {
1627                                this.httpRequestRejections = httpRequestRejections;
1628                                return this;
1629                        }
1630
1631                        /**
1632                         * Sets SSE request read failure counters keyed by failure reason.
1633                         *
1634                         * @param sseRequestReadFailures the SSE request read failure counters
1635                         * @return this builder
1636                         */
1637                        @NonNull
1638                        public Builder sseRequestReadFailures(
1639                                        @Nullable Map<@NonNull RequestReadFailureKey, @NonNull Long> sseRequestReadFailures) {
1640                                this.sseRequestReadFailures = sseRequestReadFailures;
1641                                return this;
1642                        }
1643
1644                        /**
1645                         * Sets SSE request rejection counters keyed by rejection reason.
1646                         *
1647                         * @param sseRequestRejections the SSE request rejection counters
1648                         * @return this builder
1649                         */
1650                        @NonNull
1651                        public Builder sseRequestRejections(
1652                                        @Nullable Map<@NonNull RequestRejectionKey, @NonNull Long> sseRequestRejections) {
1653                                this.sseRequestRejections = sseRequestRejections;
1654                                return this;
1655                        }
1656
1657                        /**
1658                         * Sets MCP request read failure counters keyed by failure reason.
1659                         *
1660                         * @param mcpRequestReadFailures the MCP request read failure counters
1661                         * @return this builder
1662                         */
1663                        @NonNull
1664                        public Builder mcpRequestReadFailures(
1665                                        @Nullable Map<@NonNull RequestReadFailureKey, @NonNull Long> mcpRequestReadFailures) {
1666                                this.mcpRequestReadFailures = mcpRequestReadFailures;
1667                                return this;
1668                        }
1669
1670                        /**
1671                         * Sets MCP request rejection counters keyed by rejection reason.
1672                         *
1673                         * @param mcpRequestRejections the MCP request rejection counters
1674                         * @return this builder
1675                         */
1676                        @NonNull
1677                        public Builder mcpRequestRejections(
1678                                        @Nullable Map<@NonNull RequestRejectionKey, @NonNull Long> mcpRequestRejections) {
1679                                this.mcpRequestRejections = mcpRequestRejections;
1680                                return this;
1681                        }
1682
1683                        /**
1684                         * Sets HTTP request duration histograms keyed by server route and status class.
1685                         *
1686                         * @param httpRequestDurations the HTTP request duration histograms
1687                         * @return this builder
1688                         */
1689                        @NonNull
1690                        public Builder httpRequestDurations(
1691                                        @Nullable Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpRequestDurations) {
1692                                this.httpRequestDurations = httpRequestDurations;
1693                                return this;
1694                        }
1695
1696                        /**
1697                         * Sets HTTP handler duration histograms keyed by server route and status class.
1698                         *
1699                         * @param httpHandlerDurations the HTTP handler duration histograms
1700                         * @return this builder
1701                         */
1702                        @NonNull
1703                        public Builder httpHandlerDurations(
1704                                        @Nullable Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpHandlerDurations) {
1705                                this.httpHandlerDurations = httpHandlerDurations;
1706                                return this;
1707                        }
1708
1709                        /**
1710                         * Sets HTTP time-to-first-byte histograms keyed by server route and status class.
1711                         *
1712                         * @param httpTimeToFirstByte the HTTP time-to-first-byte histograms
1713                         * @return this builder
1714                         */
1715                        @NonNull
1716                        public Builder httpTimeToFirstByte(
1717                                        @Nullable Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpTimeToFirstByte) {
1718                                this.httpTimeToFirstByte = httpTimeToFirstByte;
1719                                return this;
1720                        }
1721
1722                        /**
1723                         * Sets HTTP request body size histograms keyed by server route.
1724                         *
1725                         * @param httpRequestBodyBytes the HTTP request body size histograms
1726                         * @return this builder
1727                         */
1728                        @NonNull
1729                        public Builder httpRequestBodyBytes(
1730                                        @Nullable Map<@NonNull HttpServerRouteKey, @NonNull HistogramSnapshot> httpRequestBodyBytes) {
1731                                this.httpRequestBodyBytes = httpRequestBodyBytes;
1732                                return this;
1733                        }
1734
1735                        /**
1736                         * Sets HTTP response body size histograms keyed by server route and status class.
1737                         *
1738                         * @param httpResponseBodyBytes the HTTP response body size histograms
1739                         * @return this builder
1740                         */
1741                        @NonNull
1742                        public Builder httpResponseBodyBytes(
1743                                        @Nullable Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpResponseBodyBytes) {
1744                                this.httpResponseBodyBytes = httpResponseBodyBytes;
1745                                return this;
1746                        }
1747
1748                        /**
1749                         * Sets SSE handshake acceptance counters keyed by route.
1750                         *
1751                         * @param sseHandshakesAccepted SSE handshake acceptance counters
1752                         * @return this builder
1753                         */
1754                        @NonNull
1755                        public Builder sseHandshakesAccepted(
1756                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull Long> sseHandshakesAccepted) {
1757                                this.sseHandshakesAccepted = sseHandshakesAccepted;
1758                                return this;
1759                        }
1760
1761                        /**
1762                         * Sets SSE handshake rejection counters keyed by route and failure reason.
1763                         *
1764                         * @param sseHandshakesRejected SSE handshake rejection counters
1765                         * @return this builder
1766                         */
1767                        @NonNull
1768                        public Builder sseHandshakesRejected(
1769                                        @Nullable Map<@NonNull SseEventRouteHandshakeFailureKey, @NonNull Long> sseHandshakesRejected) {
1770                                this.sseHandshakesRejected = sseHandshakesRejected;
1771                                return this;
1772                        }
1773
1774                        /**
1775                         * Sets SSE event enqueue outcome counters keyed by route and outcome.
1776                         *
1777                         * @param sseEventEnqueueOutcomes the SSE event enqueue outcome counters
1778                         * @return this builder
1779                         */
1780                        @NonNull
1781                        public Builder sseEventEnqueueOutcomes(
1782                                        @Nullable Map<@NonNull SseEventRouteEnqueueOutcomeKey, @NonNull Long> sseEventEnqueueOutcomes) {
1783                                this.sseEventEnqueueOutcomes = sseEventEnqueueOutcomes;
1784                                return this;
1785                        }
1786
1787                        /**
1788                         * Sets SSE comment enqueue outcome counters keyed by route, comment type, and outcome.
1789                         *
1790                         * @param sseCommentEnqueueOutcomes the SSE comment enqueue outcome counters
1791                         * @return this builder
1792                         */
1793                        @NonNull
1794                        public Builder sseCommentEnqueueOutcomes(
1795                                        @Nullable Map<@NonNull SseCommentRouteEnqueueOutcomeKey, @NonNull Long> sseCommentEnqueueOutcomes) {
1796                                this.sseCommentEnqueueOutcomes = sseCommentEnqueueOutcomes;
1797                                return this;
1798                        }
1799
1800                        /**
1801                         * Sets SSE event drop counters keyed by route and drop reason.
1802                         *
1803                         * @param sseEventDrops the SSE event drop counters
1804                         * @return this builder
1805                         */
1806                        @NonNull
1807                        public Builder sseEventDrops(
1808                                        @Nullable Map<@NonNull SseEventRouteDropKey, @NonNull Long> sseEventDrops) {
1809                                this.sseEventDrops = sseEventDrops;
1810                                return this;
1811                        }
1812
1813                        /**
1814                         * Sets SSE comment drop counters keyed by route, comment type, and drop reason.
1815                         *
1816                         * @param sseCommentDrops the SSE comment drop counters
1817                         * @return this builder
1818                         */
1819                        @NonNull
1820                        public Builder sseCommentDrops(
1821                                        @Nullable Map<@NonNull SseCommentRouteDropKey, @NonNull Long> sseCommentDrops) {
1822                                this.sseCommentDrops = sseCommentDrops;
1823                                return this;
1824                        }
1825
1826                        /**
1827                         * Sets SSE time-to-first-event histograms keyed by route.
1828                         *
1829                         * @param sseTimeToFirstEvent the SSE time-to-first-event histograms
1830                         * @return this builder
1831                         */
1832                        @NonNull
1833                        public Builder sseTimeToFirstEvent(
1834                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseTimeToFirstEvent) {
1835                                this.sseTimeToFirstEvent = sseTimeToFirstEvent;
1836                                return this;
1837                        }
1838
1839                        /**
1840                         * Sets SSE event write duration histograms keyed by route.
1841                         *
1842                         * @param sseEventWriteDurations the SSE event write duration histograms
1843                         * @return this builder
1844                         */
1845                        @NonNull
1846                        public Builder sseEventWriteDurations(
1847                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventWriteDurations) {
1848                                this.sseEventWriteDurations = sseEventWriteDurations;
1849                                return this;
1850                        }
1851
1852                        /**
1853                         * Sets SSE event delivery lag histograms keyed by route.
1854                         *
1855                         * @param sseEventDeliveryLag the SSE event delivery lag histograms
1856                         * @return this builder
1857                         */
1858                        @NonNull
1859                        public Builder sseEventDeliveryLag(
1860                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventDeliveryLag) {
1861                                this.sseEventDeliveryLag = sseEventDeliveryLag;
1862                                return this;
1863                        }
1864
1865                        /**
1866                         * Sets SSE event size histograms keyed by route.
1867                         *
1868                         * @param sseEventSizes the SSE event size histograms
1869                         * @return this builder
1870                         */
1871                        @NonNull
1872                        public Builder sseEventSizes(
1873                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventSizes) {
1874                                this.sseEventSizes = sseEventSizes;
1875                                return this;
1876                        }
1877
1878                        /**
1879                         * Sets SSE queue depth histograms keyed by route.
1880                         *
1881                         * @param sseQueueDepth the SSE queue depth histograms
1882                         * @return this builder
1883                         */
1884                        @NonNull
1885                        public Builder sseQueueDepth(
1886                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseQueueDepth) {
1887                                this.sseQueueDepth = sseQueueDepth;
1888                                return this;
1889                        }
1890
1891                        /**
1892                         * Sets SSE comment delivery lag histograms keyed by route and comment type.
1893                         *
1894                         * @param sseCommentDeliveryLag the SSE comment delivery lag histograms
1895                         * @return this builder
1896                         */
1897                        @NonNull
1898                        public Builder sseCommentDeliveryLag(
1899                                        @Nullable Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentDeliveryLag) {
1900                                this.sseCommentDeliveryLag = sseCommentDeliveryLag;
1901                                return this;
1902                        }
1903
1904                        /**
1905                         * Sets SSE comment size histograms keyed by route and comment type.
1906                         *
1907                         * @param sseCommentSizes the SSE comment size histograms
1908                         * @return this builder
1909                         */
1910                        @NonNull
1911                        public Builder sseCommentSizes(
1912                                        @Nullable Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentSizes) {
1913                                this.sseCommentSizes = sseCommentSizes;
1914                                return this;
1915                        }
1916
1917                        /**
1918                         * Sets SSE comment queue depth histograms keyed by route and comment type.
1919                         *
1920                         * @param sseCommentQueueDepth the SSE comment queue depth histograms
1921                         * @return this builder
1922                         */
1923                        @NonNull
1924                        public Builder sseCommentQueueDepth(
1925                                        @Nullable Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentQueueDepth) {
1926                                this.sseCommentQueueDepth = sseCommentQueueDepth;
1927                                return this;
1928                        }
1929
1930                        /**
1931                         * Sets SSE connection duration histograms keyed by route and termination reason.
1932                         *
1933                         * @param sseConnectionDurations the SSE connection duration histograms
1934                         * @return this builder
1935                         */
1936                        @NonNull
1937                        public Builder sseConnectionDurations(
1938                                        @Nullable Map<@NonNull SseEventRouteTerminationKey, @NonNull HistogramSnapshot> sseConnectionDurations) {
1939                                this.sseConnectionDurations = sseConnectionDurations;
1940                                return this;
1941                        }
1942
1943                        /**
1944                         * Sets MCP request outcome counters keyed by endpoint, JSON-RPC method, and outcome.
1945                         *
1946                         * @param mcpRequests the MCP request outcome counters
1947                         * @return this builder
1948                         */
1949                        @NonNull
1950                        public Builder mcpRequests(
1951                                        @Nullable Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull Long> mcpRequests) {
1952                                this.mcpRequests = mcpRequests;
1953                                return this;
1954                        }
1955
1956                        /**
1957                         * Sets MCP request duration histograms keyed by endpoint, JSON-RPC method, and outcome.
1958                         *
1959                         * @param mcpRequestDurations the MCP request duration histograms
1960                         * @return this builder
1961                         */
1962                        @NonNull
1963                        public Builder mcpRequestDurations(
1964                                        @Nullable Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull HistogramSnapshot> mcpRequestDurations) {
1965                                this.mcpRequestDurations = mcpRequestDurations;
1966                                return this;
1967                        }
1968
1969                        /**
1970                         * Sets MCP session duration histograms keyed by endpoint and termination reason.
1971                         *
1972                         * @param mcpSessionDurations the MCP session duration histograms
1973                         * @return this builder
1974                         */
1975                        @NonNull
1976                        public Builder mcpSessionDurations(
1977                                        @Nullable Map<@NonNull McpEndpointSessionTerminationKey, @NonNull HistogramSnapshot> mcpSessionDurations) {
1978                                this.mcpSessionDurations = mcpSessionDurations;
1979                                return this;
1980                        }
1981
1982                        /**
1983                         * Sets MCP stream duration histograms keyed by endpoint and termination reason.
1984                         *
1985                         * @param mcpStreamDurations the MCP stream duration histograms
1986                         * @return this builder
1987                         */
1988                        @NonNull
1989                        public Builder mcpStreamDurations(
1990                                        @Nullable Map<@NonNull McpEndpointStreamTerminationKey, @NonNull HistogramSnapshot> mcpStreamDurations) {
1991                                this.mcpStreamDurations = mcpStreamDurations;
1992                                return this;
1993                        }
1994
1995                        /**
1996                         * Builds a {@link Snapshot} instance.
1997                         *
1998                         * @return the built snapshot
1999                         */
2000                        @NonNull
2001                        public Snapshot build() {
2002                                return new Snapshot(this);
2003                        }
2004                }
2005        }
2006
2007        /**
2008         * A thread-safe histogram with fixed bucket boundaries.
2009         * <p>
2010         * Negative values are ignored. Buckets use inclusive upper bounds, and snapshots include
2011         * an overflow bucket represented by a {@link HistogramSnapshot#getBucketBoundary(int)} of
2012         * {@link Long#MAX_VALUE}.
2013         *
2014         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2015         */
2016        @ThreadSafe
2017        final class Histogram {
2018                @NonNull
2019                private final long[] bucketBoundaries;
2020                @NonNull
2021                private final LongAdder[] bucketCounts;
2022                @NonNull
2023                private final LongAdder count;
2024                @NonNull
2025                private final LongAdder sum;
2026                @NonNull
2027                private final AtomicLong min;
2028                @NonNull
2029                private final AtomicLong max;
2030
2031                /**
2032                 * Creates a histogram with the provided bucket boundaries.
2033                 *
2034                 * @param bucketBoundaries inclusive upper bounds for buckets
2035                 */
2036                public Histogram(@NonNull long[] bucketBoundaries) {
2037                        requireNonNull(bucketBoundaries);
2038
2039                        this.bucketBoundaries = bucketBoundaries.clone();
2040                        Arrays.sort(this.bucketBoundaries);
2041                        this.bucketCounts = new LongAdder[this.bucketBoundaries.length + 1];
2042                        for (int i = 0; i < this.bucketCounts.length; i++)
2043                                this.bucketCounts[i] = new LongAdder();
2044                        this.count = new LongAdder();
2045                        this.sum = new LongAdder();
2046                        this.min = new AtomicLong(Long.MAX_VALUE);
2047                        this.max = new AtomicLong(Long.MIN_VALUE);
2048                }
2049
2050                /**
2051                 * Records a value into the histogram.
2052                 *
2053                 * @param value the value to record
2054                 */
2055                public void record(long value) {
2056                        if (value < 0)
2057                                return;
2058
2059                        this.count.increment();
2060                        this.sum.add(value);
2061                        updateMin(value);
2062                        updateMax(value);
2063
2064                        int bucketIndex = bucketIndex(value);
2065                        this.bucketCounts[bucketIndex].increment();
2066                }
2067
2068                /**
2069                 * Captures an immutable snapshot of the histogram.
2070                 *
2071                 * @return the histogram snapshot
2072                 */
2073                @NonNull
2074                public HistogramSnapshot snapshot() {
2075                        long[] boundariesWithOverflow = Arrays.copyOf(this.bucketBoundaries, this.bucketBoundaries.length + 1);
2076                        boundariesWithOverflow[boundariesWithOverflow.length - 1] = Long.MAX_VALUE;
2077
2078                        long[] cumulativeCounts = new long[this.bucketCounts.length];
2079                        long cumulative = 0;
2080                        for (int i = 0; i < this.bucketCounts.length; i++) {
2081                                cumulative += this.bucketCounts[i].sum();
2082                                cumulativeCounts[i] = cumulative;
2083                        }
2084
2085                        long countSnapshot = this.count.sum();
2086                        long sumSnapshot = this.sum.sum();
2087                        long minSnapshot = this.min.get();
2088                        long maxSnapshot = this.max.get();
2089
2090                        if (minSnapshot == Long.MAX_VALUE)
2091                                minSnapshot = 0;
2092                        if (maxSnapshot == Long.MIN_VALUE)
2093                                maxSnapshot = 0;
2094
2095                        return new HistogramSnapshot(boundariesWithOverflow, cumulativeCounts, countSnapshot, sumSnapshot, minSnapshot, maxSnapshot);
2096                }
2097
2098                /**
2099                 * Resets all counts and min/max values.
2100                 */
2101                public void reset() {
2102                        this.count.reset();
2103                        this.sum.reset();
2104                        this.min.set(Long.MAX_VALUE);
2105                        this.max.set(Long.MIN_VALUE);
2106                        for (LongAdder bucket : this.bucketCounts)
2107                                bucket.reset();
2108                }
2109
2110                private int bucketIndex(long value) {
2111                        for (int i = 0; i < this.bucketBoundaries.length; i++)
2112                                if (value <= this.bucketBoundaries[i])
2113                                        return i;
2114
2115                        return this.bucketBoundaries.length;
2116                }
2117
2118                private void updateMin(long value) {
2119                        long current;
2120                        while (value < (current = this.min.get())) {
2121                                if (this.min.compareAndSet(current, value))
2122                                        break;
2123                        }
2124                }
2125
2126                private void updateMax(long value) {
2127                        long current;
2128                        while (value > (current = this.max.get())) {
2129                                if (this.max.compareAndSet(current, value))
2130                                        break;
2131                        }
2132                }
2133        }
2134
2135        /**
2136         * Immutable snapshot of a {@link Histogram}.
2137         * <p>
2138         * Bucket counts are cumulative. Boundaries are inclusive upper bounds, and the final
2139         * boundary is {@link Long#MAX_VALUE} to represent the overflow bucket. Units are the same
2140         * as values passed to {@link Histogram#record(long)}.
2141         *
2142         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2143         */
2144        @ThreadSafe
2145        final class HistogramSnapshot {
2146                @NonNull
2147                private final long[] bucketBoundaries;
2148                @NonNull
2149                private final long[] bucketCumulativeCounts;
2150                private final long count;
2151                private final long sum;
2152                private final long min;
2153                private final long max;
2154
2155                /**
2156                 * Creates an immutable histogram snapshot.
2157                 *
2158                 * @param bucketBoundaries       inclusive upper bounds for buckets, including overflow
2159                 * @param bucketCumulativeCounts cumulative counts for each bucket
2160                 * @param count                  total number of samples recorded
2161                 * @param sum                    sum of all recorded values
2162                 * @param min                    smallest recorded value (or 0 if none)
2163                 * @param max                    largest recorded value (or 0 if none)
2164                 */
2165                public HistogramSnapshot(@NonNull long[] bucketBoundaries,
2166                                                                                                                 @NonNull long[] bucketCumulativeCounts,
2167                                                                                                                 long count,
2168                                                                                                                 long sum,
2169                                                                                                                 long min,
2170                                                                                                                 long max) {
2171                        requireNonNull(bucketBoundaries);
2172                        requireNonNull(bucketCumulativeCounts);
2173
2174                        if (bucketBoundaries.length != bucketCumulativeCounts.length)
2175                                throw new IllegalArgumentException("Bucket boundaries and cumulative counts must be the same length");
2176
2177                        this.bucketBoundaries = bucketBoundaries.clone();
2178                        this.bucketCumulativeCounts = bucketCumulativeCounts.clone();
2179                        this.count = count;
2180                        this.sum = sum;
2181                        this.min = min;
2182                        this.max = max;
2183                }
2184
2185                /**
2186                 * Number of histogram buckets, including the overflow bucket.
2187                 *
2188                 * @return the bucket count
2189                 */
2190                public int getBucketCount() {
2191                        return this.bucketBoundaries.length;
2192                }
2193
2194                /**
2195                 * The inclusive upper bound for the bucket at the given index.
2196                 *
2197                 * @param index the bucket index
2198                 * @return the bucket boundary
2199                 */
2200                public long getBucketBoundary(int index) {
2201                        return this.bucketBoundaries[index];
2202                }
2203
2204                /**
2205                 * The cumulative count for the bucket at the given index.
2206                 *
2207                 * @param index the bucket index
2208                 * @return the cumulative count
2209                 */
2210                public long getBucketCumulativeCount(int index) {
2211                        return this.bucketCumulativeCounts[index];
2212                }
2213
2214                /**
2215                 * Total number of recorded values.
2216                 *
2217                 * @return the count
2218                 */
2219                public long getCount() {
2220                        return this.count;
2221                }
2222
2223                /**
2224                 * Sum of all recorded values.
2225                 *
2226                 * @return the sum
2227                 */
2228                public long getSum() {
2229                        return this.sum;
2230                }
2231
2232                /**
2233                 * Smallest recorded value, or 0 if no values were recorded.
2234                 *
2235                 * @return the minimum value
2236                 */
2237                public long getMin() {
2238                        return this.min;
2239                }
2240
2241                /**
2242                 * Largest recorded value, or 0 if no values were recorded.
2243                 *
2244                 * @return the maximum value
2245                 */
2246                public long getMax() {
2247                        return this.max;
2248                }
2249
2250                /**
2251                 * Returns an approximate percentile based on bucket boundaries.
2252                 *
2253                 * @param percentile percentile between 0 and 100
2254                 * @return the approximated percentile value
2255                 */
2256                public long getPercentile(double percentile) {
2257                        if (percentile <= 0.0)
2258                                return this.min;
2259                        if (percentile >= 100.0)
2260                                return this.max;
2261                        if (this.count == 0)
2262                                return 0;
2263
2264                        long threshold = (long) Math.ceil((percentile / 100.0) * this.count);
2265
2266                        for (int i = 0; i < this.bucketCumulativeCounts.length; i++)
2267                                if (this.bucketCumulativeCounts[i] >= threshold)
2268                                        return this.bucketBoundaries[i];
2269
2270                        return this.bucketBoundaries[this.bucketBoundaries.length - 1];
2271                }
2272
2273                @Override
2274                public String toString() {
2275                        return String.format("%s{count=%d, min=%d, max=%d, sum=%d, bucketBoundaries=%s}",
2276                                        getClass().getSimpleName(), this.count, this.min, this.max, this.sum, Arrays.toString(this.bucketBoundaries));
2277                }
2278        }
2279
2280        /**
2281         * Indicates whether a request was matched to a {@link ResourcePathDeclaration}.
2282         *
2283         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2284         */
2285        enum RouteType {
2286                /**
2287                 * The request matched a {@link ResourcePathDeclaration}.
2288                 */
2289                MATCHED,
2290                /**
2291                 * The request did not match any {@link ResourcePathDeclaration}.
2292                 */
2293                UNMATCHED
2294        }
2295
2296        /**
2297         * Outcomes for a Server-Sent Event enqueue attempt.
2298         *
2299         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2300         */
2301        enum SseEventEnqueueOutcome {
2302                /**
2303                 * An enqueue attempt to a connection was made.
2304                 */
2305                ATTEMPTED,
2306                /**
2307                 * An enqueue attempt succeeded.
2308                 */
2309                ENQUEUED,
2310                /**
2311                 * An enqueue attempt failed because the payload was dropped.
2312                 */
2313                DROPPED
2314        }
2315
2316        /**
2317         * Reasons a Server-Sent Event payload or comment was dropped before it could be written.
2318         *
2319         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2320         */
2321        enum SseEventDropReason {
2322                /**
2323                 * The per-connection write queue was full.
2324                 */
2325                QUEUE_FULL
2326        }
2327
2328        /**
2329         * Key for request read failures grouped by reason.
2330         *
2331         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2332         */
2333        record RequestReadFailureKey(@NonNull RequestReadFailureReason reason) {
2334                public RequestReadFailureKey {
2335                        requireNonNull(reason);
2336                }
2337        }
2338
2339        /**
2340         * Key for request rejections grouped by reason.
2341         *
2342         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2343         */
2344        record RequestRejectionKey(@NonNull RequestRejectionReason reason) {
2345                public RequestRejectionKey {
2346                        requireNonNull(reason);
2347                }
2348        }
2349
2350        /**
2351         * Key for metrics grouped by HTTP method and route match information.
2352         *
2353         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2354         */
2355        record HttpServerRouteKey(@NonNull HttpMethod method,
2356                                                                                                @NonNull RouteType routeType,
2357                                                                                                @Nullable ResourcePathDeclaration route) {
2358                public HttpServerRouteKey {
2359                        requireNonNull(method);
2360                        requireNonNull(routeType);
2361                        if (routeType == RouteType.MATCHED && route == null)
2362                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2363                        if (routeType == RouteType.UNMATCHED && route != null)
2364                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2365                }
2366        }
2367
2368        /**
2369         * Key for metrics grouped by HTTP method, route match information, and status class (e.g. 2xx).
2370         *
2371         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2372         */
2373        record HttpServerRouteStatusKey(@NonNull HttpMethod method,
2374                                                                                                                        @NonNull RouteType routeType,
2375                                                                                                                        @Nullable ResourcePathDeclaration route,
2376                                                                                                                        @NonNull String statusClass) {
2377                public HttpServerRouteStatusKey {
2378                        requireNonNull(method);
2379                        requireNonNull(routeType);
2380                        if (routeType == RouteType.MATCHED && route == null)
2381                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2382                        if (routeType == RouteType.UNMATCHED && route != null)
2383                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2384                        requireNonNull(statusClass);
2385                }
2386        }
2387
2388        /**
2389         * Key for metrics grouped by Server-Sent Event comment type and route match information.
2390         *
2391         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2392         */
2393        record SseCommentRouteKey(@NonNull RouteType routeType,
2394                                                                                                                                                                @Nullable ResourcePathDeclaration route,
2395                                                                                                                                                                SseComment.@NonNull CommentType commentType) {
2396                public SseCommentRouteKey {
2397                        requireNonNull(routeType);
2398                        requireNonNull(commentType);
2399                        if (routeType == RouteType.MATCHED && route == null)
2400                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2401                        if (routeType == RouteType.UNMATCHED && route != null)
2402                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2403                }
2404        }
2405
2406        /**
2407         * Key for metrics grouped by Server-Sent Event route match information.
2408         *
2409         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2410         */
2411        record SseEventRouteKey(@NonNull RouteType routeType,
2412                                                                                                                                 @Nullable ResourcePathDeclaration route) {
2413                public SseEventRouteKey {
2414                        requireNonNull(routeType);
2415                        if (routeType == RouteType.MATCHED && route == null)
2416                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2417                        if (routeType == RouteType.UNMATCHED && route != null)
2418                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2419                }
2420        }
2421
2422        /**
2423         * Key for metrics grouped by Server-Sent Event route match information and handshake failure reason.
2424         *
2425         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2426         */
2427        record SseEventRouteHandshakeFailureKey(@NonNull RouteType routeType,
2428                                                                                                                                                                                                 @Nullable ResourcePathDeclaration route,
2429                                                                                                                                                                                                 SseConnection.@NonNull HandshakeFailureReason handshakeFailureReason) {
2430                public SseEventRouteHandshakeFailureKey {
2431                        requireNonNull(routeType);
2432                        if (routeType == RouteType.MATCHED && route == null)
2433                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2434                        if (routeType == RouteType.UNMATCHED && route != null)
2435                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2436                        requireNonNull(handshakeFailureReason);
2437                }
2438        }
2439
2440        /**
2441         * Key for metrics grouped by Server-Sent Event route match information and enqueue outcome.
2442         *
2443         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2444         */
2445        record SseEventRouteEnqueueOutcomeKey(@NonNull RouteType routeType,
2446                                                                                                                                                                                         @Nullable ResourcePathDeclaration route,
2447                                                                                                                                                                                         @NonNull SseEventEnqueueOutcome outcome) {
2448                public SseEventRouteEnqueueOutcomeKey {
2449                        requireNonNull(routeType);
2450                        if (routeType == RouteType.MATCHED && route == null)
2451                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2452                        if (routeType == RouteType.UNMATCHED && route != null)
2453                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2454                        requireNonNull(outcome);
2455                }
2456        }
2457
2458        /**
2459         * Key for metrics grouped by Server-Sent Event comment type, route match information, and enqueue outcome.
2460         *
2461         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2462         */
2463        record SseCommentRouteEnqueueOutcomeKey(@NonNull RouteType routeType,
2464                                                                                                                                                                                                                        @Nullable ResourcePathDeclaration route,
2465                                                                                                                                                                                                                        SseComment.@NonNull CommentType commentType,
2466                                                                                                                                                                                                                        @NonNull SseEventEnqueueOutcome outcome) {
2467                public SseCommentRouteEnqueueOutcomeKey {
2468                        requireNonNull(routeType);
2469                        requireNonNull(commentType);
2470                        if (routeType == RouteType.MATCHED && route == null)
2471                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2472                        if (routeType == RouteType.UNMATCHED && route != null)
2473                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2474                        requireNonNull(outcome);
2475                }
2476        }
2477
2478        /**
2479         * Key for metrics grouped by Server-Sent Event route match information and drop reason.
2480         *
2481         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2482         */
2483        record SseEventRouteDropKey(@NonNull RouteType routeType,
2484                                                                                                                                                 @Nullable ResourcePathDeclaration route,
2485                                                                                                                                                 @NonNull SseEventDropReason dropReason) {
2486                public SseEventRouteDropKey {
2487                        requireNonNull(routeType);
2488                        if (routeType == RouteType.MATCHED && route == null)
2489                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2490                        if (routeType == RouteType.UNMATCHED && route != null)
2491                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2492                        requireNonNull(dropReason);
2493                }
2494        }
2495
2496        /**
2497         * Key for metrics grouped by Server-Sent Event comment type, route match information, and drop reason.
2498         *
2499         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2500         */
2501        record SseCommentRouteDropKey(@NonNull RouteType routeType,
2502                                                                                                                                                                                @Nullable ResourcePathDeclaration route,
2503                                                                                                                                                                                SseComment.@NonNull CommentType commentType,
2504                                                                                                                                                                                @NonNull SseEventDropReason dropReason) {
2505                public SseCommentRouteDropKey {
2506                        requireNonNull(routeType);
2507                        requireNonNull(commentType);
2508                        if (routeType == RouteType.MATCHED && route == null)
2509                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2510                        if (routeType == RouteType.UNMATCHED && route != null)
2511                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2512                        requireNonNull(dropReason);
2513                }
2514        }
2515
2516        /**
2517         * Key for metrics grouped by Server-Sent Event route match information and termination reason.
2518         *
2519         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2520         */
2521        record SseEventRouteTerminationKey(@NonNull RouteType routeType,
2522                                                                                                                                                                                @Nullable ResourcePathDeclaration route,
2523                                                                                                                                                                                SseConnection.@NonNull TerminationReason terminationReason) {
2524                public SseEventRouteTerminationKey {
2525                        requireNonNull(routeType);
2526                        if (routeType == RouteType.MATCHED && route == null)
2527                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2528                        if (routeType == RouteType.UNMATCHED && route != null)
2529                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2530                        requireNonNull(terminationReason);
2531                }
2532        }
2533
2534        /**
2535         * Key for metrics grouped by MCP endpoint class, JSON-RPC method, and request outcome.
2536         *
2537         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2538         */
2539        record McpEndpointRequestOutcomeKey(@NonNull Class<? extends McpEndpoint> endpointClass,
2540                                                                                                                                                        @NonNull String jsonRpcMethod,
2541                                                                                                                                                        @NonNull McpRequestOutcome requestOutcome) {
2542                public McpEndpointRequestOutcomeKey {
2543                        requireNonNull(endpointClass);
2544                        requireNonNull(jsonRpcMethod);
2545                        requireNonNull(requestOutcome);
2546                }
2547        }
2548
2549        /**
2550         * Key for metrics grouped by MCP endpoint class and session termination reason.
2551         *
2552         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2553         */
2554        record McpEndpointSessionTerminationKey(@NonNull Class<? extends McpEndpoint> endpointClass,
2555                                                                                                                                                                        @NonNull McpSessionTerminationReason terminationReason) {
2556                public McpEndpointSessionTerminationKey {
2557                        requireNonNull(endpointClass);
2558                        requireNonNull(terminationReason);
2559                }
2560        }
2561
2562        /**
2563         * Key for metrics grouped by MCP endpoint class and stream termination reason.
2564         *
2565         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2566         */
2567        record McpEndpointStreamTerminationKey(@NonNull Class<? extends McpEndpoint> endpointClass,
2568                                                                                                                                                                 @NonNull McpStreamTerminationReason terminationReason) {
2569                public McpEndpointStreamTerminationKey {
2570                        requireNonNull(endpointClass);
2571                        requireNonNull(terminationReason);
2572                }
2573        }
2574
2575        /**
2576         * Acquires a threadsafe {@link MetricsCollector} instance with sensible defaults.
2577         * <p>
2578         * This method is guaranteed to return a new instance.
2579         *
2580         * @return a {@code MetricsCollector} with default settings
2581         */
2582        @NonNull
2583        static MetricsCollector defaultInstance() {
2584                return DefaultMetricsCollector.defaultInstance();
2585        }
2586
2587        /**
2588         * Acquires a threadsafe {@link MetricsCollector} instance that performs no work.
2589         * <p>
2590         * This method is useful when you want to explicitly disable metrics collection without writing your own implementation.
2591         * <p>
2592         * The returned instance is guaranteed to be a JVM-wide singleton.
2593         *
2594         * @return a no-op {@code MetricsCollector}
2595         */
2596        @NonNull
2597        static MetricsCollector disabledInstance() {
2598                return DisabledMetricsCollector.defaultInstance();
2599        }
2600}