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}