Soklet Logo

Core Concepts

Metrics Collection

Soklet provides a MetricsCollector hook for capturing operational metrics without tying you to a specific observability stack. It is invoked for HTTP requests, Model Context Protocol (MCP) activity, and Server-Sent Event (SSE) activity, and is designed to be lightweight and safe to call on hot paths.

At a high level:

  • It is a separate hook from LifecycleObserver - use metrics collectors for low-cardinality counters, gauges, and histograms; use lifecycle observers for traces, audit, logging, and cleanup hooks
  • Implementations must be thread-safe, non-blocking, and avoid I/O
  • Exceptions are caught and logged - they will not break request handling

Configuration

By default, Soklet ships with an in-memory MetricsCollector, an instance of which can be acquired by calling MetricsCollector::defaultInstance.

Soklet's default covers basic workflows, but you can also plug in the official OpenTelemetry integration (soklet-otel) or your own custom implementation.

HttpServer httpServer = HttpServer.fromPort(8080);
SokletConfig config = SokletConfig.withHttpServer(httpServer)
  // This is already the default; specifying it here is optional
  .metricsCollector(MetricsCollector.defaultInstance())
  .build();

To disable metrics collection entirely, specify Soklet's no-op implementation, MetricsCollector::disabledInstance:

HttpServer httpServer = HttpServer.fromPort(8080);
SokletConfig config = SokletConfig.withHttpServer(httpServer)
  // Use this instead of null to disable metrics collection
  .metricsCollector(MetricsCollector.disabledInstance())
  .build();

Accessing Metrics

Collectors that support snapshots expose them via MetricsCollector::snapshot. The default collector returns a MetricsCollector.Snapshot containing histogram data and gauges. If a collector does not support snapshots, it returns Optional::empty.

