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