001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.util.Collections;
025import java.util.EnumMap;
026import java.util.LinkedHashMap;
027import java.util.Map;
028import java.util.Objects;
029import java.util.TreeMap;
030
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Immutable aggregate of collected MCP metrics.
035 * <p>
036 * An empty instance permits the shared {@link MetricsCollector.Snapshot}
037 * attachment to remain non-null when no MCP metrics have been observed. The
038 * fixed core families deliberately expose no generic label map; applications
039 * that construct custom event values remain responsible for the
040 * confidentiality and cardinality of those values.
041 *
042 * @author <a href="https://www.revetkn.com">Mark Allen</a>
043 */
044@ThreadSafe
045public final class McpMetricsSnapshot {
046        @NonNull
047        private static final McpMetricsSnapshot EMPTY = builder().build();
048        @NonNull
049        private final Long activeHandlerExecutions;
050        @NonNull
051        private final Long handlerQueueDepth;
052        @NonNull
053        private final Long handlerCapacityRejections;
054        @NonNull
055        private final Map<@NonNull ShutdownComponentDisposition, @NonNull Long>
056                        serverStops;
057        @NonNull
058        private final Long connectionsAccepted;
059        @NonNull
060        private final Long connectionsRejected;
061        @NonNull
062        private final Map<MetricsCollector.@NonNull TransportFailureReason,
063                        @NonNull Long> transportFailures;
064        @NonNull
065        private final Long serverStarts;
066        @NonNull
067        private final Long requestsAccepted;
068        @NonNull
069        private final Long requestsRejected;
070        @NonNull
071        private final Long activeRequests;
072        @NonNull
073        private final Map<@NonNull RequestOutcomeKey, @NonNull Long> requests;
074        @NonNull
075        private final Map<@NonNull RequestOutcomeKey,
076                        MetricsCollector.@NonNull HistogramSnapshot> requestDurations;
077        @NonNull
078        private final Long activeRequestStreams;
079        @NonNull
080        private final Map<@NonNull RequestStreamTerminationKey,
081                        MetricsCollector.@NonNull HistogramSnapshot> requestStreamDurations;
082        @NonNull
083        private final Long activeSubscriptions;
084        @NonNull
085        private final Map<@NonNull SubscriptionTerminationKey,
086                        MetricsCollector.@NonNull HistogramSnapshot> subscriptionDurations;
087        @NonNull
088        private final Map<@NonNull SubscriptionMaintenanceKey, @NonNull Long>
089                        subscriptionMaintenance;
090        @NonNull
091        private final Map<@NonNull EndpointMethodKey, @NonNull Long>
092                        cancelationsSignaled;
093        @NonNull
094        private final Map<@NonNull EndpointMethodKey, @NonNull Long> progressEmitted;
095        @NonNull
096        private final Long keepAlivesEmitted;
097        @NonNull
098        private final Map<@NonNull Integer, @NonNull Long> protocolErrors;
099        @NonNull
100        private final Map<@NonNull EndpointMethodKey, @NonNull Long>
101                        unknownMirroredHeaders;
102
103        private McpMetricsSnapshot(@NonNull Builder builder) {
104                requireNonNull(builder);
105                this.activeHandlerExecutions = builder.activeHandlerExecutions;
106                this.handlerQueueDepth = builder.handlerQueueDepth;
107                this.handlerCapacityRejections = builder.handlerCapacityRejections;
108                this.serverStops = copyServerStops(builder.serverStops);
109                this.connectionsAccepted = builder.connectionsAccepted;
110                this.connectionsRejected = builder.connectionsRejected;
111                this.transportFailures = copyTransportFailures(builder.transportFailures);
112                this.serverStarts = builder.serverStarts;
113                this.requestsAccepted = builder.requestsAccepted;
114                this.requestsRejected = builder.requestsRejected;
115                this.activeRequests = builder.activeRequests;
116                this.requests = copyRequests(builder.requests);
117                this.requestDurations = copyRequestDurations(builder.requestDurations);
118                this.activeRequestStreams = builder.activeRequestStreams;
119                this.requestStreamDurations =
120                                copyRequestStreamDurations(builder.requestStreamDurations);
121                this.activeSubscriptions = builder.activeSubscriptions;
122                this.subscriptionDurations =
123                                copySubscriptionDurations(builder.subscriptionDurations);
124                this.subscriptionMaintenance =
125                                copySubscriptionMaintenanceCounts(builder.subscriptionMaintenance);
126                this.cancelationsSignaled = copyEndpointMethodCounts(
127                                builder.cancelationsSignaled,
128                                "MCP cancelation-signaled counts must not be negative.");
129                this.progressEmitted = copyEndpointMethodCounts(builder.progressEmitted,
130                                "MCP progress-emitted counts must not be negative.");
131                this.keepAlivesEmitted = builder.keepAlivesEmitted;
132                this.protocolErrors = copyProtocolErrorCounts(builder.protocolErrors);
133                this.unknownMirroredHeaders = copyEndpointMethodCounts(
134                                builder.unknownMirroredHeaders,
135                                "MCP unknown mirrored-header counts must not be negative.");
136        }
137
138        @NonNull
139        private static Long requireNonNegative(@NonNull Long value,
140                        @NonNull String diagnostic) {
141                long requiredValue = requireNonNull(value);
142                if (requiredValue < 0L)
143                        throw new IllegalArgumentException(requireNonNull(diagnostic));
144                return requiredValue;
145        }
146
147        @NonNull
148        private static Map<@NonNull ShutdownComponentDisposition, @NonNull Long>
149                        copyServerStops(@NonNull Map<@NonNull ShutdownComponentDisposition,
150                                        @NonNull Long> serverStops) {
151                EnumMap<ShutdownComponentDisposition, Long> copied =
152                                new EnumMap<>(ShutdownComponentDisposition.class);
153                requireNonNull(serverStops).forEach((disposition, count) -> {
154                        requireNonNull(disposition);
155                        requireNonNull(count);
156                        if (count < 0L)
157                                throw new IllegalArgumentException(
158                                                "MCP server-stop counts must not be negative.");
159                        copied.put(disposition, count);
160                });
161                return Collections.unmodifiableMap(copied);
162        }
163
164        @NonNull
165        private static Map<MetricsCollector.@NonNull TransportFailureReason,
166                        @NonNull Long> copyTransportFailures(
167                        @NonNull Map<MetricsCollector.@NonNull TransportFailureReason,
168                                        @NonNull Long> transportFailures) {
169                EnumMap<MetricsCollector.TransportFailureReason, Long> copied =
170                                new EnumMap<>(MetricsCollector.TransportFailureReason.class);
171                requireNonNull(transportFailures).forEach((reason, count) -> {
172                        requireNonNull(reason);
173                        requireNonNull(count);
174                        if (count < 0L)
175                                throw new IllegalArgumentException(
176                                                "MCP transport failure counts must not be negative.");
177                        copied.put(reason, count);
178                });
179                return Collections.unmodifiableMap(copied);
180        }
181
182        @NonNull
183        private static Map<@NonNull RequestOutcomeKey, @NonNull Long> copyRequests(
184                        @NonNull Map<@NonNull RequestOutcomeKey, @NonNull Long> requests) {
185                Map<RequestOutcomeKey, Long> copied = new LinkedHashMap<>();
186                requireNonNull(requests).forEach((key, count) -> {
187                        requireNonNull(key);
188                        requireNonNull(count);
189                        if (count < 0L)
190                                throw new IllegalArgumentException(
191                                                "MCP completed request counts must not be negative.");
192                        copied.put(key, count);
193                });
194                return Collections.unmodifiableMap(copied);
195        }
196
197        @NonNull
198        private static Map<@NonNull RequestOutcomeKey,
199                        MetricsCollector.@NonNull HistogramSnapshot> copyRequestDurations(
200                        @NonNull Map<@NonNull RequestOutcomeKey,
201                                        MetricsCollector.@NonNull HistogramSnapshot> requestDurations) {
202                Map<RequestOutcomeKey, MetricsCollector.HistogramSnapshot> copied =
203                                new LinkedHashMap<>();
204                requireNonNull(requestDurations).forEach((key, histogram) ->
205                                copied.put(requireNonNull(key), requireNonNull(histogram)));
206                return Collections.unmodifiableMap(copied);
207        }
208
209        @NonNull
210        private static Map<@NonNull RequestStreamTerminationKey,
211                        MetricsCollector.@NonNull HistogramSnapshot>
212        copyRequestStreamDurations(
213                        @NonNull Map<@NonNull RequestStreamTerminationKey,
214                                        MetricsCollector.@NonNull HistogramSnapshot>
215                                        requestStreamDurations) {
216                Map<RequestStreamTerminationKey, MetricsCollector.HistogramSnapshot>
217                                copied = new LinkedHashMap<>();
218                requireNonNull(requestStreamDurations).forEach((key, histogram) ->
219                                copied.put(requireNonNull(key), requireNonNull(histogram)));
220                return Collections.unmodifiableMap(copied);
221        }
222
223        @NonNull
224        private static Map<@NonNull SubscriptionTerminationKey,
225                        MetricsCollector.@NonNull HistogramSnapshot>
226        copySubscriptionDurations(
227                        @NonNull Map<@NonNull SubscriptionTerminationKey,
228                                        MetricsCollector.@NonNull HistogramSnapshot>
229                                        subscriptionDurations) {
230                Map<SubscriptionTerminationKey, MetricsCollector.HistogramSnapshot>
231                                copied = new LinkedHashMap<>();
232                requireNonNull(subscriptionDurations).forEach((key, histogram) ->
233                                copied.put(requireNonNull(key), requireNonNull(histogram)));
234                return Collections.unmodifiableMap(copied);
235        }
236
237        @NonNull
238        private static Map<@NonNull SubscriptionMaintenanceKey, @NonNull Long>
239        copySubscriptionMaintenanceCounts(
240                        @NonNull Map<@NonNull SubscriptionMaintenanceKey, @NonNull Long> counts) {
241                Map<SubscriptionMaintenanceKey, Long> copied = new LinkedHashMap<>();
242                requireNonNull(counts).forEach((key, count) -> {
243                        requireNonNull(key);
244                        requireNonNull(count);
245                        if (count < 0L)
246                                throw new IllegalArgumentException(
247                                                "MCP subscription-maintenance counts must not be negative.");
248                        copied.put(key, count);
249                });
250                return Collections.unmodifiableMap(copied);
251        }
252
253        @NonNull
254        private static Map<@NonNull EndpointMethodKey, @NonNull Long>
255        copyEndpointMethodCounts(
256                        @NonNull Map<@NonNull EndpointMethodKey, @NonNull Long> counts,
257                        @NonNull String diagnostic) {
258                Map<EndpointMethodKey, Long> copied = new LinkedHashMap<>();
259                requireNonNull(counts).forEach((key, count) -> {
260                        requireNonNull(key);
261                        requireNonNull(count);
262                        if (count < 0L)
263                                throw new IllegalArgumentException(requireNonNull(diagnostic));
264                        copied.put(key, count);
265                });
266                return Collections.unmodifiableMap(copied);
267        }
268
269        @NonNull
270        private static Map<@NonNull Integer, @NonNull Long>
271        copyProtocolErrorCounts(
272                        @NonNull Map<@NonNull Integer, @NonNull Long> protocolErrors) {
273                Map<Integer, Long> copied = new TreeMap<>();
274                requireNonNull(protocolErrors).forEach((code, count) -> {
275                        requireNonNull(code);
276                        requireNonNull(count);
277                        if (count < 0L)
278                                throw new IllegalArgumentException(
279                                                "MCP protocol-error counts must not be negative.");
280                        copied.put(code, count);
281                });
282                return Collections.unmodifiableMap(copied);
283        }
284
285        /**
286         * Returns the shared snapshot containing zero or empty MCP metric values.
287         *
288         * @return empty MCP metrics snapshot
289         */
290        @NonNull
291        public static McpMetricsSnapshot emptyInstance() {
292                return EMPTY;
293        }
294
295        /**
296         * Vends a builder for an MCP metrics snapshot.
297         *
298         * @return MCP metrics snapshot builder
299         */
300        @NonNull
301        public static Builder builder() {
302                return new Builder();
303        }
304
305        /**
306         * Returns the number of occupied MCP application-handler execution slots.
307         *
308         * @return active handler executions
309         */
310        @NonNull
311        public Long getActiveHandlerExecutions() {
312                return this.activeHandlerExecutions;
313        }
314
315        /**
316         * Returns the number of MCP application requests waiting for a handler slot.
317         *
318         * @return handler queue depth
319         */
320        @NonNull
321        public Long getHandlerQueueDepth() {
322                return this.handlerQueueDepth;
323        }
324
325        /**
326         * Returns the number of MCP application requests rejected because the
327         * bounded handler queue was full.
328         *
329         * @return handler capacity rejections
330         */
331        @NonNull
332        public Long getHandlerCapacityRejections() {
333                return this.handlerCapacityRejections;
334        }
335
336        /**
337         * Returns nonnegative server-stop counts grouped by fixed shutdown
338         * disposition.
339         *
340         * @return immutable, enum-ordered server-stop counts
341         */
342        @NonNull
343        public Map<@NonNull ShutdownComponentDisposition, @NonNull Long>
344                        getServerStops() {
345                return this.serverStops;
346        }
347
348        /**
349         * Returns the number of successfully accepted MCP connections admitted
350         * within the configured connection-capacity bound.
351         *
352         * @return accepted MCP connections
353         */
354        @NonNull
355        public Long getConnectionsAccepted() {
356                return this.connectionsAccepted;
357        }
358
359        /**
360         * Returns the number of MCP connections rejected because the configured
361         * connection-capacity bound was full.
362         *
363         * @return rejected MCP connections
364         */
365        @NonNull
366        public Long getConnectionsRejected() {
367                return this.connectionsRejected;
368        }
369
370        /**
371         * Returns nonnegative MCP transport-failure counts grouped by fixed reason.
372         *
373         * @return immutable, enum-ordered MCP transport-failure counts
374         */
375        @NonNull
376        public Map<MetricsCollector.@NonNull TransportFailureReason,
377                        @NonNull Long> getTransportFailures() {
378                return this.transportFailures;
379        }
380
381        /**
382         * Returns the number of successful MCP server starts.
383         *
384         * @return successful MCP server starts
385         */
386        @NonNull
387        public Long getServerStarts() {
388                return this.serverStarts;
389        }
390
391        /**
392         * Returns the number of MCP requests accepted by the bounded protocol
393         * processor. Processor acceptance can precede a later pre-admission
394         * rejection, so this count is independent of {@link #getRequestsRejected()}.
395         *
396         * @return accepted MCP requests
397         */
398        @NonNull
399        public Long getRequestsAccepted() {
400                return this.requestsAccepted;
401        }
402
403        /**
404         * Returns the number of MCP requests rejected before admitted semantic
405         * handling. This count is not the complement of
406         * {@link #getRequestsAccepted()}.
407         *
408         * @return rejected MCP requests
409         */
410        @NonNull
411        public Long getRequestsRejected() {
412                return this.requestsRejected;
413        }
414
415        /**
416         * Returns the number of currently active admitted MCP requests.
417         *
418         * @return active admitted MCP requests
419         */
420        @NonNull
421        public Long getActiveRequests() {
422                return this.activeRequests;
423        }
424
425        /**
426         * Returns nonnegative completed-request counts grouped by bounded endpoint,
427         * method, and terminal outcome dimensions.
428         *
429         * @return immutable completed-request counts
430         */
431        @NonNull
432        public Map<@NonNull RequestOutcomeKey, @NonNull Long> getRequests() {
433                return this.requests;
434        }
435
436        /**
437         * Returns request-duration histograms grouped by bounded endpoint, method,
438         * and terminal outcome dimensions.
439         *
440         * @return immutable request-duration histograms
441         */
442        @NonNull
443        public Map<@NonNull RequestOutcomeKey,
444                        MetricsCollector.@NonNull HistogramSnapshot> getRequestDurations() {
445                return this.requestDurations;
446        }
447
448        /**
449         * Returns the number of currently active MCP request streams.
450         *
451         * @return active MCP request streams
452         */
453        @NonNull
454        public Long getActiveRequestStreams() {
455                return this.activeRequestStreams;
456        }
457
458        /**
459         * Returns request-stream duration histograms grouped by bounded endpoint,
460         * method, and fixed termination reason dimensions.
461         *
462         * @return immutable request-stream duration histograms
463         */
464        @NonNull
465        public Map<@NonNull RequestStreamTerminationKey,
466                        MetricsCollector.@NonNull HistogramSnapshot>
467        getRequestStreamDurations() {
468                return this.requestStreamDurations;
469        }
470
471        /**
472         * Returns the number of currently active MCP subscriptions.
473         *
474         * @return active MCP subscriptions
475         */
476        @NonNull
477        public Long getActiveSubscriptions() {
478                return this.activeSubscriptions;
479        }
480
481        /**
482         * Returns subscription-duration histograms grouped by bounded endpoint and
483         * fixed termination reason dimensions.
484         *
485         * @return immutable subscription-duration histograms
486         */
487        @NonNull
488        public Map<@NonNull SubscriptionTerminationKey,
489                        MetricsCollector.@NonNull HistogramSnapshot> getSubscriptionDurations() {
490                return this.subscriptionDurations;
491        }
492
493        /**
494         * Returns delivered subscription-maintenance event counts grouped by bounded
495         * endpoint, fixed work and fixed outcome dimensions across supported MCP
496         * revisions. Counts include coalescing and stale-result discards; they do not
497         * measure unique subscriptions, started attempts, durations or active work.
498         *
499         * @return immutable subscription-maintenance event counts
500         */
501        @NonNull
502        public Map<@NonNull SubscriptionMaintenanceKey, @NonNull Long>
503        getSubscriptionMaintenance() {
504                return this.subscriptionMaintenance;
505        }
506
507        /**
508         * Returns cooperative request-cancelation signals grouped by bounded
509         * endpoint and method dimensions.
510         *
511         * @return immutable cancelation-signaled counts
512         */
513        @NonNull
514        public Map<@NonNull EndpointMethodKey, @NonNull Long>
515        getCancelationsSignaled() {
516                return this.cancelationsSignaled;
517        }
518
519        /**
520         * Returns progress notifications accepted for delivery grouped by bounded
521         * endpoint and method dimensions.
522         *
523         * @return immutable progress-emitted counts
524         */
525        @NonNull
526        public Map<@NonNull EndpointMethodKey, @NonNull Long> getProgressEmitted() {
527                return this.progressEmitted;
528        }
529
530        /**
531         * Returns the number of MCP keep-alive comments accepted for delivery.
532         *
533         * @return keep-alive comments accepted for delivery
534         */
535        @NonNull
536        public Long getKeepAlivesEmitted() {
537                return this.keepAlivesEmitted;
538        }
539
540        /**
541         * Returns client-visible MCP protocol errors grouped by error code.
542         *
543         * @return immutable protocol-error counts
544         */
545        @NonNull
546        public Map<@NonNull Integer, @NonNull Long> getProtocolErrors() {
547                return this.protocolErrors;
548        }
549
550        /**
551         * Returns unknown mirrored-header occurrences grouped by bounded endpoint
552         * and method dimensions.
553         *
554         * @return immutable unknown mirrored-header counts
555         */
556        @NonNull
557        public Map<@NonNull EndpointMethodKey, @NonNull Long>
558        getUnknownMirroredHeaders() {
559                return this.unknownMirroredHeaders;
560        }
561
562        /**
563         * Key for endpoint-and-method counter aggregates.
564         *
565         * @author <a href="https://www.revetkn.com">Mark Allen</a>
566         */
567        @ThreadSafe
568        public static final class EndpointMethodKey {
569                @NonNull
570                private final String endpointPath;
571                @NonNull
572                private final String jsonRpcMethod;
573
574                /**
575                 * Creates an endpoint-and-method aggregate key.
576                 *
577                 * @param endpointPath registered endpoint-path declaration
578                 * @param jsonRpcMethod bounded JSON-RPC method dimension
579                 * @return endpoint-and-method aggregate key
580                 */
581                @NonNull
582                public static EndpointMethodKey fromDimensions(
583                                @NonNull String endpointPath, @NonNull String jsonRpcMethod) {
584                        return new EndpointMethodKey(endpointPath, jsonRpcMethod);
585                }
586
587                private EndpointMethodKey(@NonNull String endpointPath,
588                                @NonNull String jsonRpcMethod) {
589                        if (requireNonNull(endpointPath).isEmpty())
590                                throw new IllegalArgumentException(
591                                                "Endpoint path must not be empty.");
592                        if (requireNonNull(jsonRpcMethod).isEmpty())
593                                throw new IllegalArgumentException(
594                                                "JSON-RPC method must not be empty.");
595                        this.endpointPath = endpointPath;
596                        this.jsonRpcMethod = jsonRpcMethod;
597                }
598
599                /** @return registered endpoint-path declaration */
600                @NonNull
601                public String getEndpointPath() {
602                        return this.endpointPath;
603                }
604
605                /** @return bounded JSON-RPC method dimension */
606                @NonNull
607                public String getJsonRpcMethod() {
608                        return this.jsonRpcMethod;
609                }
610
611                /** @return whether both aggregate dimensions are equal */
612                @Override
613                public boolean equals(@Nullable Object other) {
614                        if (this == other)
615                                return true;
616                        if (!(other instanceof EndpointMethodKey key))
617                                return false;
618                        return this.endpointPath.equals(key.endpointPath)
619                                        && this.jsonRpcMethod.equals(key.jsonRpcMethod);
620                }
621
622                /** @return value-based hash code */
623                @Override
624                public int hashCode() {
625                        return Objects.hash(this.endpointPath, this.jsonRpcMethod);
626                }
627
628                /** @return dimension-redacted diagnostic rendering */
629                @Override
630                @NonNull
631                public String toString() {
632                        return "EndpointMethodKey{endpointPath=<redacted>, "
633                                        + "jsonRpcMethod=<redacted>}";
634                }
635        }
636
637        /**
638         * Key for completed-request and request-duration aggregates.
639         *
640         * @author <a href="https://www.revetkn.com">Mark Allen</a>
641         */
642        @ThreadSafe
643        public static final class RequestOutcomeKey {
644                @NonNull
645                private final String endpointPath;
646                @NonNull
647                private final String jsonRpcMethod;
648                @NonNull
649                private final McpRequestOutcome outcome;
650
651                /**
652                 * Creates a request-outcome aggregate key.
653                 *
654                 * @param endpointPath registered endpoint-path declaration
655                 * @param jsonRpcMethod bounded JSON-RPC method dimension
656                 * @param requestOutcome fixed terminal request outcome
657                 * @return request-outcome aggregate key
658                 */
659                @NonNull
660                public static RequestOutcomeKey fromDimensions(
661                                @NonNull String endpointPath, @NonNull String jsonRpcMethod,
662                                @NonNull McpRequestOutcome requestOutcome) {
663                        return new RequestOutcomeKey(endpointPath, jsonRpcMethod,
664                                        requestOutcome);
665                }
666
667                private RequestOutcomeKey(@NonNull String endpointPath,
668                                @NonNull String jsonRpcMethod,
669                                @NonNull McpRequestOutcome outcome) {
670                        if (requireNonNull(endpointPath).isEmpty())
671                                throw new IllegalArgumentException(
672                                                "Endpoint path must not be empty.");
673                        if (requireNonNull(jsonRpcMethod).isEmpty())
674                                throw new IllegalArgumentException(
675                                                "JSON-RPC method must not be empty.");
676                        this.endpointPath = endpointPath;
677                        this.jsonRpcMethod = jsonRpcMethod;
678                        this.outcome = requireNonNull(outcome);
679                }
680
681                /** @return registered endpoint-path declaration */
682                @NonNull
683                public String getEndpointPath() {
684                        return this.endpointPath;
685                }
686
687                /** @return bounded JSON-RPC method dimension */
688                @NonNull
689                public String getJsonRpcMethod() {
690                        return this.jsonRpcMethod;
691                }
692
693                /** @return fixed terminal request outcome */
694                @NonNull
695                public McpRequestOutcome getOutcome() {
696                        return this.outcome;
697                }
698
699                /** @return whether all aggregate dimensions are equal */
700                @Override
701                public boolean equals(@Nullable Object other) {
702                        if (this == other)
703                                return true;
704                        if (!(other instanceof RequestOutcomeKey key))
705                                return false;
706                        return this.endpointPath.equals(key.endpointPath)
707                                        && this.jsonRpcMethod.equals(key.jsonRpcMethod)
708                                        && this.outcome == key.outcome;
709                }
710
711                /** @return value-based hash code */
712                @Override
713                public int hashCode() {
714                        return Objects.hash(this.endpointPath, this.jsonRpcMethod,
715                                        this.outcome);
716                }
717
718                /** @return diagnostic rendering with application dimensions redacted */
719                @Override
720                @NonNull
721                public String toString() {
722                        return "RequestOutcomeKey{endpointPath=<redacted>, "
723                                        + "jsonRpcMethod=<redacted>, outcome=" + this.outcome + "}";
724                }
725        }
726
727        /**
728         * Key for request-stream duration aggregates.
729         *
730         * @author <a href="https://www.revetkn.com">Mark Allen</a>
731         */
732        @ThreadSafe
733        public static final class RequestStreamTerminationKey {
734                @NonNull
735                private final String endpointPath;
736                @NonNull
737                private final String jsonRpcMethod;
738                @NonNull
739                private final McpStreamTerminationReason reason;
740
741                /**
742                 * Creates a request-stream termination aggregate key.
743                 *
744                 * @param endpointPath registered endpoint-path declaration
745                 * @param jsonRpcMethod bounded JSON-RPC method dimension
746                 * @param streamTerminationReason fixed request-stream termination reason
747                 * @return request-stream termination aggregate key
748                 */
749                @NonNull
750                public static RequestStreamTerminationKey fromDimensions(
751                                @NonNull String endpointPath, @NonNull String jsonRpcMethod,
752                                @NonNull McpStreamTerminationReason streamTerminationReason) {
753                        return new RequestStreamTerminationKey(endpointPath, jsonRpcMethod,
754                                        streamTerminationReason);
755                }
756
757                private RequestStreamTerminationKey(@NonNull String endpointPath,
758                                @NonNull String jsonRpcMethod,
759                                @NonNull McpStreamTerminationReason reason) {
760                        if (requireNonNull(endpointPath).isEmpty())
761                                throw new IllegalArgumentException(
762                                                "Endpoint path must not be empty.");
763                        if (requireNonNull(jsonRpcMethod).isEmpty())
764                                throw new IllegalArgumentException(
765                                                "JSON-RPC method must not be empty.");
766                        this.endpointPath = endpointPath;
767                        this.jsonRpcMethod = jsonRpcMethod;
768                        this.reason = requireNonNull(reason);
769                }
770
771                /** @return registered endpoint-path declaration */
772                @NonNull
773                public String getEndpointPath() {
774                        return this.endpointPath;
775                }
776
777                /** @return bounded JSON-RPC method dimension */
778                @NonNull
779                public String getJsonRpcMethod() {
780                        return this.jsonRpcMethod;
781                }
782
783                /** @return fixed request-stream termination reason */
784                @NonNull
785                public McpStreamTerminationReason getReason() {
786                        return this.reason;
787                }
788
789                /** @return whether all aggregate dimensions are equal */
790                @Override
791                public boolean equals(@Nullable Object other) {
792                        if (this == other)
793                                return true;
794                        if (!(other instanceof RequestStreamTerminationKey key))
795                                return false;
796                        return this.endpointPath.equals(key.endpointPath)
797                                        && this.jsonRpcMethod.equals(key.jsonRpcMethod)
798                                        && this.reason == key.reason;
799                }
800
801                /** @return value-based hash code */
802                @Override
803                public int hashCode() {
804                        return Objects.hash(this.endpointPath, this.jsonRpcMethod,
805                                        this.reason);
806                }
807
808                /** @return diagnostic rendering with application dimensions redacted */
809                @Override
810                @NonNull
811                public String toString() {
812                        return "RequestStreamTerminationKey{endpointPath=<redacted>, "
813                                        + "jsonRpcMethod=<redacted>, reason=" + this.reason + "}";
814                }
815        }
816
817        /**
818         * Key for subscription-duration aggregates.
819         *
820         * @author <a href="https://www.revetkn.com">Mark Allen</a>
821         */
822        @ThreadSafe
823        public static final class SubscriptionTerminationKey {
824                @NonNull
825                private final String endpointPath;
826                @NonNull
827                private final McpStreamTerminationReason reason;
828
829                /**
830                 * Creates a subscription termination aggregate key.
831                 *
832                 * @param endpointPath registered endpoint-path declaration
833                 * @param streamTerminationReason fixed subscription termination reason
834                 * @return subscription termination aggregate key
835                 */
836                @NonNull
837                public static SubscriptionTerminationKey fromDimensions(
838                                @NonNull String endpointPath,
839                                @NonNull McpStreamTerminationReason streamTerminationReason) {
840                        return new SubscriptionTerminationKey(endpointPath,
841                                        streamTerminationReason);
842                }
843
844                private SubscriptionTerminationKey(@NonNull String endpointPath,
845                                @NonNull McpStreamTerminationReason reason) {
846                        if (requireNonNull(endpointPath).isEmpty())
847                                throw new IllegalArgumentException(
848                                                "Endpoint path must not be empty.");
849                        this.endpointPath = endpointPath;
850                        this.reason = requireNonNull(reason);
851                }
852
853                /** @return registered endpoint-path declaration */
854                @NonNull
855                public String getEndpointPath() {
856                        return this.endpointPath;
857                }
858
859                /** @return fixed subscription termination reason */
860                @NonNull
861                public McpStreamTerminationReason getReason() {
862                        return this.reason;
863                }
864
865                /** @return whether both aggregate dimensions are equal */
866                @Override
867                public boolean equals(@Nullable Object other) {
868                        if (this == other)
869                                return true;
870                        if (!(other instanceof SubscriptionTerminationKey key))
871                                return false;
872                        return this.endpointPath.equals(key.endpointPath)
873                                        && this.reason == key.reason;
874                }
875
876                /** @return value-based hash code */
877                @Override
878                public int hashCode() {
879                        return Objects.hash(this.endpointPath, this.reason);
880                }
881
882                /** @return diagnostic rendering with the application dimension redacted */
883                @Override
884                @NonNull
885                public String toString() {
886                        return "SubscriptionTerminationKey{endpointPath=<redacted>, reason="
887                                        + this.reason + "}";
888                }
889        }
890
891        /**
892         * Immutable subscription-maintenance aggregate dimensions, shared across
893         * supported MCP revisions. The endpoint is an application-configured path
894         * declaration, never a subscription ID or arbitrary request target.
895         *
896         * @author <a href="https://www.revetkn.com">Mark Allen</a>
897         */
898        @ThreadSafe
899        public static final class SubscriptionMaintenanceKey {
900                @NonNull
901                private final String endpointPath;
902                private final McpMetricsEvent.SubscriptionMaintenance.@NonNull Work work;
903                private final McpMetricsEvent.SubscriptionMaintenance.@NonNull Outcome outcome;
904
905                /**
906                 * Creates a subscription-maintenance aggregate key.
907                 *
908                 * @param endpointPath registered endpoint-path declaration
909                 * @param maintenanceWork fixed maintenance work classification
910                 * @param maintenanceOutcome fixed observed maintenance outcome
911                 * @return subscription-maintenance aggregate key
912                 * @throws IllegalArgumentException if the endpoint path is empty
913                 */
914                @NonNull
915                public static SubscriptionMaintenanceKey fromDimensions(
916                                @NonNull String endpointPath,
917                                McpMetricsEvent.SubscriptionMaintenance.@NonNull Work maintenanceWork,
918                                McpMetricsEvent.SubscriptionMaintenance.@NonNull Outcome maintenanceOutcome) {
919                        return new SubscriptionMaintenanceKey(endpointPath, maintenanceWork,
920                                        maintenanceOutcome);
921                }
922
923                private SubscriptionMaintenanceKey(@NonNull String endpointPath,
924                                McpMetricsEvent.SubscriptionMaintenance.@NonNull Work maintenanceWork,
925                                McpMetricsEvent.SubscriptionMaintenance.@NonNull Outcome maintenanceOutcome) {
926                        if (requireNonNull(endpointPath).isEmpty())
927                                throw new IllegalArgumentException("Endpoint path must not be empty.");
928                        this.endpointPath = endpointPath;
929                        this.work = requireNonNull(maintenanceWork);
930                        this.outcome = requireNonNull(maintenanceOutcome);
931                }
932
933                /** @return registered endpoint-path declaration */
934                @NonNull
935                public String getEndpointPath() {
936                        return this.endpointPath;
937                }
938
939                /** @return fixed maintenance work classification */
940                public McpMetricsEvent.SubscriptionMaintenance.@NonNull Work getWork() {
941                        return this.work;
942                }
943
944                /** @return fixed observed maintenance outcome */
945                public McpMetricsEvent.SubscriptionMaintenance.@NonNull Outcome getOutcome() {
946                        return this.outcome;
947                }
948
949                /** @return whether all three aggregate dimensions are equal */
950                @Override
951                public boolean equals(@Nullable Object other) {
952                        if (this == other)
953                                return true;
954                        if (!(other instanceof SubscriptionMaintenanceKey key))
955                                return false;
956                        return this.endpointPath.equals(key.endpointPath)
957                                        && this.work == key.work && this.outcome == key.outcome;
958                }
959
960                /** @return value-based hash code */
961                @Override
962                public int hashCode() {
963                        return Objects.hash(this.endpointPath, this.work, this.outcome);
964                }
965
966                /** @return diagnostic rendering with the application dimension redacted */
967                @Override
968                @NonNull
969                public String toString() {
970                        return "SubscriptionMaintenanceKey{endpointPath=<redacted>, work="
971                                        + this.work + ", outcome=" + this.outcome + "}";
972                }
973        }
974
975        /**
976         * Builder for immutable {@link McpMetricsSnapshot} instances.
977         *
978         * @author <a href="https://www.revetkn.com">Mark Allen</a>
979         */
980        @NotThreadSafe
981        public static final class Builder {
982                @NonNull
983                private Long activeHandlerExecutions;
984                @NonNull
985                private Long handlerQueueDepth;
986                @NonNull
987                private Long handlerCapacityRejections;
988                @NonNull
989                private Map<@NonNull ShutdownComponentDisposition, @NonNull Long>
990                                serverStops;
991                @NonNull
992                private Long connectionsAccepted;
993                @NonNull
994                private Long connectionsRejected;
995                @NonNull
996                private Map<MetricsCollector.@NonNull TransportFailureReason,
997                                @NonNull Long> transportFailures;
998                @NonNull
999                private Long serverStarts;
1000                @NonNull
1001                private Long requestsAccepted;
1002                @NonNull
1003                private Long requestsRejected;
1004                @NonNull
1005                private Long activeRequests;
1006                @NonNull
1007                private Map<@NonNull RequestOutcomeKey, @NonNull Long> requests;
1008                @NonNull
1009                private Map<@NonNull RequestOutcomeKey,
1010                                MetricsCollector.@NonNull HistogramSnapshot> requestDurations;
1011                @NonNull
1012                private Long activeRequestStreams;
1013                @NonNull
1014                private Map<@NonNull RequestStreamTerminationKey,
1015                                MetricsCollector.@NonNull HistogramSnapshot> requestStreamDurations;
1016                @NonNull
1017                private Long activeSubscriptions;
1018                @NonNull
1019                private Map<@NonNull SubscriptionTerminationKey,
1020                                MetricsCollector.@NonNull HistogramSnapshot> subscriptionDurations;
1021                @NonNull
1022                private Map<@NonNull SubscriptionMaintenanceKey, @NonNull Long>
1023                                subscriptionMaintenance;
1024                @NonNull
1025                private Map<@NonNull EndpointMethodKey, @NonNull Long>
1026                                cancelationsSignaled;
1027                @NonNull
1028                private Map<@NonNull EndpointMethodKey, @NonNull Long> progressEmitted;
1029                @NonNull
1030                private Long keepAlivesEmitted;
1031                @NonNull
1032                private Map<@NonNull Integer, @NonNull Long> protocolErrors;
1033                @NonNull
1034                private Map<@NonNull EndpointMethodKey, @NonNull Long>
1035                                unknownMirroredHeaders;
1036
1037                private Builder() {
1038                        this.activeHandlerExecutions = 0L;
1039                        this.handlerQueueDepth = 0L;
1040                        this.handlerCapacityRejections = 0L;
1041                        this.serverStops = Map.of();
1042                        this.connectionsAccepted = 0L;
1043                        this.connectionsRejected = 0L;
1044                        this.transportFailures = Map.of();
1045                        this.serverStarts = 0L;
1046                        this.requestsAccepted = 0L;
1047                        this.requestsRejected = 0L;
1048                        this.activeRequests = 0L;
1049                        this.requests = Map.of();
1050                        this.requestDurations = Map.of();
1051                        this.activeRequestStreams = 0L;
1052                        this.requestStreamDurations = Map.of();
1053                        this.activeSubscriptions = 0L;
1054                        this.subscriptionDurations = Map.of();
1055                        this.subscriptionMaintenance = Map.of();
1056                        this.cancelationsSignaled = Map.of();
1057                        this.progressEmitted = Map.of();
1058                        this.keepAlivesEmitted = 0L;
1059                        this.protocolErrors = Map.of();
1060                        this.unknownMirroredHeaders = Map.of();
1061                }
1062
1063                /**
1064                 * Sets the number of occupied MCP application-handler execution slots.
1065                 *
1066                 * @param activeHandlerExecutions active handler executions
1067                 * @return this builder
1068                 * @throws IllegalArgumentException if the count is negative
1069                 */
1070                @NonNull
1071                public Builder activeHandlerExecutions(
1072                                @NonNull Long activeHandlerExecutions) {
1073                        this.activeHandlerExecutions = requireNonNegative(
1074                                        activeHandlerExecutions,
1075                                        "Active MCP handler executions must not be negative.");
1076                        return this;
1077                }
1078
1079                /**
1080                 * Sets the number of MCP application requests waiting for a handler slot.
1081                 *
1082                 * @param handlerQueueDepth handler queue depth
1083                 * @return this builder
1084                 * @throws IllegalArgumentException if the count is negative
1085                 */
1086                @NonNull
1087                public Builder handlerQueueDepth(@NonNull Long handlerQueueDepth) {
1088                        this.handlerQueueDepth = requireNonNegative(handlerQueueDepth,
1089                                        "MCP handler queue depth must not be negative.");
1090                        return this;
1091                }
1092
1093                /**
1094                 * Sets the number of MCP application requests rejected because the
1095                 * bounded handler queue was full.
1096                 *
1097                 * @param handlerCapacityRejections handler capacity rejections
1098                 * @return this builder
1099                 * @throws IllegalArgumentException if the count is negative
1100                 */
1101                @NonNull
1102                public Builder handlerCapacityRejections(
1103                                @NonNull Long handlerCapacityRejections) {
1104                        this.handlerCapacityRejections = requireNonNegative(
1105                                        handlerCapacityRejections,
1106                                        "MCP handler capacity rejections must not be negative.");
1107                        return this;
1108                }
1109
1110                /**
1111                 * Replaces the server-stop counts grouped by fixed shutdown disposition.
1112                 * Passing {@code null} or an empty map clears all server-stop counts.
1113                 *
1114                 * @param serverStops nonnegative server-stop counts, or {@code null} to
1115                 *                    clear them
1116                 * @return this builder
1117                 * @throws IllegalArgumentException if any count is negative
1118                 */
1119                @NonNull
1120                public Builder serverStops(
1121                                @Nullable Map<@NonNull ShutdownComponentDisposition,
1122                                                @NonNull Long> serverStops) {
1123                        this.serverStops = serverStops == null ? Map.of()
1124                                        : copyServerStops(serverStops);
1125                        return this;
1126                }
1127
1128                /**
1129                 * Sets the number of successfully accepted MCP connections admitted within
1130                 * the configured connection-capacity bound.
1131                 *
1132                 * @param connectionsAccepted accepted MCP connections
1133                 * @return this builder
1134                 * @throws IllegalArgumentException if the count is negative
1135                 */
1136                @NonNull
1137                public Builder connectionsAccepted(@NonNull Long connectionsAccepted) {
1138                        this.connectionsAccepted = requireNonNegative(connectionsAccepted,
1139                                        "Accepted MCP connection count must not be negative.");
1140                        return this;
1141                }
1142
1143                /**
1144                 * Sets the number of MCP connections rejected because the configured
1145                 * connection-capacity bound was full.
1146                 *
1147                 * @param connectionsRejected rejected MCP connections
1148                 * @return this builder
1149                 * @throws IllegalArgumentException if the count is negative
1150                 */
1151                @NonNull
1152                public Builder connectionsRejected(@NonNull Long connectionsRejected) {
1153                        this.connectionsRejected = requireNonNegative(connectionsRejected,
1154                                        "Rejected MCP connection count must not be negative.");
1155                        return this;
1156                }
1157
1158                /**
1159                 * Replaces the MCP transport-failure counts grouped by fixed reason.
1160                 * Passing {@code null} or an empty map clears all transport-failure counts.
1161                 *
1162                 * @param transportFailures nonnegative MCP transport-failure counts, or
1163                 *                          {@code null} to clear them
1164                 * @return this builder
1165                 * @throws IllegalArgumentException if any count is negative
1166                 */
1167                @NonNull
1168                public Builder transportFailures(
1169                                @Nullable Map<MetricsCollector.@NonNull TransportFailureReason,
1170                                                @NonNull Long> transportFailures) {
1171                        this.transportFailures = transportFailures == null ? Map.of()
1172                                        : copyTransportFailures(transportFailures);
1173                        return this;
1174                }
1175
1176                /**
1177                 * Sets the number of successful MCP server starts.
1178                 *
1179                 * @param serverStarts successful MCP server starts
1180                 * @return this builder
1181                 * @throws IllegalArgumentException if the count is negative
1182                 */
1183                @NonNull
1184                public Builder serverStarts(@NonNull Long serverStarts) {
1185                        this.serverStarts = requireNonNegative(serverStarts,
1186                                        "MCP server start count must not be negative.");
1187                        return this;
1188                }
1189
1190                /**
1191                 * Sets the number of MCP requests accepted by the bounded protocol
1192                 * processor.
1193                 *
1194                 * @param requestsAccepted accepted MCP requests
1195                 * @return this builder
1196                 * @throws IllegalArgumentException if the count is negative
1197                 */
1198                @NonNull
1199                public Builder requestsAccepted(@NonNull Long requestsAccepted) {
1200                        this.requestsAccepted = requireNonNegative(requestsAccepted,
1201                                        "Accepted MCP request count must not be negative.");
1202                        return this;
1203                }
1204
1205                /**
1206                 * Sets the number of MCP requests rejected before admitted semantic
1207                 * handling.
1208                 *
1209                 * @param requestsRejected rejected MCP requests
1210                 * @return this builder
1211                 * @throws IllegalArgumentException if the count is negative
1212                 */
1213                @NonNull
1214                public Builder requestsRejected(@NonNull Long requestsRejected) {
1215                        this.requestsRejected = requireNonNegative(requestsRejected,
1216                                        "Rejected MCP request count must not be negative.");
1217                        return this;
1218                }
1219
1220                /**
1221                 * Sets the number of currently active admitted MCP requests.
1222                 *
1223                 * @param activeRequests active admitted MCP requests
1224                 * @return this builder
1225                 * @throws IllegalArgumentException if the count is negative
1226                 */
1227                @NonNull
1228                public Builder activeRequests(@NonNull Long activeRequests) {
1229                        this.activeRequests = requireNonNegative(activeRequests,
1230                                        "Active MCP request count must not be negative.");
1231                        return this;
1232                }
1233
1234                /**
1235                 * Replaces completed-request counts grouped by bounded endpoint, method,
1236                 * and terminal outcome dimensions. Passing {@code null} or an empty map
1237                 * clears all completed-request counts.
1238                 *
1239                 * @param requests completed-request counts, or {@code null} to clear them
1240                 * @return this builder
1241                 * @throws IllegalArgumentException if any count is negative
1242                 */
1243                @NonNull
1244                public Builder requests(
1245                                @Nullable Map<@NonNull RequestOutcomeKey, @NonNull Long> requests) {
1246                        this.requests = requests == null ? Map.of() : copyRequests(requests);
1247                        return this;
1248                }
1249
1250                /**
1251                 * Replaces request-duration histograms grouped by bounded endpoint, method,
1252                 * and terminal outcome dimensions. Passing {@code null} or an empty map
1253                 * clears all request-duration histograms.
1254                 *
1255                 * @param requestDurations request-duration histograms, or {@code null} to
1256                 *                         clear them
1257                 * @return this builder
1258                 */
1259                @NonNull
1260                public Builder requestDurations(
1261                                @Nullable Map<@NonNull RequestOutcomeKey,
1262                                                MetricsCollector.@NonNull HistogramSnapshot> requestDurations) {
1263                        this.requestDurations = requestDurations == null ? Map.of()
1264                                        : copyRequestDurations(requestDurations);
1265                        return this;
1266                }
1267
1268                /**
1269                 * Sets the number of currently active MCP request streams.
1270                 *
1271                 * @param activeRequestStreams active MCP request streams
1272                 * @return this builder
1273                 * @throws IllegalArgumentException if the count is negative
1274                 */
1275                @NonNull
1276                public Builder activeRequestStreams(@NonNull Long activeRequestStreams) {
1277                        this.activeRequestStreams = requireNonNegative(activeRequestStreams,
1278                                        "Active MCP request-stream count must not be negative.");
1279                        return this;
1280                }
1281
1282                /**
1283                 * Replaces request-stream duration histograms grouped by bounded endpoint,
1284                 * method, and fixed termination reason dimensions. Passing {@code null} or
1285                 * an empty map clears all request-stream duration histograms.
1286                 *
1287                 * @param requestStreamDurations request-stream duration histograms, or
1288                 *                               {@code null} to clear them
1289                 * @return this builder
1290                 */
1291                @NonNull
1292                public Builder requestStreamDurations(
1293                                @Nullable Map<@NonNull RequestStreamTerminationKey,
1294                                                MetricsCollector.@NonNull HistogramSnapshot>
1295                                                requestStreamDurations) {
1296                        this.requestStreamDurations = requestStreamDurations == null
1297                                        ? Map.of()
1298                                        : copyRequestStreamDurations(requestStreamDurations);
1299                        return this;
1300                }
1301
1302                /**
1303                 * Sets the number of currently active MCP subscriptions.
1304                 *
1305                 * @param activeSubscriptions active MCP subscriptions
1306                 * @return this builder
1307                 * @throws IllegalArgumentException if the count is negative
1308                 */
1309                @NonNull
1310                public Builder activeSubscriptions(@NonNull Long activeSubscriptions) {
1311                        this.activeSubscriptions = requireNonNegative(activeSubscriptions,
1312                                        "Active MCP subscription count must not be negative.");
1313                        return this;
1314                }
1315
1316                /**
1317                 * Replaces subscription-duration histograms grouped by bounded endpoint and
1318                 * fixed termination reason dimensions. Passing {@code null} or an empty map
1319                 * clears all subscription-duration histograms.
1320                 *
1321                 * @param subscriptionDurations subscription-duration histograms, or
1322                 *                              {@code null} to clear them
1323                 * @return this builder
1324                 */
1325                @NonNull
1326                public Builder subscriptionDurations(
1327                                @Nullable Map<@NonNull SubscriptionTerminationKey,
1328                                                MetricsCollector.@NonNull HistogramSnapshot>
1329                                                subscriptionDurations) {
1330                        this.subscriptionDurations = subscriptionDurations == null
1331                                        ? Map.of()
1332                                        : copySubscriptionDurations(subscriptionDurations);
1333                        return this;
1334                }
1335
1336                /**
1337                 * Replaces delivered subscription-maintenance event counts grouped by
1338                 * bounded endpoint and fixed work/outcome dimensions. Passing {@code null}
1339                 * or an empty map clears these counts. The map is defensively copied;
1340                 * explicit zero counts are retained.
1341                 *
1342                 * @param subscriptionMaintenance subscription-maintenance event counts,
1343                 *                                or {@code null} to clear them
1344                 * @return this builder
1345                 * @throws IllegalArgumentException if any count is negative
1346                 */
1347                @NonNull
1348                public Builder subscriptionMaintenance(
1349                                @Nullable Map<@NonNull SubscriptionMaintenanceKey, @NonNull Long>
1350                                                subscriptionMaintenance) {
1351                        this.subscriptionMaintenance = subscriptionMaintenance == null
1352                                        ? Map.of() : copySubscriptionMaintenanceCounts(subscriptionMaintenance);
1353                        return this;
1354                }
1355
1356                /**
1357                 * Replaces cooperative request-cancelation signal counts grouped by bounded
1358                 * endpoint and method dimensions. Passing {@code null} or an empty map
1359                 * clears all cancelation-signal counts.
1360                 *
1361                 * @param cancelationsSignaled cancelation-signaled counts, or {@code null}
1362                 *                             to clear them
1363                 * @return this builder
1364                 * @throws IllegalArgumentException if any count is negative
1365                 */
1366                @NonNull
1367                public Builder cancelationsSignaled(
1368                                @Nullable Map<@NonNull EndpointMethodKey, @NonNull Long>
1369                                                cancelationsSignaled) {
1370                        this.cancelationsSignaled = cancelationsSignaled == null
1371                                        ? Map.of()
1372                                        : copyEndpointMethodCounts(cancelationsSignaled,
1373                                                        "MCP cancelation-signaled counts must not be negative.");
1374                        return this;
1375                }
1376
1377                /**
1378                 * Replaces progress-notification counts accepted for delivery grouped by
1379                 * bounded endpoint and method dimensions. Passing {@code null} or an empty
1380                 * map clears all progress-notification counts.
1381                 *
1382                 * @param progressEmitted progress-emitted counts, or {@code null} to clear
1383                 *                        them
1384                 * @return this builder
1385                 * @throws IllegalArgumentException if any count is negative
1386                 */
1387                @NonNull
1388                public Builder progressEmitted(
1389                                @Nullable Map<@NonNull EndpointMethodKey, @NonNull Long>
1390                                                progressEmitted) {
1391                        this.progressEmitted = progressEmitted == null ? Map.of()
1392                                        : copyEndpointMethodCounts(progressEmitted,
1393                                                        "MCP progress-emitted counts must not be negative.");
1394                        return this;
1395                }
1396
1397                /**
1398                 * Sets the number of MCP keep-alive comments accepted for delivery.
1399                 *
1400                 * @param keepAlivesEmitted keep-alive comments accepted for delivery
1401                 * @return this builder
1402                 * @throws IllegalArgumentException if the count is negative
1403                 */
1404                @NonNull
1405                public Builder keepAlivesEmitted(@NonNull Long keepAlivesEmitted) {
1406                        this.keepAlivesEmitted = requireNonNegative(keepAlivesEmitted,
1407                                        "MCP keep-alive emitted count must not be negative.");
1408                        return this;
1409                }
1410
1411                /**
1412                 * Replaces client-visible MCP protocol-error counts grouped by error code.
1413                 * Passing {@code null} or an empty map clears all protocol-error counts.
1414                 *
1415                 * @param protocolErrors protocol-error counts, or {@code null} to clear them
1416                 * @return this builder
1417                 * @throws IllegalArgumentException if any count is negative
1418                 */
1419                @NonNull
1420                public Builder protocolErrors(
1421                                @Nullable Map<@NonNull Integer, @NonNull Long> protocolErrors) {
1422                        this.protocolErrors = protocolErrors == null ? Map.of()
1423                                        : copyProtocolErrorCounts(protocolErrors);
1424                        return this;
1425                }
1426
1427                /**
1428                 * Replaces unknown mirrored-header occurrence counts grouped by bounded
1429                 * endpoint and method dimensions. Passing {@code null} or an empty map
1430                 * clears all unknown mirrored-header counts.
1431                 *
1432                 * @param unknownMirroredHeaders unknown mirrored-header counts, or
1433                 *                               {@code null} to clear them
1434                 * @return this builder
1435                 * @throws IllegalArgumentException if any count is negative
1436                 */
1437                @NonNull
1438                public Builder unknownMirroredHeaders(
1439                                @Nullable Map<@NonNull EndpointMethodKey, @NonNull Long>
1440                                                unknownMirroredHeaders) {
1441                        this.unknownMirroredHeaders = unknownMirroredHeaders == null
1442                                        ? Map.of()
1443                                        : copyEndpointMethodCounts(unknownMirroredHeaders,
1444                                                        "MCP unknown mirrored-header counts must not be negative.");
1445                        return this;
1446                }
1447
1448                /**
1449                 * Builds an immutable MCP metrics snapshot.
1450                 *
1451                 * @return built snapshot
1452                 */
1453                @NonNull
1454                public McpMetricsSnapshot build() {
1455                        return new McpMetricsSnapshot(this);
1456                }
1457        }
1458}