Core Concepts
Request Lifecycle
Soklet provides two lifecycle hooks:
- RequestInterceptor for adjusting request flow (wrapping or intercepting)
- LifecycleObserver for read-only observation of system and request events
They're useful for tasks like:
- Logging requests and responses
- Performing authentication and authorization
- Modifying requests/responses before downstream processing occurs
- Wrapping downstream code in a database transaction
- Monitoring unexpected errors that occur during internal processing (see Event Logging)
This is similar to the Jakarta EE Servlet Filter concept, but provides additional functionality beyond "wrap the whole request".
Request Interceptor
Request Wrapping
Wraps around the whole "outside" of an entire request-handling flow. The "inside" of the flow is everything from Resource Method execution to writing response bytes to the client. For a more fine-grained approach, see Request Intercepting.
Wrapping happens before Soklet resolves which Resource Method should handle the request. This means you can rewrite the HTTP method or path by returning a modified request via the consumer and Soklet will route using the wrapped request. You must call requestProcessor.accept(...) exactly once before returning; otherwise Soklet logs an error and returns a 500 response.
Wrapping a request is useful when you'd like to store off request-scoped information that needs to be made easily accessible to other parts of the system - for example, a request's Locale and ZoneId.
To implement, a useful scoping mechanism is Java's ScopedValue<T> (Java 25+) or ThreadLocal<T>. The former is demonstrated below.
// Special scoped value so anyone can access the current Locale.
// For Java < 25, use ThreadLocal instead
public static final ScopedValue<Locale> CURRENT_LOCALE;
// Spin up the ScopedValue (or ThreadLocal)
static {
CURRENT_LOCALE = ScopedValue.newInstance();
}
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).requestInterceptor(new RequestInterceptor() {
@Override
public void wrapRequest(
@NonNull ServerType serverType,
@NonNull Request request,
@NonNull Consumer<Request> requestProcessor
) {
// Make the locale accessible by other code during this request...
Locale locale = request.getLocales().get(0);
// ...by binding it to a ScopedValue (or ThreadLocal).
ScopedValue.where(CURRENT_LOCALE, locale).run(() -> {
// You must call this so downstream processing can proceed
requestProcessor.accept(request);
});
}
}).build();
// Special scoped value so anyone can access the current Locale.
// For Java < 25, use ThreadLocal instead
public static final ScopedValue<Locale> CURRENT_LOCALE;
// Spin up the ScopedValue (or ThreadLocal)
static {
CURRENT_LOCALE = ScopedValue.newInstance();
}
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).requestInterceptor(new RequestInterceptor() {
@Override
public void wrapRequest(
@NonNull ServerType serverType,
@NonNull Request request,
@NonNull Consumer<Request> requestProcessor
) {
// Make the locale accessible by other code during this request...
Locale locale = request.getLocales().get(0);
// ...by binding it to a ScopedValue (or ThreadLocal).
ScopedValue.where(CURRENT_LOCALE, locale).run(() -> {
// You must call this so downstream processing can proceed
requestProcessor.accept(request);
});
}
}).build();
Then, elsewhere in your code while a request is being processed:
class ExampleService {
void accessCurrentLocale() {
// You now have access to the Locale bound to the logical scope
// (or Thread) without having to pass it down the call stack
Locale locale = CURRENT_LOCALE.orElse(Locale.getDefault());
}
}
class ExampleService {
void accessCurrentLocale() {
// You now have access to the Locale bound to the logical scope
// (or Thread) without having to pass it down the call stack
Locale locale = CURRENT_LOCALE.orElse(Locale.getDefault());
}
}
References:
Request Intercepting
Conceptually, when a request comes in, Soklet will:
- Invoke the appropriate Resource Method to acquire a response
- Send the response over the wire to the client
Request Intercepting provides programmatic control over those two steps.
Example use cases are:
- Wrapping Resource Methods with a database transaction (e.g. to ensure atomicity)
- Customizing responses prior to sending over the wire
For a more coarse-grained approach, see Request Wrapping.
You must call responseWriter.accept(...) exactly once before returning; otherwise Soklet logs an error and returns a 500 response.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).requestInterceptor(new RequestInterceptor() {
@Override
public void interceptRequest(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull Function<Request, MarshaledResponse> responseGenerator,
@NonNull Consumer<MarshaledResponse> responseWriter
) {
// Here's where you might start a DB transaction
MyDatabase.INSTANCE.beginTransaction();
// Step 1: Invoke the Resource Method and acquire its response
MarshaledResponse response = responseGenerator.apply(request);
// Commit the DB transaction before marshaling/sending the response
// to reduce contention by keeping "open" time short
MyDatabase.INSTANCE.commitTransaction();
// You might also perform systemwide response adjustments here.
// For example, set a special header via mutable copy
response = response.copy().headers((mutableHeaders) -> {
mutableHeaders.put("X-Powered-By", Set.of("Soklet"));
}).finish();
// Step 2: Send the finalized response over the wire
responseWriter.accept(response);
}
}).build();
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).requestInterceptor(new RequestInterceptor() {
@Override
public void interceptRequest(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull Function<Request, MarshaledResponse> responseGenerator,
@NonNull Consumer<MarshaledResponse> responseWriter
) {
// Here's where you might start a DB transaction
MyDatabase.INSTANCE.beginTransaction();
// Step 1: Invoke the Resource Method and acquire its response
MarshaledResponse response = responseGenerator.apply(request);
// Commit the DB transaction before marshaling/sending the response
// to reduce contention by keeping "open" time short
MyDatabase.INSTANCE.commitTransaction();
// You might also perform systemwide response adjustments here.
// For example, set a special header via mutable copy
response = response.copy().headers((mutableHeaders) -> {
mutableHeaders.put("X-Powered-By", Set.of("Soklet"));
}).finish();
// Step 2: Send the finalized response over the wire
responseWriter.accept(response);
}
}).build();
References:
Lifecycle Observer
LifecycleObserver is the right hook for read-only request, stream, SSE, and MCP observation: tracing, audit events, structured logging, and cleanup notifications. For aggregate counters, gauges, and histograms, prefer MetricsCollector instead.
Configure one observer with SokletConfig.Builder::lifecycleObserver, or configure several observers in registration order with SokletConfig.Builder::lifecycleObservers. Calling lifecycleObserver(null) or lifecycleObservers(null) clears the observer list.
SokletConfig config = SokletConfig.withHttpServer(HttpServer.fromPort(8080))
.lifecycleObservers(List.of(
new AuditLifecycleObserver(),
new TracingLifecycleObserver()
))
.build();
SokletConfig config = SokletConfig.withHttpServer(HttpServer.fromPort(8080))
.lifecycleObservers(List.of(
new AuditLifecycleObserver(),
new TracingLifecycleObserver()
))
.build();
Soklet Start/Stop
Execute code immediately before and after a Soklet instance starts up and shuts down.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartSoklet(@NonNull Soklet soklet) {
// Perform startup tasks required prior to Soklet launch
MyPayrollSystem.INSTANCE.startLengthyWarmupProcess();
}
@Override
public void didStartSoklet(@NonNull Soklet soklet) {
// Soklet has fully started.
//@NonNull HttpServer (and optionally SSE server) are running
System.out.println("Soklet started.");
}
@Override
public void didFailToStartSoklet(
@NonNull Soklet soklet,
@NonNull Throwable throwable
) {
System.err.println("Soklet failed to start.");
throwable.printStackTrace();
}
@Override
public void willStopSoklet(@NonNull Soklet soklet) {
// Perform shutdown tasks required prior to Soklet teardown
MyPayrollSystem.INSTANCE.destroy();
}
@Override
public void didStopSoklet(@NonNull Soklet soklet) {
// Soklet has fully shut down
System.out.println("Soklet stopped.");
}
@Override
public void didFailToStopSoklet(
@NonNull Soklet soklet,
@NonNull Throwable throwable
) {
System.err.println("Soklet failed to stop cleanly.");
throwable.printStackTrace();
}
}).build();
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartSoklet(@NonNull Soklet soklet) {
// Perform startup tasks required prior to Soklet launch
MyPayrollSystem.INSTANCE.startLengthyWarmupProcess();
}
@Override
public void didStartSoklet(@NonNull Soklet soklet) {
// Soklet has fully started.
//@NonNull HttpServer (and optionally SSE server) are running
System.out.println("Soklet started.");
}
@Override
public void didFailToStartSoklet(
@NonNull Soklet soklet,
@NonNull Throwable throwable
) {
System.err.println("Soklet failed to start.");
throwable.printStackTrace();
}
@Override
public void willStopSoklet(@NonNull Soklet soklet) {
// Perform shutdown tasks required prior to Soklet teardown
MyPayrollSystem.INSTANCE.destroy();
}
@Override
public void didStopSoklet(@NonNull Soklet soklet) {
// Soklet has fully shut down
System.out.println("Soklet stopped.");
}
@Override
public void didFailToStopSoklet(
@NonNull Soklet soklet,
@NonNull Throwable throwable
) {
System.err.println("Soklet failed to stop cleanly.");
throwable.printStackTrace();
}
}).build();
References:
LifecycleObserver::willStartSokletLifecycleObserver::didStartSokletLifecycleObserver::didFailToStartSokletLifecycleObserver::willStopSokletLifecycleObserver::didStopSokletLifecycleObserver::didFailToStopSoklet
HTTP Server Start/Stop
Execute code immediately before and after the Soklet-managed HttpServer starts up and shuts down.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartHttpServer(@NonNull HttpServer httpServer) {
// Perform startup tasks required prior to server launch
MyPayrollSystem.INSTANCE.startLengthyWarmupProcess();
}
@Override
public void didStartHttpServer(@NonNull HttpServer httpServer) {
// HTTP server has fully started up and is listening
System.out.println("HTTP server started.");
}
@Override
public void didFailToStartHttpServer(
@NonNull HttpServer httpServer,
@NonNull Throwable throwable
) {
System.err.println("HTTP server failed to start.");
throwable.printStackTrace();
}
@Override
public void willStopHttpServer(@NonNull HttpServer httpServer) {
// Perform shutdown tasks required prior to server teardown
MyPayrollSystem.INSTANCE.destroy();
}
@Override
public void didStopHttpServer(@NonNull HttpServer httpServer) {
// HTTP server has fully shut down
System.out.println("HTTP server stopped.");
}
@Override
public void didFailToStopHttpServer(
@NonNull HttpServer httpServer,
@NonNull Throwable throwable
) {
System.err.println("HTTP server failed to stop cleanly.");
throwable.printStackTrace();
}
}).build();
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartHttpServer(@NonNull HttpServer httpServer) {
// Perform startup tasks required prior to server launch
MyPayrollSystem.INSTANCE.startLengthyWarmupProcess();
}
@Override
public void didStartHttpServer(@NonNull HttpServer httpServer) {
// HTTP server has fully started up and is listening
System.out.println("HTTP server started.");
}
@Override
public void didFailToStartHttpServer(
@NonNull HttpServer httpServer,
@NonNull Throwable throwable
) {
System.err.println("HTTP server failed to start.");
throwable.printStackTrace();
}
@Override
public void willStopHttpServer(@NonNull HttpServer httpServer) {
// Perform shutdown tasks required prior to server teardown
MyPayrollSystem.INSTANCE.destroy();
}
@Override
public void didStopHttpServer(@NonNull HttpServer httpServer) {
// HTTP server has fully shut down
System.out.println("HTTP server stopped.");
}
@Override
public void didFailToStopHttpServer(
@NonNull HttpServer httpServer,
@NonNull Throwable throwable
) {
System.err.println("HTTP server failed to stop cleanly.");
throwable.printStackTrace();
}
}).build();
References:
LifecycleObserver::willStartHttpServerLifecycleObserver::didStartHttpServerLifecycleObserver::didFailToStartHttpServerLifecycleObserver::willStopHttpServerLifecycleObserver::didStopHttpServerLifecycleObserver::didFailToStopHttpServer
Connection Acceptance
These methods fire when Soklet is about to accept a TCP connection, after it accepts one, or when it fails to accept a connection attempt. Each callback provides a ServerType, a best-effort remote address, and (for failures) a ConnectionRejectionReason plus an optional exception.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willAcceptConnection(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress
) {
System.out.printf("Incoming %s connection from %s\n", serverType, remoteAddress);
}
@Override
public void didAcceptConnection(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress
) {
System.out.printf("Accepted %s connection from %s\n", serverType, remoteAddress);
}
@Override
public void didFailToAcceptConnection(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@NonNull ConnectionRejectionReason reason,
@Nullable Throwable throwable
) {
System.out.printf("Failed to accept %s connection from %s (%s)\n", serverType, remoteAddress, reason);
}
}).build();
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willAcceptConnection(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress
) {
System.out.printf("Incoming %s connection from %s\n", serverType, remoteAddress);
}
@Override
public void didAcceptConnection(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress
) {
System.out.printf("Accepted %s connection from %s\n", serverType, remoteAddress);
}
@Override
public void didFailToAcceptConnection(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@NonNull ConnectionRejectionReason reason,
@Nullable Throwable throwable
) {
System.out.printf("Failed to accept %s connection from %s (%s)\n", serverType, remoteAddress, reason);
}
}).build();
References:
LifecycleObserver::willAcceptConnectionLifecycleObserver::didAcceptConnectionLifecycleObserver::didFailToAcceptConnection
Request Acceptance
These hooks fire as Soklet accepts requests for application-level handling and reads/parses them into Request instances.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willAcceptRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget
) {
System.out.printf("About to accept request: %s%n", requestTarget);
}
@Override
public void didAcceptRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget
) {
System.out.printf("Accepted request: %s%n", requestTarget);
}
@Override
public void didFailToAcceptRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget,
@NonNull RequestRejectionReason reason,
@Nullable Throwable throwable
) {
System.err.printf("Failed to accept request (%s): %s%n", reason, requestTarget);
}
@Override
public void willReadRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget
) {
System.out.printf("About to read request: %s%n", requestTarget);
}
@Override
public void didReadRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget
) {
System.out.printf("Read request: %s%n", requestTarget);
}
@Override
public void didFailToReadRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget,
@NonNull RequestReadFailureReason reason,
@Nullable Throwable throwable
) {
System.err.printf("Failed to read request (%s): %s%n", reason, requestTarget);
}
}).build();
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willAcceptRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget
) {
System.out.printf("About to accept request: %s%n", requestTarget);
}
@Override
public void didAcceptRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget
) {
System.out.printf("Accepted request: %s%n", requestTarget);
}
@Override
public void didFailToAcceptRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget,
@NonNull RequestRejectionReason reason,
@Nullable Throwable throwable
) {
System.err.printf("Failed to accept request (%s): %s%n", reason, requestTarget);
}
@Override
public void willReadRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget
) {
System.out.printf("About to read request: %s%n", requestTarget);
}
@Override
public void didReadRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget
) {
System.out.printf("Read request: %s%n", requestTarget);
}
@Override
public void didFailToReadRequest(
@NonNull ServerType serverType,
@Nullable InetSocketAddress remoteAddress,
@Nullable String requestTarget,
@NonNull RequestReadFailureReason reason,
@Nullable Throwable throwable
) {
System.err.printf("Failed to read request (%s): %s%n", reason, requestTarget);
}
}).build();
References:
LifecycleObserver::willAcceptRequestLifecycleObserver::didAcceptRequestLifecycleObserver::didFailToAcceptRequestLifecycleObserver::willReadRequestLifecycleObserver::didReadRequestLifecycleObserver::didFailToReadRequestRequestReadFailureReasonRequestRejectionReason
Request Handling
These methods are fired at the very start of request processing and the very end, respectively.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void didStartRequestHandling(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod
) {
System.out.printf("Received request: %s\n", request);
// If there was no resourceMethod matching the request, expect a 404
if(resourceMethod != null)
System.out.printf("Request to be handled by: %s\n", resourceMethod);
else
System.out.println("This will be a 404.");
}
@Override
public void didFinishRequestHandling(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse,
@NonNull Duration processingDuration,
@NonNull List<Throwable> throwables
) {
// We have access to a few things here...
// * marshaledResponse is what was ultimately sent
// over the wire
// * processingDuration is how long everything took,
// including sending the response to the client
// * throwables is the ordered list of exceptions
// thrown during execution (if any)
double millis = processingDuration.toNanos() / 1_000_000.0;
System.out.printf("Entire request took %dms\n", millis);
}
}).build();
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void didStartRequestHandling(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod
) {
System.out.printf("Received request: %s\n", request);
// If there was no resourceMethod matching the request, expect a 404
if(resourceMethod != null)
System.out.printf("Request to be handled by: %s\n", resourceMethod);
else
System.out.println("This will be a 404.");
}
@Override
public void didFinishRequestHandling(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse,
@NonNull Duration processingDuration,
@NonNull List<Throwable> throwables
) {
// We have access to a few things here...
// * marshaledResponse is what was ultimately sent
// over the wire
// * processingDuration is how long everything took,
// including sending the response to the client
// * throwables is the ordered list of exceptions
// thrown during execution (if any)
double millis = processingDuration.toNanos() / 1_000_000.0;
System.out.printf("Entire request took %dms\n", millis);
}
}).build();
References:
Response Writing
Monitor the response writing process - sending bytes over the wire. You can respond to successful writes or failed writes (e.g. unexpected client disconnect).
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willWriteResponse(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse
) {
// Access to marshaledResponse here lets us see exactly
// how much response body data will be written
Long bodyLength = marshaledResponse.getBodyLength();
System.out.printf("About to start writing response with " +
"a %d-byte body...\n", bodyLength);
}
@Override
public void didWriteResponse(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse,
@NonNull Duration responseWriteDuration
) {
double millis = responseWriteDuration.toNanos() / 1_000_000.0;
System.out.printf("Took %dms to write response\n", millis);
}
@Override
public void didFailToWriteResponse(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse,
@NonNull Duration responseWriteDuration,
@NonNull Throwable throwable
) {
double millis = responseWriteDuration.toNanos() / 1_000_000.0;
System.out.printf("Response write failed after %dms\n", millis);
// Use this to monitor trends in unexpected client disconnects
System.err.println("Exception occurred while writing response");
throwable.printStackTrace();
}
@Override
public void willTerminateResponseStream(
@NonNull StreamingResponseHandle streamingResponse,
@NonNull StreamTermination termination
) {
System.out.printf("Streaming response terminating: %s%n",
termination.getReason());
}
@Override
public void didTerminateResponseStream(
@NonNull StreamingResponseHandle streamingResponse,
@NonNull StreamTermination termination
) {
if (termination.getReason() == StreamTerminationReason.COMPLETED) {
System.out.println("Streaming response completed normally");
} else {
System.err.printf("Streaming response ended after %dms: %s%n",
termination.getDuration().toMillis(), termination.getReason());
}
}
}).build();
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willWriteResponse(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse
) {
// Access to marshaledResponse here lets us see exactly
// how much response body data will be written
Long bodyLength = marshaledResponse.getBodyLength();
System.out.printf("About to start writing response with " +
"a %d-byte body...\n", bodyLength);
}
@Override
public void didWriteResponse(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse,
@NonNull Duration responseWriteDuration
) {
double millis = responseWriteDuration.toNanos() / 1_000_000.0;
System.out.printf("Took %dms to write response\n", millis);
}
@Override
public void didFailToWriteResponse(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse,
@NonNull Duration responseWriteDuration,
@NonNull Throwable throwable
) {
double millis = responseWriteDuration.toNanos() / 1_000_000.0;
System.out.printf("Response write failed after %dms\n", millis);
// Use this to monitor trends in unexpected client disconnects
System.err.println("Exception occurred while writing response");
throwable.printStackTrace();
}
@Override
public void willTerminateResponseStream(
@NonNull StreamingResponseHandle streamingResponse,
@NonNull StreamTermination termination
) {
System.out.printf("Streaming response terminating: %s%n",
termination.getReason());
}
@Override
public void didTerminateResponseStream(
@NonNull StreamingResponseHandle streamingResponse,
@NonNull StreamTermination termination
) {
if (termination.getReason() == StreamTerminationReason.COMPLETED) {
System.out.println("Streaming response completed normally");
} else {
System.err.printf("Streaming response ended after %dms: %s%n",
termination.getDuration().toMillis(), termination.getReason());
}
}
}).build();
For non-streaming responses, didWriteResponse and didFailToWriteResponse cover the response write handoff. For StreamingResponseBody, willTerminateResponseStream and didTerminateResponseStream are invoked when the stream completes, is canceled, or fails. These hooks record stream duration, distinguish client disconnects from producer failures, and clean up application-side accounting. Standard HTTP stream termination callbacks are normally back-to-back because there is no broadcaster or session registry cleanup phase between them.
For request correlation, prefer the request-bearing objects already passed through the lifecycle API. StreamingResponseHandle, SseConnection, and McpSseStream expose their originating Request via getRequest(), and LogEvent exposes it via getRequest() when available. Use Request::getId() for local log and metric correlation. For distributed trace correlation, these same request-bearing objects can reach Request::getTraceContext; see Trace Context.
References:
LifecycleObserver::willWriteResponseLifecycleObserver::didWriteResponseLifecycleObserver::didFailToWriteResponseLifecycleObserver::willTerminateResponseStreamLifecycleObserver::didTerminateResponseStream
Stream Termination
Soklet uses the same StreamTermination and StreamTerminationReason model for streaming HTTP responses, dedicated SSE connections, and MCP SSE streams. The callback remains protocol-specific, but the termination payload always includes a reason, duration, and optional cause.
| Reason | Common sources |
|---|---|
COMPLETED | Streaming HTTP response producer finished normally |
CLIENT_DISCONNECTED | HTTP streaming client, SSE client, or MCP stream client closed the connection |
SERVER_STOPPING | The owning server stopped while the stream was active |
PROTOCOL_UNSUPPORTED | A request protocol cannot support the stream, such as HTTP/1.0 for chunked streaming |
RESPONSE_TIMEOUT | Streaming HTTP response exceeded its total timeout |
RESPONSE_IDLE_TIMEOUT | Streaming HTTP response producer exceeded its idle timeout |
APPLICATION_CANCELED | Producer code intentionally threw StreamingResponseCanceledException |
BACKPRESSURE | SSE connection was closed because its write queue reached capacity |
SESSION_TERMINATED | MCP session termination closed its active SSE stream |
WRITE_FAILED | Stream write or transport processing failed |
PRODUCER_FAILED | Streaming HTTP response producer threw unexpectedly |
INTERNAL_ERROR | Unexpected internal error |
SIMULATOR_LIMIT_EXCEEDED | Simulator refused to materialize more streaming response bytes |
UNKNOWN | Stream ended and no more specific reason was available |
Server-Sent Events
If your application configures a SseServer, additional lifecycle hooks are available for SSE server and connection activity.
SSE Server Start/Stop
SokletConfig config = SokletConfig.withSseServer(
SseServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartSseServer(
@NonNull SseServer sseServer
) {
System.out.println("SSE server starting.");
}
@Override
public void didStartSseServer(
@NonNull SseServer sseServer
) {
System.out.println("SSE server started.");
}
@Override
public void didFailToStartSseServer(
@NonNull SseServer sseServer,
@NonNull Throwable throwable
) {
System.err.println("SSE server failed to start.");
throwable.printStackTrace();
}
@Override
public void willStopSseServer(
@NonNull SseServer sseServer
) {
System.out.println("SSE server stopping.");
}
@Override
public void didStopSseServer(
@NonNull SseServer sseServer
) {
System.out.println("SSE server stopped.");
}
@Override
public void didFailToStopSseServer(
@NonNull SseServer sseServer,
@NonNull Throwable throwable
) {
System.err.println("SSE server failed to stop cleanly.");
throwable.printStackTrace();
}
}).build();
SokletConfig config = SokletConfig.withSseServer(
SseServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartSseServer(
@NonNull SseServer sseServer
) {
System.out.println("SSE server starting.");
}
@Override
public void didStartSseServer(
@NonNull SseServer sseServer
) {
System.out.println("SSE server started.");
}
@Override
public void didFailToStartSseServer(
@NonNull SseServer sseServer,
@NonNull Throwable throwable
) {
System.err.println("SSE server failed to start.");
throwable.printStackTrace();
}
@Override
public void willStopSseServer(
@NonNull SseServer sseServer
) {
System.out.println("SSE server stopping.");
}
@Override
public void didStopSseServer(
@NonNull SseServer sseServer
) {
System.out.println("SSE server stopped.");
}
@Override
public void didFailToStopSseServer(
@NonNull SseServer sseServer,
@NonNull Throwable throwable
) {
System.err.println("SSE server failed to stop cleanly.");
throwable.printStackTrace();
}
}).build();
References:
LifecycleObserver::willStartSseServerLifecycleObserver::didStartSseServerLifecycleObserver::didFailToStartSseServerLifecycleObserver::willStopSseServerLifecycleObserver::didStopSseServerLifecycleObserver::didFailToStopSseServer
SSE Connections
SokletConfig config = SokletConfig.withSseServer(
SseServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willEstablishSseConnection(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod
) {
System.out.printf("Establishing SSE connection for %s\n", request);
}
@Override
public void didEstablishSseConnection(
@NonNull SseConnection sseConnection
) {
System.out.printf("SSE connection established: %s\n", sseConnection);
}
@Override
public void didFailToEstablishSseConnection(
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull SseConnection.HandshakeFailureReason reason,
@Nullable Throwable throwable
) {
System.err.println("SSE connection establishment failed.");
if (throwable != null)
throwable.printStackTrace();
}
@Override
public void willTerminateSseConnection(
@NonNull SseConnection sseConnection,
@NonNull StreamTermination termination
) {
System.out.printf("SSE connection terminating (%s)\n", termination.getReason());
}
@Override
public void didTerminateSseConnection(
@NonNull SseConnection sseConnection,
@NonNull StreamTermination termination
) {
System.out.printf("SSE connection terminated after %dms (%s)\n",
termination.getDuration().toMillis(), termination.getReason());
}
}).build();
SokletConfig config = SokletConfig.withSseServer(
SseServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willEstablishSseConnection(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod
) {
System.out.printf("Establishing SSE connection for %s\n", request);
}
@Override
public void didEstablishSseConnection(
@NonNull SseConnection sseConnection
) {
System.out.printf("SSE connection established: %s\n", sseConnection);
}
@Override
public void didFailToEstablishSseConnection(
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull SseConnection.HandshakeFailureReason reason,
@Nullable Throwable throwable
) {
System.err.println("SSE connection establishment failed.");
if (throwable != null)
throwable.printStackTrace();
}
@Override
public void willTerminateSseConnection(
@NonNull SseConnection sseConnection,
@NonNull StreamTermination termination
) {
System.out.printf("SSE connection terminating (%s)\n", termination.getReason());
}
@Override
public void didTerminateSseConnection(
@NonNull SseConnection sseConnection,
@NonNull StreamTermination termination
) {
System.out.printf("SSE connection terminated after %dms (%s)\n",
termination.getDuration().toMillis(), termination.getReason());
}
}).build();
References:
LifecycleObserver::willEstablishSseConnectionLifecycleObserver::didEstablishSseConnectionLifecycleObserver::didFailToEstablishSseConnectionLifecycleObserver::willTerminateSseConnectionLifecycleObserver::didTerminateSseConnection
SSE Event and Comment Writes
SokletConfig config = SokletConfig.withSseServer(
SseServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willWriteSseEvent(
@NonNull SseConnection sseConnection,
@NonNull SseEvent sseEvent
) {
System.out.printf("Writing SSE event %s\n", sseEvent.getEvent().orElse("message"));
}
@Override
public void didWriteSseEvent(
@NonNull SseConnection sseConnection,
@NonNull SseEvent sseEvent,
@NonNull Duration writeDuration
) {
System.out.printf("SSE event write took %dms\n", writeDuration.toMillis());
}
@Override
public void didFailToWriteSseEvent(
@NonNull SseConnection sseConnection,
@NonNull SseEvent sseEvent,
@NonNull Duration writeDuration,
@NonNull Throwable throwable
) {
System.err.println("SSE event write failed.");
throwable.printStackTrace();
}
@Override
public void willWriteSseComment(
@NonNull SseConnection sseConnection,
@NonNull SseComment sseComment
) {
System.out.printf("Writing SSE comment (%s): %s\n",
sseComment.getCommentType(),
sseComment.getComment().orElse("<heartbeat>"));
}
@Override
public void didWriteSseComment(
@NonNull SseConnection sseConnection,
@NonNull SseComment sseComment,
@NonNull Duration writeDuration
) {
System.out.printf("SSE comment write took %dms\n", writeDuration.toMillis());
}
@Override
public void didFailToWriteSseComment(
@NonNull SseConnection sseConnection,
@NonNull SseComment sseComment,
@NonNull Duration writeDuration,
@NonNull Throwable throwable
) {
System.err.println("SSE comment write failed.");
throwable.printStackTrace();
}
}).build();
SokletConfig config = SokletConfig.withSseServer(
SseServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willWriteSseEvent(
@NonNull SseConnection sseConnection,
@NonNull SseEvent sseEvent
) {
System.out.printf("Writing SSE event %s\n", sseEvent.getEvent().orElse("message"));
}
@Override
public void didWriteSseEvent(
@NonNull SseConnection sseConnection,
@NonNull SseEvent sseEvent,
@NonNull Duration writeDuration
) {
System.out.printf("SSE event write took %dms\n", writeDuration.toMillis());
}
@Override
public void didFailToWriteSseEvent(
@NonNull SseConnection sseConnection,
@NonNull SseEvent sseEvent,
@NonNull Duration writeDuration,
@NonNull Throwable throwable
) {
System.err.println("SSE event write failed.");
throwable.printStackTrace();
}
@Override
public void willWriteSseComment(
@NonNull SseConnection sseConnection,
@NonNull SseComment sseComment
) {
System.out.printf("Writing SSE comment (%s): %s\n",
sseComment.getCommentType(),
sseComment.getComment().orElse("<heartbeat>"));
}
@Override
public void didWriteSseComment(
@NonNull SseConnection sseConnection,
@NonNull SseComment sseComment,
@NonNull Duration writeDuration
) {
System.out.printf("SSE comment write took %dms\n", writeDuration.toMillis());
}
@Override
public void didFailToWriteSseComment(
@NonNull SseConnection sseConnection,
@NonNull SseComment sseComment,
@NonNull Duration writeDuration,
@NonNull Throwable throwable
) {
System.err.println("SSE comment write failed.");
throwable.printStackTrace();
}
}).build();
References:
LifecycleObserver::willWriteSseEventLifecycleObserver::didWriteSseEventLifecycleObserver::didFailToWriteSseEventLifecycleObserver::willWriteSseCommentLifecycleObserver::didWriteSseCommentLifecycleObserver::didFailToWriteSseComment
Model Context Protocol
If your application configures an McpServer, additional lifecycle hooks are available for MCP server startup and shutdown, session creation and termination, JSON-RPC request handling, and session-bound GET stream activity.
The ordinary connection and request hooks described earlier still fire for MCP transport traffic. The methods below add MCP-specific context such as endpoint class, session ID, JSON-RPC method name, request outcome, and stream termination reason.
MCP Server Start/Stop
SokletConfig config = SokletConfig.withMcpServer(
McpServer.withPort(8082)
.handlerResolver(McpHandlerResolver.fromClasspathIntrospection())
.build()
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartMcpServer(@NonNull McpServer mcpServer) {
System.out.println("MCP server starting.");
}
@Override
public void didStartMcpServer(@NonNull McpServer mcpServer) {
System.out.println("MCP server started.");
}
@Override
public void didFailToStartMcpServer(
@NonNull McpServer mcpServer,
@NonNull Throwable throwable
) {
System.err.println("MCP server failed to start.");
throwable.printStackTrace();
}
@Override
public void willStopMcpServer(@NonNull McpServer mcpServer) {
System.out.println("MCP server stopping.");
}
@Override
public void didStopMcpServer(@NonNull McpServer mcpServer) {
System.out.println("MCP server stopped.");
}
@Override
public void didFailToStopMcpServer(
@NonNull McpServer mcpServer,
@NonNull Throwable throwable
) {
System.err.println("MCP server failed to stop cleanly.");
throwable.printStackTrace();
}
}).build();
SokletConfig config = SokletConfig.withMcpServer(
McpServer.withPort(8082)
.handlerResolver(McpHandlerResolver.fromClasspathIntrospection())
.build()
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartMcpServer(@NonNull McpServer mcpServer) {
System.out.println("MCP server starting.");
}
@Override
public void didStartMcpServer(@NonNull McpServer mcpServer) {
System.out.println("MCP server started.");
}
@Override
public void didFailToStartMcpServer(
@NonNull McpServer mcpServer,
@NonNull Throwable throwable
) {
System.err.println("MCP server failed to start.");
throwable.printStackTrace();
}
@Override
public void willStopMcpServer(@NonNull McpServer mcpServer) {
System.out.println("MCP server stopping.");
}
@Override
public void didStopMcpServer(@NonNull McpServer mcpServer) {
System.out.println("MCP server stopped.");
}
@Override
public void didFailToStopMcpServer(
@NonNull McpServer mcpServer,
@NonNull Throwable throwable
) {
System.err.println("MCP server failed to stop cleanly.");
throwable.printStackTrace();
}
}).build();
References:
LifecycleObserver::willStartMcpServerLifecycleObserver::didStartMcpServerLifecycleObserver::didFailToStartMcpServerLifecycleObserver::willStopMcpServerLifecycleObserver::didStopMcpServerLifecycleObserver::didFailToStopMcpServer
MCP Sessions and JSON-RPC Requests
These hooks are useful when you want to observe MCP at the protocol level rather than the raw HTTP transport level. For example, didStartMcpRequestHandling and didFinishMcpRequestHandling expose the JSON-RPC method name (initialize, tools/call, and so on), the resolved endpoint class, the session ID if one exists, the final McpRequestOutcome, and any returned McpJsonRpcError.
SokletConfig config = SokletConfig.withMcpServer(
McpServer.withPort(8082)
.handlerResolver(McpHandlerResolver.fromClasspathIntrospection())
.build()
).lifecycleObserver(new LifecycleObserver() {
@Override
public void didCreateMcpSession(
@NonNull Request request,
@NonNull Class<? extends McpEndpoint> endpointClass,
@NonNull String sessionId
) {
System.out.printf("Created MCP session %s for %s%n",
sessionId, endpointClass.getSimpleName());
}
@Override
public void didStartMcpRequestHandling(
@NonNull Request request,
@NonNull Class<? extends McpEndpoint> endpointClass,
@Nullable String sessionId,
@NonNull String jsonRpcMethod,
@Nullable McpJsonRpcRequestId jsonRpcRequestId
) {
System.out.printf("Starting MCP %s on %s (session=%s, requestId=%s)%n",
jsonRpcMethod, endpointClass.getSimpleName(), sessionId, jsonRpcRequestId);
}
@Override
public void didFinishMcpRequestHandling(
@NonNull Request request,
@NonNull Class<? extends McpEndpoint> endpointClass,
@Nullable String sessionId,
@NonNull String jsonRpcMethod,
@Nullable McpJsonRpcRequestId jsonRpcRequestId,
@NonNull McpRequestOutcome requestOutcome,
@Nullable McpJsonRpcError jsonRpcError,
@NonNull Duration duration,
@NonNull List<@NonNull Throwable> throwables
) {
System.out.printf("Finished MCP %s with %s in %dms%n",
jsonRpcMethod, requestOutcome, duration.toMillis());
if (jsonRpcError != null)
System.out.printf("JSON-RPC error: %s%n", jsonRpcError);
}
@Override
public void didTerminateMcpSession(
@NonNull Class<? extends McpEndpoint> endpointClass,
@NonNull String sessionId,
@NonNull Duration sessionDuration,
@NonNull McpSessionTerminationReason terminationReason,
@Nullable Throwable throwable
) {
System.out.printf("Terminated MCP session %s after %dms (%s)%n",
sessionId, sessionDuration.toMillis(), terminationReason);
if (throwable != null)
throwable.printStackTrace();
}
}).build();
SokletConfig config = SokletConfig.withMcpServer(
McpServer.withPort(8082)
.handlerResolver(McpHandlerResolver.fromClasspathIntrospection())
.build()
).lifecycleObserver(new LifecycleObserver() {
@Override
public void didCreateMcpSession(
@NonNull Request request,
@NonNull Class<? extends McpEndpoint> endpointClass,
@NonNull String sessionId
) {
System.out.printf("Created MCP session %s for %s%n",
sessionId, endpointClass.getSimpleName());
}
@Override
public void didStartMcpRequestHandling(
@NonNull Request request,
@NonNull Class<? extends McpEndpoint> endpointClass,
@Nullable String sessionId,
@NonNull String jsonRpcMethod,
@Nullable McpJsonRpcRequestId jsonRpcRequestId
) {
System.out.printf("Starting MCP %s on %s (session=%s, requestId=%s)%n",
jsonRpcMethod, endpointClass.getSimpleName(), sessionId, jsonRpcRequestId);
}
@Override
public void didFinishMcpRequestHandling(
@NonNull Request request,
@NonNull Class<? extends McpEndpoint> endpointClass,
@Nullable String sessionId,
@NonNull String jsonRpcMethod,
@Nullable McpJsonRpcRequestId jsonRpcRequestId,
@NonNull McpRequestOutcome requestOutcome,
@Nullable McpJsonRpcError jsonRpcError,
@NonNull Duration duration,
@NonNull List<@NonNull Throwable> throwables
) {
System.out.printf("Finished MCP %s with %s in %dms%n",
jsonRpcMethod, requestOutcome, duration.toMillis());
if (jsonRpcError != null)
System.out.printf("JSON-RPC error: %s%n", jsonRpcError);
}
@Override
public void didTerminateMcpSession(
@NonNull Class<? extends McpEndpoint> endpointClass,
@NonNull String sessionId,
@NonNull Duration sessionDuration,
@NonNull McpSessionTerminationReason terminationReason,
@Nullable Throwable throwable
) {
System.out.printf("Terminated MCP session %s after %dms (%s)%n",
sessionId, sessionDuration.toMillis(), terminationReason);
if (throwable != null)
throwable.printStackTrace();
}
}).build();
References:
LifecycleObserver::didCreateMcpSessionLifecycleObserver::didStartMcpRequestHandlingLifecycleObserver::didFinishMcpRequestHandlingLifecycleObserver::didTerminateMcpSessionMcpRequestOutcomeMcpJsonRpcErrorMcpSessionTerminationReason
MCP Streams
MCP GET requests establish session-bound SSE streams for server-originated messages. Soklet exposes lifecycle hooks for when those streams are established and when they terminate. Unlike the dedicated SSE APIs above, MCP currently does not expose separate per-message write callbacks here.
SokletConfig config = SokletConfig.withMcpServer(
McpServer.withPort(8082)
.handlerResolver(McpHandlerResolver.fromClasspathIntrospection())
.build()
).lifecycleObserver(new LifecycleObserver() {
@Override
public void didEstablishMcpSseStream(@NonNull McpSseStream stream) {
System.out.printf("Established MCP stream for session %s on %s%n",
stream.getSessionId(), stream.getEndpointClass().getSimpleName());
}
@Override
public void willTerminateMcpSseStream(
@NonNull McpSseStream stream,
@NonNull StreamTermination termination
) {
System.out.printf("MCP stream for session %s terminating (%s)%n",
stream.getSessionId(), termination.getReason());
}
@Override
public void didTerminateMcpSseStream(
@NonNull McpSseStream stream,
@NonNull StreamTermination termination
) {
System.out.printf("MCP stream for session %s terminated after %dms (%s)%n",
stream.getSessionId(), termination.getDuration().toMillis(), termination.getReason());
termination.getCause().ifPresent(Throwable::printStackTrace);
}
}).build();
SokletConfig config = SokletConfig.withMcpServer(
McpServer.withPort(8082)
.handlerResolver(McpHandlerResolver.fromClasspathIntrospection())
.build()
).lifecycleObserver(new LifecycleObserver() {
@Override
public void didEstablishMcpSseStream(@NonNull McpSseStream stream) {
System.out.printf("Established MCP stream for session %s on %s%n",
stream.getSessionId(), stream.getEndpointClass().getSimpleName());
}
@Override
public void willTerminateMcpSseStream(
@NonNull McpSseStream stream,
@NonNull StreamTermination termination
) {
System.out.printf("MCP stream for session %s terminating (%s)%n",
stream.getSessionId(), termination.getReason());
}
@Override
public void didTerminateMcpSseStream(
@NonNull McpSseStream stream,
@NonNull StreamTermination termination
) {
System.out.printf("MCP stream for session %s terminated after %dms (%s)%n",
stream.getSessionId(), termination.getDuration().toMillis(), termination.getReason());
termination.getCause().ifPresent(Throwable::printStackTrace);
}
}).build();
References:
LifecycleObserver::didEstablishMcpSseStreamLifecycleObserver::willTerminateMcpSseStreamLifecycleObserver::didTerminateMcpSseStreamStreamTerminationReason
Event Logging
Soklet provides insight into unexpected errors that occur during internal processing that are not otherwise surfaced via LogEvent objects provided to LifecycleObserver::didReceiveLogEvent.
For example, you might want to specially monitor scenarios in which your ResponseMarshaler::forThrowable failed (that is, you attempted to write an error response for an exception that bubbled out, but that attempt threw an exception, forcing Soklet to write its own failsafe response) - you could do this by observing events with type LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED. Low-level HTTP, SSE, and MCP transport failures such as response write-idle timeouts, write timeouts, accept-loop failures, event-loop task failures, and selection-key failures are emitted as LogEventType.SERVER_TRANSPORT_FAILURE.
Your LifecycleObserver can listen for LogEvents like this:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
// This example uses SLF4J. See https://www.slf4j.org
private final Logger logger =
LoggerFactory.getLogger("com.soklet.example.LifecycleObserver");
@Override
public void didReceiveLogEvent(@NonNull LogEvent logEvent) {
// These properties are available in LogEvent
LogEventType logEventType = logEvent.getLogEventType();
String message = logEvent.getMessage();
Optional<Throwable> throwable = logEvent.getThrowable();
Optional<Request> request = logEvent.getRequest();
Optional<ResourceMethod> resourceMethod = logEvent.getResourceMethod();
Optional<MarshaledResponse> marshaledResponse = logEvent.getMarshaledResponse();
// Log the message however you like
logger.warn(message, throwable.orElse(null));
}
}).build();
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
// This example uses SLF4J. See https://www.slf4j.org
private final Logger logger =
LoggerFactory.getLogger("com.soklet.example.LifecycleObserver");
@Override
public void didReceiveLogEvent(@NonNull LogEvent logEvent) {
// These properties are available in LogEvent
LogEventType logEventType = logEvent.getLogEventType();
String message = logEvent.getMessage();
Optional<Throwable> throwable = logEvent.getThrowable();
Optional<Request> request = logEvent.getRequest();
Optional<ResourceMethod> resourceMethod = logEvent.getResourceMethod();
Optional<MarshaledResponse> marshaledResponse = logEvent.getMarshaledResponse();
// Log the message however you like
logger.warn(message, throwable.orElse(null));
}
}).build();

