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}