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.otel; 018 019import com.soklet.ConnectionRejectionReason; 020import com.soklet.MarshaledResponse; 021import com.soklet.McpEndpoint; 022import com.soklet.McpSessionTerminationReason; 023import com.soklet.MetricsCollector; 024import com.soklet.Request; 025import com.soklet.RequestReadFailureReason; 026import com.soklet.RequestRejectionReason; 027import com.soklet.ResourceMethod; 028import com.soklet.ResourcePathDeclaration; 029import com.soklet.ServerType; 030import com.soklet.SseComment; 031import com.soklet.SseConnection; 032import com.soklet.SseEvent; 033import com.soklet.StreamTermination; 034import io.opentelemetry.api.GlobalOpenTelemetry; 035import io.opentelemetry.api.OpenTelemetry; 036import io.opentelemetry.api.common.AttributeKey; 037import io.opentelemetry.api.common.Attributes; 038import io.opentelemetry.api.metrics.DoubleHistogram; 039import io.opentelemetry.api.metrics.LongCounter; 040import io.opentelemetry.api.metrics.LongHistogram; 041import io.opentelemetry.api.metrics.LongUpDownCounter; 042import io.opentelemetry.api.metrics.Meter; 043import io.opentelemetry.api.metrics.MeterBuilder; 044import org.jspecify.annotations.NonNull; 045import org.jspecify.annotations.Nullable; 046 047import javax.annotation.concurrent.NotThreadSafe; 048import javax.annotation.concurrent.ThreadSafe; 049import java.net.InetSocketAddress; 050import java.time.Duration; 051import java.util.List; 052import java.util.Locale; 053 054import static java.util.Objects.requireNonNull; 055 056/** 057 * OpenTelemetry-backed {@link MetricsCollector} for Soklet HTTP, SSE, and MCP telemetry. 058 * <p> 059 * This implementation records counters/histograms via OpenTelemetry's metrics API and is designed to be 060 * lightweight, thread-safe, and non-blocking in request hot paths. 061 * <p> 062 * By default, standard HTTP metrics use OpenTelemetry Semantic Convention names. Soklet-specific concepts 063 * (for example SSE queue/drop/broadcast details) are emitted with {@code soklet.*} names. 064 * For request-body size, the default semantic-convention strategy records the encoded payload size as 065 * transferred, while the {@link MetricNamingStrategy#SOKLET} strategy records the handler-visible body size. 066 * If an oversized request is rejected before its complete encoded payload size is known, the 067 * semantic-convention body-size sample is omitted instead of recording an inaccurate zero. 068 * Response-body size is based on the finalized {@link MarshaledResponse}; if the HTTP transport applies 069 * dynamic gzip afterward, that metric remains the pre-compression size. 070 * <p> 071 * If inbound requests include W3C trace context, Soklet exposes it via {@link Request#getTraceContext()} to 072 * custom metrics collectors and application code. This metrics-only implementation intentionally does not emit 073 * trace IDs, parent IDs, or {@code tracestate} values as metric attributes because those values are high-cardinality 074 * and belong in logs, spans, or exemplar-aware tracing integrations instead. 075 * <p> 076 * See <a href="https://soklet.com/docs/metrics-collection">https://soklet.com/docs/metrics-collection</a> for Soklet's metrics/telemetry documentation. 077 * 078 * @author <a href="https://www.revetkn.com">Mark Allen</a> 079 */ 080@ThreadSafe 081public final class OpenTelemetryMetricsCollector implements MetricsCollector { 082 @NonNull 083 private static final String UNMATCHED_ROUTE; 084 @NonNull 085 private static final String UNKNOWN_COMMENT_TYPE; 086 @NonNull 087 private static final String BROADCAST_PAYLOAD_EVENT; 088 @NonNull 089 private static final String BROADCAST_PAYLOAD_COMMENT; 090 @NonNull 091 private static final String DEFAULT_INSTRUMENTATION_NAME; 092 @NonNull 093 private static final String URL_SCHEME_HTTP; 094 095 @NonNull 096 private static final AttributeKey<String> SERVER_TYPE_ATTRIBUTE_KEY; 097 @NonNull 098 private static final AttributeKey<String> FAILURE_REASON_ATTRIBUTE_KEY; 099 @NonNull 100 private static final AttributeKey<String> ERROR_TYPE_ATTRIBUTE_KEY; 101 @NonNull 102 private static final AttributeKey<String> HTTP_METHOD_ATTRIBUTE_KEY; 103 @NonNull 104 private static final AttributeKey<String> HTTP_ROUTE_ATTRIBUTE_KEY; 105 @NonNull 106 private static final AttributeKey<String> URL_SCHEME_ATTRIBUTE_KEY; 107 @NonNull 108 private static final AttributeKey<Long> HTTP_STATUS_CODE_ATTRIBUTE_KEY; 109 @NonNull 110 private static final AttributeKey<String> SSE_TERMINATION_REASON_ATTRIBUTE_KEY; 111 @NonNull 112 private static final AttributeKey<String> SSE_DROP_REASON_ATTRIBUTE_KEY; 113 @NonNull 114 private static final AttributeKey<String> SSE_COMMENT_TYPE_ATTRIBUTE_KEY; 115 @NonNull 116 private static final AttributeKey<String> SSE_BROADCAST_PAYLOAD_TYPE_ATTRIBUTE_KEY; 117 @NonNull 118 private static final AttributeKey<String> MCP_ENDPOINT_CLASS_ATTRIBUTE_KEY; 119 @NonNull 120 private static final AttributeKey<String> MCP_SESSION_TERMINATION_REASON_ATTRIBUTE_KEY; 121 @NonNull 122 private static final List<Double> LONG_LIVED_DURATION_BUCKET_BOUNDARIES; 123 124 static { 125 UNMATCHED_ROUTE = "_unmatched"; 126 UNKNOWN_COMMENT_TYPE = "unknown"; 127 BROADCAST_PAYLOAD_EVENT = "event"; 128 BROADCAST_PAYLOAD_COMMENT = "comment"; 129 DEFAULT_INSTRUMENTATION_NAME = "com.soklet.otel"; 130 URL_SCHEME_HTTP = "http"; 131 132 SERVER_TYPE_ATTRIBUTE_KEY = AttributeKey.stringKey("soklet.server.type"); 133 FAILURE_REASON_ATTRIBUTE_KEY = AttributeKey.stringKey("soklet.failure.reason"); 134 ERROR_TYPE_ATTRIBUTE_KEY = AttributeKey.stringKey("error.type"); 135 HTTP_METHOD_ATTRIBUTE_KEY = AttributeKey.stringKey("http.request.method"); 136 HTTP_ROUTE_ATTRIBUTE_KEY = AttributeKey.stringKey("http.route"); 137 URL_SCHEME_ATTRIBUTE_KEY = AttributeKey.stringKey("url.scheme"); 138 HTTP_STATUS_CODE_ATTRIBUTE_KEY = AttributeKey.longKey("http.response.status_code"); 139 SSE_TERMINATION_REASON_ATTRIBUTE_KEY = AttributeKey.stringKey("soklet.sse.termination.reason"); 140 SSE_DROP_REASON_ATTRIBUTE_KEY = AttributeKey.stringKey("soklet.sse.drop.reason"); 141 SSE_COMMENT_TYPE_ATTRIBUTE_KEY = AttributeKey.stringKey("soklet.sse.comment.type"); 142 SSE_BROADCAST_PAYLOAD_TYPE_ATTRIBUTE_KEY = AttributeKey.stringKey("soklet.sse.broadcast.payload.type"); 143 MCP_ENDPOINT_CLASS_ATTRIBUTE_KEY = AttributeKey.stringKey("soklet.mcp.endpoint.class"); 144 MCP_SESSION_TERMINATION_REASON_ATTRIBUTE_KEY = AttributeKey.stringKey("soklet.mcp.session.termination.reason"); 145 // SSE streams and MCP sessions live for minutes-to-hours (MCP's default idle timeout is 24 hours), 146 // so OpenTelemetry's request-oriented default buckets (which top out at 10 seconds) would collapse 147 // nearly all measurements into the +Inf bucket. Advise boundaries suited to those lifetimes instead. 148 LONG_LIVED_DURATION_BUCKET_BOUNDARIES = List.of(1D, 10D, 60D, 300D, 1_800D, 3_600D, 14_400D, 86_400D); 149 } 150 151 @NonNull 152 private final LongCounter connectionsAcceptedCounter; 153 @NonNull 154 private final LongCounter connectionsRejectedCounter; 155 @NonNull 156 private final LongCounter requestsAcceptedCounter; 157 @NonNull 158 private final LongCounter requestsRejectedCounter; 159 @NonNull 160 private final LongCounter requestReadFailureCounter; 161 @NonNull 162 private final LongCounter transportFailureCounter; 163 @NonNull 164 private final LongUpDownCounter activeRequestsCounter; 165 @NonNull 166 private final DoubleHistogram requestDurationHistogram; 167 @NonNull 168 private final DoubleHistogram responseWriteDurationHistogram; 169 @NonNull 170 private final LongCounter responseWriteFailureCounter; 171 @NonNull 172 private final LongCounter requestThrowableCounter; 173 @NonNull 174 private final LongHistogram requestBodySizeHistogram; 175 @NonNull 176 private final LongHistogram responseBodySizeHistogram; 177 178 @NonNull 179 private final LongUpDownCounter activeServerSentEventStreamsCounter; 180 @NonNull 181 private final LongCounter serverSentEventStreamsEstablishedCounter; 182 @NonNull 183 private final LongCounter serverSentEventHandshakeFailureCounter; 184 @NonNull 185 private final LongCounter serverSentEventStreamsTerminatedCounter; 186 @NonNull 187 private final DoubleHistogram serverSentEventStreamDurationHistogram; 188 @NonNull 189 private final LongCounter serverSentEventWrittenCounter; 190 @NonNull 191 private final LongCounter serverSentEventWriteFailureCounter; 192 @NonNull 193 private final DoubleHistogram serverSentEventWriteDurationHistogram; 194 @NonNull 195 private final DoubleHistogram serverSentEventDeliveryLagHistogram; 196 @NonNull 197 private final LongHistogram serverSentEventPayloadSizeHistogram; 198 @NonNull 199 private final LongHistogram serverSentEventQueueDepthHistogram; 200 @NonNull 201 private final LongCounter serverSentEventDropCounter; 202 203 @NonNull 204 private final LongCounter serverSentEventCommentWrittenCounter; 205 @NonNull 206 private final LongCounter serverSentEventCommentWriteFailureCounter; 207 @NonNull 208 private final DoubleHistogram serverSentEventCommentWriteDurationHistogram; 209 @NonNull 210 private final DoubleHistogram serverSentEventCommentDeliveryLagHistogram; 211 @NonNull 212 private final LongHistogram serverSentEventCommentPayloadSizeHistogram; 213 @NonNull 214 private final LongHistogram serverSentEventCommentQueueDepthHistogram; 215 @NonNull 216 private final LongCounter serverSentEventCommentDropCounter; 217 218 @NonNull 219 private final LongCounter serverSentEventBroadcastAttemptCounter; 220 @NonNull 221 private final LongCounter serverSentEventBroadcastEnqueuedCounter; 222 @NonNull 223 private final LongCounter serverSentEventBroadcastDroppedCounter; 224 225 @NonNull 226 private final LongUpDownCounter activeMcpSessionsCounter; 227 @NonNull 228 private final LongCounter mcpSessionsCreatedCounter; 229 @NonNull 230 private final LongCounter mcpSessionsTerminatedCounter; 231 @NonNull 232 private final DoubleHistogram mcpSessionDurationHistogram; 233 234 @NonNull 235 private final MetricNamingStrategy metricNamingStrategy; 236 237 /** 238 * Acquires a builder for {@link OpenTelemetryMetricsCollector} instances, using {@link GlobalOpenTelemetry} 239 * by default. 240 * 241 * @return the builder 242 */ 243 @NonNull 244 public static Builder builder() { 245 return new Builder(); 246 } 247 248 /** 249 * Acquires a builder seeded with a required {@link Meter}. 250 * 251 * @param meter the meter used to build instruments 252 * @return the builder 253 */ 254 @NonNull 255 public static Builder withMeter(@NonNull Meter meter) { 256 requireNonNull(meter); 257 return builder().meter(meter); 258 } 259 260 /** 261 * Acquires a builder seeded with a required {@link OpenTelemetry} instance. 262 * 263 * @param openTelemetry the OpenTelemetry instance used to build a meter 264 * @return the builder 265 */ 266 @NonNull 267 public static Builder withOpenTelemetry(@NonNull OpenTelemetry openTelemetry) { 268 requireNonNull(openTelemetry); 269 return builder().openTelemetry(openTelemetry); 270 } 271 272 /** 273 * Creates an instance from a required {@link Meter} without additional customization. 274 * 275 * @param meter the meter used to build instruments 276 * @return an {@link OpenTelemetryMetricsCollector} instance 277 */ 278 @NonNull 279 public static OpenTelemetryMetricsCollector fromMeter(@NonNull Meter meter) { 280 return withMeter(meter).build(); 281 } 282 283 /** 284 * Creates an instance from a required {@link OpenTelemetry} without additional customization. 285 * 286 * @param openTelemetry the OpenTelemetry instance used to build a meter 287 * @return an {@link OpenTelemetryMetricsCollector} instance 288 */ 289 @NonNull 290 public static OpenTelemetryMetricsCollector fromOpenTelemetry(@NonNull OpenTelemetry openTelemetry) { 291 return withOpenTelemetry(openTelemetry).build(); 292 } 293 294 private OpenTelemetryMetricsCollector(@NonNull Builder builder) { 295 requireNonNull(builder); 296 Meter meter = requireNonNull(builder.resolveMeter()); 297 this.metricNamingStrategy = requireNonNull(builder.metricNamingStrategy); 298 299 String activeRequestsMetricName = activeRequestsMetricNameFor(this.metricNamingStrategy); 300 String requestDurationMetricName = requestDurationMetricNameFor(this.metricNamingStrategy); 301 String requestBodySizeMetricName = requestBodySizeMetricNameFor(this.metricNamingStrategy); 302 String requestBodySizeDescription = requestBodySizeDescriptionFor(this.metricNamingStrategy); 303 String responseBodySizeMetricName = responseBodySizeMetricNameFor(this.metricNamingStrategy); 304 305 this.connectionsAcceptedCounter = meter.counterBuilder("soklet.server.connections.accepted") 306 .setDescription("Total number of accepted inbound TCP connections.") 307 .setUnit("{connection}") 308 .build(); 309 this.connectionsRejectedCounter = meter.counterBuilder("soklet.server.connections.rejected") 310 .setDescription("Total number of rejected inbound TCP connections.") 311 .setUnit("{connection}") 312 .build(); 313 this.requestsAcceptedCounter = meter.counterBuilder("soklet.server.requests.accepted") 314 .setDescription("Total number of accepted requests before app-level handling.") 315 .setUnit("{request}") 316 .build(); 317 this.requestsRejectedCounter = meter.counterBuilder("soklet.server.requests.rejected") 318 .setDescription("Total number of rejected requests before app-level handling.") 319 .setUnit("{request}") 320 .build(); 321 this.requestReadFailureCounter = meter.counterBuilder("soklet.server.request.read.failures") 322 .setDescription("Total number of request read/parse failures.") 323 .setUnit("{request}") 324 .build(); 325 this.transportFailureCounter = meter.counterBuilder("soklet.server.transport.failures") 326 .setDescription("Total number of low-level transport failures.") 327 .setUnit("{failure}") 328 .build(); 329 this.activeRequestsCounter = meter.upDownCounterBuilder(activeRequestsMetricName) 330 .setDescription("Number of in-flight requests currently being handled.") 331 .setUnit("{request}") 332 .build(); 333 this.requestDurationHistogram = meter.histogramBuilder(requestDurationMetricName) 334 .setDescription("Total request handling duration.") 335 .setUnit("s") 336 .build(); 337 this.responseWriteDurationHistogram = meter.histogramBuilder("soklet.server.response.write.duration") 338 .setDescription("Duration spent writing response bytes.") 339 .setUnit("s") 340 .build(); 341 this.responseWriteFailureCounter = meter.counterBuilder("soklet.server.response.write.failures") 342 .setDescription("Total number of response write failures.") 343 .setUnit("{response}") 344 .build(); 345 this.requestThrowableCounter = meter.counterBuilder("soklet.server.request.throwables") 346 .setDescription("Total number of throwables observed during request handling.") 347 .setUnit("{throwable}") 348 .build(); 349 this.requestBodySizeHistogram = meter.histogramBuilder(requestBodySizeMetricName) 350 .ofLongs() 351 .setDescription(requestBodySizeDescription) 352 .setUnit("By") 353 .build(); 354 this.responseBodySizeHistogram = meter.histogramBuilder(responseBodySizeMetricName) 355 .ofLongs() 356 .setDescription("Response body size in bytes.") 357 .setUnit("By") 358 .build(); 359 360 this.activeServerSentEventStreamsCounter = meter.upDownCounterBuilder("soklet.sse.streams.active") 361 .setDescription("Number of active SSE streams.") 362 .setUnit("{stream}") 363 .build(); 364 this.serverSentEventStreamsEstablishedCounter = meter.counterBuilder("soklet.sse.streams.established") 365 .setDescription("Total number of SSE streams established.") 366 .setUnit("{stream}") 367 .build(); 368 this.serverSentEventHandshakeFailureCounter = meter.counterBuilder("soklet.sse.handshakes.rejected") 369 .setDescription("Total number of rejected SSE handshakes.") 370 .setUnit("{handshake}") 371 .build(); 372 this.serverSentEventStreamsTerminatedCounter = meter.counterBuilder("soklet.sse.streams.terminated") 373 .setDescription("Total number of terminated SSE streams.") 374 .setUnit("{stream}") 375 .build(); 376 this.serverSentEventStreamDurationHistogram = meter.histogramBuilder("soklet.sse.stream.duration") 377 .setDescription("SSE stream duration.") 378 .setUnit("s") 379 .setExplicitBucketBoundariesAdvice(LONG_LIVED_DURATION_BUCKET_BOUNDARIES) 380 .build(); 381 this.serverSentEventWrittenCounter = meter.counterBuilder("soklet.sse.events.written") 382 .setDescription("Total number of SSE events successfully written.") 383 .setUnit("{event}") 384 .build(); 385 this.serverSentEventWriteFailureCounter = meter.counterBuilder("soklet.sse.events.write.failures") 386 .setDescription("Total number of SSE events that failed to write.") 387 .setUnit("{event}") 388 .build(); 389 this.serverSentEventWriteDurationHistogram = meter.histogramBuilder("soklet.sse.events.write.duration") 390 .setDescription("SSE event write duration.") 391 .setUnit("s") 392 .build(); 393 this.serverSentEventDeliveryLagHistogram = meter.histogramBuilder("soklet.sse.events.delivery.lag") 394 .setDescription("Time spent waiting in the SSE queue before write.") 395 .setUnit("s") 396 .build(); 397 this.serverSentEventPayloadSizeHistogram = meter.histogramBuilder("soklet.sse.events.payload.size") 398 .ofLongs() 399 .setDescription("Serialized SSE event payload size in bytes.") 400 .setUnit("By") 401 .build(); 402 this.serverSentEventQueueDepthHistogram = meter.histogramBuilder("soklet.sse.events.queue.depth") 403 .ofLongs() 404 .setDescription("Queued element depth when SSE event write/drop outcome is observed.") 405 .setUnit("{item}") 406 .build(); 407 this.serverSentEventDropCounter = meter.counterBuilder("soklet.sse.events.dropped") 408 .setDescription("Total number of SSE events dropped before enqueue.") 409 .setUnit("{event}") 410 .build(); 411 412 this.serverSentEventCommentWrittenCounter = meter.counterBuilder("soklet.sse.comments.written") 413 .setDescription("Total number of SSE comments successfully written.") 414 .setUnit("{comment}") 415 .build(); 416 this.serverSentEventCommentWriteFailureCounter = meter.counterBuilder("soklet.sse.comments.write.failures") 417 .setDescription("Total number of SSE comments that failed to write.") 418 .setUnit("{comment}") 419 .build(); 420 this.serverSentEventCommentWriteDurationHistogram = meter.histogramBuilder("soklet.sse.comments.write.duration") 421 .setDescription("SSE comment write duration.") 422 .setUnit("s") 423 .build(); 424 this.serverSentEventCommentDeliveryLagHistogram = meter.histogramBuilder("soklet.sse.comments.delivery.lag") 425 .setDescription("Time spent waiting in the SSE queue before comment write.") 426 .setUnit("s") 427 .build(); 428 this.serverSentEventCommentPayloadSizeHistogram = meter.histogramBuilder("soklet.sse.comments.payload.size") 429 .ofLongs() 430 .setDescription("Serialized SSE comment payload size in bytes.") 431 .setUnit("By") 432 .build(); 433 this.serverSentEventCommentQueueDepthHistogram = meter.histogramBuilder("soklet.sse.comments.queue.depth") 434 .ofLongs() 435 .setDescription("Queued element depth when SSE comment write/drop outcome is observed.") 436 .setUnit("{item}") 437 .build(); 438 this.serverSentEventCommentDropCounter = meter.counterBuilder("soklet.sse.comments.dropped") 439 .setDescription("Total number of SSE comments dropped before enqueue.") 440 .setUnit("{comment}") 441 .build(); 442 443 this.serverSentEventBroadcastAttemptCounter = meter.counterBuilder("soklet.sse.broadcast.attempted") 444 .setDescription("Total number of attempted SSE broadcast deliveries.") 445 .setUnit("{delivery}") 446 .build(); 447 this.serverSentEventBroadcastEnqueuedCounter = meter.counterBuilder("soklet.sse.broadcast.enqueued") 448 .setDescription("Total number of SSE broadcast deliveries successfully enqueued.") 449 .setUnit("{delivery}") 450 .build(); 451 this.serverSentEventBroadcastDroppedCounter = meter.counterBuilder("soklet.sse.broadcast.dropped") 452 .setDescription("Total number of SSE broadcast deliveries dropped before enqueue.") 453 .setUnit("{delivery}") 454 .build(); 455 456 this.activeMcpSessionsCounter = meter.upDownCounterBuilder("soklet.mcp.sessions.active") 457 .setDescription("Number of active MCP sessions.") 458 .setUnit("{session}") 459 .build(); 460 this.mcpSessionsCreatedCounter = meter.counterBuilder("soklet.mcp.sessions.created") 461 .setDescription("Total number of MCP sessions created.") 462 .setUnit("{session}") 463 .build(); 464 this.mcpSessionsTerminatedCounter = meter.counterBuilder("soklet.mcp.sessions.terminated") 465 .setDescription("Total number of MCP sessions terminated.") 466 .setUnit("{session}") 467 .build(); 468 this.mcpSessionDurationHistogram = meter.histogramBuilder("soklet.mcp.session.duration") 469 .setDescription("MCP session lifetime from creation to termination.") 470 .setUnit("s") 471 .setExplicitBucketBoundariesAdvice(LONG_LIVED_DURATION_BUCKET_BOUNDARIES) 472 .build(); 473 } 474 475 @Override 476 public void didAcceptConnection(@NonNull ServerType serverType, 477 @Nullable InetSocketAddress remoteAddress) { 478 requireNonNull(serverType); 479 this.connectionsAcceptedCounter.add(1, serverTypeAttributes(serverType)); 480 } 481 482 @Override 483 public void didFailToAcceptConnection(@NonNull ServerType serverType, 484 @Nullable InetSocketAddress remoteAddress, 485 @NonNull ConnectionRejectionReason reason, 486 @Nullable Throwable throwable) { 487 requireNonNull(serverType); 488 requireNonNull(reason); 489 this.connectionsRejectedCounter.add(1, serverTypeAndReasonAttributes(serverType, reason)); 490 } 491 492 @Override 493 public void didAcceptRequest(@NonNull ServerType serverType, 494 @Nullable InetSocketAddress remoteAddress, 495 @Nullable String requestTarget) { 496 requireNonNull(serverType); 497 this.requestsAcceptedCounter.add(1, serverTypeAttributes(serverType)); 498 } 499 500 @Override 501 public void didFailToAcceptRequest(@NonNull ServerType serverType, 502 @Nullable InetSocketAddress remoteAddress, 503 @Nullable String requestTarget, 504 @NonNull RequestRejectionReason reason, 505 @Nullable Throwable throwable) { 506 requireNonNull(serverType); 507 requireNonNull(reason); 508 this.requestsRejectedCounter.add(1, serverTypeAndReasonAttributes(serverType, reason)); 509 } 510 511 @Override 512 public void didFailToReadRequest(@NonNull ServerType serverType, 513 @Nullable InetSocketAddress remoteAddress, 514 @Nullable String requestTarget, 515 @NonNull RequestReadFailureReason reason, 516 @Nullable Throwable throwable) { 517 requireNonNull(serverType); 518 requireNonNull(reason); 519 this.requestReadFailureCounter.add(1, serverTypeAndReasonAttributes(serverType, reason)); 520 } 521 522 @Override 523 public void didRecordTransportFailure(@NonNull ServerType serverType, 524 @NonNull TransportFailureReason reason, 525 @Nullable Throwable throwable) { 526 requireNonNull(serverType); 527 requireNonNull(reason); 528 this.transportFailureCounter.add(1, transportFailureAttributes(serverType, reason, throwable)); 529 } 530 531 @Override 532 public void didStartRequestHandling(@NonNull ServerType serverType, 533 @NonNull Request request, 534 @Nullable ResourceMethod resourceMethod) { 535 requireNonNull(serverType); 536 requireNonNull(request); 537 538 this.activeRequestsCounter.add(1, activeRequestAttributes(serverType, request)); 539 } 540 541 @Override 542 public void didFinishRequestHandling(@NonNull ServerType serverType, 543 @NonNull Request request, 544 @Nullable ResourceMethod resourceMethod, 545 @NonNull MarshaledResponse marshaledResponse, 546 @NonNull Duration duration, 547 @NonNull List<@NonNull Throwable> throwables) { 548 requireNonNull(serverType); 549 requireNonNull(request); 550 requireNonNull(marshaledResponse); 551 requireNonNull(duration); 552 requireNonNull(throwables); 553 554 Throwable throwable = throwables.isEmpty() ? null : throwables.get(0); 555 Attributes attributes = requestAttributes(serverType, request, resourceMethod, marshaledResponse.getStatusCode(), throwable); 556 557 this.activeRequestsCounter.add(-1, activeRequestAttributes(serverType, request)); 558 this.requestDurationHistogram.record(seconds(duration), attributes); 559 long requestBodySizeInBytes = this.metricNamingStrategy == MetricNamingStrategy.SEMCONV 560 ? request.getEncodedBodySizeInBytes().longValue() 561 : request.getBody().map(body -> (long) body.length).orElse(0L); 562 563 if (this.metricNamingStrategy != MetricNamingStrategy.SEMCONV 564 || !request.isContentTooLarge() 565 || requestBodySizeInBytes > 0) 566 this.requestBodySizeHistogram.record(requestBodySizeInBytes, attributes); 567 this.responseBodySizeHistogram.record(marshaledResponse.getBodyLength(), attributes); 568 569 if (!throwables.isEmpty()) 570 this.requestThrowableCounter.add(throwables.size(), attributes); 571 } 572 573 @Override 574 public void didWriteResponse(@NonNull ServerType serverType, 575 @NonNull Request request, 576 @Nullable ResourceMethod resourceMethod, 577 @NonNull MarshaledResponse marshaledResponse, 578 @NonNull Duration responseWriteDuration) { 579 requireNonNull(serverType); 580 requireNonNull(request); 581 requireNonNull(marshaledResponse); 582 requireNonNull(responseWriteDuration); 583 584 this.responseWriteDurationHistogram.record( 585 seconds(responseWriteDuration), 586 requestAttributes(serverType, request, resourceMethod, marshaledResponse.getStatusCode(), null) 587 ); 588 } 589 590 @Override 591 public void didFailToWriteResponse(@NonNull ServerType serverType, 592 @NonNull Request request, 593 @Nullable ResourceMethod resourceMethod, 594 @NonNull MarshaledResponse marshaledResponse, 595 @NonNull Duration responseWriteDuration, 596 @NonNull Throwable throwable) { 597 requireNonNull(serverType); 598 requireNonNull(request); 599 requireNonNull(marshaledResponse); 600 requireNonNull(responseWriteDuration); 601 requireNonNull(throwable); 602 603 Attributes attributes = requestAttributes(serverType, request, resourceMethod, marshaledResponse.getStatusCode(), throwable); 604 this.responseWriteFailureCounter.add(1, attributes); 605 this.responseWriteDurationHistogram.record(seconds(responseWriteDuration), attributes); 606 } 607 608 @Override 609 public void didEstablishSseConnection(@NonNull SseConnection sseConnection) { 610 requireNonNull(sseConnection); 611 612 Attributes attributes = serverSentEventAttributes(sseConnection); 613 this.activeServerSentEventStreamsCounter.add(1, attributes); 614 this.serverSentEventStreamsEstablishedCounter.add(1, attributes); 615 } 616 617 @Override 618 public void didFailToEstablishSseConnection(@NonNull Request request, 619 @Nullable ResourceMethod resourceMethod, 620 SseConnection.@NonNull HandshakeFailureReason reason, 621 @Nullable Throwable throwable) { 622 requireNonNull(request); 623 requireNonNull(reason); 624 625 this.serverSentEventHandshakeFailureCounter.add(1, 626 Attributes.builder() 627 .put(HTTP_METHOD_ATTRIBUTE_KEY, request.getHttpMethod().name()) 628 .put(HTTP_ROUTE_ATTRIBUTE_KEY, routeFor(resourceMethod)) 629 .put(FAILURE_REASON_ATTRIBUTE_KEY, enumValue(reason)) 630 .build() 631 ); 632 } 633 634 @Override 635 public void didTerminateSseConnection(@NonNull SseConnection sseConnection, 636 @NonNull StreamTermination termination) { 637 requireNonNull(sseConnection); 638 requireNonNull(termination); 639 640 Attributes routeAttributes = serverSentEventAttributes(sseConnection); 641 Attributes durationAttributes = Attributes.builder() 642 .putAll(routeAttributes) 643 .put(SSE_TERMINATION_REASON_ATTRIBUTE_KEY, enumValue(termination.getReason())) 644 .build(); 645 646 this.activeServerSentEventStreamsCounter.add(-1, routeAttributes); 647 this.serverSentEventStreamsTerminatedCounter.add(1, durationAttributes); 648 this.serverSentEventStreamDurationHistogram.record(seconds(termination.getDuration()), durationAttributes); 649 } 650 651 @Override 652 public void didWriteSseEvent(@NonNull SseConnection sseConnection, 653 @NonNull SseEvent sseEvent, 654 @NonNull Duration writeDuration, 655 @Nullable Duration deliveryLag, 656 @Nullable Integer payloadBytes, 657 @Nullable Integer queueDepth) { 658 requireNonNull(sseConnection); 659 requireNonNull(sseEvent); 660 requireNonNull(writeDuration); 661 662 Attributes attributes = serverSentEventAttributes(sseConnection); 663 this.serverSentEventWrittenCounter.add(1, attributes); 664 this.serverSentEventWriteDurationHistogram.record(seconds(writeDuration), attributes); 665 666 if (deliveryLag != null) 667 this.serverSentEventDeliveryLagHistogram.record(seconds(deliveryLag), attributes); 668 if (payloadBytes != null) 669 this.serverSentEventPayloadSizeHistogram.record(payloadBytes, attributes); 670 if (queueDepth != null) 671 this.serverSentEventQueueDepthHistogram.record(queueDepth, attributes); 672 } 673 674 @Override 675 public void didFailToWriteSseEvent(@NonNull SseConnection sseConnection, 676 @NonNull SseEvent sseEvent, 677 @NonNull Duration writeDuration, 678 @NonNull Throwable throwable, 679 @Nullable Duration deliveryLag, 680 @Nullable Integer payloadBytes, 681 @Nullable Integer queueDepth) { 682 requireNonNull(sseConnection); 683 requireNonNull(sseEvent); 684 requireNonNull(writeDuration); 685 requireNonNull(throwable); 686 687 Attributes attributes = serverSentEventAttributes(sseConnection); 688 this.serverSentEventWriteFailureCounter.add(1, attributes); 689 this.serverSentEventWriteDurationHistogram.record(seconds(writeDuration), attributes); 690 691 if (deliveryLag != null) 692 this.serverSentEventDeliveryLagHistogram.record(seconds(deliveryLag), attributes); 693 if (payloadBytes != null) 694 this.serverSentEventPayloadSizeHistogram.record(payloadBytes, attributes); 695 if (queueDepth != null) 696 this.serverSentEventQueueDepthHistogram.record(queueDepth, attributes); 697 } 698 699 @Override 700 public void didDropSseEvent(@NonNull SseConnection sseConnection, 701 @NonNull SseEvent sseEvent, 702 @NonNull SseEventDropReason reason, 703 @Nullable Integer payloadBytes, 704 @Nullable Integer queueDepth) { 705 requireNonNull(sseConnection); 706 requireNonNull(sseEvent); 707 requireNonNull(reason); 708 709 Attributes attributes = Attributes.builder() 710 .putAll(serverSentEventAttributes(sseConnection)) 711 .put(SSE_DROP_REASON_ATTRIBUTE_KEY, enumValue(reason)) 712 .build(); 713 this.serverSentEventDropCounter.add(1, attributes); 714 715 if (payloadBytes != null) 716 this.serverSentEventPayloadSizeHistogram.record(payloadBytes, attributes); 717 if (queueDepth != null) 718 this.serverSentEventQueueDepthHistogram.record(queueDepth, attributes); 719 } 720 721 @Override 722 public void didWriteSseComment(@NonNull SseConnection sseConnection, 723 @NonNull SseComment sseComment, 724 @NonNull Duration writeDuration, 725 @Nullable Duration deliveryLag, 726 @Nullable Integer payloadBytes, 727 @Nullable Integer queueDepth) { 728 requireNonNull(sseConnection); 729 requireNonNull(sseComment); 730 requireNonNull(writeDuration); 731 732 Attributes attributes = serverSentEventCommentAttributes(sseConnection, sseComment.getCommentType()); 733 this.serverSentEventCommentWrittenCounter.add(1, attributes); 734 this.serverSentEventCommentWriteDurationHistogram.record(seconds(writeDuration), attributes); 735 736 if (deliveryLag != null) 737 this.serverSentEventCommentDeliveryLagHistogram.record(seconds(deliveryLag), attributes); 738 if (payloadBytes != null) 739 this.serverSentEventCommentPayloadSizeHistogram.record(payloadBytes, attributes); 740 if (queueDepth != null) 741 this.serverSentEventCommentQueueDepthHistogram.record(queueDepth, attributes); 742 } 743 744 @Override 745 public void didFailToWriteSseComment(@NonNull SseConnection sseConnection, 746 @NonNull SseComment sseComment, 747 @NonNull Duration writeDuration, 748 @NonNull Throwable throwable, 749 @Nullable Duration deliveryLag, 750 @Nullable Integer payloadBytes, 751 @Nullable Integer queueDepth) { 752 requireNonNull(sseConnection); 753 requireNonNull(sseComment); 754 requireNonNull(writeDuration); 755 requireNonNull(throwable); 756 757 Attributes attributes = serverSentEventCommentAttributes(sseConnection, sseComment.getCommentType()); 758 this.serverSentEventCommentWriteFailureCounter.add(1, attributes); 759 this.serverSentEventCommentWriteDurationHistogram.record(seconds(writeDuration), attributes); 760 761 if (deliveryLag != null) 762 this.serverSentEventCommentDeliveryLagHistogram.record(seconds(deliveryLag), attributes); 763 if (payloadBytes != null) 764 this.serverSentEventCommentPayloadSizeHistogram.record(payloadBytes, attributes); 765 if (queueDepth != null) 766 this.serverSentEventCommentQueueDepthHistogram.record(queueDepth, attributes); 767 } 768 769 @Override 770 public void didDropSseComment(@NonNull SseConnection sseConnection, 771 @NonNull SseComment sseComment, 772 @NonNull SseEventDropReason reason, 773 @Nullable Integer payloadBytes, 774 @Nullable Integer queueDepth) { 775 requireNonNull(sseConnection); 776 requireNonNull(sseComment); 777 requireNonNull(reason); 778 779 Attributes attributes = Attributes.builder() 780 .putAll(serverSentEventCommentAttributes(sseConnection, sseComment.getCommentType())) 781 .put(SSE_DROP_REASON_ATTRIBUTE_KEY, enumValue(reason)) 782 .build(); 783 this.serverSentEventCommentDropCounter.add(1, attributes); 784 785 if (payloadBytes != null) 786 this.serverSentEventCommentPayloadSizeHistogram.record(payloadBytes, attributes); 787 if (queueDepth != null) 788 this.serverSentEventCommentQueueDepthHistogram.record(queueDepth, attributes); 789 } 790 791 @Override 792 public void didBroadcastSseEvent(@NonNull ResourcePathDeclaration route, 793 int attempted, 794 int enqueued, 795 int dropped) { 796 requireNonNull(route); 797 recordBroadcastTotals(route, BROADCAST_PAYLOAD_EVENT, UNKNOWN_COMMENT_TYPE, attempted, enqueued, dropped); 798 } 799 800 @Override 801 public void didBroadcastSseComment(@NonNull ResourcePathDeclaration route, 802 SseComment.@NonNull CommentType commentType, 803 int attempted, 804 int enqueued, 805 int dropped) { 806 requireNonNull(route); 807 requireNonNull(commentType); 808 recordBroadcastTotals(route, BROADCAST_PAYLOAD_COMMENT, enumValue(commentType), attempted, enqueued, dropped); 809 } 810 811 @Override 812 public void didCreateMcpSession(@NonNull Request request, 813 @NonNull Class<? extends McpEndpoint> endpointClass, 814 @NonNull String sessionId) { 815 requireNonNull(request); 816 requireNonNull(endpointClass); 817 requireNonNull(sessionId); 818 819 // The session ID is intentionally not emitted as an attribute: it is unbounded-cardinality. 820 Attributes attributes = mcpSessionAttributes(endpointClass); 821 this.activeMcpSessionsCounter.add(1, attributes); 822 this.mcpSessionsCreatedCounter.add(1, attributes); 823 } 824 825 @Override 826 public void didTerminateMcpSession(@NonNull Class<? extends McpEndpoint> endpointClass, 827 @NonNull String sessionId, 828 @NonNull Duration sessionDuration, 829 @NonNull McpSessionTerminationReason terminationReason, 830 @Nullable Throwable throwable) { 831 requireNonNull(endpointClass); 832 requireNonNull(sessionId); 833 requireNonNull(sessionDuration); 834 requireNonNull(terminationReason); 835 836 // The active counter's decrement must use the SAME attribute set as didCreateMcpSession's 837 // increment (endpoint class only - creation cannot know the eventual termination reason), 838 // otherwise per-series values would never net back to zero. 839 this.activeMcpSessionsCounter.add(-1, mcpSessionAttributes(endpointClass)); 840 841 Attributes terminationAttributes = Attributes.builder() 842 .putAll(mcpSessionAttributes(endpointClass)) 843 .put(MCP_SESSION_TERMINATION_REASON_ATTRIBUTE_KEY, enumValue(terminationReason)) 844 .build(); 845 846 this.mcpSessionsTerminatedCounter.add(1, terminationAttributes); 847 this.mcpSessionDurationHistogram.record(seconds(sessionDuration), terminationAttributes); 848 } 849 850 @NonNull 851 private Attributes serverTypeAttributes(@NonNull ServerType serverType) { 852 requireNonNull(serverType); 853 return Attributes.of(SERVER_TYPE_ATTRIBUTE_KEY, enumValue(serverType)); 854 } 855 856 @NonNull 857 private Attributes serverTypeAndReasonAttributes(@NonNull ServerType serverType, 858 @NonNull Enum<?> reason) { 859 requireNonNull(serverType); 860 requireNonNull(reason); 861 return Attributes.builder() 862 .put(SERVER_TYPE_ATTRIBUTE_KEY, enumValue(serverType)) 863 .put(FAILURE_REASON_ATTRIBUTE_KEY, enumValue(reason)) 864 .build(); 865 } 866 867 @NonNull 868 private Attributes transportFailureAttributes(@NonNull ServerType serverType, 869 @NonNull TransportFailureReason reason, 870 @Nullable Throwable throwable) { 871 requireNonNull(serverType); 872 requireNonNull(reason); 873 874 var builder = Attributes.builder() 875 .put(SERVER_TYPE_ATTRIBUTE_KEY, enumValue(serverType)) 876 .put(FAILURE_REASON_ATTRIBUTE_KEY, enumValue(reason)); 877 878 if (throwable != null) 879 builder.put(ERROR_TYPE_ATTRIBUTE_KEY, throwable.getClass().getName()); 880 881 return builder.build(); 882 } 883 884 @NonNull 885 private Attributes activeRequestAttributes(@NonNull ServerType serverType, 886 @NonNull Request request) { 887 requireNonNull(serverType); 888 requireNonNull(request); 889 890 var builder = Attributes.builder() 891 .put(HTTP_METHOD_ATTRIBUTE_KEY, request.getHttpMethod().name()); 892 893 if (this.metricNamingStrategy == MetricNamingStrategy.SEMCONV) { 894 builder.put(URL_SCHEME_ATTRIBUTE_KEY, URL_SCHEME_HTTP); 895 } else { 896 builder.put(SERVER_TYPE_ATTRIBUTE_KEY, enumValue(serverType)); 897 } 898 899 return builder.build(); 900 } 901 902 @NonNull 903 private Attributes requestAttributes(@NonNull ServerType serverType, 904 @NonNull Request request, 905 @Nullable ResourceMethod resourceMethod, 906 @Nullable Integer statusCode, 907 @Nullable Throwable throwable) { 908 requireNonNull(serverType); 909 requireNonNull(request); 910 911 var builder = Attributes.builder() 912 .put(HTTP_METHOD_ATTRIBUTE_KEY, request.getHttpMethod().name()); 913 914 if (this.metricNamingStrategy == MetricNamingStrategy.SEMCONV) { 915 builder.put(URL_SCHEME_ATTRIBUTE_KEY, URL_SCHEME_HTTP); 916 917 if (resourceMethod != null) 918 builder.put(HTTP_ROUTE_ATTRIBUTE_KEY, routeFor(resourceMethod)); 919 } else { 920 builder.put(SERVER_TYPE_ATTRIBUTE_KEY, enumValue(serverType)) 921 .put(HTTP_ROUTE_ATTRIBUTE_KEY, routeFor(resourceMethod)); 922 } 923 924 if (statusCode != null) 925 builder.put(HTTP_STATUS_CODE_ATTRIBUTE_KEY, statusCode.longValue()); 926 927 String errorType = errorTypeFor(statusCode, throwable); 928 929 if (errorType != null) 930 builder.put(ERROR_TYPE_ATTRIBUTE_KEY, errorType); 931 932 return builder.build(); 933 } 934 935 @NonNull 936 private static Attributes mcpSessionAttributes(@NonNull Class<? extends McpEndpoint> endpointClass) { 937 requireNonNull(endpointClass); 938 return Attributes.of(MCP_ENDPOINT_CLASS_ATTRIBUTE_KEY, endpointClass.getName()); 939 } 940 941 @NonNull 942 private Attributes serverSentEventAttributes(@NonNull SseConnection sseConnection) { 943 requireNonNull(sseConnection); 944 return Attributes.of( 945 HTTP_ROUTE_ATTRIBUTE_KEY, 946 routeFor(sseConnection.getResourceMethod()) 947 ); 948 } 949 950 @NonNull 951 private Attributes serverSentEventCommentAttributes(@NonNull SseConnection sseConnection, 952 SseComment.@NonNull CommentType commentType) { 953 requireNonNull(sseConnection); 954 requireNonNull(commentType); 955 return Attributes.builder() 956 .putAll(serverSentEventAttributes(sseConnection)) 957 .put(SSE_COMMENT_TYPE_ATTRIBUTE_KEY, enumValue(commentType)) 958 .build(); 959 } 960 961 private void recordBroadcastTotals(@NonNull ResourcePathDeclaration route, 962 @NonNull String payloadType, 963 @NonNull String commentType, 964 int attempted, 965 int enqueued, 966 int dropped) { 967 requireNonNull(route); 968 requireNonNull(payloadType); 969 requireNonNull(commentType); 970 971 Attributes attributes = Attributes.builder() 972 .put(HTTP_ROUTE_ATTRIBUTE_KEY, route.getPath()) 973 .put(SSE_BROADCAST_PAYLOAD_TYPE_ATTRIBUTE_KEY, payloadType) 974 .put(SSE_COMMENT_TYPE_ATTRIBUTE_KEY, commentType) 975 .build(); 976 977 if (attempted > 0) 978 this.serverSentEventBroadcastAttemptCounter.add(attempted, attributes); 979 if (enqueued > 0) 980 this.serverSentEventBroadcastEnqueuedCounter.add(enqueued, attributes); 981 if (dropped > 0) 982 this.serverSentEventBroadcastDroppedCounter.add(dropped, attributes); 983 } 984 985 @NonNull 986 private static String activeRequestsMetricNameFor(@NonNull MetricNamingStrategy metricNamingStrategy) { 987 requireNonNull(metricNamingStrategy); 988 if (metricNamingStrategy == MetricNamingStrategy.SEMCONV) 989 return "http.server.active_requests"; 990 return "soklet.server.requests.active"; 991 } 992 993 @NonNull 994 private static String requestDurationMetricNameFor(@NonNull MetricNamingStrategy metricNamingStrategy) { 995 requireNonNull(metricNamingStrategy); 996 if (metricNamingStrategy == MetricNamingStrategy.SEMCONV) 997 return "http.server.request.duration"; 998 return "soklet.server.request.duration"; 999 } 1000 1001 @NonNull 1002 private static String requestBodySizeMetricNameFor(@NonNull MetricNamingStrategy metricNamingStrategy) { 1003 requireNonNull(metricNamingStrategy); 1004 if (metricNamingStrategy == MetricNamingStrategy.SEMCONV) 1005 return "http.server.request.body.size"; 1006 return "soklet.server.request.body.size"; 1007 } 1008 1009 @NonNull 1010 private static String requestBodySizeDescriptionFor(@NonNull MetricNamingStrategy metricNamingStrategy) { 1011 requireNonNull(metricNamingStrategy); 1012 if (metricNamingStrategy == MetricNamingStrategy.SEMCONV) 1013 return "Encoded request payload body size in bytes as transferred, excluding headers and transfer framing."; 1014 return "Request body size in bytes as observed by handlers."; 1015 } 1016 1017 @NonNull 1018 private static String responseBodySizeMetricNameFor(@NonNull MetricNamingStrategy metricNamingStrategy) { 1019 requireNonNull(metricNamingStrategy); 1020 if (metricNamingStrategy == MetricNamingStrategy.SEMCONV) 1021 return "http.server.response.body.size"; 1022 return "soklet.server.response.body.size"; 1023 } 1024 1025 @NonNull 1026 private static String routeFor(@Nullable ResourceMethod resourceMethod) { 1027 if (resourceMethod == null) 1028 return UNMATCHED_ROUTE; 1029 return resourceMethod.getResourcePathDeclaration().getPath(); 1030 } 1031 1032 @NonNull 1033 private static String enumValue(@NonNull Enum<?> value) { 1034 requireNonNull(value); 1035 return value.name().toLowerCase(Locale.ROOT); 1036 } 1037 1038 @Nullable 1039 private String errorTypeFor(@Nullable Integer statusCode, 1040 @Nullable Throwable throwable) { 1041 if (throwable != null) 1042 return throwable.getClass().getName(); 1043 1044 if (this.metricNamingStrategy == MetricNamingStrategy.SEMCONV 1045 && statusCode != null 1046 && statusCode >= 500) 1047 return String.valueOf(statusCode); 1048 1049 return null; 1050 } 1051 1052 private static double seconds(@NonNull Duration duration) { 1053 requireNonNull(duration); 1054 return duration.toNanos() / 1_000_000_000D; 1055 } 1056 1057 /** 1058 * Naming strategy for HTTP metric instrument names. 1059 * <p> 1060 * SSE- and Soklet-specific concepts remain under the {@code soklet.*} namespace in all strategies. 1061 */ 1062 public enum MetricNamingStrategy { 1063 /** 1064 * Use OpenTelemetry Semantic Convention names for standard HTTP server metrics. 1065 * Request-body size records the encoded payload size as transferred. 1066 */ 1067 SEMCONV, 1068 /** 1069 * Use {@code soklet.*} names for all metrics. 1070 * Request-body size records the body size visible to handlers. 1071 */ 1072 SOKLET 1073 } 1074 1075 /** 1076 * Builder used to construct instances of {@link OpenTelemetryMetricsCollector}. 1077 */ 1078 @NotThreadSafe 1079 public static final class Builder { 1080 @Nullable 1081 private Meter meter; 1082 @Nullable 1083 private OpenTelemetry openTelemetry; 1084 @NonNull 1085 private MetricNamingStrategy metricNamingStrategy; 1086 @NonNull 1087 private String instrumentationName; 1088 @Nullable 1089 private String instrumentationVersion; 1090 1091 private Builder() { 1092 this.openTelemetry = GlobalOpenTelemetry.get(); 1093 this.metricNamingStrategy = MetricNamingStrategy.SEMCONV; 1094 this.instrumentationName = DEFAULT_INSTRUMENTATION_NAME; 1095 this.instrumentationVersion = null; 1096 } 1097 1098 /** 1099 * Sets a specific meter to use for metric instruments. 1100 * 1101 * @param meter the meter to use 1102 * @return this builder 1103 */ 1104 @NonNull 1105 public Builder meter(@NonNull Meter meter) { 1106 this.meter = requireNonNull(meter); 1107 return this; 1108 } 1109 1110 /** 1111 * Sets the OpenTelemetry API object used to construct a meter if {@link #meter(Meter)} is not set. 1112 * 1113 * @param openTelemetry the OpenTelemetry instance 1114 * @return this builder 1115 */ 1116 @NonNull 1117 public Builder openTelemetry(@NonNull OpenTelemetry openTelemetry) { 1118 this.openTelemetry = requireNonNull(openTelemetry); 1119 return this; 1120 } 1121 1122 /** 1123 * Sets the naming strategy for HTTP metrics. 1124 * 1125 * @param metricNamingStrategy the naming strategy 1126 * @return this builder 1127 */ 1128 @NonNull 1129 public Builder metricNamingStrategy(@NonNull MetricNamingStrategy metricNamingStrategy) { 1130 this.metricNamingStrategy = requireNonNull(metricNamingStrategy); 1131 return this; 1132 } 1133 1134 /** 1135 * Sets the instrumentation scope name to use when constructing a meter. 1136 * 1137 * @param instrumentationName the instrumentation scope name 1138 * @return this builder 1139 */ 1140 @NonNull 1141 public Builder instrumentationName(@NonNull String instrumentationName) { 1142 this.instrumentationName = requireNonNull(instrumentationName); 1143 return this; 1144 } 1145 1146 /** 1147 * Sets an optional instrumentation scope version to use when constructing a meter. 1148 * 1149 * @param instrumentationVersion the instrumentation scope version, or {@code null} 1150 * @return this builder 1151 */ 1152 @NonNull 1153 public Builder instrumentationVersion(@Nullable String instrumentationVersion) { 1154 this.instrumentationVersion = instrumentationVersion; 1155 return this; 1156 } 1157 1158 @NonNull 1159 private Meter resolveMeter() { 1160 if (this.meter != null) 1161 return this.meter; 1162 1163 MeterBuilder meterBuilder = requireNonNull(this.openTelemetry).meterBuilder(this.instrumentationName); 1164 1165 if (this.instrumentationVersion != null) 1166 meterBuilder = meterBuilder.setInstrumentationVersion(this.instrumentationVersion); 1167 1168 return meterBuilder.build(); 1169 } 1170 1171 /** 1172 * Builds the collector. 1173 * 1174 * @return the collector instance 1175 */ 1176 @NonNull 1177 public OpenTelemetryMetricsCollector build() { 1178 return new OpenTelemetryMetricsCollector(this); 1179 } 1180 } 1181}