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                 *
1188                 * @return HTTP request body size histograms
1189                 */
1190                @NonNull
1191                public Map<@NonNull HttpServerRouteKey, @NonNull HistogramSnapshot> getHttpRequestBodyBytes() {
1192                        return this.httpRequestBodyBytes;
1193                }
1194
1195                /**
1196                 * Returns HTTP response body size histograms keyed by server route and status class.
1197                 *
1198                 * @return HTTP response body size histograms
1199                 */
1200                @NonNull
1201                public Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpResponseBodyBytes() {
1202                        return this.httpResponseBodyBytes;
1203                }
1204
1205                /**
1206                 * Returns SSE handshake acceptance counters keyed by route.
1207                 *
1208                 * @return SSE handshake acceptance counters
1209                 */
1210                @NonNull
1211                public Map<@NonNull SseEventRouteKey, @NonNull Long> getSseHandshakesAccepted() {
1212                        return this.sseHandshakesAccepted;
1213                }
1214
1215                /**
1216                 * Returns SSE handshake rejection counters keyed by route and failure reason.
1217                 *
1218                 * @return SSE handshake rejection counters
1219                 */
1220                @NonNull
1221                public Map<@NonNull SseEventRouteHandshakeFailureKey, @NonNull Long> getSseHandshakesRejected() {
1222                        return this.sseHandshakesRejected;
1223                }
1224
1225                /**
1226                 * Returns SSE event enqueue outcome counters keyed by route and outcome.
1227                 *
1228                 * @return SSE event enqueue outcome counters
1229                 */
1230                @NonNull
1231                public Map<@NonNull SseEventRouteEnqueueOutcomeKey, @NonNull Long> getSseEventEnqueueOutcomes() {
1232                        return this.sseEventEnqueueOutcomes;
1233                }
1234
1235                /**
1236                 * Returns SSE comment enqueue outcome counters keyed by route, comment type, and outcome.
1237                 *
1238                 * @return SSE comment enqueue outcome counters
1239                 */
1240                @NonNull
1241                public Map<@NonNull SseCommentRouteEnqueueOutcomeKey, @NonNull Long> getSseCommentEnqueueOutcomes() {
1242                        return this.sseCommentEnqueueOutcomes;
1243                }
1244
1245                /**
1246                 * Returns SSE event drop counters keyed by route and drop reason.
1247                 *
1248                 * @return SSE event drop counters
1249                 */
1250                @NonNull
1251                public Map<@NonNull SseEventRouteDropKey, @NonNull Long> getSseEventDrops() {
1252                        return this.sseEventDrops;
1253                }
1254
1255                /**
1256                 * Returns SSE comment drop counters keyed by route, comment type, and drop reason.
1257                 *
1258                 * @return SSE comment drop counters
1259                 */
1260                @NonNull
1261                public Map<@NonNull SseCommentRouteDropKey, @NonNull Long> getSseCommentDrops() {
1262                        return this.sseCommentDrops;
1263                }
1264
1265                /**
1266                 * Returns SSE time-to-first-event histograms keyed by route.
1267                 *
1268                 * @return SSE time-to-first-event histograms
1269                 */
1270                @NonNull
1271                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseTimeToFirstEvent() {
1272                        return this.sseTimeToFirstEvent;
1273                }
1274
1275                /**
1276                 * Returns SSE event write duration histograms keyed by route.
1277                 *
1278                 * @return SSE event write duration histograms
1279                 */
1280                @NonNull
1281                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseEventWriteDurations() {
1282                        return this.sseEventWriteDurations;
1283                }
1284
1285                /**
1286                 * Returns SSE event delivery lag histograms keyed by route.
1287                 *
1288                 * @return SSE event delivery lag histograms
1289                 */
1290                @NonNull
1291                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseEventDeliveryLag() {
1292                        return this.sseEventDeliveryLag;
1293                }
1294
1295                /**
1296                 * Returns SSE event size histograms keyed by route.
1297                 *
1298                 * @return SSE event size histograms
1299                 */
1300                @NonNull
1301                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseEventSizes() {
1302                        return this.sseEventSizes;
1303                }
1304
1305                /**
1306                 * Returns SSE queue depth histograms keyed by route.
1307                 *
1308                 * @return SSE queue depth histograms
1309                 */
1310                @NonNull
1311                public Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> getSseQueueDepth() {
1312                        return this.sseQueueDepth;
1313                }
1314
1315                /**
1316                 * Returns SSE comment delivery lag histograms keyed by route and comment type.
1317                 *
1318                 * @return SSE comment delivery lag histograms
1319                 */
1320                @NonNull
1321                public Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentDeliveryLag() {
1322                        return this.sseCommentDeliveryLag;
1323                }
1324
1325                /**
1326                 * Returns SSE comment size histograms keyed by route and comment type.
1327                 *
1328                 * @return SSE comment size histograms
1329                 */
1330                @NonNull
1331                public Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentSizes() {
1332                        return this.sseCommentSizes;
1333                }
1334
1335                /**
1336                 * Returns SSE comment queue depth histograms keyed by route and comment type.
1337                 *
1338                 * @return SSE comment queue depth histograms
1339                 */
1340                @NonNull
1341                public Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentQueueDepth() {
1342                        return this.sseCommentQueueDepth;
1343                }
1344
1345                /**
1346                 * Returns SSE stream duration histograms keyed by route and termination reason.
1347                 *
1348                 * @return SSE stream duration histograms
1349                 */
1350                @NonNull
1351                public Map<@NonNull SseStreamRouteTerminationKey, @NonNull HistogramSnapshot> getSseStreamDurations() {
1352                        return this.sseStreamDurations;
1353                }
1354
1355                /**
1356                 * Returns MCP request outcome counters keyed by endpoint, JSON-RPC method, and outcome.
1357                 *
1358                 * @return MCP request outcome counters
1359                 */
1360                @NonNull
1361                public Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull Long> getMcpRequests() {
1362                        return this.mcpRequests;
1363                }
1364
1365                /**
1366                 * Returns MCP request duration histograms keyed by endpoint, JSON-RPC method, and outcome.
1367                 *
1368                 * @return MCP request duration histograms
1369                 */
1370                @NonNull
1371                public Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull HistogramSnapshot> getMcpRequestDurations() {
1372                        return this.mcpRequestDurations;
1373                }
1374
1375                /**
1376                 * Returns MCP session duration histograms keyed by endpoint and termination reason.
1377                 *
1378                 * @return MCP session duration histograms
1379                 */
1380                @NonNull
1381                public Map<@NonNull McpEndpointSessionTerminationKey, @NonNull HistogramSnapshot> getMcpSessionDurations() {
1382                        return this.mcpSessionDurations;
1383                }
1384
1385                /**
1386                 * Returns MCP SSE stream duration histograms keyed by endpoint and termination reason.
1387                 *
1388                 * @return MCP SSE stream duration histograms
1389                 */
1390                @NonNull
1391                public Map<@NonNull McpEndpointSseStreamTerminationKey, @NonNull HistogramSnapshot> getMcpSseStreamDurations() {
1392                        return this.mcpSseStreamDurations;
1393                }
1394
1395                @NonNull
1396                private static <K, V> Map<K, V> copyOrEmpty(@Nullable Map<K, V> map) {
1397                        return map == null ? Map.of() : Map.copyOf(map);
1398                }
1399
1400                /**
1401                 * Builder used to construct instances of {@link Snapshot}.
1402                 * <p>
1403                 * This class is intended for use by a single thread.
1404                 *
1405                 * @author <a href="https://www.revetkn.com">Mark Allen</a>
1406                 */
1407                @NotThreadSafe
1408                public static final class Builder {
1409                        @NonNull
1410                        private Long activeRequests;
1411                        @NonNull
1412                        private Long activeSseStreams;
1413                        @NonNull
1414                        private Long activeMcpSessions;
1415                        @NonNull
1416                        private Long activeMcpSseStreams;
1417                        @NonNull
1418                        private Long httpConnectionsAccepted;
1419                        @NonNull
1420                        private Long httpConnectionsRejected;
1421                        @NonNull
1422                        private Long sseConnectionsAccepted;
1423                        @NonNull
1424                        private Long sseConnectionsRejected;
1425                        @NonNull
1426                        private Long mcpConnectionsAccepted;
1427                        @NonNull
1428                        private Long mcpConnectionsRejected;
1429                        @Nullable
1430                        private Map<@NonNull TransportFailureKey, @NonNull Long> transportFailures;
1431                        @Nullable
1432                        private Map<@NonNull RequestReadFailureKey, @NonNull Long> httpRequestReadFailures;
1433                        @Nullable
1434                        private Map<@NonNull RequestRejectionKey, @NonNull Long> httpRequestRejections;
1435                        @Nullable
1436                        private Map<@NonNull RequestReadFailureKey, @NonNull Long> sseRequestReadFailures;
1437                        @Nullable
1438                        private Map<@NonNull RequestRejectionKey, @NonNull Long> sseRequestRejections;
1439                        @Nullable
1440                        private Map<@NonNull RequestReadFailureKey, @NonNull Long> mcpRequestReadFailures;
1441                        @Nullable
1442                        private Map<@NonNull RequestRejectionKey, @NonNull Long> mcpRequestRejections;
1443                        @Nullable
1444                        private Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpRequestDurations;
1445                        @Nullable
1446                        private Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpHandlerDurations;
1447                        @Nullable
1448                        private Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpTimeToFirstByte;
1449                        @Nullable
1450                        private Map<@NonNull HttpServerRouteKey, @NonNull HistogramSnapshot> httpRequestBodyBytes;
1451                        @Nullable
1452                        private Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpResponseBodyBytes;
1453                        @Nullable
1454                        private Map<@NonNull SseEventRouteKey, @NonNull Long> sseHandshakesAccepted;
1455                        @Nullable
1456                        private Map<@NonNull SseEventRouteHandshakeFailureKey, @NonNull Long> sseHandshakesRejected;
1457                        @Nullable
1458                        private Map<@NonNull SseEventRouteEnqueueOutcomeKey, @NonNull Long> sseEventEnqueueOutcomes;
1459                        @Nullable
1460                        private Map<@NonNull SseCommentRouteEnqueueOutcomeKey, @NonNull Long> sseCommentEnqueueOutcomes;
1461                        @Nullable
1462                        private Map<@NonNull SseEventRouteDropKey, @NonNull Long> sseEventDrops;
1463                        @Nullable
1464                        private Map<@NonNull SseCommentRouteDropKey, @NonNull Long> sseCommentDrops;
1465                        @Nullable
1466                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseTimeToFirstEvent;
1467                        @Nullable
1468                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventWriteDurations;
1469                        @Nullable
1470                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventDeliveryLag;
1471                        @Nullable
1472                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventSizes;
1473                        @Nullable
1474                        private Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseQueueDepth;
1475                        @Nullable
1476                        private Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentDeliveryLag;
1477                        @Nullable
1478                        private Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentSizes;
1479                        @Nullable
1480                        private Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentQueueDepth;
1481                        @Nullable
1482                        private Map<@NonNull SseStreamRouteTerminationKey, @NonNull HistogramSnapshot> sseStreamDurations;
1483                        @Nullable
1484                        private Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull Long> mcpRequests;
1485                        @Nullable
1486                        private Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull HistogramSnapshot> mcpRequestDurations;
1487                        @Nullable
1488                        private Map<@NonNull McpEndpointSessionTerminationKey, @NonNull HistogramSnapshot> mcpSessionDurations;
1489                        @Nullable
1490                        private Map<@NonNull McpEndpointSseStreamTerminationKey, @NonNull HistogramSnapshot> mcpSseStreamDurations;
1491
1492                        private Builder() {
1493                                this.activeRequests = 0L;
1494                                this.activeSseStreams = 0L;
1495                                this.activeMcpSessions = 0L;
1496                                this.activeMcpSseStreams = 0L;
1497                                this.httpConnectionsAccepted = 0L;
1498                                this.httpConnectionsRejected = 0L;
1499                                this.sseConnectionsAccepted = 0L;
1500                                this.sseConnectionsRejected = 0L;
1501                                this.mcpConnectionsAccepted = 0L;
1502                                this.mcpConnectionsRejected = 0L;
1503                        }
1504
1505                        /**
1506                         * Sets the active HTTP request count.
1507                         *
1508                         * @param activeRequests the active HTTP request count
1509                         * @return this builder
1510                         */
1511                        @NonNull
1512                        public Builder activeRequests(@NonNull Long activeRequests) {
1513                                this.activeRequests = requireNonNull(activeRequests);
1514                                return this;
1515                        }
1516
1517                        /**
1518                         * Sets the active server-sent event stream count.
1519                         *
1520                         * @param activeSseStreams the active SSE stream count
1521                         * @return this builder
1522                         */
1523                        @NonNull
1524                        public Builder activeSseStreams(@NonNull Long activeSseStreams) {
1525                                this.activeSseStreams = requireNonNull(activeSseStreams);
1526                                return this;
1527                        }
1528
1529                        /**
1530                         * Sets the active MCP session count.
1531                         *
1532                         * @param activeMcpSessions the active MCP session count
1533                         * @return this builder
1534                         */
1535                        @NonNull
1536                        public Builder activeMcpSessions(@NonNull Long activeMcpSessions) {
1537                                this.activeMcpSessions = requireNonNull(activeMcpSessions);
1538                                return this;
1539                        }
1540
1541                        /**
1542                         * Sets the active MCP SSE stream count.
1543                         *
1544                         * @param activeMcpSseStreams the active MCP SSE stream count
1545                         * @return this builder
1546                         */
1547                        @NonNull
1548                        public Builder activeMcpSseStreams(@NonNull Long activeMcpSseStreams) {
1549                                this.activeMcpSseStreams = requireNonNull(activeMcpSseStreams);
1550                                return this;
1551                        }
1552
1553                        /**
1554                         * Sets the total number of accepted HTTP connections.
1555                         *
1556                         * @param httpConnectionsAccepted total accepted HTTP connections
1557                         * @return this builder
1558                         */
1559                        @NonNull
1560                        public Builder httpConnectionsAccepted(@NonNull Long httpConnectionsAccepted) {
1561                                this.httpConnectionsAccepted = requireNonNull(httpConnectionsAccepted);
1562                                return this;
1563                        }
1564
1565                        /**
1566                         * Sets the total number of rejected HTTP connections.
1567                         *
1568                         * @param httpConnectionsRejected total rejected HTTP connections
1569                         * @return this builder
1570                         */
1571                        @NonNull
1572                        public Builder httpConnectionsRejected(@NonNull Long httpConnectionsRejected) {
1573                                this.httpConnectionsRejected = requireNonNull(httpConnectionsRejected);
1574                                return this;
1575                        }
1576
1577                        /**
1578                         * Sets the total number of accepted SSE connections.
1579                         *
1580                         * @param sseConnectionsAccepted total accepted SSE connections
1581                         * @return this builder
1582                         */
1583                        @NonNull
1584                        public Builder sseConnectionsAccepted(@NonNull Long sseConnectionsAccepted) {
1585                                this.sseConnectionsAccepted = requireNonNull(sseConnectionsAccepted);
1586                                return this;
1587                        }
1588
1589                        /**
1590                         * Sets the total number of rejected SSE connections.
1591                         *
1592                         * @param sseConnectionsRejected total rejected SSE connections
1593                         * @return this builder
1594                         */
1595                        @NonNull
1596                        public Builder sseConnectionsRejected(@NonNull Long sseConnectionsRejected) {
1597                                this.sseConnectionsRejected = requireNonNull(sseConnectionsRejected);
1598                                return this;
1599                        }
1600
1601                        /**
1602                         * Sets the total number of accepted MCP connections.
1603                         *
1604                         * @param mcpConnectionsAccepted total accepted MCP connections
1605                         * @return this builder
1606                         */
1607                        @NonNull
1608                        public Builder mcpConnectionsAccepted(@NonNull Long mcpConnectionsAccepted) {
1609                                this.mcpConnectionsAccepted = requireNonNull(mcpConnectionsAccepted);
1610                                return this;
1611                        }
1612
1613                        /**
1614                         * Sets the total number of rejected MCP connections.
1615                         *
1616                         * @param mcpConnectionsRejected total rejected MCP connections
1617                         * @return this builder
1618                         */
1619                        @NonNull
1620                        public Builder mcpConnectionsRejected(@NonNull Long mcpConnectionsRejected) {
1621                                this.mcpConnectionsRejected = requireNonNull(mcpConnectionsRejected);
1622                                return this;
1623                        }
1624
1625                        /**
1626                         * Sets transport failure counters keyed by server type and failure reason.
1627                         *
1628                         * @param transportFailures the transport failure counters
1629                         * @return this builder
1630                         */
1631                        @NonNull
1632                        public Builder transportFailures(
1633                                        @Nullable Map<@NonNull TransportFailureKey, @NonNull Long> transportFailures) {
1634                                this.transportFailures = transportFailures;
1635                                return this;
1636                        }
1637
1638                        /**
1639                         * Sets HTTP request read failure counters keyed by failure reason.
1640                         *
1641                         * @param httpRequestReadFailures the HTTP request read failure counters
1642                         * @return this builder
1643                         */
1644                        @NonNull
1645                        public Builder httpRequestReadFailures(
1646                                        @Nullable Map<@NonNull RequestReadFailureKey, @NonNull Long> httpRequestReadFailures) {
1647                                this.httpRequestReadFailures = httpRequestReadFailures;
1648                                return this;
1649                        }
1650
1651                        /**
1652                         * Sets HTTP request rejection counters keyed by rejection reason.
1653                         *
1654                         * @param httpRequestRejections the HTTP request rejection counters
1655                         * @return this builder
1656                         */
1657                        @NonNull
1658                        public Builder httpRequestRejections(
1659                                        @Nullable Map<@NonNull RequestRejectionKey, @NonNull Long> httpRequestRejections) {
1660                                this.httpRequestRejections = httpRequestRejections;
1661                                return this;
1662                        }
1663
1664                        /**
1665                         * Sets SSE request read failure counters keyed by failure reason.
1666                         *
1667                         * @param sseRequestReadFailures the SSE request read failure counters
1668                         * @return this builder
1669                         */
1670                        @NonNull
1671                        public Builder sseRequestReadFailures(
1672                                        @Nullable Map<@NonNull RequestReadFailureKey, @NonNull Long> sseRequestReadFailures) {
1673                                this.sseRequestReadFailures = sseRequestReadFailures;
1674                                return this;
1675                        }
1676
1677                        /**
1678                         * Sets SSE request rejection counters keyed by rejection reason.
1679                         *
1680                         * @param sseRequestRejections the SSE request rejection counters
1681                         * @return this builder
1682                         */
1683                        @NonNull
1684                        public Builder sseRequestRejections(
1685                                        @Nullable Map<@NonNull RequestRejectionKey, @NonNull Long> sseRequestRejections) {
1686                                this.sseRequestRejections = sseRequestRejections;
1687                                return this;
1688                        }
1689
1690                        /**
1691                         * Sets MCP request read failure counters keyed by failure reason.
1692                         *
1693                         * @param mcpRequestReadFailures the MCP request read failure counters
1694                         * @return this builder
1695                         */
1696                        @NonNull
1697                        public Builder mcpRequestReadFailures(
1698                                        @Nullable Map<@NonNull RequestReadFailureKey, @NonNull Long> mcpRequestReadFailures) {
1699                                this.mcpRequestReadFailures = mcpRequestReadFailures;
1700                                return this;
1701                        }
1702
1703                        /**
1704                         * Sets MCP request rejection counters keyed by rejection reason.
1705                         *
1706                         * @param mcpRequestRejections the MCP request rejection counters
1707                         * @return this builder
1708                         */
1709                        @NonNull
1710                        public Builder mcpRequestRejections(
1711                                        @Nullable Map<@NonNull RequestRejectionKey, @NonNull Long> mcpRequestRejections) {
1712                                this.mcpRequestRejections = mcpRequestRejections;
1713                                return this;
1714                        }
1715
1716                        /**
1717                         * Sets HTTP request duration histograms keyed by server route and status class.
1718                         *
1719                         * @param httpRequestDurations the HTTP request duration histograms
1720                         * @return this builder
1721                         */
1722                        @NonNull
1723                        public Builder httpRequestDurations(
1724                                        @Nullable Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpRequestDurations) {
1725                                this.httpRequestDurations = httpRequestDurations;
1726                                return this;
1727                        }
1728
1729                        /**
1730                         * Sets HTTP handler duration histograms keyed by server route and status class.
1731                         *
1732                         * @param httpHandlerDurations the HTTP handler duration histograms
1733                         * @return this builder
1734                         */
1735                        @NonNull
1736                        public Builder httpHandlerDurations(
1737                                        @Nullable Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpHandlerDurations) {
1738                                this.httpHandlerDurations = httpHandlerDurations;
1739                                return this;
1740                        }
1741
1742                        /**
1743                         * Sets HTTP time-to-first-byte histograms keyed by server route and status class.
1744                         *
1745                         * @param httpTimeToFirstByte the HTTP time-to-first-byte histograms
1746                         * @return this builder
1747                         */
1748                        @NonNull
1749                        public Builder httpTimeToFirstByte(
1750                                        @Nullable Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpTimeToFirstByte) {
1751                                this.httpTimeToFirstByte = httpTimeToFirstByte;
1752                                return this;
1753                        }
1754
1755                        /**
1756                         * Sets HTTP request body size histograms keyed by server route.
1757                         *
1758                         * @param httpRequestBodyBytes the HTTP request body size histograms
1759                         * @return this builder
1760                         */
1761                        @NonNull
1762                        public Builder httpRequestBodyBytes(
1763                                        @Nullable Map<@NonNull HttpServerRouteKey, @NonNull HistogramSnapshot> httpRequestBodyBytes) {
1764                                this.httpRequestBodyBytes = httpRequestBodyBytes;
1765                                return this;
1766                        }
1767
1768                        /**
1769                         * Sets HTTP response body size histograms keyed by server route and status class.
1770                         *
1771                         * @param httpResponseBodyBytes the HTTP response body size histograms
1772                         * @return this builder
1773                         */
1774                        @NonNull
1775                        public Builder httpResponseBodyBytes(
1776                                        @Nullable Map<@NonNull HttpServerRouteStatusKey, @NonNull HistogramSnapshot> httpResponseBodyBytes) {
1777                                this.httpResponseBodyBytes = httpResponseBodyBytes;
1778                                return this;
1779                        }
1780
1781                        /**
1782                         * Sets SSE handshake acceptance counters keyed by route.
1783                         *
1784                         * @param sseHandshakesAccepted SSE handshake acceptance counters
1785                         * @return this builder
1786                         */
1787                        @NonNull
1788                        public Builder sseHandshakesAccepted(
1789                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull Long> sseHandshakesAccepted) {
1790                                this.sseHandshakesAccepted = sseHandshakesAccepted;
1791                                return this;
1792                        }
1793
1794                        /**
1795                         * Sets SSE handshake rejection counters keyed by route and failure reason.
1796                         *
1797                         * @param sseHandshakesRejected SSE handshake rejection counters
1798                         * @return this builder
1799                         */
1800                        @NonNull
1801                        public Builder sseHandshakesRejected(
1802                                        @Nullable Map<@NonNull SseEventRouteHandshakeFailureKey, @NonNull Long> sseHandshakesRejected) {
1803                                this.sseHandshakesRejected = sseHandshakesRejected;
1804                                return this;
1805                        }
1806
1807                        /**
1808                         * Sets SSE event enqueue outcome counters keyed by route and outcome.
1809                         *
1810                         * @param sseEventEnqueueOutcomes the SSE event enqueue outcome counters
1811                         * @return this builder
1812                         */
1813                        @NonNull
1814                        public Builder sseEventEnqueueOutcomes(
1815                                        @Nullable Map<@NonNull SseEventRouteEnqueueOutcomeKey, @NonNull Long> sseEventEnqueueOutcomes) {
1816                                this.sseEventEnqueueOutcomes = sseEventEnqueueOutcomes;
1817                                return this;
1818                        }
1819
1820                        /**
1821                         * Sets SSE comment enqueue outcome counters keyed by route, comment type, and outcome.
1822                         *
1823                         * @param sseCommentEnqueueOutcomes the SSE comment enqueue outcome counters
1824                         * @return this builder
1825                         */
1826                        @NonNull
1827                        public Builder sseCommentEnqueueOutcomes(
1828                                        @Nullable Map<@NonNull SseCommentRouteEnqueueOutcomeKey, @NonNull Long> sseCommentEnqueueOutcomes) {
1829                                this.sseCommentEnqueueOutcomes = sseCommentEnqueueOutcomes;
1830                                return this;
1831                        }
1832
1833                        /**
1834                         * Sets SSE event drop counters keyed by route and drop reason.
1835                         *
1836                         * @param sseEventDrops the SSE event drop counters
1837                         * @return this builder
1838                         */
1839                        @NonNull
1840                        public Builder sseEventDrops(
1841                                        @Nullable Map<@NonNull SseEventRouteDropKey, @NonNull Long> sseEventDrops) {
1842                                this.sseEventDrops = sseEventDrops;
1843                                return this;
1844                        }
1845
1846                        /**
1847                         * Sets SSE comment drop counters keyed by route, comment type, and drop reason.
1848                         *
1849                         * @param sseCommentDrops the SSE comment drop counters
1850                         * @return this builder
1851                         */
1852                        @NonNull
1853                        public Builder sseCommentDrops(
1854                                        @Nullable Map<@NonNull SseCommentRouteDropKey, @NonNull Long> sseCommentDrops) {
1855                                this.sseCommentDrops = sseCommentDrops;
1856                                return this;
1857                        }
1858
1859                        /**
1860                         * Sets SSE time-to-first-event histograms keyed by route.
1861                         *
1862                         * @param sseTimeToFirstEvent the SSE time-to-first-event histograms
1863                         * @return this builder
1864                         */
1865                        @NonNull
1866                        public Builder sseTimeToFirstEvent(
1867                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseTimeToFirstEvent) {
1868                                this.sseTimeToFirstEvent = sseTimeToFirstEvent;
1869                                return this;
1870                        }
1871
1872                        /**
1873                         * Sets SSE event write duration histograms keyed by route.
1874                         *
1875                         * @param sseEventWriteDurations the SSE event write duration histograms
1876                         * @return this builder
1877                         */
1878                        @NonNull
1879                        public Builder sseEventWriteDurations(
1880                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventWriteDurations) {
1881                                this.sseEventWriteDurations = sseEventWriteDurations;
1882                                return this;
1883                        }
1884
1885                        /**
1886                         * Sets SSE event delivery lag histograms keyed by route.
1887                         *
1888                         * @param sseEventDeliveryLag the SSE event delivery lag histograms
1889                         * @return this builder
1890                         */
1891                        @NonNull
1892                        public Builder sseEventDeliveryLag(
1893                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventDeliveryLag) {
1894                                this.sseEventDeliveryLag = sseEventDeliveryLag;
1895                                return this;
1896                        }
1897
1898                        /**
1899                         * Sets SSE event size histograms keyed by route.
1900                         *
1901                         * @param sseEventSizes the SSE event size histograms
1902                         * @return this builder
1903                         */
1904                        @NonNull
1905                        public Builder sseEventSizes(
1906                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseEventSizes) {
1907                                this.sseEventSizes = sseEventSizes;
1908                                return this;
1909                        }
1910
1911                        /**
1912                         * Sets SSE queue depth histograms keyed by route.
1913                         *
1914                         * @param sseQueueDepth the SSE queue depth histograms
1915                         * @return this builder
1916                         */
1917                        @NonNull
1918                        public Builder sseQueueDepth(
1919                                        @Nullable Map<@NonNull SseEventRouteKey, @NonNull HistogramSnapshot> sseQueueDepth) {
1920                                this.sseQueueDepth = sseQueueDepth;
1921                                return this;
1922                        }
1923
1924                        /**
1925                         * Sets SSE comment delivery lag histograms keyed by route and comment type.
1926                         *
1927                         * @param sseCommentDeliveryLag the SSE comment delivery lag histograms
1928                         * @return this builder
1929                         */
1930                        @NonNull
1931                        public Builder sseCommentDeliveryLag(
1932                                        @Nullable Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentDeliveryLag) {
1933                                this.sseCommentDeliveryLag = sseCommentDeliveryLag;
1934                                return this;
1935                        }
1936
1937                        /**
1938                         * Sets SSE comment size histograms keyed by route and comment type.
1939                         *
1940                         * @param sseCommentSizes the SSE comment size histograms
1941                         * @return this builder
1942                         */
1943                        @NonNull
1944                        public Builder sseCommentSizes(
1945                                        @Nullable Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentSizes) {
1946                                this.sseCommentSizes = sseCommentSizes;
1947                                return this;
1948                        }
1949
1950                        /**
1951                         * Sets SSE comment queue depth histograms keyed by route and comment type.
1952                         *
1953                         * @param sseCommentQueueDepth the SSE comment queue depth histograms
1954                         * @return this builder
1955                         */
1956                        @NonNull
1957                        public Builder sseCommentQueueDepth(
1958                                        @Nullable Map<@NonNull SseCommentRouteKey, @NonNull HistogramSnapshot> sseCommentQueueDepth) {
1959                                this.sseCommentQueueDepth = sseCommentQueueDepth;
1960                                return this;
1961                        }
1962
1963                        /**
1964                         * Sets SSE stream duration histograms keyed by route and termination reason.
1965                         *
1966                         * @param sseStreamDurations the SSE stream duration histograms
1967                         * @return this builder
1968                         */
1969                        @NonNull
1970                        public Builder sseStreamDurations(
1971                                        @Nullable Map<@NonNull SseStreamRouteTerminationKey, @NonNull HistogramSnapshot> sseStreamDurations) {
1972                                this.sseStreamDurations = sseStreamDurations;
1973                                return this;
1974                        }
1975
1976                        /**
1977                         * Sets MCP request outcome counters keyed by endpoint, JSON-RPC method, and outcome.
1978                         *
1979                         * @param mcpRequests the MCP request outcome counters
1980                         * @return this builder
1981                         */
1982                        @NonNull
1983                        public Builder mcpRequests(
1984                                        @Nullable Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull Long> mcpRequests) {
1985                                this.mcpRequests = mcpRequests;
1986                                return this;
1987                        }
1988
1989                        /**
1990                         * Sets MCP request duration histograms keyed by endpoint, JSON-RPC method, and outcome.
1991                         *
1992                         * @param mcpRequestDurations the MCP request duration histograms
1993                         * @return this builder
1994                         */
1995                        @NonNull
1996                        public Builder mcpRequestDurations(
1997                                        @Nullable Map<@NonNull McpEndpointRequestOutcomeKey, @NonNull HistogramSnapshot> mcpRequestDurations) {
1998                                this.mcpRequestDurations = mcpRequestDurations;
1999                                return this;
2000                        }
2001
2002                        /**
2003                         * Sets MCP session duration histograms keyed by endpoint and termination reason.
2004                         *
2005                         * @param mcpSessionDurations the MCP session duration histograms
2006                         * @return this builder
2007                         */
2008                        @NonNull
2009                        public Builder mcpSessionDurations(
2010                                        @Nullable Map<@NonNull McpEndpointSessionTerminationKey, @NonNull HistogramSnapshot> mcpSessionDurations) {
2011                                this.mcpSessionDurations = mcpSessionDurations;
2012                                return this;
2013                        }
2014
2015                        /**
2016                         * Sets MCP SSE stream duration histograms keyed by endpoint and termination reason.
2017                         *
2018                         * @param mcpSseStreamDurations the MCP SSE stream duration histograms
2019                         * @return this builder
2020                         */
2021                        @NonNull
2022                        public Builder mcpSseStreamDurations(
2023                                        @Nullable Map<@NonNull McpEndpointSseStreamTerminationKey, @NonNull HistogramSnapshot> mcpSseStreamDurations) {
2024                                this.mcpSseStreamDurations = mcpSseStreamDurations;
2025                                return this;
2026                        }
2027
2028                        /**
2029                         * Builds a {@link Snapshot} instance.
2030                         *
2031                         * @return the built snapshot
2032                         */
2033                        @NonNull
2034                        public Snapshot build() {
2035                                return new Snapshot(this);
2036                        }
2037                }
2038        }
2039
2040        /**
2041         * A thread-safe histogram with fixed bucket boundaries.
2042         * <p>
2043         * Negative values are ignored. Buckets use inclusive upper bounds, and snapshots include
2044         * an overflow bucket represented by a {@link HistogramSnapshot#getBucketBoundary(int)} of
2045         * {@link Long#MAX_VALUE}.
2046         *
2047         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2048         */
2049        @ThreadSafe
2050        final class Histogram {
2051                private final long @NonNull [] bucketBoundaries;
2052                @NonNull
2053                private final LongAdder[] bucketCounts;
2054                @NonNull
2055                private final LongAdder count;
2056                @NonNull
2057                private final LongAdder sum;
2058                @NonNull
2059                private final AtomicLong min;
2060                @NonNull
2061                private final AtomicLong max;
2062
2063                /**
2064                 * Creates a histogram with the provided bucket boundaries.
2065                 *
2066                 * @param bucketBoundaries inclusive upper bounds for buckets
2067                 */
2068                public Histogram(long @NonNull [] bucketBoundaries) {
2069                        requireNonNull(bucketBoundaries);
2070
2071                        this.bucketBoundaries = bucketBoundaries.clone();
2072                        Arrays.sort(this.bucketBoundaries);
2073                        this.bucketCounts = new LongAdder[this.bucketBoundaries.length + 1];
2074                        for (int i = 0; i < this.bucketCounts.length; i++)
2075                                this.bucketCounts[i] = new LongAdder();
2076                        this.count = new LongAdder();
2077                        this.sum = new LongAdder();
2078                        this.min = new AtomicLong(Long.MAX_VALUE);
2079                        this.max = new AtomicLong(Long.MIN_VALUE);
2080                }
2081
2082                /**
2083                 * Records a value into the histogram.
2084                 *
2085                 * @param value the value to record
2086                 */
2087                public void record(long value) {
2088                        if (value < 0)
2089                                return;
2090
2091                        this.count.increment();
2092                        this.sum.add(value);
2093                        updateMin(value);
2094                        updateMax(value);
2095
2096                        int bucketIndex = bucketIndex(value);
2097                        this.bucketCounts[bucketIndex].increment();
2098                }
2099
2100                /**
2101                 * Captures an immutable snapshot of the histogram.
2102                 *
2103                 * @return the histogram snapshot
2104                 */
2105                @NonNull
2106                public HistogramSnapshot snapshot() {
2107                        long[] boundariesWithOverflow = Arrays.copyOf(this.bucketBoundaries, this.bucketBoundaries.length + 1);
2108                        boundariesWithOverflow[boundariesWithOverflow.length - 1] = Long.MAX_VALUE;
2109
2110                        long[] cumulativeCounts = new long[this.bucketCounts.length];
2111                        long cumulative = 0;
2112                        for (int i = 0; i < this.bucketCounts.length; i++) {
2113                                cumulative += this.bucketCounts[i].sum();
2114                                cumulativeCounts[i] = cumulative;
2115                        }
2116
2117                        long countSnapshot = this.count.sum();
2118                        long sumSnapshot = this.sum.sum();
2119                        long minSnapshot = this.min.get();
2120                        long maxSnapshot = this.max.get();
2121
2122                        if (minSnapshot == Long.MAX_VALUE)
2123                                minSnapshot = 0;
2124                        if (maxSnapshot == Long.MIN_VALUE)
2125                                maxSnapshot = 0;
2126
2127                        return new HistogramSnapshot(boundariesWithOverflow, cumulativeCounts, countSnapshot, sumSnapshot, minSnapshot, maxSnapshot);
2128                }
2129
2130                /**
2131                 * Resets all counts and min/max values.
2132                 */
2133                public void reset() {
2134                        this.count.reset();
2135                        this.sum.reset();
2136                        this.min.set(Long.MAX_VALUE);
2137                        this.max.set(Long.MIN_VALUE);
2138                        for (LongAdder bucket : this.bucketCounts)
2139                                bucket.reset();
2140                }
2141
2142                private int bucketIndex(long value) {
2143                        for (int i = 0; i < this.bucketBoundaries.length; i++)
2144                                if (value <= this.bucketBoundaries[i])
2145                                        return i;
2146
2147                        return this.bucketBoundaries.length;
2148                }
2149
2150                private void updateMin(long value) {
2151                        long current;
2152                        while (value < (current = this.min.get())) {
2153                                if (this.min.compareAndSet(current, value))
2154                                        break;
2155                        }
2156                }
2157
2158                private void updateMax(long value) {
2159                        long current;
2160                        while (value > (current = this.max.get())) {
2161                                if (this.max.compareAndSet(current, value))
2162                                        break;
2163                        }
2164                }
2165        }
2166
2167        /**
2168         * Immutable snapshot of a {@link Histogram}.
2169         * <p>
2170         * Bucket counts are cumulative. Boundaries are inclusive upper bounds, and the final
2171         * boundary is {@link Long#MAX_VALUE} to represent the overflow bucket. Units are the same
2172         * as values passed to {@link Histogram#record(long)}.
2173         *
2174         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2175         */
2176        @ThreadSafe
2177        final class HistogramSnapshot {
2178                private final long @NonNull [] bucketBoundaries;
2179                private final long @NonNull [] bucketCumulativeCounts;
2180                private final long count;
2181                private final long sum;
2182                private final long min;
2183                private final long max;
2184
2185                /**
2186                 * Creates an immutable histogram snapshot.
2187                 *
2188                 * @param bucketBoundaries       inclusive upper bounds for buckets, including overflow
2189                 * @param bucketCumulativeCounts cumulative counts for each bucket
2190                 * @param count                  total number of samples recorded
2191                 * @param sum                    sum of all recorded values
2192                 * @param min                    smallest recorded value (or 0 if none)
2193                 * @param max                    largest recorded value (or 0 if none)
2194                 */
2195                public HistogramSnapshot(long @NonNull [] bucketBoundaries,
2196                                                                                                                 long @NonNull [] bucketCumulativeCounts,
2197                                                                                                                 long count,
2198                                                                                                                 long sum,
2199                                                                                                                 long min,
2200                                                                                                                 long max) {
2201                        requireNonNull(bucketBoundaries);
2202                        requireNonNull(bucketCumulativeCounts);
2203
2204                        if (bucketBoundaries.length != bucketCumulativeCounts.length)
2205                                throw new IllegalArgumentException("Bucket boundaries and cumulative counts must be the same length");
2206
2207                        this.bucketBoundaries = bucketBoundaries.clone();
2208                        this.bucketCumulativeCounts = bucketCumulativeCounts.clone();
2209                        this.count = count;
2210                        this.sum = sum;
2211                        this.min = min;
2212                        this.max = max;
2213                }
2214
2215                /**
2216                 * Number of histogram buckets, including the overflow bucket.
2217                 *
2218                 * @return the bucket count
2219                 */
2220                public int getBucketCount() {
2221                        return this.bucketBoundaries.length;
2222                }
2223
2224                /**
2225                 * The inclusive upper bound for the bucket at the given index.
2226                 *
2227                 * @param index the bucket index
2228                 * @return the bucket boundary
2229                 */
2230                public long getBucketBoundary(int index) {
2231                        return this.bucketBoundaries[index];
2232                }
2233
2234                /**
2235                 * The cumulative count for the bucket at the given index.
2236                 *
2237                 * @param index the bucket index
2238                 * @return the cumulative count
2239                 */
2240                public long getBucketCumulativeCount(int index) {
2241                        return this.bucketCumulativeCounts[index];
2242                }
2243
2244                /**
2245                 * Total number of recorded values.
2246                 *
2247                 * @return the count
2248                 */
2249                public long getCount() {
2250                        return this.count;
2251                }
2252
2253                /**
2254                 * Sum of all recorded values.
2255                 *
2256                 * @return the sum
2257                 */
2258                public long getSum() {
2259                        return this.sum;
2260                }
2261
2262                /**
2263                 * Smallest recorded value, or 0 if no values were recorded.
2264                 *
2265                 * @return the minimum value
2266                 */
2267                public long getMin() {
2268                        return this.min;
2269                }
2270
2271                /**
2272                 * Largest recorded value, or 0 if no values were recorded.
2273                 *
2274                 * @return the maximum value
2275                 */
2276                public long getMax() {
2277                        return this.max;
2278                }
2279
2280                /**
2281                 * Returns an approximate percentile based on bucket boundaries.
2282                 *
2283                 * @param percentile percentile between 0 and 100
2284                 * @return the approximated percentile value
2285                 */
2286                public long getPercentile(double percentile) {
2287                        if (percentile <= 0.0)
2288                                return this.min;
2289                        if (percentile >= 100.0)
2290                                return this.max;
2291                        if (this.count == 0)
2292                                return 0;
2293
2294                        long threshold = (long) Math.ceil((percentile / 100.0) * this.count);
2295
2296                        for (int i = 0; i < this.bucketCumulativeCounts.length; i++)
2297                                if (this.bucketCumulativeCounts[i] >= threshold)
2298                                        return this.bucketBoundaries[i];
2299
2300                        return this.bucketBoundaries[this.bucketBoundaries.length - 1];
2301                }
2302
2303                @Override
2304                public String toString() {
2305                        return String.format("%s{count=%d, min=%d, max=%d, sum=%d, bucketBoundaries=%s}",
2306                                        getClass().getSimpleName(), this.count, this.min, this.max, this.sum, Arrays.toString(this.bucketBoundaries));
2307                }
2308        }
2309
2310        /**
2311         * Low-level server transport failure reasons.
2312         *
2313         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2314         */
2315        enum TransportFailureReason {
2316                /**
2317                 * A request header/body read timed out.
2318                 */
2319                REQUEST_READ_TIMEOUT,
2320                /**
2321                 * A request exceeded configured transport limits.
2322                 */
2323                REQUEST_TOO_LARGE,
2324                /**
2325                 * A request could not be parsed by the transport.
2326                 */
2327                MALFORMED_REQUEST,
2328                /**
2329                 * A socket read failed.
2330                 */
2331                READ_ERROR,
2332                /**
2333                 * A response write failed.
2334                 */
2335                WRITE_ERROR,
2336                /**
2337                 * A non-streaming response made no write progress before its idle timeout.
2338                 */
2339                RESPONSE_WRITE_IDLE_TIMEOUT,
2340                /**
2341                 * A response-ready callback failed before the response could be written.
2342                 */
2343                RESPONSE_READY_ERROR,
2344                /**
2345                 * A request-timeout callback failed.
2346                 */
2347                REQUEST_READ_TIMEOUT_ERROR,
2348                /**
2349                 * A response-write-idle-timeout callback failed.
2350                 */
2351                RESPONSE_WRITE_IDLE_TIMEOUT_ERROR,
2352                /**
2353                 * A server accept loop failed while accepting or preparing a connection.
2354                 */
2355                ACCEPT_LOOP_ERROR,
2356                /**
2357                 * An accepted connection could not be prepared before request handling began.
2358                 */
2359                CONNECTION_SETUP_ERROR,
2360                /**
2361                 * A transport event-loop task failed.
2362                 */
2363                TASK_ERROR,
2364                /**
2365                 * A timeout scheduler task failed.
2366                 */
2367                TIMEOUT_TASK_ERROR,
2368                /**
2369                 * A selection-key dispatch failed.
2370                 */
2371                SELECTION_KEY_ERROR,
2372                /**
2373                 * A connection registration failed.
2374                 */
2375                REGISTER_ERROR,
2376                /**
2377                 * A socket write made no progress before its timeout.
2378                 */
2379                WRITE_TIMEOUT,
2380                /**
2381                 * A transport event-loop terminated after an unrecoverable error.
2382                 */
2383                EVENT_LOOP_TERMINATED,
2384                /**
2385                 * The transport reported a failure that Soklet does not classify.
2386                 */
2387                UNKNOWN
2388        }
2389
2390        /**
2391         * Indicates whether a request was matched to a {@link ResourcePathDeclaration}.
2392         *
2393         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2394         */
2395        enum RouteType {
2396                /**
2397                 * The request matched a {@link ResourcePathDeclaration}.
2398                 */
2399                MATCHED,
2400                /**
2401                 * The request did not match any {@link ResourcePathDeclaration}.
2402                 */
2403                UNMATCHED
2404        }
2405
2406        /**
2407         * Outcomes for a Server-Sent Event enqueue attempt.
2408         *
2409         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2410         */
2411        enum SseEventEnqueueOutcome {
2412                /**
2413                 * An enqueue attempt to a connection was made.
2414                 */
2415                ATTEMPTED,
2416                /**
2417                 * An enqueue attempt succeeded.
2418                 */
2419                ENQUEUED,
2420                /**
2421                 * An enqueue attempt failed because the payload was dropped.
2422                 */
2423                DROPPED
2424        }
2425
2426        /**
2427         * Reasons a Server-Sent Event payload or comment was dropped before it could be written.
2428         *
2429         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2430         */
2431        enum SseEventDropReason {
2432                /**
2433                 * The per-connection write queue was full.
2434                 */
2435                QUEUE_FULL
2436        }
2437
2438        /**
2439         * Key for transport failures grouped by server type and reason.
2440         *
2441         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2442         */
2443        record TransportFailureKey(@NonNull ServerType serverType,
2444                                                                                                                 @NonNull TransportFailureReason reason) {
2445                public TransportFailureKey {
2446                        requireNonNull(serverType);
2447                        requireNonNull(reason);
2448                }
2449        }
2450
2451        /**
2452         * Key for request read failures grouped by reason.
2453         *
2454         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2455         */
2456        record RequestReadFailureKey(@NonNull RequestReadFailureReason reason) {
2457                public RequestReadFailureKey {
2458                        requireNonNull(reason);
2459                }
2460        }
2461
2462        /**
2463         * Key for request rejections grouped by reason.
2464         *
2465         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2466         */
2467        record RequestRejectionKey(@NonNull RequestRejectionReason reason) {
2468                public RequestRejectionKey {
2469                        requireNonNull(reason);
2470                }
2471        }
2472
2473        /**
2474         * Key for metrics grouped by HTTP method and route match information.
2475         *
2476         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2477         */
2478        record HttpServerRouteKey(@NonNull HttpMethod method,
2479                                                                                                @NonNull RouteType routeType,
2480                                                                                                @Nullable ResourcePathDeclaration route) {
2481                public HttpServerRouteKey {
2482                        requireNonNull(method);
2483                        requireNonNull(routeType);
2484                        if (routeType == RouteType.MATCHED && route == null)
2485                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2486                        if (routeType == RouteType.UNMATCHED && route != null)
2487                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2488                }
2489        }
2490
2491        /**
2492         * Key for metrics grouped by HTTP method, route match information, and status class (e.g. 2xx).
2493         *
2494         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2495         */
2496        record HttpServerRouteStatusKey(@NonNull HttpMethod method,
2497                                                                                                                        @NonNull RouteType routeType,
2498                                                                                                                        @Nullable ResourcePathDeclaration route,
2499                                                                                                                        @NonNull String statusClass) {
2500                public HttpServerRouteStatusKey {
2501                        requireNonNull(method);
2502                        requireNonNull(routeType);
2503                        if (routeType == RouteType.MATCHED && route == null)
2504                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2505                        if (routeType == RouteType.UNMATCHED && route != null)
2506                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2507                        requireNonNull(statusClass);
2508                }
2509        }
2510
2511        /**
2512         * Key for metrics grouped by Server-Sent Event comment type and route match information.
2513         *
2514         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2515         */
2516        record SseCommentRouteKey(@NonNull RouteType routeType,
2517                                                                                                                                                                @Nullable ResourcePathDeclaration route,
2518                                                                                                                                                                SseComment.@NonNull CommentType commentType) {
2519                public SseCommentRouteKey {
2520                        requireNonNull(routeType);
2521                        requireNonNull(commentType);
2522                        if (routeType == RouteType.MATCHED && route == null)
2523                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2524                        if (routeType == RouteType.UNMATCHED && route != null)
2525                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2526                }
2527        }
2528
2529        /**
2530         * Key for metrics grouped by Server-Sent Event route match information.
2531         *
2532         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2533         */
2534        record SseEventRouteKey(@NonNull RouteType routeType,
2535                                                                                                                                 @Nullable ResourcePathDeclaration route) {
2536                public SseEventRouteKey {
2537                        requireNonNull(routeType);
2538                        if (routeType == RouteType.MATCHED && route == null)
2539                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2540                        if (routeType == RouteType.UNMATCHED && route != null)
2541                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2542                }
2543        }
2544
2545        /**
2546         * Key for metrics grouped by Server-Sent Event route match information and handshake failure reason.
2547         *
2548         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2549         */
2550        record SseEventRouteHandshakeFailureKey(@NonNull RouteType routeType,
2551                                                                                                                                                                                                 @Nullable ResourcePathDeclaration route,
2552                                                                                                                                                                                                 SseConnection.@NonNull HandshakeFailureReason handshakeFailureReason) {
2553                public SseEventRouteHandshakeFailureKey {
2554                        requireNonNull(routeType);
2555                        if (routeType == RouteType.MATCHED && route == null)
2556                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2557                        if (routeType == RouteType.UNMATCHED && route != null)
2558                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2559                        requireNonNull(handshakeFailureReason);
2560                }
2561        }
2562
2563        /**
2564         * Key for metrics grouped by Server-Sent Event route match information and enqueue outcome.
2565         *
2566         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2567         */
2568        record SseEventRouteEnqueueOutcomeKey(@NonNull RouteType routeType,
2569                                                                                                                                                                                         @Nullable ResourcePathDeclaration route,
2570                                                                                                                                                                                         @NonNull SseEventEnqueueOutcome outcome) {
2571                public SseEventRouteEnqueueOutcomeKey {
2572                        requireNonNull(routeType);
2573                        if (routeType == RouteType.MATCHED && route == null)
2574                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2575                        if (routeType == RouteType.UNMATCHED && route != null)
2576                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2577                        requireNonNull(outcome);
2578                }
2579        }
2580
2581        /**
2582         * Key for metrics grouped by Server-Sent Event comment type, route match information, and enqueue outcome.
2583         *
2584         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2585         */
2586        record SseCommentRouteEnqueueOutcomeKey(@NonNull RouteType routeType,
2587                                                                                                                                                                                                                        @Nullable ResourcePathDeclaration route,
2588                                                                                                                                                                                                                        SseComment.@NonNull CommentType commentType,
2589                                                                                                                                                                                                                        @NonNull SseEventEnqueueOutcome outcome) {
2590                public SseCommentRouteEnqueueOutcomeKey {
2591                        requireNonNull(routeType);
2592                        requireNonNull(commentType);
2593                        if (routeType == RouteType.MATCHED && route == null)
2594                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2595                        if (routeType == RouteType.UNMATCHED && route != null)
2596                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2597                        requireNonNull(outcome);
2598                }
2599        }
2600
2601        /**
2602         * Key for metrics grouped by Server-Sent Event route match information and drop reason.
2603         *
2604         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2605         */
2606        record SseEventRouteDropKey(@NonNull RouteType routeType,
2607                                                                                                                                                 @Nullable ResourcePathDeclaration route,
2608                                                                                                                                                 @NonNull SseEventDropReason dropReason) {
2609                public SseEventRouteDropKey {
2610                        requireNonNull(routeType);
2611                        if (routeType == RouteType.MATCHED && route == null)
2612                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2613                        if (routeType == RouteType.UNMATCHED && route != null)
2614                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2615                        requireNonNull(dropReason);
2616                }
2617        }
2618
2619        /**
2620         * Key for metrics grouped by Server-Sent Event comment type, route match information, and drop reason.
2621         *
2622         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2623         */
2624        record SseCommentRouteDropKey(@NonNull RouteType routeType,
2625                                                                                                                                                                                @Nullable ResourcePathDeclaration route,
2626                                                                                                                                                                                SseComment.@NonNull CommentType commentType,
2627                                                                                                                                                                                @NonNull SseEventDropReason dropReason) {
2628                public SseCommentRouteDropKey {
2629                        requireNonNull(routeType);
2630                        requireNonNull(commentType);
2631                        if (routeType == RouteType.MATCHED && route == null)
2632                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2633                        if (routeType == RouteType.UNMATCHED && route != null)
2634                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2635                        requireNonNull(dropReason);
2636                }
2637        }
2638
2639        /**
2640         * Key for metrics grouped by Server-Sent Event stream route match information and termination reason.
2641         *
2642         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2643         */
2644        record SseStreamRouteTerminationKey(@NonNull RouteType routeType,
2645                                                                                                                                                                                @Nullable ResourcePathDeclaration route,
2646                                                                                                                                                                                @NonNull StreamTerminationReason terminationReason) {
2647                public SseStreamRouteTerminationKey {
2648                        requireNonNull(routeType);
2649                        if (routeType == RouteType.MATCHED && route == null)
2650                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2651                        if (routeType == RouteType.UNMATCHED && route != null)
2652                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2653                        requireNonNull(terminationReason);
2654                }
2655        }
2656
2657        /**
2658         * Key for metrics grouped by MCP endpoint class, JSON-RPC method, and request outcome.
2659         *
2660         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2661         */
2662        record McpEndpointRequestOutcomeKey(@NonNull Class<? extends McpEndpoint> endpointClass,
2663                                                                                                                                                        @NonNull String jsonRpcMethod,
2664                                                                                                                                                        @NonNull McpRequestOutcome requestOutcome) {
2665                public McpEndpointRequestOutcomeKey {
2666                        requireNonNull(endpointClass);
2667                        requireNonNull(jsonRpcMethod);
2668                        requireNonNull(requestOutcome);
2669                }
2670        }
2671
2672        /**
2673         * Key for metrics grouped by MCP endpoint class and session termination reason.
2674         *
2675         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2676         */
2677        record McpEndpointSessionTerminationKey(@NonNull Class<? extends McpEndpoint> endpointClass,
2678                                                                                                                                                                        @NonNull McpSessionTerminationReason terminationReason) {
2679                public McpEndpointSessionTerminationKey {
2680                        requireNonNull(endpointClass);
2681                        requireNonNull(terminationReason);
2682                }
2683        }
2684
2685        /**
2686         * Key for metrics grouped by MCP endpoint class and SSE stream termination reason.
2687         *
2688         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2689         */
2690        record McpEndpointSseStreamTerminationKey(@NonNull Class<? extends McpEndpoint> endpointClass,
2691                                                                                                                                                                 @NonNull StreamTerminationReason terminationReason) {
2692                public McpEndpointSseStreamTerminationKey {
2693                        requireNonNull(endpointClass);
2694                        requireNonNull(terminationReason);
2695                }
2696        }
2697
2698        /**
2699         * Acquires a threadsafe {@link MetricsCollector} instance with sensible defaults.
2700         * <p>
2701         * This method is guaranteed to return a new instance.
2702         *
2703         * @return a {@code MetricsCollector} with default settings
2704         */
2705        @NonNull
2706        static MetricsCollector defaultInstance() {
2707                return DefaultMetricsCollector.defaultInstance();
2708        }
2709
2710        /**
2711         * Acquires a threadsafe {@link MetricsCollector} instance that performs no work.
2712         * <p>
2713         * This method is useful when you want to explicitly disable metrics collection without writing your own implementation.
2714         * <p>
2715         * The returned instance is guaranteed to be a JVM-wide singleton.
2716         *
2717         * @return a no-op {@code MetricsCollector}
2718         */
2719        @NonNull
2720        static MetricsCollector disabledInstance() {
2721                return DisabledMetricsCollector.defaultInstance();
2722        }
2723}