This section describes snapshot-capable collectors (for example, Soklet's default in-memory collector).

The MetricsCollector is injectable as a Resource Method parameter, so you can easily expose a GET /metrics endpoint which can be scraped by your telemetry tooling:

@GET("/metrics")
public MarshaledResponse getMetrics(@NonNull MetricsCollector metricsCollector) {
  // Configure export to use Prometheus format
  SnapshotTextOptions options = SnapshotTextOptions
    .fromMetricsFormat(MetricsFormat.PROMETHEUS);

  // Generate our export (if supported by the MetricsCollector)
  String body = metricsCollector.snapshotText(options).orElse(null);

  if (body == null)
    return MarshaledResponse.fromStatusCode(204);

  return MarshaledResponse.withStatusCode(200)
    .headers(Map.of("Content-Type", Set.of("text/plain; charset=UTF-8")))
    .body(body.getBytes(StandardCharsets.UTF_8))
    .build();
}

MetricsCollector::snapshotText returns data compatible with the Prometheus (v0.0.4) and OpenMetrics (1.0) text exposition formats with HTTP, MCP, and SSE metrics. MCP and SSE data are only included when the corresponding servers are configured.

SnapshotTextOptions also lets you filter samples, drop zero-count buckets, or collapse histograms to just count/sum when you want to reduce payload size.

Here's an example that more fully exercises SnapshotTextOptions:

SnapshotTextOptions options = SnapshotTextOptions
  .withMetricsFormat(MetricsFormat.OPEN_METRICS_1_0)
  .histogramFormat(HistogramFormat.COUNT_SUM_ONLY)
  .includeZeroBuckets(false)
  .metricFilter(MetricSample metricSample -> {
    String name = metricSample.getName();
    Map<String, String> labels = metricSample.getLabels();

    // Keep only HTTP/MCP/SSE metrics
    if (!name.startsWith("soklet_http_")
        && !name.startsWith("soklet_mcp_")
        && !name.startsWith("soklet_sse_"))
      return false;

    // Keep only /toys routes (route label is "unmatched" for unmatched requests)
    String route = labels.get("route");

    if (route != null && !route.startsWith("/toys"))
      return false;

    // Drop 5xx responses when status_class is present
    String statusClass = labels.get("status_class");

    if (statusClass != null && statusClass.startsWith("5"))
      return false;

    return true;
  })
  .build();

String body = metricsCollector.snapshotText(options).orElse(null);

This configuration emits OpenMetrics 1.0, collapses histograms to count/sum, omits empty buckets, and filters the sample stream by name and labels.

Sample Names

All possible sample names emitted by the default collector:

  • soklet_http_requests_active - Gauge of currently active HTTP requests (no labels).
  • soklet_http_connections_accepted_total - Counter of total accepted HTTP connections (no labels).
  • soklet_http_connections_rejected_total - Counter of total rejected HTTP connections (no labels).
  • soklet_transport_failures_total - Counter of low-level transport failures across HTTP, SSE, and MCP transports (labels: server_type, reason with values such as WRITE_ERROR, WRITE_TIMEOUT, RESPONSE_WRITE_IDLE_TIMEOUT, ACCEPT_LOOP_ERROR, CONNECTION_SETUP_ERROR, TASK_ERROR, SELECTION_KEY_ERROR, or REGISTER_ERROR).
  • soklet_http_request_read_failures_total - Counter of HTTP request read failures (labels: reason with values UNPARSEABLE_REQUEST, REQUEST_READ_TIMEOUT, REQUEST_READ_REJECTED, or INTERNAL_ERROR).
  • soklet_http_requests_rejected_total - Counter of HTTP requests rejected before handling begins (labels: reason with values REQUEST_HANDLER_QUEUE_FULL, REQUEST_HANDLER_EXECUTOR_SHUTDOWN, or INTERNAL_ERROR).
  • soklet_http_request_duration_nanos_bucket - Cumulative histogram bucket counts for HTTP request duration in nanoseconds (labels: method, route, status_class, plus le).
  • soklet_http_request_duration_nanos_count - Histogram sample count for HTTP request duration in nanoseconds (labels: method, route, status_class).
  • soklet_http_request_duration_nanos_sum - Histogram sample sum for HTTP request duration in nanoseconds (labels: method, route, status_class).
  • soklet_http_handler_duration_nanos_bucket - Cumulative histogram bucket counts for HTTP handler duration in nanoseconds (labels: method, route, status_class, plus le).
  • soklet_http_handler_duration_nanos_count - Histogram sample count for HTTP handler duration in nanoseconds (labels: method, route, status_class).
  • soklet_http_handler_duration_nanos_sum - Histogram sample sum for HTTP handler duration in nanoseconds (labels: method, route, status_class).
  • soklet_http_ttfb_nanos_bucket - Cumulative histogram bucket counts for HTTP time-to-first-byte in nanoseconds (labels: method, route, status_class, plus le).
  • soklet_http_ttfb_nanos_count - Histogram sample count for HTTP time-to-first-byte in nanoseconds (labels: method, route, status_class).
  • soklet_http_ttfb_nanos_sum - Histogram sample sum for HTTP time-to-first-byte in nanoseconds (labels: method, route, status_class).
  • soklet_http_request_body_bytes_bucket - Cumulative histogram bucket counts for HTTP request body size in bytes (labels: method, route, plus le).
  • soklet_http_request_body_bytes_count - Histogram sample count for HTTP request body size in bytes (labels: method, route).
  • soklet_http_request_body_bytes_sum - Histogram sample sum for HTTP request body size in bytes (labels: method, route).
  • soklet_http_response_body_bytes_bucket - Cumulative histogram bucket counts for HTTP response body size in bytes (labels: method, route, status_class, plus le).
  • soklet_http_response_body_bytes_count - Histogram sample count for HTTP response body size in bytes (labels: method, route, status_class).
  • soklet_http_response_body_bytes_sum - Histogram sample sum for HTTP response body size in bytes (labels: method, route, status_class).
  • soklet_mcp_sessions_active - Gauge of currently active MCP sessions (no labels).
  • soklet_mcp_sse_streams_active - Gauge of currently active MCP SSE streams (no labels).
  • soklet_mcp_connections_accepted_total - Counter of total accepted MCP TCP connections (no labels).
  • soklet_mcp_connections_rejected_total - Counter of total rejected MCP TCP connections (no labels).
  • soklet_mcp_request_read_failures_total - Counter of MCP request read failures (labels: reason with values UNPARSEABLE_REQUEST, REQUEST_READ_TIMEOUT, REQUEST_READ_REJECTED, or INTERNAL_ERROR).
  • soklet_mcp_requests_rejected_total - Counter of MCP requests rejected before handling begins (labels: reason with values REQUEST_HANDLER_QUEUE_FULL, REQUEST_HANDLER_EXECUTOR_SHUTDOWN, or INTERNAL_ERROR).
  • soklet_mcp_requests_total - Counter of MCP JSON-RPC request outcomes (labels: endpoint_class, json_rpc_method, request_outcome keyed by McpRequestOutcome).
  • soklet_mcp_request_duration_nanos_bucket - Cumulative histogram bucket counts for MCP request duration in nanoseconds (labels: endpoint_class, json_rpc_method, request_outcome, plus le).
  • soklet_mcp_request_duration_nanos_count - Histogram sample count for MCP request duration in nanoseconds (labels: endpoint_class, json_rpc_method, request_outcome).
  • soklet_mcp_request_duration_nanos_sum - Histogram sample sum for MCP request duration in nanoseconds (labels: endpoint_class, json_rpc_method, request_outcome).
  • soklet_mcp_session_duration_nanos_bucket - Cumulative histogram bucket counts for MCP session duration in nanoseconds (labels: endpoint_class, termination_reason keyed by McpSessionTerminationReason, plus le).
  • soklet_mcp_session_duration_nanos_count - Histogram sample count for MCP session duration in nanoseconds (labels: endpoint_class, termination_reason).
  • soklet_mcp_session_duration_nanos_sum - Histogram sample sum for MCP session duration in nanoseconds (labels: endpoint_class, termination_reason).
  • soklet_mcp_sse_stream_duration_nanos_bucket - Cumulative histogram bucket counts for MCP SSE stream duration in nanoseconds (labels: endpoint_class, termination_reason keyed by StreamTerminationReason, plus le).
  • soklet_mcp_sse_stream_duration_nanos_count - Histogram sample count for MCP SSE stream duration in nanoseconds (labels: endpoint_class, termination_reason).
  • soklet_mcp_sse_stream_duration_nanos_sum - Histogram sample sum for MCP SSE stream duration in nanoseconds (labels: endpoint_class, termination_reason).
  • soklet_sse_connections_accepted_total - Counter of total accepted SSE TCP connections (no labels).
  • soklet_sse_connections_rejected_total - Counter of total rejected SSE TCP connections (no labels).
  • soklet_sse_request_read_failures_total - Counter of SSE request read failures (labels: reason with values UNPARSEABLE_REQUEST, REQUEST_READ_TIMEOUT, REQUEST_READ_REJECTED, or INTERNAL_ERROR).
  • soklet_sse_requests_rejected_total - Counter of SSE requests rejected before handling begins (labels: reason with values REQUEST_HANDLER_QUEUE_FULL, REQUEST_HANDLER_EXECUTOR_SHUTDOWN, or INTERNAL_ERROR).
  • soklet_sse_streams_active - Gauge of currently active SSE streams (no labels).
  • soklet_sse_handshakes_accepted_total - Counter of total accepted SSE handshakes (labels: route).
  • soklet_sse_handshakes_rejected_total - Counter of total rejected SSE handshakes (labels: route, handshake_failure_reason with values HANDSHAKE_TIMEOUT, HANDSHAKE_REJECTED, or INTERNAL_ERROR).
  • soklet_sse_event_broadcasts_total - Counter of SSE event enqueue outcomes (labels: route, outcome with values ATTEMPTED, ENQUEUED, or DROPPED).
  • soklet_sse_comment_broadcasts_total - Counter of SSE comment enqueue outcomes (labels: route, comment_type, outcome with values ATTEMPTED, ENQUEUED, or DROPPED).
  • soklet_sse_events_dropped_total - Counter of SSE events dropped before enqueue (labels: route, drop_reason with values QUEUE_FULL).
  • soklet_sse_comments_dropped_total - Counter of SSE comments dropped before enqueue (labels: route, comment_type, drop_reason with values QUEUE_FULL).
  • soklet_sse_time_to_first_event_nanos_bucket - Cumulative histogram bucket counts for SSE time to first event in nanoseconds (labels: route, plus le).
  • soklet_sse_time_to_first_event_nanos_count - Histogram sample count for SSE time to first event in nanoseconds (labels: route).
  • soklet_sse_time_to_first_event_nanos_sum - Histogram sample sum for SSE time to first event in nanoseconds (labels: route).
  • soklet_sse_event_write_duration_nanos_bucket - Cumulative histogram bucket counts for SSE event write duration in nanoseconds (labels: route, plus le).
  • soklet_sse_event_write_duration_nanos_count - Histogram sample count for SSE event write duration in nanoseconds (labels: route).
  • soklet_sse_event_write_duration_nanos_sum - Histogram sample sum for SSE event write duration in nanoseconds (labels: route).
  • soklet_sse_event_delivery_lag_nanos_bucket - Cumulative histogram bucket counts for SSE event delivery lag in nanoseconds (labels: route, plus le).
  • soklet_sse_event_delivery_lag_nanos_count - Histogram sample count for SSE event delivery lag in nanoseconds (labels: route).
  • soklet_sse_event_delivery_lag_nanos_sum - Histogram sample sum for SSE event delivery lag in nanoseconds (labels: route).
  • soklet_sse_event_size_bytes_bucket - Cumulative histogram bucket counts for SSE event size in bytes (labels: route, plus le).
  • soklet_sse_event_size_bytes_count - Histogram sample count for SSE event size in bytes (labels: route).
  • soklet_sse_event_size_bytes_sum - Histogram sample sum for SSE event size in bytes (labels: route).
  • soklet_sse_queue_depth_bucket - Cumulative histogram bucket counts for SSE per-stream queue depth (events) (labels: route, plus le).
  • soklet_sse_queue_depth_count - Histogram sample count for SSE per-stream queue depth (events) (labels: route).
  • soklet_sse_queue_depth_sum - Histogram sample sum for SSE per-stream queue depth (events) (labels: route).
  • soklet_sse_comment_delivery_lag_nanos_bucket - Cumulative histogram bucket counts for SSE comment delivery lag in nanoseconds (labels: route, comment_type, plus le).
  • soklet_sse_comment_delivery_lag_nanos_count - Histogram sample count for SSE comment delivery lag in nanoseconds (labels: route, comment_type).
  • soklet_sse_comment_delivery_lag_nanos_sum - Histogram sample sum for SSE comment delivery lag in nanoseconds (labels: route, comment_type).
  • soklet_sse_comment_size_bytes_bucket - Cumulative histogram bucket counts for SSE comment size in bytes (labels: route, comment_type, plus le).
  • soklet_sse_comment_size_bytes_count - Histogram sample count for SSE comment size in bytes (labels: route, comment_type).
  • soklet_sse_comment_size_bytes_sum - Histogram sample sum for SSE comment size in bytes (labels: route, comment_type).
  • soklet_sse_comment_queue_depth_bucket - Cumulative histogram bucket counts for SSE comment queue depth (comments) (labels: route, comment_type, plus le).
  • soklet_sse_comment_queue_depth_count - Histogram sample count for SSE comment queue depth (comments) (labels: route, comment_type).
  • soklet_sse_comment_queue_depth_sum - Histogram sample sum for SSE comment queue depth (comments) (labels: route, comment_type).
  • soklet_sse_stream_duration_nanos_bucket - Cumulative histogram bucket counts for SSE stream duration in nanoseconds (labels: route, termination_reason keyed by StreamTerminationReason, plus le).
  • soklet_sse_stream_duration_nanos_count - Histogram sample count for SSE stream duration in nanoseconds (labels: route, termination_reason).
  • soklet_sse_stream_duration_nanos_sum - Histogram sample sum for SSE stream duration in nanoseconds (labels: route, termination_reason).

Histogram samples vary with histogramFormat: COUNT_SUM_ONLY omits *_bucket samples, and NONE omits all histogram samples.


Metrics Collected

The default collector focuses on high-signal, low-cardinality metrics:

  • HTTP request duration, handler duration, and time-to-first-byte (TTFB), grouped by HttpMethod, ResourcePathDeclaration, and status class (2xx, 3xx, ...)
  • HTTP request/response body sizes, grouped by HttpMethod and ResourcePathDeclaration
  • HTTP, MCP, and SSE connection accept/reject counters (TCP connection attempts, not per-request activity)
  • Low-level transport failures across HTTP, SSE, and MCP transports, including write errors, write timeouts, HTTP read errors with request data in flight, response write-idle timeouts, accept-loop failures, event-loop task failures, selection-key failures, and registration failures
  • HTTP, MCP, and SSE request read failures (parse errors, read timeouts, and reader rejections)
  • HTTP, MCP, and SSE request rejections (request-handler queue or executor saturation)
  • SSE handshake accept/reject counters (handshake failures are keyed by HandshakeFailureReason)
  • SSE enqueue outcome counters (attempted/enqueued/dropped) and dropped-event counters (when queues are full)
  • SSE stream duration, time to first event, event write duration
  • SSE delivery lag, payload size, and per-stream queue depth for events, plus separate comment metrics split by comment type (SseComment.CommentType.COMMENT vs SseComment.CommentType.HEARTBEAT)
  • MCP session counts, MCP SSE stream counts, MCP request outcomes, and MCP request durations grouped by endpoint class and JSON-RPC method
  • MCP session duration and MCP SSE stream duration grouped by endpoint class and termination reason

Routes are keyed on ResourcePathDeclaration (for example, /accounts/{id} as opposed to /accounts/123) to keep label cardinality low.

Metrics callbacks that receive a Request or a request-bearing stream object can read W3C trace data via Request::getTraceContext. Do not put trace IDs in aggregate metric labels: they are high-cardinality values and belong in logs, spans, or exemplars rather than default counters and histograms.

Snapshot histogram durations are in nanoseconds, sizes are in bytes, and queue depths are counts. Callback APIs provide Duration values for write and delivery lag timing.

For HTTP response metrics, byte counts refer to the response body only (not headers or transport overhead), so they remain consistent across server implementations.


Snapshot Structure

The MetricsCollector.Snapshot exposes the collected data as immutable maps keyed by small, stable records. HTTP and SSE route-based keys carry a ResourcePathDeclaration for the templated route and a RouteType that indicates whether the request matched a route. When RouteType is RouteType.UNMATCHED, the route is null. MCP keys are endpoint-oriented and use the concrete endpoint class plus JSON-RPC method or termination reason. SSE stream and MCP SSE stream duration keys use StreamTerminationReason. Comment metrics also include the comment type (SseComment.CommentType.COMMENT vs SseComment.CommentType.HEARTBEAT) in the key. Handshake failure metrics include the HandshakeFailureReason:

The MetricsCollector.Snapshot also includes total accepted/rejected connection counts for HTTP, MCP, and SSE servers.

Histogram values are returned as MetricsCollector.HistogramSnapshot instances.


Resetting Metrics

MetricsCollector::reset is supported by the default collector and is useful for tests or ad-hoc sampling. Custom collectors may implement it as a no-op.

metricsCollector.reset();

OpenTelemetry

If you are standardizing on OpenTelemetry, use soklet-otel, which provides:

Metrics answer "how much, how often, how slow" and must keep low-cardinality labels. Traces answer "what happened to this request" and carry trace IDs/span IDs by design. Soklet keeps those concerns separate: configure the metrics collector for aggregate telemetry and the lifecycle observer for spans.

Installation

Maven:

<dependency>
  <groupId>com.soklet</groupId>
  <artifactId>soklet-otel</artifactId>
  <version>1.3.0</version>
</dependency>

Gradle:

repositories {
  mavenCentral()
}

dependencies {
  implementation 'com.soklet:soklet-otel:1.3.0'
}

Configuration

Provide your application's OpenTelemetry instance:

// Acquire an OpenTelemetry instance from wherever you'd like...
OpenTelemetry openTelemetry = myOpenTelemetry();

// ...and use it to drive Soklet's OpenTelemetry integrations.
SokletConfig config = SokletConfig.withHttpServer(
  HttpServer.fromPort(8080)
).metricsCollector(
  OpenTelemetryMetricsCollector.withOpenTelemetry(openTelemetry)
    .instrumentationName("com.mycompany.myapp.soklet")
    .instrumentationVersion("1.0.0")
    .build()
).lifecycleObservers(List.of(
  OpenTelemetryLifecycleObserver.withOpenTelemetry(openTelemetry)
    .instrumentationName("com.mycompany.myapp.soklet")
    .instrumentationVersion("1.0.0")
    .build()
)
).build();

If you already have a Meter, you can wire directly via OpenTelemetryMetricsCollector::withMeter.

By default, MetricNamingStrategy.SEMCONV is used:

  • Standard HTTP metrics use OpenTelemetry semantic convention names (for example, http.server.request.duration)
  • Soklet-specific telemetry (especially SSE details) is emitted with soklet.* names
  • For unmatched HTTP requests, http.route is omitted

OpenTelemetryMetricsCollector intentionally does not emit W3C trace IDs, parent IDs, or tracestate values as metric attributes. Use logs, spans, or exemplar-aware tracing integrations for per-trace correlation.

OpenTelemetryLifecycleObserver creates SERVER spans for standard HTTP requests, streaming HTTP responses, SSE connections, MCP JSON-RPC requests, and MCP SSE streams. It uses Request::getTraceContext as the remote parent when valid W3C trace context is present. Malformed or absent trace context produces a root span.

Long-lived SSE and MCP SSE spans may not appear in some trace backends until the stream ends. This is normal for exporters/backends that batch or display spans only after completion.

To use Soklet-prefixed HTTP metric names, set MetricNamingStrategy.SOKLET:

// Use Soklet's metric names
OpenTelemetryMetricsCollector.withOpenTelemetry(openTelemetry)
  .metricNamingStrategy(OpenTelemetryMetricsCollector.MetricNamingStrategy.SOKLET)
  .build();

Unlike the default in-memory collector, OpenTelemetryMetricsCollector does not implement MetricsCollector::snapshot or MetricsCollector::snapshotText. It emits data through your OpenTelemetry SDK/exporter pipeline instead.

Previous
Testing