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