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