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.ThreadSafe; 023import java.time.Duration; 024 025import static java.util.Objects.requireNonNull; 026 027/** 028 * A Soklet-owned immutable semantic event delivered to a 029 * {@link MetricsCollector}. 030 * <p> 031 * The hierarchy deliberately has no generic value, label map, or bag of 032 * optional fields. Each event exposes only the dimensions meaningful for its 033 * transition. Framework-produced endpoint paths are finite registered 034 * declarations, and framework-produced method values are recognized names or 035 * {@link #UNRECOGNIZED_JSON_RPC_METHOD}. Public event factories enforce only 036 * the documented value shape; applications construct events through these 037 * factories and own the confidentiality and cardinality of supplied values. No raw 038 * unrecognized method, operation name, resource URI, principal, header, trace 039 * data, argument, or result is a framework-produced built-in metric dimension. 040 * 041 * <p> 042 * This type may gain reviewed permitted subtypes in Soklet 4.x; use a forward-compatible default when switching over its values. 043 * 044 * @author <a href="https://www.revetkn.com">Mark Allen</a> 045 */ 046@ThreadSafe 047public sealed interface McpMetricsEvent permits 048 McpMetricsEvent.ServerStarted, 049 McpMetricsEvent.ConnectionAccepted, 050 McpMetricsEvent.ConnectionRejected, 051 McpMetricsEvent.RequestAccepted, 052 McpMetricsEvent.RequestRejected, 053 McpMetricsEvent.RequestStarted, 054 McpMetricsEvent.RequestFinished, 055 McpMetricsEvent.RequestStreamOpened, 056 McpMetricsEvent.RequestStreamClosed, 057 McpMetricsEvent.SubscriptionOpened, 058 McpMetricsEvent.SubscriptionClosed, 059 McpMetricsEvent.SubscriptionMaintenance, 060 McpMetricsEvent.CancelationSignaled, 061 McpMetricsEvent.ProgressEmitted, 062 McpMetricsEvent.KeepAliveEmitted, 063 McpMetricsEvent.ProtocolError, 064 McpMetricsEvent.UnknownMirroredHeader, 065 McpMetricsEvent.HandlerExecutionStarted, 066 McpMetricsEvent.HandlerExecutionFinished, 067 McpMetricsEvent.HandlerQueued, 068 McpMetricsEvent.HandlerDequeued, 069 McpMetricsEvent.HandlerCapacityRejected, 070 McpMetricsEvent.TransportFailure, 071 McpMetricsEvent.ServerStopped { 072 /** 073 * Fixed bounded-cardinality method dimension used for every unrecognized 074 * or attacker-supplied JSON-RPC method. 075 */ 076 @NonNull 077 String UNRECOGNIZED_JSON_RPC_METHOD = "<unrecognized>"; 078 079 /** @return event indicating that a listener generation started */ 080 @NonNull 081 static ServerStarted serverStarted() { 082 return ServerStarted.INSTANCE; 083 } 084 085 /** @return event indicating that a TCP connection was accepted */ 086 @NonNull 087 static ConnectionAccepted connectionAccepted() { 088 return ConnectionAccepted.INSTANCE; 089 } 090 091 /** @return event indicating that a TCP connection was rejected */ 092 @NonNull 093 static ConnectionRejected connectionRejected() { 094 return ConnectionRejected.INSTANCE; 095 } 096 097 /** @return event indicating that an HTTP request was accepted */ 098 @NonNull 099 static RequestAccepted requestAccepted() { 100 return RequestAccepted.INSTANCE; 101 } 102 103 /** @return event indicating that a request was rejected */ 104 @NonNull 105 static RequestRejected requestRejected() { 106 return RequestRejected.INSTANCE; 107 } 108 109 /** 110 * Creates an event indicating that an admitted semantic request started. 111 * 112 * @param endpointPath finite registered endpoint-path declaration 113 * @param jsonRpcMethod bounded JSON-RPC method dimension 114 * @return request-started event 115 */ 116 @NonNull 117 static RequestStarted requestStarted(@NonNull String endpointPath, 118 @NonNull String jsonRpcMethod) { 119 return new RequestStarted(endpointPath, jsonRpcMethod); 120 } 121 122 /** 123 * Creates an event indicating that an admitted semantic request finished. 124 * 125 * @param endpointPath finite registered endpoint-path declaration 126 * @param jsonRpcMethod bounded JSON-RPC method dimension 127 * @param requestOutcome fixed terminal outcome 128 * @param requestDuration nonnegative request duration 129 * @return request-finished event 130 */ 131 @NonNull 132 static RequestFinished requestFinished(@NonNull String endpointPath, 133 @NonNull String jsonRpcMethod, @NonNull McpRequestOutcome requestOutcome, 134 @NonNull Duration requestDuration) { 135 return new RequestFinished(endpointPath, jsonRpcMethod, requestOutcome, 136 requestDuration); 137 } 138 139 /** 140 * Creates an event indicating that a request response stream opened. 141 * 142 * @param endpointPath finite registered endpoint-path declaration 143 * @param jsonRpcMethod bounded JSON-RPC method dimension 144 * @return request-stream-opened event 145 */ 146 @NonNull 147 static RequestStreamOpened requestStreamOpened( 148 @NonNull String endpointPath, @NonNull String jsonRpcMethod) { 149 return new RequestStreamOpened(endpointPath, jsonRpcMethod); 150 } 151 152 /** 153 * Creates an event indicating that a request response stream closed. 154 * 155 * @param endpointPath finite registered endpoint-path declaration 156 * @param jsonRpcMethod bounded JSON-RPC method dimension 157 * @param streamTerminationReason fixed stream termination reason 158 * @param streamDuration nonnegative stream duration 159 * @return request-stream-closed event 160 */ 161 @NonNull 162 static RequestStreamClosed requestStreamClosed( 163 @NonNull String endpointPath, @NonNull String jsonRpcMethod, 164 @NonNull McpStreamTerminationReason streamTerminationReason, 165 @NonNull Duration streamDuration) { 166 return new RequestStreamClosed(endpointPath, jsonRpcMethod, 167 streamTerminationReason, streamDuration); 168 } 169 170 /** 171 * Creates an event indicating that an MCP subscription became active. 172 * 173 * @param endpointPath finite registered endpoint-path declaration 174 * @return subscription-opened event 175 */ 176 @NonNull 177 static SubscriptionOpened subscriptionOpened(@NonNull String endpointPath) { 178 return new SubscriptionOpened(endpointPath); 179 } 180 181 /** 182 * Creates an event indicating that an MCP subscription terminated. 183 * 184 * @param endpointPath finite registered endpoint-path declaration 185 * @param streamTerminationReason fixed stream termination reason 186 * @param subscriptionDuration nonnegative subscription duration 187 * @return subscription-closed event 188 */ 189 @NonNull 190 static SubscriptionClosed subscriptionClosed(@NonNull String endpointPath, 191 @NonNull McpStreamTerminationReason streamTerminationReason, 192 @NonNull Duration subscriptionDuration) { 193 return new SubscriptionClosed(endpointPath, streamTerminationReason, 194 subscriptionDuration); 195 } 196 197 /** 198 * Creates an event describing one bounded subscription-maintenance outcome. 199 * 200 * @param endpointPath finite registered endpoint-path declaration 201 * @param maintenanceWork fixed maintenance work family 202 * @param maintenanceOutcome fixed maintenance outcome 203 * @return subscription-maintenance event 204 */ 205 @NonNull 206 static SubscriptionMaintenance subscriptionMaintenance( 207 @NonNull String endpointPath, 208 SubscriptionMaintenance.@NonNull Work maintenanceWork, 209 SubscriptionMaintenance.@NonNull Outcome maintenanceOutcome) { 210 return new SubscriptionMaintenance(endpointPath, maintenanceWork, 211 maintenanceOutcome); 212 } 213 214 /** 215 * Creates an event indicating that cooperative cancelation was signaled. 216 * 217 * @param endpointPath finite registered endpoint-path declaration 218 * @param jsonRpcMethod bounded JSON-RPC method dimension 219 * @return cancelation-signaled event 220 */ 221 @NonNull 222 static CancelationSignaled cancelationSignaled( 223 @NonNull String endpointPath, @NonNull String jsonRpcMethod) { 224 return new CancelationSignaled(endpointPath, jsonRpcMethod); 225 } 226 227 /** 228 * Creates an event indicating that a progress notification was emitted. 229 * 230 * @param endpointPath finite registered endpoint-path declaration 231 * @param jsonRpcMethod bounded JSON-RPC method dimension 232 * @return progress-emitted event 233 */ 234 @NonNull 235 static ProgressEmitted progressEmitted(@NonNull String endpointPath, 236 @NonNull String jsonRpcMethod) { 237 return new ProgressEmitted(endpointPath, jsonRpcMethod); 238 } 239 240 /** @return event indicating that a keep-alive was emitted */ 241 @NonNull 242 static KeepAliveEmitted keepAliveEmitted() { 243 return KeepAliveEmitted.INSTANCE; 244 } 245 246 /** 247 * Creates an event indicating that a fixed protocol error was produced. 248 * 249 * @param code fixed JSON-RPC error code 250 * @return protocol-error event 251 */ 252 @NonNull 253 static ProtocolError protocolError(@NonNull Integer code) { 254 return new ProtocolError(code); 255 } 256 257 /** 258 * Creates an event indicating that an unknown mirrored header was counted. 259 * 260 * @param endpointPath finite registered endpoint-path declaration 261 * @param jsonRpcMethod bounded JSON-RPC method dimension 262 * @return unknown-mirrored-header event 263 */ 264 @NonNull 265 static UnknownMirroredHeader unknownMirroredHeader( 266 @NonNull String endpointPath, @NonNull String jsonRpcMethod) { 267 return new UnknownMirroredHeader(endpointPath, jsonRpcMethod); 268 } 269 270 /** @return event indicating that handler execution started */ 271 @NonNull 272 static HandlerExecutionStarted handlerExecutionStarted() { 273 return HandlerExecutionStarted.INSTANCE; 274 } 275 276 /** @return event indicating that handler execution finished */ 277 @NonNull 278 static HandlerExecutionFinished handlerExecutionFinished() { 279 return HandlerExecutionFinished.INSTANCE; 280 } 281 282 /** @return event indicating that a handler request entered the queue */ 283 @NonNull 284 static HandlerQueued handlerQueued() { 285 return HandlerQueued.INSTANCE; 286 } 287 288 /** @return event indicating that a handler request left the queue */ 289 @NonNull 290 static HandlerDequeued handlerDequeued() { 291 return HandlerDequeued.INSTANCE; 292 } 293 294 /** @return event indicating that handler dispatch was rejected */ 295 @NonNull 296 static HandlerCapacityRejected handlerCapacityRejected() { 297 return HandlerCapacityRejected.INSTANCE; 298 } 299 300 /** 301 * Creates an event indicating that the transport recorded a failure. 302 * 303 * @param reason fixed low-level transport failure reason 304 * @return transport-failure event 305 */ 306 @NonNull 307 static TransportFailure transportFailure( 308 MetricsCollector.@NonNull TransportFailureReason reason) { 309 return new TransportFailure(reason); 310 } 311 312 /** 313 * Creates an event indicating that a real listener stop completed. 314 * 315 * @param shutdownComponentDisposition fixed listener shutdown-component 316 * disposition 317 * @return server-stopped event 318 */ 319 @NonNull 320 static ServerStopped serverStopped( 321 @NonNull ShutdownComponentDisposition shutdownComponentDisposition) { 322 return new ServerStopped(shutdownComponentDisposition); 323 } 324 325 /** 326 * A listener generation started successfully. 327 * 328 * @author <a href="https://www.revetkn.com">Mark Allen</a> 329 */ 330 @ThreadSafe 331 public final class ServerStarted implements McpMetricsEvent { 332 @NonNull 333 private static final ServerStarted INSTANCE = new ServerStarted(); 334 335 private ServerStarted() { 336 } 337 338 /** @return whether this object is another server-started event */ 339 @Override 340 public boolean equals(@Nullable Object other) { 341 return other != null && getClass() == other.getClass(); 342 } 343 344 /** @return value-based hash code */ 345 @Override 346 public int hashCode() { 347 return 0; 348 } 349 350 /** @return diagnostic rendering */ 351 @Override 352 @NonNull 353 public String toString() { 354 return "ServerStarted{}"; 355 } 356 } 357 358 /** 359 * A successfully accepted TCP connection was admitted within the configured connection-capacity bound. 360 * 361 * @author <a href="https://www.revetkn.com">Mark Allen</a> 362 */ 363 @ThreadSafe 364 public final class ConnectionAccepted implements McpMetricsEvent { 365 @NonNull 366 private static final ConnectionAccepted INSTANCE = 367 new ConnectionAccepted(); 368 369 private ConnectionAccepted() { 370 } 371 372 /** @return whether this object is another connection-accepted event */ 373 @Override 374 public boolean equals(@Nullable Object other) { 375 return other != null && getClass() == other.getClass(); 376 } 377 378 /** @return value-based hash code */ 379 @Override 380 public int hashCode() { 381 return 0; 382 } 383 384 /** @return diagnostic rendering */ 385 @Override 386 @NonNull 387 public String toString() { 388 return "ConnectionAccepted{}"; 389 } 390 } 391 392 /** 393 * A successfully accepted TCP connection was rejected because the configured maximum-connection capacity was full. 394 * 395 * @author <a href="https://www.revetkn.com">Mark Allen</a> 396 */ 397 @ThreadSafe 398 public final class ConnectionRejected implements McpMetricsEvent { 399 @NonNull 400 private static final ConnectionRejected INSTANCE = 401 new ConnectionRejected(); 402 403 private ConnectionRejected() { 404 } 405 406 /** @return whether this object is another connection-rejected event */ 407 @Override 408 public boolean equals(@Nullable Object other) { 409 return other != null && getClass() == other.getClass(); 410 } 411 412 /** @return value-based hash code */ 413 @Override 414 public int hashCode() { 415 return 0; 416 } 417 418 /** @return diagnostic rendering */ 419 @Override 420 @NonNull 421 public String toString() { 422 return "ConnectionRejected{}"; 423 } 424 } 425 426 /** 427 * An HTTP request was accepted into the bounded protocol processor. 428 * 429 * @author <a href="https://www.revetkn.com">Mark Allen</a> 430 */ 431 @ThreadSafe 432 public final class RequestAccepted implements McpMetricsEvent { 433 @NonNull 434 private static final RequestAccepted INSTANCE = new RequestAccepted(); 435 436 private RequestAccepted() { 437 } 438 439 /** @return whether this object is another request-accepted event */ 440 @Override 441 public boolean equals(@Nullable Object other) { 442 return other != null && getClass() == other.getClass(); 443 } 444 445 /** @return value-based hash code */ 446 @Override 447 public int hashCode() { 448 return 0; 449 } 450 451 /** @return diagnostic rendering */ 452 @Override 453 @NonNull 454 public String toString() { 455 return "RequestAccepted{}"; 456 } 457 } 458 459 /** 460 * A request was rejected before admitted semantic handling began. 461 * 462 * @author <a href="https://www.revetkn.com">Mark Allen</a> 463 */ 464 @ThreadSafe 465 public final class RequestRejected implements McpMetricsEvent { 466 @NonNull 467 private static final RequestRejected INSTANCE = new RequestRejected(); 468 469 private RequestRejected() { 470 } 471 472 /** @return whether this object is another request-rejected event */ 473 @Override 474 public boolean equals(@Nullable Object other) { 475 return other != null && getClass() == other.getClass(); 476 } 477 478 /** @return value-based hash code */ 479 @Override 480 public int hashCode() { 481 return 0; 482 } 483 484 /** @return diagnostic rendering */ 485 @Override 486 @NonNull 487 public String toString() { 488 return "RequestRejected{}"; 489 } 490 } 491 492 /** 493 * An admitted semantic request started. 494 * 495 * @author <a href="https://www.revetkn.com">Mark Allen</a> 496 */ 497 @ThreadSafe 498 public final class RequestStarted implements McpMetricsEvent { 499 @NonNull 500 private final String endpointPath; 501 @NonNull 502 private final String jsonRpcMethod; 503 504 private RequestStarted(@NonNull String endpointPath, 505 @NonNull String jsonRpcMethod) { 506 requireRoutedDimensions(endpointPath, jsonRpcMethod); 507 this.endpointPath = endpointPath; 508 this.jsonRpcMethod = jsonRpcMethod; 509 } 510 511 /** @return finite registered endpoint-path declaration */ 512 @NonNull 513 public String getEndpointPath() { 514 return this.endpointPath; 515 } 516 517 /** @return bounded JSON-RPC method dimension */ 518 @NonNull 519 public String getJsonRpcMethod() { 520 return this.jsonRpcMethod; 521 } 522 523 /** @return whether this object contains the same dimensions */ 524 @Override 525 public boolean equals(@Nullable Object other) { 526 if (this == other) 527 return true; 528 if (other == null || getClass() != other.getClass()) 529 return false; 530 RequestStarted that = (RequestStarted) other; 531 return this.endpointPath.equals(that.endpointPath) 532 && this.jsonRpcMethod.equals(that.jsonRpcMethod); 533 } 534 535 /** @return value-based hash code */ 536 @Override 537 public int hashCode() { 538 return 31 * this.endpointPath.hashCode() 539 + this.jsonRpcMethod.hashCode(); 540 } 541 542 /** @return redacted diagnostic rendering */ 543 @Override 544 @NonNull 545 public String toString() { 546 return "RequestStarted{endpointPath=<redacted>, " 547 + "jsonRpcMethod=<redacted>}"; 548 } 549 } 550 551 /** 552 * An admitted semantic request reached its client-visible terminal outcome. 553 * 554 * @author <a href="https://www.revetkn.com">Mark Allen</a> 555 */ 556 @ThreadSafe 557 public final class RequestFinished implements McpMetricsEvent { 558 @NonNull 559 private final String endpointPath; 560 @NonNull 561 private final String jsonRpcMethod; 562 @NonNull 563 private final McpRequestOutcome outcome; 564 @NonNull 565 private final Duration duration; 566 567 private RequestFinished(@NonNull String endpointPath, 568 @NonNull String jsonRpcMethod, 569 @NonNull McpRequestOutcome outcome, 570 @NonNull Duration duration) { 571 requireRoutedDimensions(endpointPath, jsonRpcMethod); 572 this.endpointPath = endpointPath; 573 this.jsonRpcMethod = jsonRpcMethod; 574 this.outcome = requireNonNull(outcome); 575 requireNonNegative(duration); 576 this.duration = duration; 577 } 578 579 /** @return finite registered endpoint-path declaration */ 580 @NonNull 581 public String getEndpointPath() { 582 return this.endpointPath; 583 } 584 585 /** @return bounded JSON-RPC method dimension */ 586 @NonNull 587 public String getJsonRpcMethod() { 588 return this.jsonRpcMethod; 589 } 590 591 /** @return fixed terminal outcome */ 592 @NonNull 593 public McpRequestOutcome getOutcome() { 594 return this.outcome; 595 } 596 597 /** @return nonnegative request duration */ 598 @NonNull 599 public Duration getDuration() { 600 return this.duration; 601 } 602 603 /** @return whether this object contains the same event values */ 604 @Override 605 public boolean equals(@Nullable Object other) { 606 if (this == other) 607 return true; 608 if (other == null || getClass() != other.getClass()) 609 return false; 610 RequestFinished that = (RequestFinished) other; 611 return this.endpointPath.equals(that.endpointPath) 612 && this.jsonRpcMethod.equals(that.jsonRpcMethod) 613 && this.outcome.equals(that.outcome) 614 && this.duration.equals(that.duration); 615 } 616 617 /** @return value-based hash code */ 618 @Override 619 public int hashCode() { 620 int result = this.endpointPath.hashCode(); 621 result = 31 * result + this.jsonRpcMethod.hashCode(); 622 result = 31 * result + this.outcome.hashCode(); 623 return 31 * result + this.duration.hashCode(); 624 } 625 626 /** @return redacted diagnostic rendering */ 627 @Override 628 @NonNull 629 public String toString() { 630 return "RequestFinished{endpointPath=<redacted>, " 631 + "jsonRpcMethod=<redacted>, outcome=" + this.outcome 632 + ", duration=" + this.duration + "}"; 633 } 634 } 635 636 /** 637 * A request response stream opened. 638 * 639 * @author <a href="https://www.revetkn.com">Mark Allen</a> 640 */ 641 @ThreadSafe 642 public final class RequestStreamOpened implements McpMetricsEvent { 643 @NonNull 644 private final String endpointPath; 645 @NonNull 646 private final String jsonRpcMethod; 647 648 private RequestStreamOpened(@NonNull String endpointPath, 649 @NonNull String jsonRpcMethod) { 650 requireRoutedDimensions(endpointPath, jsonRpcMethod); 651 this.endpointPath = endpointPath; 652 this.jsonRpcMethod = jsonRpcMethod; 653 } 654 655 /** @return finite registered endpoint-path declaration */ 656 @NonNull 657 public String getEndpointPath() { 658 return this.endpointPath; 659 } 660 661 /** @return bounded JSON-RPC method dimension */ 662 @NonNull 663 public String getJsonRpcMethod() { 664 return this.jsonRpcMethod; 665 } 666 667 /** @return whether this object contains the same dimensions */ 668 @Override 669 public boolean equals(@Nullable Object other) { 670 if (this == other) 671 return true; 672 if (other == null || getClass() != other.getClass()) 673 return false; 674 RequestStreamOpened that = (RequestStreamOpened) other; 675 return this.endpointPath.equals(that.endpointPath) 676 && this.jsonRpcMethod.equals(that.jsonRpcMethod); 677 } 678 679 /** @return value-based hash code */ 680 @Override 681 public int hashCode() { 682 return 31 * this.endpointPath.hashCode() 683 + this.jsonRpcMethod.hashCode(); 684 } 685 686 /** @return redacted diagnostic rendering */ 687 @Override 688 @NonNull 689 public String toString() { 690 return "RequestStreamOpened{endpointPath=<redacted>, " 691 + "jsonRpcMethod=<redacted>}"; 692 } 693 } 694 695 /** 696 * A request response stream closed. 697 * 698 * @author <a href="https://www.revetkn.com">Mark Allen</a> 699 */ 700 @ThreadSafe 701 public final class RequestStreamClosed implements McpMetricsEvent { 702 @NonNull 703 private final String endpointPath; 704 @NonNull 705 private final String jsonRpcMethod; 706 @NonNull 707 private final McpStreamTerminationReason reason; 708 @NonNull 709 private final Duration duration; 710 711 private RequestStreamClosed(@NonNull String endpointPath, 712 @NonNull String jsonRpcMethod, 713 @NonNull McpStreamTerminationReason reason, 714 @NonNull Duration duration) { 715 requireRoutedDimensions(endpointPath, jsonRpcMethod); 716 this.endpointPath = endpointPath; 717 this.jsonRpcMethod = jsonRpcMethod; 718 this.reason = requireNonNull(reason); 719 requireNonNegative(duration); 720 this.duration = duration; 721 } 722 723 /** @return finite registered endpoint-path declaration */ 724 @NonNull 725 public String getEndpointPath() { 726 return this.endpointPath; 727 } 728 729 /** @return bounded JSON-RPC method dimension */ 730 @NonNull 731 public String getJsonRpcMethod() { 732 return this.jsonRpcMethod; 733 } 734 735 /** @return fixed stream termination reason */ 736 @NonNull 737 public McpStreamTerminationReason getReason() { 738 return this.reason; 739 } 740 741 /** @return nonnegative stream duration */ 742 @NonNull 743 public Duration getDuration() { 744 return this.duration; 745 } 746 747 /** @return whether this object contains the same event values */ 748 @Override 749 public boolean equals(@Nullable Object other) { 750 if (this == other) 751 return true; 752 if (other == null || getClass() != other.getClass()) 753 return false; 754 RequestStreamClosed that = (RequestStreamClosed) other; 755 return this.endpointPath.equals(that.endpointPath) 756 && this.jsonRpcMethod.equals(that.jsonRpcMethod) 757 && this.reason.equals(that.reason) 758 && this.duration.equals(that.duration); 759 } 760 761 /** @return value-based hash code */ 762 @Override 763 public int hashCode() { 764 int result = this.endpointPath.hashCode(); 765 result = 31 * result + this.jsonRpcMethod.hashCode(); 766 result = 31 * result + this.reason.hashCode(); 767 return 31 * result + this.duration.hashCode(); 768 } 769 770 /** @return redacted diagnostic rendering */ 771 @Override 772 @NonNull 773 public String toString() { 774 return "RequestStreamClosed{endpointPath=<redacted>, " 775 + "jsonRpcMethod=<redacted>, reason=" + this.reason 776 + ", duration=" + this.duration + "}"; 777 } 778 } 779 780 /** 781 * An MCP subscription became active. 782 * 783 * @author <a href="https://www.revetkn.com">Mark Allen</a> 784 */ 785 @ThreadSafe 786 public final class SubscriptionOpened implements McpMetricsEvent { 787 @NonNull 788 private final String endpointPath; 789 790 private SubscriptionOpened(@NonNull String endpointPath) { 791 requireEndpointPath(endpointPath); 792 this.endpointPath = endpointPath; 793 } 794 795 /** @return finite registered endpoint-path declaration */ 796 @NonNull 797 public String getEndpointPath() { 798 return this.endpointPath; 799 } 800 801 /** @return whether this object contains the same endpoint path */ 802 @Override 803 public boolean equals(@Nullable Object other) { 804 if (this == other) 805 return true; 806 if (other == null || getClass() != other.getClass()) 807 return false; 808 SubscriptionOpened that = (SubscriptionOpened) other; 809 return this.endpointPath.equals(that.endpointPath); 810 } 811 812 /** @return value-based hash code */ 813 @Override 814 public int hashCode() { 815 return this.endpointPath.hashCode(); 816 } 817 818 /** @return redacted diagnostic rendering */ 819 @Override 820 @NonNull 821 public String toString() { 822 return "SubscriptionOpened{endpointPath=<redacted>}"; 823 } 824 } 825 826 /** 827 * An MCP subscription terminated. 828 * 829 * @author <a href="https://www.revetkn.com">Mark Allen</a> 830 */ 831 @ThreadSafe 832 public final class SubscriptionClosed implements McpMetricsEvent { 833 @NonNull 834 private final String endpointPath; 835 @NonNull 836 private final McpStreamTerminationReason reason; 837 @NonNull 838 private final Duration duration; 839 840 private SubscriptionClosed(@NonNull String endpointPath, 841 @NonNull McpStreamTerminationReason reason, 842 @NonNull Duration duration) { 843 requireEndpointPath(endpointPath); 844 this.endpointPath = endpointPath; 845 this.reason = requireNonNull(reason); 846 requireNonNegative(duration); 847 this.duration = duration; 848 } 849 850 /** @return finite registered endpoint-path declaration */ 851 @NonNull 852 public String getEndpointPath() { 853 return this.endpointPath; 854 } 855 856 /** @return fixed stream termination reason */ 857 @NonNull 858 public McpStreamTerminationReason getReason() { 859 return this.reason; 860 } 861 862 /** @return nonnegative subscription duration */ 863 @NonNull 864 public Duration getDuration() { 865 return this.duration; 866 } 867 868 /** @return whether this object contains the same event values */ 869 @Override 870 public boolean equals(@Nullable Object other) { 871 if (this == other) 872 return true; 873 if (other == null || getClass() != other.getClass()) 874 return false; 875 SubscriptionClosed that = (SubscriptionClosed) other; 876 return this.endpointPath.equals(that.endpointPath) 877 && this.reason.equals(that.reason) 878 && this.duration.equals(that.duration); 879 } 880 881 /** @return value-based hash code */ 882 @Override 883 public int hashCode() { 884 int result = this.endpointPath.hashCode(); 885 result = 31 * result + this.reason.hashCode(); 886 return 31 * result + this.duration.hashCode(); 887 } 888 889 /** @return redacted diagnostic rendering */ 890 @Override 891 @NonNull 892 public String toString() { 893 return "SubscriptionClosed{endpointPath=<redacted>, reason=" 894 + this.reason + ", duration=" + this.duration + "}"; 895 } 896 } 897 898 /** 899 * One bounded subscription-authorization, catalog-projection, or 900 * reconciliation maintenance outcome. 901 * 902 * @author <a href="https://www.revetkn.com">Mark Allen</a> 903 */ 904 @ThreadSafe 905 public final class SubscriptionMaintenance implements McpMetricsEvent { 906 /** 907 * Bounded subscription-maintenance work families. 908 * 909 * <p> 910 * This type may gain reviewed constants in Soklet 4.x; use a forward-compatible default when switching over its values. 911 * 912 * @author <a href="https://www.revetkn.com">Mark Allen</a> 913 */ 914 public enum Work { 915 /** A subscription authorization check. */ 916 AUTHORIZATION, 917 /** A caller-visible catalog projection. */ 918 CATALOG_PROJECTION, 919 /** Explicit subscription reconciliation. */ 920 RECONCILIATION 921 } 922 923 /** 924 * Bounded subscription-maintenance outcomes. 925 * 926 * <p> 927 * This type may gain reviewed constants in Soklet 4.x; use a forward-compatible default when switching over its values. 928 * 929 * @author <a href="https://www.revetkn.com">Mark Allen</a> 930 */ 931 public enum Outcome { 932 /** The maintenance work completed successfully. */ 933 SUCCEEDED, 934 /** Authorization was denied. */ 935 DENIED, 936 /** The queue-inclusive maintenance deadline elapsed. */ 937 TIMED_OUT, 938 /** Bounded execution capacity rejected the work. */ 939 CAPACITY_REJECTED, 940 /** The maintenance work failed. */ 941 FAILED, 942 /** An equivalent pending generation already represented the work. */ 943 COALESCED, 944 /** A superseded maintenance result was discarded. */ 945 STALE_RESULT_DISCARDED 946 } 947 948 @NonNull 949 private final String endpointPath; 950 @NonNull 951 private final Work work; 952 @NonNull 953 private final Outcome outcome; 954 955 private SubscriptionMaintenance(@NonNull String endpointPath, 956 @NonNull Work work, @NonNull Outcome outcome) { 957 requireEndpointPath(endpointPath); 958 this.endpointPath = endpointPath; 959 this.work = requireNonNull(work); 960 this.outcome = requireNonNull(outcome); 961 } 962 963 /** @return finite registered endpoint-path declaration */ 964 @NonNull 965 public String getEndpointPath() { 966 return this.endpointPath; 967 } 968 969 /** @return fixed maintenance work family */ 970 @NonNull 971 public Work getWork() { 972 return this.work; 973 } 974 975 /** @return fixed maintenance outcome */ 976 @NonNull 977 public Outcome getOutcome() { 978 return this.outcome; 979 } 980 981 /** @return whether this object contains the same event values */ 982 @Override 983 public boolean equals(@Nullable Object other) { 984 if (this == other) 985 return true; 986 if (other == null || getClass() != other.getClass()) 987 return false; 988 SubscriptionMaintenance that = (SubscriptionMaintenance) other; 989 return this.endpointPath.equals(that.endpointPath) 990 && this.work.equals(that.work) 991 && this.outcome.equals(that.outcome); 992 } 993 994 /** @return value-based hash code */ 995 @Override 996 public int hashCode() { 997 int result = this.endpointPath.hashCode(); 998 result = 31 * result + this.work.hashCode(); 999 return 31 * result + this.outcome.hashCode(); 1000 } 1001 1002 /** @return diagnostic rendering with the endpoint path redacted */ 1003 @Override 1004 @NonNull 1005 public String toString() { 1006 return "SubscriptionMaintenance{endpointPath=<redacted>, work=" 1007 + this.work + ", outcome=" + this.outcome + "}"; 1008 } 1009 } 1010 1011 /** 1012 * Cooperative request cancelation was signaled. 1013 * 1014 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1015 */ 1016 @ThreadSafe 1017 public final class CancelationSignaled implements McpMetricsEvent { 1018 @NonNull 1019 private final String endpointPath; 1020 @NonNull 1021 private final String jsonRpcMethod; 1022 1023 private CancelationSignaled(@NonNull String endpointPath, 1024 @NonNull String jsonRpcMethod) { 1025 requireRoutedDimensions(endpointPath, jsonRpcMethod); 1026 this.endpointPath = endpointPath; 1027 this.jsonRpcMethod = jsonRpcMethod; 1028 } 1029 1030 /** @return finite registered endpoint-path declaration */ 1031 @NonNull 1032 public String getEndpointPath() { 1033 return this.endpointPath; 1034 } 1035 1036 /** @return bounded JSON-RPC method dimension */ 1037 @NonNull 1038 public String getJsonRpcMethod() { 1039 return this.jsonRpcMethod; 1040 } 1041 1042 /** @return whether this object contains the same dimensions */ 1043 @Override 1044 public boolean equals(@Nullable Object other) { 1045 if (this == other) 1046 return true; 1047 if (other == null || getClass() != other.getClass()) 1048 return false; 1049 CancelationSignaled that = (CancelationSignaled) other; 1050 return this.endpointPath.equals(that.endpointPath) 1051 && this.jsonRpcMethod.equals(that.jsonRpcMethod); 1052 } 1053 1054 /** @return value-based hash code */ 1055 @Override 1056 public int hashCode() { 1057 return 31 * this.endpointPath.hashCode() 1058 + this.jsonRpcMethod.hashCode(); 1059 } 1060 1061 /** @return redacted diagnostic rendering */ 1062 @Override 1063 @NonNull 1064 public String toString() { 1065 return "CancelationSignaled{endpointPath=<redacted>, " 1066 + "jsonRpcMethod=<redacted>}"; 1067 } 1068 } 1069 1070 /** 1071 * A progress notification was accepted for delivery. 1072 * 1073 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1074 */ 1075 @ThreadSafe 1076 public final class ProgressEmitted implements McpMetricsEvent { 1077 @NonNull 1078 private final String endpointPath; 1079 @NonNull 1080 private final String jsonRpcMethod; 1081 1082 private ProgressEmitted(@NonNull String endpointPath, 1083 @NonNull String jsonRpcMethod) { 1084 requireRoutedDimensions(endpointPath, jsonRpcMethod); 1085 this.endpointPath = endpointPath; 1086 this.jsonRpcMethod = jsonRpcMethod; 1087 } 1088 1089 /** @return finite registered endpoint-path declaration */ 1090 @NonNull 1091 public String getEndpointPath() { 1092 return this.endpointPath; 1093 } 1094 1095 /** @return bounded JSON-RPC method dimension */ 1096 @NonNull 1097 public String getJsonRpcMethod() { 1098 return this.jsonRpcMethod; 1099 } 1100 1101 /** @return whether this object contains the same dimensions */ 1102 @Override 1103 public boolean equals(@Nullable Object other) { 1104 if (this == other) 1105 return true; 1106 if (other == null || getClass() != other.getClass()) 1107 return false; 1108 ProgressEmitted that = (ProgressEmitted) other; 1109 return this.endpointPath.equals(that.endpointPath) 1110 && this.jsonRpcMethod.equals(that.jsonRpcMethod); 1111 } 1112 1113 /** @return value-based hash code */ 1114 @Override 1115 public int hashCode() { 1116 return 31 * this.endpointPath.hashCode() 1117 + this.jsonRpcMethod.hashCode(); 1118 } 1119 1120 /** @return redacted diagnostic rendering */ 1121 @Override 1122 @NonNull 1123 public String toString() { 1124 return "ProgressEmitted{endpointPath=<redacted>, " 1125 + "jsonRpcMethod=<redacted>}"; 1126 } 1127 } 1128 1129 /** 1130 * A keep-alive comment was accepted for delivery. 1131 * 1132 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1133 */ 1134 @ThreadSafe 1135 public final class KeepAliveEmitted implements McpMetricsEvent { 1136 @NonNull 1137 private static final KeepAliveEmitted INSTANCE = 1138 new KeepAliveEmitted(); 1139 1140 private KeepAliveEmitted() { 1141 } 1142 1143 /** @return whether this object is another keep-alive-emitted event */ 1144 @Override 1145 public boolean equals(@Nullable Object other) { 1146 return other != null && getClass() == other.getClass(); 1147 } 1148 1149 /** @return value-based hash code */ 1150 @Override 1151 public int hashCode() { 1152 return 0; 1153 } 1154 1155 /** @return diagnostic rendering */ 1156 @Override 1157 @NonNull 1158 public String toString() { 1159 return "KeepAliveEmitted{}"; 1160 } 1161 } 1162 1163 /** 1164 * A fixed JSON-RPC or MCP protocol error was produced. 1165 * 1166 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1167 */ 1168 @ThreadSafe 1169 public final class ProtocolError implements McpMetricsEvent { 1170 @NonNull 1171 private final Integer code; 1172 1173 private ProtocolError(@NonNull Integer code) { 1174 this.code = requireNonNull(code); 1175 } 1176 1177 /** @return fixed JSON-RPC error code */ 1178 @NonNull 1179 public Integer getCode() { 1180 return this.code; 1181 } 1182 1183 /** @return whether this object contains the same protocol error code */ 1184 @Override 1185 public boolean equals(@Nullable Object other) { 1186 if (this == other) 1187 return true; 1188 if (other == null || getClass() != other.getClass()) 1189 return false; 1190 ProtocolError that = (ProtocolError) other; 1191 return this.code.equals(that.code); 1192 } 1193 1194 /** @return value-based hash code */ 1195 @Override 1196 public int hashCode() { 1197 return this.code.hashCode(); 1198 } 1199 1200 /** @return diagnostic rendering */ 1201 @Override 1202 @NonNull 1203 public String toString() { 1204 return "ProtocolError{code=" + this.code + "}"; 1205 } 1206 } 1207 1208 /** 1209 * An unknown mirrored-header occurrence was counted without its name. 1210 * 1211 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1212 */ 1213 @ThreadSafe 1214 public final class UnknownMirroredHeader implements McpMetricsEvent { 1215 @NonNull 1216 private final String endpointPath; 1217 @NonNull 1218 private final String jsonRpcMethod; 1219 1220 private UnknownMirroredHeader(@NonNull String endpointPath, 1221 @NonNull String jsonRpcMethod) { 1222 requireRoutedDimensions(endpointPath, jsonRpcMethod); 1223 this.endpointPath = endpointPath; 1224 this.jsonRpcMethod = jsonRpcMethod; 1225 } 1226 1227 /** @return finite registered endpoint-path declaration */ 1228 @NonNull 1229 public String getEndpointPath() { 1230 return this.endpointPath; 1231 } 1232 1233 /** @return bounded JSON-RPC method dimension */ 1234 @NonNull 1235 public String getJsonRpcMethod() { 1236 return this.jsonRpcMethod; 1237 } 1238 1239 /** @return whether this object contains the same dimensions */ 1240 @Override 1241 public boolean equals(@Nullable Object other) { 1242 if (this == other) 1243 return true; 1244 if (other == null || getClass() != other.getClass()) 1245 return false; 1246 UnknownMirroredHeader that = (UnknownMirroredHeader) other; 1247 return this.endpointPath.equals(that.endpointPath) 1248 && this.jsonRpcMethod.equals(that.jsonRpcMethod); 1249 } 1250 1251 /** @return value-based hash code */ 1252 @Override 1253 public int hashCode() { 1254 return 31 * this.endpointPath.hashCode() 1255 + this.jsonRpcMethod.hashCode(); 1256 } 1257 1258 /** @return redacted diagnostic rendering */ 1259 @Override 1260 @NonNull 1261 public String toString() { 1262 return "UnknownMirroredHeader{endpointPath=<redacted>, " 1263 + "jsonRpcMethod=<redacted>}"; 1264 } 1265 } 1266 1267 /** 1268 * An application handler acquired an execution slot. 1269 * 1270 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1271 */ 1272 @ThreadSafe 1273 public final class HandlerExecutionStarted implements McpMetricsEvent { 1274 @NonNull 1275 private static final HandlerExecutionStarted INSTANCE = 1276 new HandlerExecutionStarted(); 1277 1278 private HandlerExecutionStarted() { 1279 } 1280 1281 /** @return whether this object is another handler-started event */ 1282 @Override 1283 public boolean equals(@Nullable Object other) { 1284 return other != null && getClass() == other.getClass(); 1285 } 1286 1287 /** @return value-based hash code */ 1288 @Override 1289 public int hashCode() { 1290 return 0; 1291 } 1292 1293 /** @return diagnostic rendering */ 1294 @Override 1295 @NonNull 1296 public String toString() { 1297 return "HandlerExecutionStarted{}"; 1298 } 1299 } 1300 1301 /** 1302 * A reserved application-handler execution slot was released after actual 1303 * handler exit or failed executor submission. 1304 * 1305 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1306 */ 1307 @ThreadSafe 1308 public final class HandlerExecutionFinished implements McpMetricsEvent { 1309 @NonNull 1310 private static final HandlerExecutionFinished INSTANCE = 1311 new HandlerExecutionFinished(); 1312 1313 private HandlerExecutionFinished() { 1314 } 1315 1316 /** @return whether this object is another handler-finished event */ 1317 @Override 1318 public boolean equals(@Nullable Object other) { 1319 return other != null && getClass() == other.getClass(); 1320 } 1321 1322 /** @return value-based hash code */ 1323 @Override 1324 public int hashCode() { 1325 return 0; 1326 } 1327 1328 /** @return diagnostic rendering */ 1329 @Override 1330 @NonNull 1331 public String toString() { 1332 return "HandlerExecutionFinished{}"; 1333 } 1334 } 1335 1336 /** 1337 * An application handler request entered the bounded queue. 1338 * 1339 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1340 */ 1341 @ThreadSafe 1342 public final class HandlerQueued implements McpMetricsEvent { 1343 @NonNull 1344 private static final HandlerQueued INSTANCE = new HandlerQueued(); 1345 1346 private HandlerQueued() { 1347 } 1348 1349 /** @return whether this object is another handler-queued event */ 1350 @Override 1351 public boolean equals(@Nullable Object other) { 1352 return other != null && getClass() == other.getClass(); 1353 } 1354 1355 /** @return value-based hash code */ 1356 @Override 1357 public int hashCode() { 1358 return 0; 1359 } 1360 1361 /** @return diagnostic rendering */ 1362 @Override 1363 @NonNull 1364 public String toString() { 1365 return "HandlerQueued{}"; 1366 } 1367 } 1368 1369 /** 1370 * An application handler request left the bounded queue. 1371 * 1372 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1373 */ 1374 @ThreadSafe 1375 public final class HandlerDequeued implements McpMetricsEvent { 1376 @NonNull 1377 private static final HandlerDequeued INSTANCE = new HandlerDequeued(); 1378 1379 private HandlerDequeued() { 1380 } 1381 1382 /** @return whether this object is another handler-dequeued event */ 1383 @Override 1384 public boolean equals(@Nullable Object other) { 1385 return other != null && getClass() == other.getClass(); 1386 } 1387 1388 /** @return value-based hash code */ 1389 @Override 1390 public int hashCode() { 1391 return 0; 1392 } 1393 1394 /** @return diagnostic rendering */ 1395 @Override 1396 @NonNull 1397 public String toString() { 1398 return "HandlerDequeued{}"; 1399 } 1400 } 1401 1402 /** 1403 * Handler dispatch was rejected because the bounded queue was full. 1404 * 1405 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1406 */ 1407 @ThreadSafe 1408 public final class HandlerCapacityRejected implements McpMetricsEvent { 1409 @NonNull 1410 private static final HandlerCapacityRejected INSTANCE = 1411 new HandlerCapacityRejected(); 1412 1413 private HandlerCapacityRejected() { 1414 } 1415 1416 /** @return whether this object is another capacity-rejected event */ 1417 @Override 1418 public boolean equals(@Nullable Object other) { 1419 return other != null && getClass() == other.getClass(); 1420 } 1421 1422 /** @return value-based hash code */ 1423 @Override 1424 public int hashCode() { 1425 return 0; 1426 } 1427 1428 /** @return diagnostic rendering */ 1429 @Override 1430 @NonNull 1431 public String toString() { 1432 return "HandlerCapacityRejected{}"; 1433 } 1434 } 1435 1436 /** 1437 * The dedicated MCP transport recorded a bounded failure reason. 1438 * 1439 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1440 */ 1441 @ThreadSafe 1442 public final class TransportFailure implements McpMetricsEvent { 1443 private final MetricsCollector.@NonNull TransportFailureReason reason; 1444 1445 private TransportFailure( 1446 MetricsCollector.@NonNull TransportFailureReason reason) { 1447 this.reason = requireNonNull(reason); 1448 } 1449 1450 /** @return fixed low-level transport failure reason */ 1451 public MetricsCollector.@NonNull TransportFailureReason getReason() { 1452 return this.reason; 1453 } 1454 1455 /** @return whether this object contains the same failure reason */ 1456 @Override 1457 public boolean equals(@Nullable Object other) { 1458 if (this == other) 1459 return true; 1460 if (other == null || getClass() != other.getClass()) 1461 return false; 1462 TransportFailure that = (TransportFailure) other; 1463 return this.reason.equals(that.reason); 1464 } 1465 1466 /** @return value-based hash code */ 1467 @Override 1468 public int hashCode() { 1469 return this.reason.hashCode(); 1470 } 1471 1472 /** @return diagnostic rendering */ 1473 @Override 1474 @NonNull 1475 public String toString() { 1476 return "TransportFailure{reason=" + this.reason + "}"; 1477 } 1478 } 1479 1480 /** 1481 * A real listener stop completed with one fixed outcome. 1482 * 1483 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1484 */ 1485 @ThreadSafe 1486 public final class ServerStopped implements McpMetricsEvent { 1487 @NonNull 1488 private final ShutdownComponentDisposition shutdownComponentDisposition; 1489 1490 private ServerStopped( 1491 @NonNull ShutdownComponentDisposition shutdownComponentDisposition) { 1492 this.shutdownComponentDisposition = 1493 requireNonNull(shutdownComponentDisposition); 1494 } 1495 1496 /** @return fixed listener shutdown-component disposition */ 1497 @NonNull 1498 public ShutdownComponentDisposition getShutdownComponentDisposition() { 1499 return this.shutdownComponentDisposition; 1500 } 1501 1502 /** @return whether this object contains the same shutdown disposition */ 1503 @Override 1504 public boolean equals(@Nullable Object other) { 1505 if (this == other) 1506 return true; 1507 if (other == null || getClass() != other.getClass()) 1508 return false; 1509 ServerStopped that = (ServerStopped) other; 1510 return this.shutdownComponentDisposition.equals( 1511 that.shutdownComponentDisposition); 1512 } 1513 1514 /** @return value-based hash code */ 1515 @Override 1516 public int hashCode() { 1517 return this.shutdownComponentDisposition.hashCode(); 1518 } 1519 1520 /** @return diagnostic rendering */ 1521 @Override 1522 @NonNull 1523 public String toString() { 1524 return "ServerStopped{shutdownComponentDisposition=" 1525 + this.shutdownComponentDisposition + "}"; 1526 } 1527 } 1528 1529 private static void requireRoutedDimensions(@NonNull String endpointPath, 1530 @NonNull String jsonRpcMethod) { 1531 requireEndpointPath(endpointPath); 1532 if (requireNonNull(jsonRpcMethod).isEmpty()) 1533 throw new IllegalArgumentException( 1534 "JSON-RPC method must not be empty."); 1535 } 1536 1537 private static void requireEndpointPath(@NonNull String endpointPath) { 1538 if (requireNonNull(endpointPath).isEmpty()) 1539 throw new IllegalArgumentException( 1540 "Endpoint path must not be empty."); 1541 } 1542 1543 private static void requireNonNegative(@NonNull Duration duration) { 1544 if (requireNonNull(duration).isNegative()) 1545 throw new IllegalArgumentException("Duration must not be negative."); 1546 } 1547}