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