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 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 it for telemetry, not business logic - 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 might choose to swap in your own implementation if you have special monitoring requirements (OpenTelemetry, Micrometer, etc.)
Server server = Server.fromPort(8080);
SokletConfig config = SokletConfig.withServer(server)
// This is already the default; specifying it here is optional
.metricsCollector(MetricsCollector.defaultInstance())
.build();
Server server = Server.fromPort(8080);
SokletConfig config = SokletConfig.withServer(server)
// 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:
Server server = Server.fromPort(8080);
SokletConfig config = SokletConfig.withServer(server)
// Use this instead of null to disable metrics collection
.metricsCollector(MetricsCollector.disabledInstance())
.build();
Server server = Server.fromPort(8080);
SokletConfig config = SokletConfig.withServer(server)
// 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.
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();
}
@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 and SSE metrics. SSE data is only included when an SSE server is 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/SSE metrics
if (!name.startsWith("soklet_http_") && !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);
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/SSE metrics
if (!name.startsWith("soklet_http_") && !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_http_request_read_failures_total- Counter of HTTP request read failures (labels:reasonwith valuesUNPARSEABLE_REQUEST,REQUEST_READ_TIMEOUT,REQUEST_READ_REJECTED, orINTERNAL_ERROR).soklet_http_requests_rejected_total- Counter of HTTP requests rejected before handling begins (labels:reasonwith valuesREQUEST_HANDLER_QUEUE_FULL,REQUEST_HANDLER_EXECUTOR_SHUTDOWN, orINTERNAL_ERROR).soklet_http_request_duration_nanos_bucket- Cumulative histogram bucket counts for HTTP request duration in nanoseconds (labels:method,route,status_class, plusle).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, plusle).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, plusle).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, plusle).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, plusle).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_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:reasonwith valuesUNPARSEABLE_REQUEST,REQUEST_READ_TIMEOUT,REQUEST_READ_REJECTED, orINTERNAL_ERROR).soklet_sse_requests_rejected_total- Counter of SSE requests rejected before handling begins (labels:reasonwith valuesREQUEST_HANDLER_QUEUE_FULL,REQUEST_HANDLER_EXECUTOR_SHUTDOWN, orINTERNAL_ERROR).soklet_sse_connections_active- Gauge of currently active SSE connections (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_reasonwith valuesHANDSHAKE_TIMEOUT,HANDSHAKE_REJECTED, orINTERNAL_ERROR).soklet_sse_event_broadcasts_total- Counter of SSE event enqueue outcomes (labels:route,outcomewith valuesATTEMPTED,ENQUEUED, orDROPPED).soklet_sse_comment_broadcasts_total- Counter of SSE comment enqueue outcomes (labels:route,comment_type,outcomewith valuesATTEMPTED,ENQUEUED, orDROPPED).soklet_sse_events_dropped_total- Counter of SSE events dropped before enqueue (labels:route,drop_reasonwith valuesQUEUE_FULL).soklet_sse_comments_dropped_total- Counter of SSE comments dropped before enqueue (labels:route,comment_type,drop_reasonwith valuesQUEUE_FULL).soklet_sse_time_to_first_event_nanos_bucket- Cumulative histogram bucket counts for SSE time to first event in nanoseconds (labels:route, plusle).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, plusle).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, plusle).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, plusle).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-connection queue depth (events) (labels:route, plusle).soklet_sse_queue_depth_count- Histogram sample count for SSE per-connection queue depth (events) (labels:route).soklet_sse_queue_depth_sum- Histogram sample sum for SSE per-connection 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, plusle).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, plusle).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, plusle).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_connection_duration_nanos_bucket- Cumulative histogram bucket counts for SSE connection duration in nanoseconds (labels:route,termination_reason, plusle).soklet_sse_connection_duration_nanos_count- Histogram sample count for SSE connection duration in nanoseconds (labels:route,termination_reason).soklet_sse_connection_duration_nanos_sum- Histogram sample sum for SSE connection 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
HttpMethodandResourcePathDeclaration - HTTP and SSE connection accept/reject counters (TCP connection attempts, not per-request activity)
- HTTP and SSE request read failures (parse errors, read timeouts, and reader rejections)
- HTTP 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 connection duration, time to first event, event write duration
- SSE delivery lag, payload size, and per-connection queue depth for events, plus separate comment metrics split by comment type (
COMMENTvsHEARTBEAT)
Routes are keyed on ResourcePathDeclaration (for example, /accounts/{id} as opposed to /accounts/123) to keep label cardinality low.
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. Each key carries a ResourcePathDeclaration for the templated route and a RouteType that indicates whether the request matched a route. When RouteType is UNMATCHED, the route is null. Comment metrics also include the comment type (COMMENT vs HEARTBEAT) in the key. Handshake failure metrics include the HandshakeFailureReason:
MetricsCollector.ServerRouteKeyMetricsCollector.ServerRouteStatusKeyMetricsCollector.RequestReadFailureKeyMetricsCollector.RequestRejectionKeyMetricsCollector.ServerSentEventCommentRouteKeyMetricsCollector.ServerSentEventRouteKeyMetricsCollector.ServerSentEventRouteHandshakeFailureKeyMetricsCollector.ServerSentEventRouteEnqueueOutcomeKeyMetricsCollector.ServerSentEventCommentRouteEnqueueOutcomeKeyMetricsCollector.ServerSentEventRouteDropKeyMetricsCollector.ServerSentEventCommentRouteDropKeyMetricsCollector.ServerSentEventRouteTerminationKeyMetricsCollector.RouteType
The MetricsCollector.Snapshot also includes total accepted/rejected connection counts for both HTTP 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();
metricsCollector.reset();

