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