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.withServer(Server.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.withServer(Server.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 before an SSE connection is established.
278         */
279        default void willEstablishServerSentEventConnection(@NonNull Request request,
280                                                                                                                                                                                                                        @Nullable ResourceMethod resourceMethod) {
281                // No-op by default
282        }
283
284        /**
285         * Called after an SSE connection is established.
286         */
287        default void didEstablishServerSentEventConnection(@NonNull ServerSentEventConnection serverSentEventConnection) {
288                // No-op by default
289        }
290
291        /**
292         * Called if an SSE connection fails to establish.
293         *
294         * @param reason    the handshake failure reason
295         * @param throwable an optional underlying cause, or {@code null} if not applicable
296         */
297        default void didFailToEstablishServerSentEventConnection(@NonNull Request request,
298                                                                                                                                                                                                                                         @Nullable ResourceMethod resourceMethod,
299                                                                                                                                                                                                                                         ServerSentEventConnection.@NonNull HandshakeFailureReason reason,
300                                                                                                                                                                                                                                         @Nullable Throwable throwable) {
301                // No-op by default
302        }
303
304        /**
305         * Called before an SSE connection is terminated.
306         */
307        default void willTerminateServerSentEventConnection(@NonNull ServerSentEventConnection serverSentEventConnection,
308                                                                                                                                                                                                                        ServerSentEventConnection.@NonNull TerminationReason terminationReason,
309                                                                                                                                                                                                                        @Nullable Throwable throwable) {
310                // No-op by default
311        }
312
313        /**
314         * Called after an SSE connection is terminated.
315         */
316        default void didTerminateServerSentEventConnection(@NonNull ServerSentEventConnection serverSentEventConnection,
317                                                                                                                                                                                                                 @NonNull Duration connectionDuration,
318                                                                                                                                                                                                                 ServerSentEventConnection.@NonNull TerminationReason terminationReason,
319                                                                                                                                                                                                                 @Nullable Throwable throwable) {
320                // No-op by default
321        }
322
323        /**
324         * Called before an SSE event is written.
325         */
326        default void willWriteServerSentEvent(@NonNull ServerSentEventConnection serverSentEventConnection,
327                                                                                                                                                                @NonNull ServerSentEvent serverSentEvent) {
328                // No-op by default
329        }
330
331        /**
332         * Called after an SSE event is written.
333         *
334         * @param serverSentEventConnection the connection the event was written to
335         * @param serverSentEvent           the event that was written
336         * @param writeDuration             how long it took to write the event
337         * @param deliveryLag               elapsed time between enqueue and write start, or {@code null} if unknown
338         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
339         * @param queueDepth                number of queued elements remaining at write time, or {@code null} if unknown
340         */
341        default void didWriteServerSentEvent(@NonNull ServerSentEventConnection serverSentEventConnection,
342                                                                                                                                                         @NonNull ServerSentEvent serverSentEvent,
343                                                                                                                                                         @NonNull Duration writeDuration,
344                                                                                                                                                         @Nullable Duration deliveryLag,
345                                                                                                                                                         @Nullable Integer payloadBytes,
346                                                                                                                                                         @Nullable Integer queueDepth) {
347                // No-op by default
348        }
349
350        /**
351         * Called after an SSE event fails to write.
352         *
353         * @param serverSentEventConnection the connection the event was written to
354         * @param serverSentEvent           the event that was written
355         * @param writeDuration             how long it took to attempt the write
356         * @param throwable                 the failure cause
357         * @param deliveryLag               elapsed time between enqueue and write start, or {@code null} if unknown
358         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
359         * @param queueDepth                number of queued elements remaining at write time, or {@code null} if unknown
360         */
361        default void didFailToWriteServerSentEvent(@NonNull ServerSentEventConnection serverSentEventConnection,
362                                                                                                                                                                                 @NonNull ServerSentEvent serverSentEvent,
363                                                                                                                                                                                 @NonNull Duration writeDuration,
364                                                                                                                                                                                 @NonNull Throwable throwable,
365                                                                                                                                                                                 @Nullable Duration deliveryLag,
366                                                                                                                                                                                 @Nullable Integer payloadBytes,
367                                                                                                                                                                                 @Nullable Integer queueDepth) {
368                // No-op by default
369        }
370
371        /**
372         * Called before an SSE comment is written.
373         */
374        default void willWriteServerSentEventComment(@NonNull ServerSentEventConnection serverSentEventConnection,
375                                                                                                                                                                                         @NonNull ServerSentEventComment serverSentEventComment) {
376                // No-op by default
377        }
378
379        /**
380         * Called after an SSE comment is written.
381         *
382         * @param serverSentEventConnection the connection the comment was written to
383         * @param serverSentEventComment    the comment that was written
384         * @param writeDuration             how long it took to write the comment
385         * @param deliveryLag               elapsed time between enqueue and write start, or {@code null} if unknown
386         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
387         * @param queueDepth                number of queued elements remaining at write time, or {@code null} if unknown
388         */
389        default void didWriteServerSentEventComment(@NonNull ServerSentEventConnection serverSentEventConnection,
390                                                                                                                                                                                        @NonNull ServerSentEventComment serverSentEventComment,
391                                                                                                                                                                                        @NonNull Duration writeDuration,
392                                                                                                                                                                                        @Nullable Duration deliveryLag,
393                                                                                                                                                                                        @Nullable Integer payloadBytes,
394                                                                                                                                                                                        @Nullable Integer queueDepth) {
395                // No-op by default
396        }
397
398        /**
399         * Called after an SSE comment fails to write.
400         *
401         * @param serverSentEventConnection the connection the comment was written to
402         * @param serverSentEventComment    the comment that was written
403         * @param writeDuration             how long it took to attempt the write
404         * @param throwable                 the failure cause
405         * @param deliveryLag               elapsed time between enqueue and write start, or {@code null} if unknown
406         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
407         * @param queueDepth                number of queued elements remaining at write time, or {@code null} if unknown
408         */
409        default void didFailToWriteServerSentEventComment(@NonNull ServerSentEventConnection serverSentEventConnection,
410                                                                                                                                                                                                                @NonNull ServerSentEventComment serverSentEventComment,
411                                                                                                                                                                                                                @NonNull Duration writeDuration,
412                                                                                                                                                                                                                @NonNull Throwable throwable,
413                                                                                                                                                                                                                @Nullable Duration deliveryLag,
414                                                                                                                                                                                                                @Nullable Integer payloadBytes,
415                                                                                                                                                                                                                @Nullable Integer queueDepth) {
416                // No-op by default
417        }
418
419        /**
420         * Called after an SSE event is dropped before it can be enqueued for delivery.
421         *
422         * @param serverSentEventConnection the connection the event was targeting
423         * @param serverSentEvent           the event that was dropped
424         * @param reason                    the drop reason
425         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
426         * @param queueDepth                number of queued elements at drop time, or {@code null} if unknown
427         */
428        default void didDropServerSentEvent(@NonNull ServerSentEventConnection serverSentEventConnection,
429                                                                                                                                                        @NonNull ServerSentEvent serverSentEvent,
430                                                                                                                                                        @NonNull ServerSentEventDropReason reason,
431                                                                                                                                                        @Nullable Integer payloadBytes,
432                                                                                                                                                        @Nullable Integer queueDepth) {
433                // No-op by default
434        }
435
436        /**
437         * Called after an SSE comment is dropped before it can be enqueued for delivery.
438         *
439         * @param serverSentEventConnection the connection the comment was targeting
440         * @param serverSentEventComment    the comment that was dropped
441         * @param reason                    the drop reason
442         * @param payloadBytes              size of the serialized payload in bytes, or {@code null} if unknown
443         * @param queueDepth                number of queued elements at drop time, or {@code null} if unknown
444         */
445        default void didDropServerSentEventComment(@NonNull ServerSentEventConnection serverSentEventConnection,
446                                                                                                                                                                                 @NonNull ServerSentEventComment serverSentEventComment,
447                                                                                                                                                                                 @NonNull ServerSentEventDropReason reason,
448                                                                                                                                                                                 @Nullable Integer payloadBytes,
449                                                                                                                                                                                 @Nullable Integer queueDepth) {
450                // No-op by default
451        }
452
453        /**
454         * Called after a broadcast attempt for a Server-Sent Event payload.
455         *
456         * @param route     the route declaration that was broadcast to
457         * @param attempted number of connections targeted
458         * @param enqueued  number of connections for which enqueue succeeded
459         * @param dropped   number of connections for which enqueue failed
460         */
461        default void didBroadcastServerSentEvent(@NonNull ResourcePathDeclaration route,
462                                                                                                                                                                 int attempted,
463                                                                                                                                                                 int enqueued,
464                                                                                                                                                                 int dropped) {
465                // No-op by default
466        }
467
468        /**
469         * Called after a broadcast attempt for a Server-Sent Event comment payload.
470         *
471         * @param route       the route declaration that was broadcast to
472         * @param commentType the comment type
473         * @param attempted   number of connections targeted
474         * @param enqueued    number of connections for which enqueue succeeded
475         * @param dropped     number of connections for which enqueue failed
476         */
477        default void didBroadcastServerSentEventComment(@NonNull ResourcePathDeclaration route,
478                                                                                                                                                                                                        ServerSentEventComment.@NonNull CommentType commentType,
479                                                                                                                                                                                                        int attempted,
480                                                                                                                                                                                                        int enqueued,
481                                                                                                                                                                                                        int dropped) {
482                // No-op by default
483        }
484
485        /**
486         * Returns a snapshot of metrics collected so far, if supported.
487         *
488         * @return an optional metrics snapshot
489         */
490        @NonNull
491        default Optional<Snapshot> snapshot() {
492                return Optional.empty();
493        }
494
495        /**
496         * Returns a text snapshot of metrics collected so far, if supported.
497         * <p>
498         * The default collector supports Prometheus (text format v0.0.4) and OpenMetrics (1.0) text exposition formats.
499         *
500         * @param options the snapshot rendering options
501         * @return a textual metrics snapshot, or {@link Optional#empty()} if unsupported
502         */
503        @NonNull
504        default Optional<String> snapshotText(@NonNull SnapshotTextOptions options) {
505                requireNonNull(options);
506                return Optional.empty();
507        }
508
509        /**
510         * Resets any in-memory metrics state, if supported.
511         */
512        default void reset() {
513                // No-op by default
514        }
515
516        /**
517         * Text format to use for {@link #snapshotText(SnapshotTextOptions)}.
518         * <p>
519         * This controls serialization only; the underlying metrics data is unchanged.
520         */
521        enum MetricsFormat {
522                /**
523                 * Prometheus text exposition format (v0.0.4).
524                 */
525                PROMETHEUS,
526                /**
527                 * OpenMetrics text exposition format (1.0), including the {@code # EOF} trailer.
528                 */
529                OPEN_METRICS_1_0
530        }
531
532        /**
533         * Options for rendering a textual metrics snapshot.
534         * <p>
535         * Use {@link #withMetricsFormat(MetricsFormat)} to obtain a builder and customize output.
536         * <p>
537         * Key options:
538         * <ul>
539         *   <li>{@code metricFilter} allows per-sample filtering by name and labels</li>
540         *   <li>{@code histogramFormat} controls bucket vs count/sum output</li>
541         *   <li>{@code includeZeroBuckets} drops empty bucket samples when false</li>
542         * </ul>
543         */
544        @ThreadSafe
545        final class SnapshotTextOptions {
546                @NonNull
547                private final MetricsFormat metricsFormat;
548                @Nullable
549                private final Predicate<MetricSample> metricFilter;
550                @NonNull
551                private final HistogramFormat histogramFormat;
552                @NonNull
553                private final Boolean includeZeroBuckets;
554
555                private SnapshotTextOptions(@NonNull Builder builder) {
556                        requireNonNull(builder);
557
558                        this.metricsFormat = requireNonNull(builder.metricsFormat);
559                        this.metricFilter = builder.metricFilter;
560                        this.histogramFormat = requireNonNull(builder.histogramFormat);
561                        this.includeZeroBuckets = builder.includeZeroBuckets == null ? true : builder.includeZeroBuckets;
562                }
563
564                /**
565                 * Begins building options with the specified format.
566                 *
567                 * @param metricsFormat the text exposition format
568                 * @return a builder seeded with the format
569                 */
570                @NonNull
571                public static Builder withMetricsFormat(@NonNull MetricsFormat metricsFormat) {
572                        return new Builder(metricsFormat);
573                }
574
575                /**
576                 * Creates options with the specified format and defaults for all other fields.
577                 *
578                 * @param metricsFormat the text exposition format
579                 * @return a {@link SnapshotTextOptions} instance
580                 */
581                @NonNull
582                public static SnapshotTextOptions fromMetricsFormat(@NonNull MetricsFormat metricsFormat) {
583                        return withMetricsFormat(metricsFormat).build();
584                }
585
586                /**
587                 * The text exposition format to emit.
588                 *
589                 * @return the metrics format
590                 */
591                @NonNull
592                public MetricsFormat getMetricsFormat() {
593                        return this.metricsFormat;
594                }
595
596                /**
597                 * Optional filter for rendered samples.
598                 *
599                 * @return the filter, if present
600                 */
601                @NonNull
602                public Optional<Predicate<MetricSample>> getMetricFilter() {
603                        return Optional.ofNullable(this.metricFilter);
604                }
605
606                /**
607                 * The histogram rendering strategy.
608                 *
609                 * @return the histogram format
610                 */
611                @NonNull
612                public HistogramFormat getHistogramFormat() {
613                        return this.histogramFormat;
614                }
615
616                /**
617                 * Whether zero-count buckets should be emitted.
618                 *
619                 * @return {@code true} if zero-count buckets are included
620                 */
621                @NonNull
622                public Boolean getIncludeZeroBuckets() {
623                        return this.includeZeroBuckets;
624                }
625
626                /**
627                 * Supported histogram rendering strategies.
628                 */
629                public enum HistogramFormat {
630                        /**
631                         * Emit full histogram series (buckets, count, and sum).
632                         */
633                        FULL_BUCKETS,
634                        /**
635                         * Emit only {@code _count} and {@code _sum} samples (omit buckets).
636                         */
637                        COUNT_SUM_ONLY,
638                        /**
639                         * Suppress histogram output entirely.
640                         */
641                        NONE
642                }
643
644                /**
645                 * A single text-format sample with its label set.
646                 * <p>
647                 * Filters receive these instances for each rendered sample. For histogram buckets,
648                 * the sample name includes {@code _bucket} and the labels include {@code le}.
649                 * Label maps are immutable and preserve insertion order.
650                 */
651                public static final class MetricSample {
652                        @NonNull
653                        private final String name;
654                        @NonNull
655                        private final Map<@NonNull String, @NonNull String> labels;
656
657                        /**
658                         * Creates a metrics sample definition.
659                         *
660                         * @param name   the sample name (e.g. {@code soklet_http_request_duration_nanos_bucket})
661                         * @param labels the sample labels
662                         */
663                        public MetricSample(@NonNull String name,
664                                                                                                        @NonNull Map<@NonNull String, @NonNull String> labels) {
665                                this.name = requireNonNull(name);
666                                this.labels = Collections.unmodifiableMap(new LinkedHashMap<>(requireNonNull(labels)));
667                        }
668
669                        /**
670                         * The name for this sample.
671                         *
672                         * @return the sample name
673                         */
674                        @NonNull
675                        public String getName() {
676                                return this.name;
677                        }
678
679                        /**
680                         * The label set for this sample.
681                         *
682                         * @return immutable labels
683                         */
684                        @NonNull
685                        public Map<@NonNull String, @NonNull String> getLabels() {
686                                return this.labels;
687                        }
688                }
689
690                /**
691                 * Builder for {@link SnapshotTextOptions}.
692                 * <p>
693                 * Defaults are {@link HistogramFormat#FULL_BUCKETS} and {@code includeZeroBuckets=true}.
694                 */
695                @ThreadSafe
696                public static final class Builder {
697                        @NonNull
698                        private final MetricsFormat metricsFormat;
699                        @Nullable
700                        private Predicate<MetricSample> metricFilter;
701                        @NonNull
702                        private HistogramFormat histogramFormat;
703                        @Nullable
704                        private Boolean includeZeroBuckets;
705
706                        private Builder(@NonNull MetricsFormat metricsFormat) {
707                                this.metricsFormat = requireNonNull(metricsFormat);
708                                this.histogramFormat = HistogramFormat.FULL_BUCKETS;
709                                this.includeZeroBuckets = true;
710                        }
711
712                        /**
713                         * Sets an optional per-sample filter.
714                         *
715                         * @param metricFilter the filter to apply, or {@code null} to disable filtering
716                         * @return this builder
717                         */
718                        @NonNull
719                        public Builder metricFilter(@Nullable Predicate<MetricSample> metricFilter) {
720                                this.metricFilter = metricFilter;
721                                return this;
722                        }
723
724                        /**
725                         * Sets how histograms are rendered in the text snapshot.
726                         *
727                         * @param histogramFormat the histogram format
728                         * @return this builder
729                         */
730                        @NonNull
731                        public Builder histogramFormat(@NonNull HistogramFormat histogramFormat) {
732                                this.histogramFormat = requireNonNull(histogramFormat);
733                                return this;
734                        }
735
736                        /**
737                         * Controls whether zero-count buckets are emitted.
738                         *
739                         * @param includeZeroBuckets {@code true} to include zero-count buckets, {@code false} to omit them
740                         * @return this builder
741                         */
742                        @NonNull
743                        public Builder includeZeroBuckets(@Nullable Boolean includeZeroBuckets) {
744                                this.includeZeroBuckets = includeZeroBuckets;
745                                return this;
746                        }
747
748                        /**
749                         * Builds a {@link SnapshotTextOptions} instance.
750                         *
751                         * @return the built options
752                         */
753                        @NonNull
754                        public SnapshotTextOptions build() {
755                                return new SnapshotTextOptions(this);
756                        }
757                }
758        }
759
760        /**
761         * Immutable snapshot of collected metrics.
762         * <p>
763         * Durations are in nanoseconds, sizes are in bytes, and queue depths are raw counts.
764         * Histogram values are captured as {@link HistogramSnapshot} instances.
765         * Connection counts report total accepted/rejected connections for the HTTP and SSE servers.
766         * Request read failures and request rejections are reported separately for HTTP and SSE traffic.
767         * Instances are typically produced by {@link MetricsCollector#snapshot()} but can also be built
768         * manually via {@link #builder()}.
769         *
770         * @author <a href="https://www.revetkn.com">Mark Allen</a>
771         */
772        @ThreadSafe
773        final class Snapshot {
774                @NonNull
775                private final Long activeRequests;
776                @NonNull
777                private final Long activeSseConnections;
778                @NonNull
779                private final Long httpConnectionsAccepted;
780                @NonNull
781                private final Long httpConnectionsRejected;
782                @NonNull
783                private final Long sseConnectionsAccepted;
784                @NonNull
785                private final Long sseConnectionsRejected;
786                @NonNull
787                private final Map<@NonNull RequestReadFailureKey, @NonNull Long> httpRequestReadFailures;
788                @NonNull
789                private final Map<@NonNull RequestRejectionKey, @NonNull Long> httpRequestRejections;
790                @NonNull
791                private final Map<@NonNull RequestReadFailureKey, @NonNull Long> sseRequestReadFailures;
792                @NonNull
793                private final Map<@NonNull RequestRejectionKey, @NonNull Long> sseRequestRejections;
794                @NonNull
795                private final Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpRequestDurations;
796                @NonNull
797                private final Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpHandlerDurations;
798                @NonNull
799                private final Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpTimeToFirstByte;
800                @NonNull
801                private final Map<@NonNull ServerRouteKey, @NonNull HistogramSnapshot> httpRequestBodyBytes;
802                @NonNull
803                private final Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpResponseBodyBytes;
804                @NonNull
805                private final Map<@NonNull ServerSentEventRouteKey, @NonNull Long> sseHandshakesAccepted;
806                @NonNull
807                private final Map<@NonNull ServerSentEventRouteHandshakeFailureKey, @NonNull Long> sseHandshakesRejected;
808                @NonNull
809                private final Map<@NonNull ServerSentEventRouteEnqueueOutcomeKey, @NonNull Long> sseEventEnqueueOutcomes;
810                @NonNull
811                private final Map<@NonNull ServerSentEventCommentRouteEnqueueOutcomeKey, @NonNull Long> sseCommentEnqueueOutcomes;
812                @NonNull
813                private final Map<@NonNull ServerSentEventRouteDropKey, @NonNull Long> sseEventDrops;
814                @NonNull
815                private final Map<@NonNull ServerSentEventCommentRouteDropKey, @NonNull Long> sseCommentDrops;
816                @NonNull
817                private final Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseTimeToFirstEvent;
818                @NonNull
819                private final Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventWriteDurations;
820                @NonNull
821                private final Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventDeliveryLag;
822                @NonNull
823                private final Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventSizes;
824                @NonNull
825                private final Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseQueueDepth;
826                @NonNull
827                private final Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentDeliveryLag;
828                @NonNull
829                private final Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentSizes;
830                @NonNull
831                private final Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentQueueDepth;
832                @NonNull
833                private final Map<@NonNull ServerSentEventRouteTerminationKey, @NonNull HistogramSnapshot> sseConnectionDurations;
834
835                /**
836                 * Acquires an "empty" builder for {@link Snapshot} instances.
837                 *
838                 * @return the builder
839                 */
840                @NonNull
841                public static Builder builder() {
842                        return new Builder();
843                }
844
845                private Snapshot(@NonNull Builder builder) {
846                        requireNonNull(builder);
847
848                        this.activeRequests = requireNonNull(builder.activeRequests);
849                        this.activeSseConnections = requireNonNull(builder.activeSseConnections);
850                        this.httpConnectionsAccepted = requireNonNull(builder.httpConnectionsAccepted);
851                        this.httpConnectionsRejected = requireNonNull(builder.httpConnectionsRejected);
852                        this.sseConnectionsAccepted = requireNonNull(builder.sseConnectionsAccepted);
853                        this.sseConnectionsRejected = requireNonNull(builder.sseConnectionsRejected);
854                        this.httpRequestReadFailures = copyOrEmpty(builder.httpRequestReadFailures);
855                        this.httpRequestRejections = copyOrEmpty(builder.httpRequestRejections);
856                        this.sseRequestReadFailures = copyOrEmpty(builder.sseRequestReadFailures);
857                        this.sseRequestRejections = copyOrEmpty(builder.sseRequestRejections);
858                        this.httpRequestDurations = copyOrEmpty(builder.httpRequestDurations);
859                        this.httpHandlerDurations = copyOrEmpty(builder.httpHandlerDurations);
860                        this.httpTimeToFirstByte = copyOrEmpty(builder.httpTimeToFirstByte);
861                        this.httpRequestBodyBytes = copyOrEmpty(builder.httpRequestBodyBytes);
862                        this.httpResponseBodyBytes = copyOrEmpty(builder.httpResponseBodyBytes);
863                        this.sseHandshakesAccepted = copyOrEmpty(builder.sseHandshakesAccepted);
864                        this.sseHandshakesRejected = copyOrEmpty(builder.sseHandshakesRejected);
865                        this.sseEventEnqueueOutcomes = copyOrEmpty(builder.sseEventEnqueueOutcomes);
866                        this.sseCommentEnqueueOutcomes = copyOrEmpty(builder.sseCommentEnqueueOutcomes);
867                        this.sseEventDrops = copyOrEmpty(builder.sseEventDrops);
868                        this.sseCommentDrops = copyOrEmpty(builder.sseCommentDrops);
869                        this.sseTimeToFirstEvent = copyOrEmpty(builder.sseTimeToFirstEvent);
870                        this.sseEventWriteDurations = copyOrEmpty(builder.sseEventWriteDurations);
871                        this.sseEventDeliveryLag = copyOrEmpty(builder.sseEventDeliveryLag);
872                        this.sseEventSizes = copyOrEmpty(builder.sseEventSizes);
873                        this.sseQueueDepth = copyOrEmpty(builder.sseQueueDepth);
874                        this.sseCommentDeliveryLag = copyOrEmpty(builder.sseCommentDeliveryLag);
875                        this.sseCommentSizes = copyOrEmpty(builder.sseCommentSizes);
876                        this.sseCommentQueueDepth = copyOrEmpty(builder.sseCommentQueueDepth);
877                        this.sseConnectionDurations = copyOrEmpty(builder.sseConnectionDurations);
878                }
879
880                /**
881                 * Returns the number of active HTTP requests.
882                 *
883                 * @return the active HTTP request count
884                 */
885                @NonNull
886                public Long getActiveRequests() {
887                        return this.activeRequests;
888                }
889
890                /**
891                 * Returns the number of active server-sent event connections.
892                 *
893                 * @return the active SSE connection count
894                 */
895                @NonNull
896                public Long getActiveSseConnections() {
897                        return this.activeSseConnections;
898                }
899
900                /**
901                 * Returns the total number of accepted HTTP connections.
902                 *
903                 * @return total accepted HTTP connections
904                 */
905                @NonNull
906                public Long getHttpConnectionsAccepted() {
907                        return this.httpConnectionsAccepted;
908                }
909
910                /**
911                 * Returns the total number of rejected HTTP connections.
912                 *
913                 * @return total rejected HTTP connections
914                 */
915                @NonNull
916                public Long getHttpConnectionsRejected() {
917                        return this.httpConnectionsRejected;
918                }
919
920                /**
921                 * Returns the total number of accepted SSE connections.
922                 *
923                 * @return total accepted SSE connections
924                 */
925                @NonNull
926                public Long getSseConnectionsAccepted() {
927                        return this.sseConnectionsAccepted;
928                }
929
930                /**
931                 * Returns the total number of rejected SSE connections.
932                 *
933                 * @return total rejected SSE connections
934                 */
935                @NonNull
936                public Long getSseConnectionsRejected() {
937                        return this.sseConnectionsRejected;
938                }
939
940                /**
941                 * Returns HTTP request read failure counters keyed by failure reason.
942                 *
943                 * @return HTTP request read failure counters
944                 */
945                @NonNull
946                public Map<@NonNull RequestReadFailureKey, @NonNull Long> getHttpRequestReadFailures() {
947                        return this.httpRequestReadFailures;
948                }
949
950                /**
951                 * Returns HTTP request rejection counters keyed by rejection reason.
952                 *
953                 * @return HTTP request rejection counters
954                 */
955                @NonNull
956                public Map<@NonNull RequestRejectionKey, @NonNull Long> getHttpRequestRejections() {
957                        return this.httpRequestRejections;
958                }
959
960                /**
961                 * Returns SSE request read failure counters keyed by failure reason.
962                 *
963                 * @return SSE request read failure counters
964                 */
965                @NonNull
966                public Map<@NonNull RequestReadFailureKey, @NonNull Long> getSseRequestReadFailures() {
967                        return this.sseRequestReadFailures;
968                }
969
970                /**
971                 * Returns SSE request rejection counters keyed by rejection reason.
972                 *
973                 * @return SSE request rejection counters
974                 */
975                @NonNull
976                public Map<@NonNull RequestRejectionKey, @NonNull Long> getSseRequestRejections() {
977                        return this.sseRequestRejections;
978                }
979
980                /**
981                 * Returns HTTP request duration histograms keyed by server route and status class.
982                 *
983                 * @return HTTP request duration histograms
984                 */
985                @NonNull
986                public Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpRequestDurations() {
987                        return this.httpRequestDurations;
988                }
989
990                /**
991                 * Returns HTTP handler duration histograms keyed by server route and status class.
992                 *
993                 * @return HTTP handler duration histograms
994                 */
995                @NonNull
996                public Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpHandlerDurations() {
997                        return this.httpHandlerDurations;
998                }
999
1000                /**
1001                 * Returns HTTP time-to-first-byte histograms keyed by server route and status class.
1002                 *
1003                 * @return HTTP time-to-first-byte histograms
1004                 */
1005                @NonNull
1006                public Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpTimeToFirstByte() {
1007                        return this.httpTimeToFirstByte;
1008                }
1009
1010                /**
1011                 * Returns HTTP request body size histograms keyed by server route.
1012                 *
1013                 * @return HTTP request body size histograms
1014                 */
1015                @NonNull
1016                public Map<@NonNull ServerRouteKey, @NonNull HistogramSnapshot> getHttpRequestBodyBytes() {
1017                        return this.httpRequestBodyBytes;
1018                }
1019
1020                /**
1021                 * Returns HTTP response body size histograms keyed by server route and status class.
1022                 *
1023                 * @return HTTP response body size histograms
1024                 */
1025                @NonNull
1026                public Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> getHttpResponseBodyBytes() {
1027                        return this.httpResponseBodyBytes;
1028                }
1029
1030                /**
1031                 * Returns SSE handshake acceptance counters keyed by route.
1032                 *
1033                 * @return SSE handshake acceptance counters
1034                 */
1035                @NonNull
1036                public Map<@NonNull ServerSentEventRouteKey, @NonNull Long> getSseHandshakesAccepted() {
1037                        return this.sseHandshakesAccepted;
1038                }
1039
1040                /**
1041                 * Returns SSE handshake rejection counters keyed by route and failure reason.
1042                 *
1043                 * @return SSE handshake rejection counters
1044                 */
1045                @NonNull
1046                public Map<@NonNull ServerSentEventRouteHandshakeFailureKey, @NonNull Long> getSseHandshakesRejected() {
1047                        return this.sseHandshakesRejected;
1048                }
1049
1050                /**
1051                 * Returns SSE event enqueue outcome counters keyed by route and outcome.
1052                 *
1053                 * @return SSE event enqueue outcome counters
1054                 */
1055                @NonNull
1056                public Map<@NonNull ServerSentEventRouteEnqueueOutcomeKey, @NonNull Long> getSseEventEnqueueOutcomes() {
1057                        return this.sseEventEnqueueOutcomes;
1058                }
1059
1060                /**
1061                 * Returns SSE comment enqueue outcome counters keyed by route, comment type, and outcome.
1062                 *
1063                 * @return SSE comment enqueue outcome counters
1064                 */
1065                @NonNull
1066                public Map<@NonNull ServerSentEventCommentRouteEnqueueOutcomeKey, @NonNull Long> getSseCommentEnqueueOutcomes() {
1067                        return this.sseCommentEnqueueOutcomes;
1068                }
1069
1070                /**
1071                 * Returns SSE event drop counters keyed by route and drop reason.
1072                 *
1073                 * @return SSE event drop counters
1074                 */
1075                @NonNull
1076                public Map<@NonNull ServerSentEventRouteDropKey, @NonNull Long> getSseEventDrops() {
1077                        return this.sseEventDrops;
1078                }
1079
1080                /**
1081                 * Returns SSE comment drop counters keyed by route, comment type, and drop reason.
1082                 *
1083                 * @return SSE comment drop counters
1084                 */
1085                @NonNull
1086                public Map<@NonNull ServerSentEventCommentRouteDropKey, @NonNull Long> getSseCommentDrops() {
1087                        return this.sseCommentDrops;
1088                }
1089
1090                /**
1091                 * Returns SSE time-to-first-event histograms keyed by route.
1092                 *
1093                 * @return SSE time-to-first-event histograms
1094                 */
1095                @NonNull
1096                public Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> getSseTimeToFirstEvent() {
1097                        return this.sseTimeToFirstEvent;
1098                }
1099
1100                /**
1101                 * Returns SSE event write duration histograms keyed by route.
1102                 *
1103                 * @return SSE event write duration histograms
1104                 */
1105                @NonNull
1106                public Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> getSseEventWriteDurations() {
1107                        return this.sseEventWriteDurations;
1108                }
1109
1110                /**
1111                 * Returns SSE event delivery lag histograms keyed by route.
1112                 *
1113                 * @return SSE event delivery lag histograms
1114                 */
1115                @NonNull
1116                public Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> getSseEventDeliveryLag() {
1117                        return this.sseEventDeliveryLag;
1118                }
1119
1120                /**
1121                 * Returns SSE event size histograms keyed by route.
1122                 *
1123                 * @return SSE event size histograms
1124                 */
1125                @NonNull
1126                public Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> getSseEventSizes() {
1127                        return this.sseEventSizes;
1128                }
1129
1130                /**
1131                 * Returns SSE queue depth histograms keyed by route.
1132                 *
1133                 * @return SSE queue depth histograms
1134                 */
1135                @NonNull
1136                public Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> getSseQueueDepth() {
1137                        return this.sseQueueDepth;
1138                }
1139
1140                /**
1141                 * Returns SSE comment delivery lag histograms keyed by route and comment type.
1142                 *
1143                 * @return SSE comment delivery lag histograms
1144                 */
1145                @NonNull
1146                public Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentDeliveryLag() {
1147                        return this.sseCommentDeliveryLag;
1148                }
1149
1150                /**
1151                 * Returns SSE comment size histograms keyed by route and comment type.
1152                 *
1153                 * @return SSE comment size histograms
1154                 */
1155                @NonNull
1156                public Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentSizes() {
1157                        return this.sseCommentSizes;
1158                }
1159
1160                /**
1161                 * Returns SSE comment queue depth histograms keyed by route and comment type.
1162                 *
1163                 * @return SSE comment queue depth histograms
1164                 */
1165                @NonNull
1166                public Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> getSseCommentQueueDepth() {
1167                        return this.sseCommentQueueDepth;
1168                }
1169
1170                /**
1171                 * Returns SSE connection duration histograms keyed by route and termination reason.
1172                 *
1173                 * @return SSE connection duration histograms
1174                 */
1175                @NonNull
1176                public Map<@NonNull ServerSentEventRouteTerminationKey, @NonNull HistogramSnapshot> getSseConnectionDurations() {
1177                        return this.sseConnectionDurations;
1178                }
1179
1180                @NonNull
1181                private static <K, V> Map<K, V> copyOrEmpty(@Nullable Map<K, V> map) {
1182                        return map == null ? Map.of() : Map.copyOf(map);
1183                }
1184
1185                /**
1186                 * Builder used to construct instances of {@link Snapshot}.
1187                 * <p>
1188                 * This class is intended for use by a single thread.
1189                 *
1190                 * @author <a href="https://www.revetkn.com">Mark Allen</a>
1191                 */
1192                @NotThreadSafe
1193                public static final class Builder {
1194                        @NonNull
1195                        private Long activeRequests;
1196                        @NonNull
1197                        private Long activeSseConnections;
1198                        @NonNull
1199                        private Long httpConnectionsAccepted;
1200                        @NonNull
1201                        private Long httpConnectionsRejected;
1202                        @NonNull
1203                        private Long sseConnectionsAccepted;
1204                        @NonNull
1205                        private Long sseConnectionsRejected;
1206                        @Nullable
1207                        private Map<@NonNull RequestReadFailureKey, @NonNull Long> httpRequestReadFailures;
1208                        @Nullable
1209                        private Map<@NonNull RequestRejectionKey, @NonNull Long> httpRequestRejections;
1210                        @Nullable
1211                        private Map<@NonNull RequestReadFailureKey, @NonNull Long> sseRequestReadFailures;
1212                        @Nullable
1213                        private Map<@NonNull RequestRejectionKey, @NonNull Long> sseRequestRejections;
1214                        @Nullable
1215                        private Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpRequestDurations;
1216                        @Nullable
1217                        private Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpHandlerDurations;
1218                        @Nullable
1219                        private Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpTimeToFirstByte;
1220                        @Nullable
1221                        private Map<@NonNull ServerRouteKey, @NonNull HistogramSnapshot> httpRequestBodyBytes;
1222                        @Nullable
1223                        private Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpResponseBodyBytes;
1224                        @Nullable
1225                        private Map<@NonNull ServerSentEventRouteKey, @NonNull Long> sseHandshakesAccepted;
1226                        @Nullable
1227                        private Map<@NonNull ServerSentEventRouteHandshakeFailureKey, @NonNull Long> sseHandshakesRejected;
1228                        @Nullable
1229                        private Map<@NonNull ServerSentEventRouteEnqueueOutcomeKey, @NonNull Long> sseEventEnqueueOutcomes;
1230                        @Nullable
1231                        private Map<@NonNull ServerSentEventCommentRouteEnqueueOutcomeKey, @NonNull Long> sseCommentEnqueueOutcomes;
1232                        @Nullable
1233                        private Map<@NonNull ServerSentEventRouteDropKey, @NonNull Long> sseEventDrops;
1234                        @Nullable
1235                        private Map<@NonNull ServerSentEventCommentRouteDropKey, @NonNull Long> sseCommentDrops;
1236                        @Nullable
1237                        private Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseTimeToFirstEvent;
1238                        @Nullable
1239                        private Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventWriteDurations;
1240                        @Nullable
1241                        private Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventDeliveryLag;
1242                        @Nullable
1243                        private Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventSizes;
1244                        @Nullable
1245                        private Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseQueueDepth;
1246                        @Nullable
1247                        private Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentDeliveryLag;
1248                        @Nullable
1249                        private Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentSizes;
1250                        @Nullable
1251                        private Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentQueueDepth;
1252                        @Nullable
1253                        private Map<@NonNull ServerSentEventRouteTerminationKey, @NonNull HistogramSnapshot> sseConnectionDurations;
1254
1255                        private Builder() {
1256                                this.activeRequests = 0L;
1257                                this.activeSseConnections = 0L;
1258                                this.httpConnectionsAccepted = 0L;
1259                                this.httpConnectionsRejected = 0L;
1260                                this.sseConnectionsAccepted = 0L;
1261                                this.sseConnectionsRejected = 0L;
1262                        }
1263
1264                        /**
1265                         * Sets the active HTTP request count.
1266                         *
1267                         * @param activeRequests the active HTTP request count
1268                         * @return this builder
1269                         */
1270                        @NonNull
1271                        public Builder activeRequests(@NonNull Long activeRequests) {
1272                                this.activeRequests = requireNonNull(activeRequests);
1273                                return this;
1274                        }
1275
1276                        /**
1277                         * Sets the active server-sent event connection count.
1278                         *
1279                         * @param activeSseConnections the active SSE connection count
1280                         * @return this builder
1281                         */
1282                        @NonNull
1283                        public Builder activeSseConnections(@NonNull Long activeSseConnections) {
1284                                this.activeSseConnections = requireNonNull(activeSseConnections);
1285                                return this;
1286                        }
1287
1288                        /**
1289                         * Sets the total number of accepted HTTP connections.
1290                         *
1291                         * @param httpConnectionsAccepted total accepted HTTP connections
1292                         * @return this builder
1293                         */
1294                        @NonNull
1295                        public Builder httpConnectionsAccepted(@NonNull Long httpConnectionsAccepted) {
1296                                this.httpConnectionsAccepted = requireNonNull(httpConnectionsAccepted);
1297                                return this;
1298                        }
1299
1300                        /**
1301                         * Sets the total number of rejected HTTP connections.
1302                         *
1303                         * @param httpConnectionsRejected total rejected HTTP connections
1304                         * @return this builder
1305                         */
1306                        @NonNull
1307                        public Builder httpConnectionsRejected(@NonNull Long httpConnectionsRejected) {
1308                                this.httpConnectionsRejected = requireNonNull(httpConnectionsRejected);
1309                                return this;
1310                        }
1311
1312                        /**
1313                         * Sets the total number of accepted SSE connections.
1314                         *
1315                         * @param sseConnectionsAccepted total accepted SSE connections
1316                         * @return this builder
1317                         */
1318                        @NonNull
1319                        public Builder sseConnectionsAccepted(@NonNull Long sseConnectionsAccepted) {
1320                                this.sseConnectionsAccepted = requireNonNull(sseConnectionsAccepted);
1321                                return this;
1322                        }
1323
1324                        /**
1325                         * Sets the total number of rejected SSE connections.
1326                         *
1327                         * @param sseConnectionsRejected total rejected SSE connections
1328                         * @return this builder
1329                         */
1330                        @NonNull
1331                        public Builder sseConnectionsRejected(@NonNull Long sseConnectionsRejected) {
1332                                this.sseConnectionsRejected = requireNonNull(sseConnectionsRejected);
1333                                return this;
1334                        }
1335
1336                        /**
1337                         * Sets HTTP request read failure counters keyed by failure reason.
1338                         *
1339                         * @param httpRequestReadFailures the HTTP request read failure counters
1340                         * @return this builder
1341                         */
1342                        @NonNull
1343                        public Builder httpRequestReadFailures(
1344                                        @Nullable Map<@NonNull RequestReadFailureKey, @NonNull Long> httpRequestReadFailures) {
1345                                this.httpRequestReadFailures = httpRequestReadFailures;
1346                                return this;
1347                        }
1348
1349                        /**
1350                         * Sets HTTP request rejection counters keyed by rejection reason.
1351                         *
1352                         * @param httpRequestRejections the HTTP request rejection counters
1353                         * @return this builder
1354                         */
1355                        @NonNull
1356                        public Builder httpRequestRejections(
1357                                        @Nullable Map<@NonNull RequestRejectionKey, @NonNull Long> httpRequestRejections) {
1358                                this.httpRequestRejections = httpRequestRejections;
1359                                return this;
1360                        }
1361
1362                        /**
1363                         * Sets SSE request read failure counters keyed by failure reason.
1364                         *
1365                         * @param sseRequestReadFailures the SSE request read failure counters
1366                         * @return this builder
1367                         */
1368                        @NonNull
1369                        public Builder sseRequestReadFailures(
1370                                        @Nullable Map<@NonNull RequestReadFailureKey, @NonNull Long> sseRequestReadFailures) {
1371                                this.sseRequestReadFailures = sseRequestReadFailures;
1372                                return this;
1373                        }
1374
1375                        /**
1376                         * Sets SSE request rejection counters keyed by rejection reason.
1377                         *
1378                         * @param sseRequestRejections the SSE request rejection counters
1379                         * @return this builder
1380                         */
1381                        @NonNull
1382                        public Builder sseRequestRejections(
1383                                        @Nullable Map<@NonNull RequestRejectionKey, @NonNull Long> sseRequestRejections) {
1384                                this.sseRequestRejections = sseRequestRejections;
1385                                return this;
1386                        }
1387
1388                        /**
1389                         * Sets HTTP request duration histograms keyed by server route and status class.
1390                         *
1391                         * @param httpRequestDurations the HTTP request duration histograms
1392                         * @return this builder
1393                         */
1394                        @NonNull
1395                        public Builder httpRequestDurations(
1396                                        @Nullable Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpRequestDurations) {
1397                                this.httpRequestDurations = httpRequestDurations;
1398                                return this;
1399                        }
1400
1401                        /**
1402                         * Sets HTTP handler duration histograms keyed by server route and status class.
1403                         *
1404                         * @param httpHandlerDurations the HTTP handler duration histograms
1405                         * @return this builder
1406                         */
1407                        @NonNull
1408                        public Builder httpHandlerDurations(
1409                                        @Nullable Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpHandlerDurations) {
1410                                this.httpHandlerDurations = httpHandlerDurations;
1411                                return this;
1412                        }
1413
1414                        /**
1415                         * Sets HTTP time-to-first-byte histograms keyed by server route and status class.
1416                         *
1417                         * @param httpTimeToFirstByte the HTTP time-to-first-byte histograms
1418                         * @return this builder
1419                         */
1420                        @NonNull
1421                        public Builder httpTimeToFirstByte(
1422                                        @Nullable Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpTimeToFirstByte) {
1423                                this.httpTimeToFirstByte = httpTimeToFirstByte;
1424                                return this;
1425                        }
1426
1427                        /**
1428                         * Sets HTTP request body size histograms keyed by server route.
1429                         *
1430                         * @param httpRequestBodyBytes the HTTP request body size histograms
1431                         * @return this builder
1432                         */
1433                        @NonNull
1434                        public Builder httpRequestBodyBytes(
1435                                        @Nullable Map<@NonNull ServerRouteKey, @NonNull HistogramSnapshot> httpRequestBodyBytes) {
1436                                this.httpRequestBodyBytes = httpRequestBodyBytes;
1437                                return this;
1438                        }
1439
1440                        /**
1441                         * Sets HTTP response body size histograms keyed by server route and status class.
1442                         *
1443                         * @param httpResponseBodyBytes the HTTP response body size histograms
1444                         * @return this builder
1445                         */
1446                        @NonNull
1447                        public Builder httpResponseBodyBytes(
1448                                        @Nullable Map<@NonNull ServerRouteStatusKey, @NonNull HistogramSnapshot> httpResponseBodyBytes) {
1449                                this.httpResponseBodyBytes = httpResponseBodyBytes;
1450                                return this;
1451                        }
1452
1453                        /**
1454                         * Sets SSE handshake acceptance counters keyed by route.
1455                         *
1456                         * @param sseHandshakesAccepted SSE handshake acceptance counters
1457                         * @return this builder
1458                         */
1459                        @NonNull
1460                        public Builder sseHandshakesAccepted(
1461                                        @Nullable Map<@NonNull ServerSentEventRouteKey, @NonNull Long> sseHandshakesAccepted) {
1462                                this.sseHandshakesAccepted = sseHandshakesAccepted;
1463                                return this;
1464                        }
1465
1466                        /**
1467                         * Sets SSE handshake rejection counters keyed by route and failure reason.
1468                         *
1469                         * @param sseHandshakesRejected SSE handshake rejection counters
1470                         * @return this builder
1471                         */
1472                        @NonNull
1473                        public Builder sseHandshakesRejected(
1474                                        @Nullable Map<@NonNull ServerSentEventRouteHandshakeFailureKey, @NonNull Long> sseHandshakesRejected) {
1475                                this.sseHandshakesRejected = sseHandshakesRejected;
1476                                return this;
1477                        }
1478
1479                        /**
1480                         * Sets SSE event enqueue outcome counters keyed by route and outcome.
1481                         *
1482                         * @param sseEventEnqueueOutcomes the SSE event enqueue outcome counters
1483                         * @return this builder
1484                         */
1485                        @NonNull
1486                        public Builder sseEventEnqueueOutcomes(
1487                                        @Nullable Map<@NonNull ServerSentEventRouteEnqueueOutcomeKey, @NonNull Long> sseEventEnqueueOutcomes) {
1488                                this.sseEventEnqueueOutcomes = sseEventEnqueueOutcomes;
1489                                return this;
1490                        }
1491
1492                        /**
1493                         * Sets SSE comment enqueue outcome counters keyed by route, comment type, and outcome.
1494                         *
1495                         * @param sseCommentEnqueueOutcomes the SSE comment enqueue outcome counters
1496                         * @return this builder
1497                         */
1498                        @NonNull
1499                        public Builder sseCommentEnqueueOutcomes(
1500                                        @Nullable Map<@NonNull ServerSentEventCommentRouteEnqueueOutcomeKey, @NonNull Long> sseCommentEnqueueOutcomes) {
1501                                this.sseCommentEnqueueOutcomes = sseCommentEnqueueOutcomes;
1502                                return this;
1503                        }
1504
1505                        /**
1506                         * Sets SSE event drop counters keyed by route and drop reason.
1507                         *
1508                         * @param sseEventDrops the SSE event drop counters
1509                         * @return this builder
1510                         */
1511                        @NonNull
1512                        public Builder sseEventDrops(
1513                                        @Nullable Map<@NonNull ServerSentEventRouteDropKey, @NonNull Long> sseEventDrops) {
1514                                this.sseEventDrops = sseEventDrops;
1515                                return this;
1516                        }
1517
1518                        /**
1519                         * Sets SSE comment drop counters keyed by route, comment type, and drop reason.
1520                         *
1521                         * @param sseCommentDrops the SSE comment drop counters
1522                         * @return this builder
1523                         */
1524                        @NonNull
1525                        public Builder sseCommentDrops(
1526                                        @Nullable Map<@NonNull ServerSentEventCommentRouteDropKey, @NonNull Long> sseCommentDrops) {
1527                                this.sseCommentDrops = sseCommentDrops;
1528                                return this;
1529                        }
1530
1531                        /**
1532                         * Sets SSE time-to-first-event histograms keyed by route.
1533                         *
1534                         * @param sseTimeToFirstEvent the SSE time-to-first-event histograms
1535                         * @return this builder
1536                         */
1537                        @NonNull
1538                        public Builder sseTimeToFirstEvent(
1539                                        @Nullable Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseTimeToFirstEvent) {
1540                                this.sseTimeToFirstEvent = sseTimeToFirstEvent;
1541                                return this;
1542                        }
1543
1544                        /**
1545                         * Sets SSE event write duration histograms keyed by route.
1546                         *
1547                         * @param sseEventWriteDurations the SSE event write duration histograms
1548                         * @return this builder
1549                         */
1550                        @NonNull
1551                        public Builder sseEventWriteDurations(
1552                                        @Nullable Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventWriteDurations) {
1553                                this.sseEventWriteDurations = sseEventWriteDurations;
1554                                return this;
1555                        }
1556
1557                        /**
1558                         * Sets SSE event delivery lag histograms keyed by route.
1559                         *
1560                         * @param sseEventDeliveryLag the SSE event delivery lag histograms
1561                         * @return this builder
1562                         */
1563                        @NonNull
1564                        public Builder sseEventDeliveryLag(
1565                                        @Nullable Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventDeliveryLag) {
1566                                this.sseEventDeliveryLag = sseEventDeliveryLag;
1567                                return this;
1568                        }
1569
1570                        /**
1571                         * Sets SSE event size histograms keyed by route.
1572                         *
1573                         * @param sseEventSizes the SSE event size histograms
1574                         * @return this builder
1575                         */
1576                        @NonNull
1577                        public Builder sseEventSizes(
1578                                        @Nullable Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseEventSizes) {
1579                                this.sseEventSizes = sseEventSizes;
1580                                return this;
1581                        }
1582
1583                        /**
1584                         * Sets SSE queue depth histograms keyed by route.
1585                         *
1586                         * @param sseQueueDepth the SSE queue depth histograms
1587                         * @return this builder
1588                         */
1589                        @NonNull
1590                        public Builder sseQueueDepth(
1591                                        @Nullable Map<@NonNull ServerSentEventRouteKey, @NonNull HistogramSnapshot> sseQueueDepth) {
1592                                this.sseQueueDepth = sseQueueDepth;
1593                                return this;
1594                        }
1595
1596                        /**
1597                         * Sets SSE comment delivery lag histograms keyed by route and comment type.
1598                         *
1599                         * @param sseCommentDeliveryLag the SSE comment delivery lag histograms
1600                         * @return this builder
1601                         */
1602                        @NonNull
1603                        public Builder sseCommentDeliveryLag(
1604                                        @Nullable Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentDeliveryLag) {
1605                                this.sseCommentDeliveryLag = sseCommentDeliveryLag;
1606                                return this;
1607                        }
1608
1609                        /**
1610                         * Sets SSE comment size histograms keyed by route and comment type.
1611                         *
1612                         * @param sseCommentSizes the SSE comment size histograms
1613                         * @return this builder
1614                         */
1615                        @NonNull
1616                        public Builder sseCommentSizes(
1617                                        @Nullable Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentSizes) {
1618                                this.sseCommentSizes = sseCommentSizes;
1619                                return this;
1620                        }
1621
1622                        /**
1623                         * Sets SSE comment queue depth histograms keyed by route and comment type.
1624                         *
1625                         * @param sseCommentQueueDepth the SSE comment queue depth histograms
1626                         * @return this builder
1627                         */
1628                        @NonNull
1629                        public Builder sseCommentQueueDepth(
1630                                        @Nullable Map<@NonNull ServerSentEventCommentRouteKey, @NonNull HistogramSnapshot> sseCommentQueueDepth) {
1631                                this.sseCommentQueueDepth = sseCommentQueueDepth;
1632                                return this;
1633                        }
1634
1635                        /**
1636                         * Sets SSE connection duration histograms keyed by route and termination reason.
1637                         *
1638                         * @param sseConnectionDurations the SSE connection duration histograms
1639                         * @return this builder
1640                         */
1641                        @NonNull
1642                        public Builder sseConnectionDurations(
1643                                        @Nullable Map<@NonNull ServerSentEventRouteTerminationKey, @NonNull HistogramSnapshot> sseConnectionDurations) {
1644                                this.sseConnectionDurations = sseConnectionDurations;
1645                                return this;
1646                        }
1647
1648                        /**
1649                         * Builds a {@link Snapshot} instance.
1650                         *
1651                         * @return the built snapshot
1652                         */
1653                        @NonNull
1654                        public Snapshot build() {
1655                                return new Snapshot(this);
1656                        }
1657                }
1658        }
1659
1660        /**
1661         * A thread-safe histogram with fixed bucket boundaries.
1662         * <p>
1663         * Negative values are ignored. Buckets use inclusive upper bounds, and snapshots include
1664         * an overflow bucket represented by a {@link HistogramSnapshot#getBucketBoundary(int)} of
1665         * {@link Long#MAX_VALUE}.
1666         *
1667         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1668         */
1669        @ThreadSafe
1670        final class Histogram {
1671                @NonNull
1672                private final long[] bucketBoundaries;
1673                @NonNull
1674                private final LongAdder[] bucketCounts;
1675                @NonNull
1676                private final LongAdder count;
1677                @NonNull
1678                private final LongAdder sum;
1679                @NonNull
1680                private final AtomicLong min;
1681                @NonNull
1682                private final AtomicLong max;
1683
1684                /**
1685                 * Creates a histogram with the provided bucket boundaries.
1686                 *
1687                 * @param bucketBoundaries inclusive upper bounds for buckets
1688                 */
1689                public Histogram(@NonNull long[] bucketBoundaries) {
1690                        requireNonNull(bucketBoundaries);
1691
1692                        this.bucketBoundaries = bucketBoundaries.clone();
1693                        Arrays.sort(this.bucketBoundaries);
1694                        this.bucketCounts = new LongAdder[this.bucketBoundaries.length + 1];
1695                        for (int i = 0; i < this.bucketCounts.length; i++)
1696                                this.bucketCounts[i] = new LongAdder();
1697                        this.count = new LongAdder();
1698                        this.sum = new LongAdder();
1699                        this.min = new AtomicLong(Long.MAX_VALUE);
1700                        this.max = new AtomicLong(Long.MIN_VALUE);
1701                }
1702
1703                /**
1704                 * Records a value into the histogram.
1705                 *
1706                 * @param value the value to record
1707                 */
1708                public void record(long value) {
1709                        if (value < 0)
1710                                return;
1711
1712                        this.count.increment();
1713                        this.sum.add(value);
1714                        updateMin(value);
1715                        updateMax(value);
1716
1717                        int bucketIndex = bucketIndex(value);
1718                        this.bucketCounts[bucketIndex].increment();
1719                }
1720
1721                /**
1722                 * Captures an immutable snapshot of the histogram.
1723                 *
1724                 * @return the histogram snapshot
1725                 */
1726                @NonNull
1727                public HistogramSnapshot snapshot() {
1728                        long[] boundariesWithOverflow = Arrays.copyOf(this.bucketBoundaries, this.bucketBoundaries.length + 1);
1729                        boundariesWithOverflow[boundariesWithOverflow.length - 1] = Long.MAX_VALUE;
1730
1731                        long[] cumulativeCounts = new long[this.bucketCounts.length];
1732                        long cumulative = 0;
1733                        for (int i = 0; i < this.bucketCounts.length; i++) {
1734                                cumulative += this.bucketCounts[i].sum();
1735                                cumulativeCounts[i] = cumulative;
1736                        }
1737
1738                        long countSnapshot = this.count.sum();
1739                        long sumSnapshot = this.sum.sum();
1740                        long minSnapshot = this.min.get();
1741                        long maxSnapshot = this.max.get();
1742
1743                        if (minSnapshot == Long.MAX_VALUE)
1744                                minSnapshot = 0;
1745                        if (maxSnapshot == Long.MIN_VALUE)
1746                                maxSnapshot = 0;
1747
1748                        return new HistogramSnapshot(boundariesWithOverflow, cumulativeCounts, countSnapshot, sumSnapshot, minSnapshot, maxSnapshot);
1749                }
1750
1751                /**
1752                 * Resets all counts and min/max values.
1753                 */
1754                public void reset() {
1755                        this.count.reset();
1756                        this.sum.reset();
1757                        this.min.set(Long.MAX_VALUE);
1758                        this.max.set(Long.MIN_VALUE);
1759                        for (LongAdder bucket : this.bucketCounts)
1760                                bucket.reset();
1761                }
1762
1763                private int bucketIndex(long value) {
1764                        for (int i = 0; i < this.bucketBoundaries.length; i++)
1765                                if (value <= this.bucketBoundaries[i])
1766                                        return i;
1767
1768                        return this.bucketBoundaries.length;
1769                }
1770
1771                private void updateMin(long value) {
1772                        long current;
1773                        while (value < (current = this.min.get())) {
1774                                if (this.min.compareAndSet(current, value))
1775                                        break;
1776                        }
1777                }
1778
1779                private void updateMax(long value) {
1780                        long current;
1781                        while (value > (current = this.max.get())) {
1782                                if (this.max.compareAndSet(current, value))
1783                                        break;
1784                        }
1785                }
1786        }
1787
1788        /**
1789         * Immutable snapshot of a {@link Histogram}.
1790         * <p>
1791         * Bucket counts are cumulative. Boundaries are inclusive upper bounds, and the final
1792         * boundary is {@link Long#MAX_VALUE} to represent the overflow bucket. Units are the same
1793         * as values passed to {@link Histogram#record(long)}.
1794         *
1795         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1796         */
1797        @ThreadSafe
1798        final class HistogramSnapshot {
1799                @NonNull
1800                private final long[] bucketBoundaries;
1801                @NonNull
1802                private final long[] bucketCumulativeCounts;
1803                private final long count;
1804                private final long sum;
1805                private final long min;
1806                private final long max;
1807
1808                /**
1809                 * Creates an immutable histogram snapshot.
1810                 *
1811                 * @param bucketBoundaries       inclusive upper bounds for buckets, including overflow
1812                 * @param bucketCumulativeCounts cumulative counts for each bucket
1813                 * @param count                  total number of samples recorded
1814                 * @param sum                    sum of all recorded values
1815                 * @param min                    smallest recorded value (or 0 if none)
1816                 * @param max                    largest recorded value (or 0 if none)
1817                 */
1818                public HistogramSnapshot(@NonNull long[] bucketBoundaries,
1819                                                                                                                 @NonNull long[] bucketCumulativeCounts,
1820                                                                                                                 long count,
1821                                                                                                                 long sum,
1822                                                                                                                 long min,
1823                                                                                                                 long max) {
1824                        requireNonNull(bucketBoundaries);
1825                        requireNonNull(bucketCumulativeCounts);
1826
1827                        if (bucketBoundaries.length != bucketCumulativeCounts.length)
1828                                throw new IllegalArgumentException("Bucket boundaries and cumulative counts must be the same length");
1829
1830                        this.bucketBoundaries = bucketBoundaries.clone();
1831                        this.bucketCumulativeCounts = bucketCumulativeCounts.clone();
1832                        this.count = count;
1833                        this.sum = sum;
1834                        this.min = min;
1835                        this.max = max;
1836                }
1837
1838                /**
1839                 * Number of histogram buckets, including the overflow bucket.
1840                 *
1841                 * @return the bucket count
1842                 */
1843                public int getBucketCount() {
1844                        return this.bucketBoundaries.length;
1845                }
1846
1847                /**
1848                 * The inclusive upper bound for the bucket at the given index.
1849                 *
1850                 * @param index the bucket index
1851                 * @return the bucket boundary
1852                 */
1853                public long getBucketBoundary(int index) {
1854                        return this.bucketBoundaries[index];
1855                }
1856
1857                /**
1858                 * The cumulative count for the bucket at the given index.
1859                 *
1860                 * @param index the bucket index
1861                 * @return the cumulative count
1862                 */
1863                public long getBucketCumulativeCount(int index) {
1864                        return this.bucketCumulativeCounts[index];
1865                }
1866
1867                /**
1868                 * Total number of recorded values.
1869                 *
1870                 * @return the count
1871                 */
1872                public long getCount() {
1873                        return this.count;
1874                }
1875
1876                /**
1877                 * Sum of all recorded values.
1878                 *
1879                 * @return the sum
1880                 */
1881                public long getSum() {
1882                        return this.sum;
1883                }
1884
1885                /**
1886                 * Smallest recorded value, or 0 if no values were recorded.
1887                 *
1888                 * @return the minimum value
1889                 */
1890                public long getMin() {
1891                        return this.min;
1892                }
1893
1894                /**
1895                 * Largest recorded value, or 0 if no values were recorded.
1896                 *
1897                 * @return the maximum value
1898                 */
1899                public long getMax() {
1900                        return this.max;
1901                }
1902
1903                /**
1904                 * Returns an approximate percentile based on bucket boundaries.
1905                 *
1906                 * @param percentile percentile between 0 and 100
1907                 * @return the approximated percentile value
1908                 */
1909                public long getPercentile(double percentile) {
1910                        if (percentile <= 0.0)
1911                                return this.min;
1912                        if (percentile >= 100.0)
1913                                return this.max;
1914                        if (this.count == 0)
1915                                return 0;
1916
1917                        long threshold = (long) Math.ceil((percentile / 100.0) * this.count);
1918
1919                        for (int i = 0; i < this.bucketCumulativeCounts.length; i++)
1920                                if (this.bucketCumulativeCounts[i] >= threshold)
1921                                        return this.bucketBoundaries[i];
1922
1923                        return this.bucketBoundaries[this.bucketBoundaries.length - 1];
1924                }
1925
1926                @Override
1927                public String toString() {
1928                        return String.format("%s{count=%d, min=%d, max=%d, sum=%d, bucketBoundaries=%s}",
1929                                        getClass().getSimpleName(), this.count, this.min, this.max, this.sum, Arrays.toString(this.bucketBoundaries));
1930                }
1931        }
1932
1933        /**
1934         * Indicates whether a request was matched to a {@link ResourcePathDeclaration}.
1935         *
1936         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1937         */
1938        enum RouteType {
1939                /**
1940                 * The request matched a {@link ResourcePathDeclaration}.
1941                 */
1942                MATCHED,
1943                /**
1944                 * The request did not match any {@link ResourcePathDeclaration}.
1945                 */
1946                UNMATCHED
1947        }
1948
1949        /**
1950         * Outcomes for a Server-Sent Event enqueue attempt.
1951         *
1952         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1953         */
1954        enum ServerSentEventEnqueueOutcome {
1955                /**
1956                 * An enqueue attempt to a connection was made.
1957                 */
1958                ATTEMPTED,
1959                /**
1960                 * An enqueue attempt succeeded.
1961                 */
1962                ENQUEUED,
1963                /**
1964                 * An enqueue attempt failed because the payload was dropped.
1965                 */
1966                DROPPED
1967        }
1968
1969        /**
1970         * Reasons a Server-Sent Event payload or comment was dropped before it could be written.
1971         *
1972         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1973         */
1974        enum ServerSentEventDropReason {
1975                /**
1976                 * The per-connection write queue was full.
1977                 */
1978                QUEUE_FULL
1979        }
1980
1981        /**
1982         * Key for request read failures grouped by reason.
1983         *
1984         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1985         */
1986        record RequestReadFailureKey(@NonNull RequestReadFailureReason reason) {
1987                public RequestReadFailureKey {
1988                        requireNonNull(reason);
1989                }
1990        }
1991
1992        /**
1993         * Key for request rejections grouped by reason.
1994         *
1995         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1996         */
1997        record RequestRejectionKey(@NonNull RequestRejectionReason reason) {
1998                public RequestRejectionKey {
1999                        requireNonNull(reason);
2000                }
2001        }
2002
2003        /**
2004         * Key for metrics grouped by HTTP method and route match information.
2005         *
2006         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2007         */
2008        record ServerRouteKey(@NonNull HttpMethod method,
2009                                                                                                @NonNull RouteType routeType,
2010                                                                                                @Nullable ResourcePathDeclaration route) {
2011                public ServerRouteKey {
2012                        requireNonNull(method);
2013                        requireNonNull(routeType);
2014                        if (routeType == RouteType.MATCHED && route == null)
2015                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2016                        if (routeType == RouteType.UNMATCHED && route != null)
2017                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2018                }
2019        }
2020
2021        /**
2022         * Key for metrics grouped by HTTP method, route match information, and status class (e.g. 2xx).
2023         *
2024         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2025         */
2026        record ServerRouteStatusKey(@NonNull HttpMethod method,
2027                                                                                                                        @NonNull RouteType routeType,
2028                                                                                                                        @Nullable ResourcePathDeclaration route,
2029                                                                                                                        @NonNull String statusClass) {
2030                public ServerRouteStatusKey {
2031                        requireNonNull(method);
2032                        requireNonNull(routeType);
2033                        if (routeType == RouteType.MATCHED && route == null)
2034                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2035                        if (routeType == RouteType.UNMATCHED && route != null)
2036                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2037                        requireNonNull(statusClass);
2038                }
2039        }
2040
2041        /**
2042         * Key for metrics grouped by Server-Sent Event comment type and route match information.
2043         *
2044         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2045         */
2046        record ServerSentEventCommentRouteKey(@NonNull RouteType routeType,
2047                                                                                                                                                                @Nullable ResourcePathDeclaration route,
2048                                                                                                                                                                ServerSentEventComment.@NonNull CommentType commentType) {
2049                public ServerSentEventCommentRouteKey {
2050                        requireNonNull(routeType);
2051                        requireNonNull(commentType);
2052                        if (routeType == RouteType.MATCHED && route == null)
2053                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2054                        if (routeType == RouteType.UNMATCHED && route != null)
2055                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2056                }
2057        }
2058
2059        /**
2060         * Key for metrics grouped by Server-Sent Event route match information.
2061         *
2062         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2063         */
2064        record ServerSentEventRouteKey(@NonNull RouteType routeType,
2065                                                                                                                                 @Nullable ResourcePathDeclaration route) {
2066                public ServerSentEventRouteKey {
2067                        requireNonNull(routeType);
2068                        if (routeType == RouteType.MATCHED && route == null)
2069                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2070                        if (routeType == RouteType.UNMATCHED && route != null)
2071                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2072                }
2073        }
2074
2075        /**
2076         * Key for metrics grouped by Server-Sent Event route match information and handshake failure reason.
2077         *
2078         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2079         */
2080        record ServerSentEventRouteHandshakeFailureKey(@NonNull RouteType routeType,
2081                                                                                                                                                                                                 @Nullable ResourcePathDeclaration route,
2082                                                                                                                                                                                                 ServerSentEventConnection.@NonNull HandshakeFailureReason handshakeFailureReason) {
2083                public ServerSentEventRouteHandshakeFailureKey {
2084                        requireNonNull(routeType);
2085                        if (routeType == RouteType.MATCHED && route == null)
2086                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2087                        if (routeType == RouteType.UNMATCHED && route != null)
2088                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2089                        requireNonNull(handshakeFailureReason);
2090                }
2091        }
2092
2093        /**
2094         * Key for metrics grouped by Server-Sent Event route match information and enqueue outcome.
2095         *
2096         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2097         */
2098        record ServerSentEventRouteEnqueueOutcomeKey(@NonNull RouteType routeType,
2099                                                                                                                                                                                                 @Nullable ResourcePathDeclaration route,
2100                                                                                                                                                                                                 @NonNull ServerSentEventEnqueueOutcome outcome) {
2101                public ServerSentEventRouteEnqueueOutcomeKey {
2102                        requireNonNull(routeType);
2103                        if (routeType == RouteType.MATCHED && route == null)
2104                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2105                        if (routeType == RouteType.UNMATCHED && route != null)
2106                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2107                        requireNonNull(outcome);
2108                }
2109        }
2110
2111        /**
2112         * Key for metrics grouped by Server-Sent Event comment type, route match information, and enqueue outcome.
2113         *
2114         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2115         */
2116        record ServerSentEventCommentRouteEnqueueOutcomeKey(@NonNull RouteType routeType,
2117                                                                                                                                                                                                                         @Nullable ResourcePathDeclaration route,
2118                                                                                                                                                                                                                         ServerSentEventComment.@NonNull CommentType commentType,
2119                                                                                                                                                                                                                         @NonNull ServerSentEventEnqueueOutcome outcome) {
2120                public ServerSentEventCommentRouteEnqueueOutcomeKey {
2121                        requireNonNull(routeType);
2122                        requireNonNull(commentType);
2123                        if (routeType == RouteType.MATCHED && route == null)
2124                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2125                        if (routeType == RouteType.UNMATCHED && route != null)
2126                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2127                        requireNonNull(outcome);
2128                }
2129        }
2130
2131        /**
2132         * Key for metrics grouped by Server-Sent Event route match information and drop reason.
2133         *
2134         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2135         */
2136        record ServerSentEventRouteDropKey(@NonNull RouteType routeType,
2137                                                                                                                                                 @Nullable ResourcePathDeclaration route,
2138                                                                                                                                                 @NonNull ServerSentEventDropReason dropReason) {
2139                public ServerSentEventRouteDropKey {
2140                        requireNonNull(routeType);
2141                        if (routeType == RouteType.MATCHED && route == null)
2142                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2143                        if (routeType == RouteType.UNMATCHED && route != null)
2144                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2145                        requireNonNull(dropReason);
2146                }
2147        }
2148
2149        /**
2150         * Key for metrics grouped by Server-Sent Event comment type, route match information, and drop reason.
2151         *
2152         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2153         */
2154        record ServerSentEventCommentRouteDropKey(@NonNull RouteType routeType,
2155                                                                                                                                                                         @Nullable ResourcePathDeclaration route,
2156                                                                                                                                                                         ServerSentEventComment.@NonNull CommentType commentType,
2157                                                                                                                                                                         @NonNull ServerSentEventDropReason dropReason) {
2158                public ServerSentEventCommentRouteDropKey {
2159                        requireNonNull(routeType);
2160                        requireNonNull(commentType);
2161                        if (routeType == RouteType.MATCHED && route == null)
2162                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2163                        if (routeType == RouteType.UNMATCHED && route != null)
2164                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2165                        requireNonNull(dropReason);
2166                }
2167        }
2168
2169        /**
2170         * Key for metrics grouped by Server-Sent Event route match information and termination reason.
2171         *
2172         * @author <a href="https://www.revetkn.com">Mark Allen</a>
2173         */
2174        record ServerSentEventRouteTerminationKey(@NonNull RouteType routeType,
2175                                                                                                                                                                                @Nullable ResourcePathDeclaration route,
2176                                                                                                                                                                                ServerSentEventConnection.@NonNull TerminationReason terminationReason) {
2177                public ServerSentEventRouteTerminationKey {
2178                        requireNonNull(routeType);
2179                        if (routeType == RouteType.MATCHED && route == null)
2180                                throw new IllegalArgumentException("Route must be provided when RouteType is MATCHED");
2181                        if (routeType == RouteType.UNMATCHED && route != null)
2182                                throw new IllegalArgumentException("Route must be null when RouteType is UNMATCHED");
2183                        requireNonNull(terminationReason);
2184                }
2185        }
2186
2187        /**
2188         * Acquires a threadsafe {@link MetricsCollector} instance with sensible defaults.
2189         * <p>
2190         * This method is guaranteed to return a new instance.
2191         *
2192         * @return a {@code MetricsCollector} with default settings
2193         */
2194        @NonNull
2195        static MetricsCollector defaultInstance() {
2196                return DefaultMetricsCollector.defaultInstance();
2197        }
2198
2199        /**
2200         * Acquires a threadsafe {@link MetricsCollector} instance that performs no work.
2201         * <p>
2202         * This method is useful when you want to explicitly disable metrics collection without writing your own implementation.
2203         * <p>
2204         * The returned instance is guaranteed to be a JVM-wide singleton.
2205         *
2206         * @return a no-op {@code MetricsCollector}
2207         */
2208        @NonNull
2209        static MetricsCollector disabledInstance() {
2210                return DisabledMetricsCollector.defaultInstance();
2211        }
2212}