Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/197-inbound-web-observability-probes.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

162 lines
12 KiB
Plaintext

Sub-scope 08 (inbound-web) observability + proxy + filter + mvc/webflux remainder
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-30T07:37:35+00:00
=== OWNED FILES ===
ad29fda6cf666e83673267bf4a5e9f8e1398f9c1 main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcDisconnectDetector.java 86
479a4cb2cd2cd52a9c4209a55ced6c892f2f6b50 main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcStreamWriter.java 117
5e11dd088b32542688596629d9ba2bda45d266f5 main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcStreamingExecutorConfiguration.java 86
9bec0eae7bdf9aed96fdbe07eb34b0c60fb4209a main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/VirtualThreadMvcConfiguration.java 77
e53c1995fe4995c04af32faafd868735c2f9f8f2 main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/VirtualThreadSettings.java 103
ee140492d372bea76d992f1058c6b244f6da6346 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/ControlledBlockingBridge.java 70
857177965a147a7debc03b0e60fb4828b6520631 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/SlowConsumerClosedException.java 32
31fa7e1b220c27fcff7709efc086a83dbe304e91 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxDisconnectDetector.java 72
64cb4db2c9e741b3f780a63b0e3e9e9d25466398 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxSseAdapter.java 117
77d744ec0b05c69a27f3f30e6fab7e6d428d045e main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxSseHeartbeat.java 40
be63236a6dfaaee182f394c2edadce3a03427b34 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxStreamAdmission.java 124
f88258c1b19569a1765ed563e0866e062772b0e5 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxStreamWriter.java 70
841c398a94920699fde61451184f61c243a4a7e1 main/java/dev/caskeleton/adapter/inbound/web/filter/CacheControlFilter.java 35
0032f474ad554cd7276b3da819ad4d091d9c8d11 main/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilter.java 147
4a2ddb15de9ee9d6d7ecb6c9d3d5386fc36686f0 main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextArgumentResolver.java 42
2a32453e80f6539a5427be10dbcf6634df561586 main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextHolder.java 66
250ebee6de1c046ea069d6b19e8927d77506fe49 main/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcEvidenceFilter.java 114
13aff34a24f8c0224de66bb6bbe528411bfa495d main/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcRequestIdFilter.java 146
dd75b418a205c76415f14ee546493e6b077739de main/java/dev/caskeleton/adapter/inbound/web/observability/HeaderSanitizer.java 25
ef9f1d3afe0b5e3f9cf29492c01ea0fdfbbb1695 main/java/dev/caskeleton/adapter/inbound/web/observability/MdcCorrelationIdPortAdapter.java 16
5e4cfcea7b360e37b132f51c9ddbb532511d753b main/java/dev/caskeleton/adapter/inbound/web/observability/MdcKeys.java 13
a790a5ee999056de8a0238ada60a319a61b3b45b main/java/dev/caskeleton/adapter/inbound/web/observability/ResponseMetaFactory.java 18
e18414ead91ed2945a8d32cd221b09754b2942a0 main/java/dev/caskeleton/adapter/inbound/web/observability/RetryAfterAdvisor.java 43
7a8ab4e5f306fe176e4fe503bc9ae25e59b97b5e main/java/dev/caskeleton/adapter/inbound/web/observability/WebAccessLogEvent.java 65
fd84d9ed27536784f87a92a247b7845fa524b4f0 main/java/dev/caskeleton/adapter/inbound/web/observability/WebAccessLogger.java 19
9a3056466bd7efac7ebf88e459a51c4bddbdc050 main/java/dev/caskeleton/adapter/inbound/web/observability/WebAuditAction.java 41
7118a82494a8b306838c92da8a181482a512cb7c main/java/dev/caskeleton/adapter/inbound/web/observability/WebAuditEvent.java 55
43cca5eb139a9ce6ccd417b9dbbab1b624f4b57b main/java/dev/caskeleton/adapter/inbound/web/observability/WebAuditPublisher.java 17
3c8f55205e241c8b9f6a95e76f9046dd78612080 main/java/dev/caskeleton/adapter/inbound/web/observability/WebMetricCardinalityPolicy.java 112
de2b95f45330d7c180edd647222741f8cc5103c5 main/java/dev/caskeleton/adapter/inbound/web/observability/WebObservationConvention.java 72
b9f52ddfb6d07054753d320b4bc5888a7011390a main/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizer.java 72
086d1b881e91c24e5e043fd0a7a7d7e263bb4f35 main/java/dev/caskeleton/adapter/inbound/web/proxy/NormalizedForwardedHeaders.java 158
f39d3d998e2851c95a2a1cd0bcda239ee751e4f1 main/java/dev/caskeleton/adapter/inbound/web/proxy/TrustedProxyPolicy.java 161
e1b3b88b46aecb7ccb502b9202623bbde9507549 main/java/dev/caskeleton/adapter/inbound/web/proxy/UntrustedForwardedHeaderException.java 30
d2ffecb95fce15ef375f368f706a8ff834e6368b main/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextAccessor.java 73
88a2668dc8cd7323a80ef1cccedc6467c0df56c5 main/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextFilter.java 147
1afebbb62bb9d3a468fe7dbc325873d889b5eeda main/java/dev/caskeleton/adapter/inbound/web/webflux/guard/BlockingCallDetectedException.java 26
b467ec7a0458c29b875e6eee1559b835e80223b5 main/java/dev/caskeleton/adapter/inbound/web/webflux/guard/BlockingDependencyGuard.java 79
cba639f6f927be1139777ac2295ed5228fc95f58 test/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcAdvancedConfigurationTest.java 145
a3bd77e8e99c29eb47ee968fa02f4d71ae373148 test/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcStreamingWiringTest.java 175
f200ed3eb0fe0db3b5c3fdc7f905f826d8e51b61 test/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxStreamingTest.java 309
c9a604d350f8de1b7884aa2558bc0f02cbb4bfc3 test/java/dev/caskeleton/adapter/inbound/web/filter/CacheControlFilterTest.java 52
dd6e28c879899589a97a0fe2227935b6a25c4615 test/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilterTest.java 296
0be7101fe31a3fba785072a2a84aa39bdd5bfb8a test/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcEvidenceFilterTest.java 78
4810257df962a1b5d914e13cbb51a1e774f4003e test/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcRequestIdFilterTest.java 71
793dbbbb7030d97017fa2ceb879b94b4622cda5e test/java/dev/caskeleton/adapter/inbound/web/observability/HeaderSanitizerTest.java 39
bdfb21f0cc2ff54ef55b1616067c5ce72131e1fc test/java/dev/caskeleton/adapter/inbound/web/observability/MdcCorrelationIdPortAdapterTest.java 36
4e01d57fc524f6ce9e529e3c67a24ab303c33185 test/java/dev/caskeleton/adapter/inbound/web/observability/ResponseMetaFactoryTest.java 34
9d8bfb0ccfdd2510d49e6b044b9bc690fdd6029e test/java/dev/caskeleton/adapter/inbound/web/observability/RetryAfterAdvisorTest.java 85
b6d0c36a1538ddd23daa94b47eb6af86a4acdbb5 test/java/dev/caskeleton/adapter/inbound/web/observability/WebObservabilityPolicyTest.java 188
bc4a4a26a90697c783240dcf4f70d2cd3a3fa4bc test/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizerTest.java 129
a9023edbbe9bae94c9539ec423a354af82467382 test/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextAccessorTest.java 93
6dd0ac4a3cddcf083a07d1132211e209933d169d test/java/dev/caskeleton/adapter/inbound/web/webflux/guard/BlockingDependencyGuardTest.java 92
file_count=53
=== 8.2 X-Request-Id 를 쓰는 두 배선 필터 ===
$ grep -n 'trustInboundRequestId\|REQUEST_ID_HEADER\|setHeader' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcRequestIdFilter.java
42: public static final String REQUEST_ID_HEADER = "X-Request-Id";
54: private final boolean trustInboundRequestId;
65: * @param trustInboundRequestId whether a client may choose the request id, which only a trusted
69: public WebMvcRequestIdFilter(boolean trustInboundRequestId, int order) {
70: this.trustInboundRequestId = trustInboundRequestId;
85: response.setHeader(REQUEST_ID_HEADER, requestId.value());
106: if (!trustInboundRequestId) {
109: return sanitized(request.getHeader(REQUEST_ID_HEADER))
exit=0
$ grep -n 'HEADER_REQUEST_ID\|resolveOrGenerate\|setHeader\|MDC.put' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilter.java | head -12
40: private static final String HEADER_REQUEST_ID = "X-Request-Id";
56: String requestId = resolveOrGenerate(req.getHeader(HEADER_REQUEST_ID));
57: String correlationId = resolveOrGenerate(req.getHeader(HEADER_CORRELATION_ID));
58: res.setHeader(HEADER_REQUEST_ID, requestId);
59: res.setHeader(HEADER_CORRELATION_ID, correlationId);
61: MDC.put(MdcKeys.REQUEST_ID, requestId);
62: MDC.put(MdcKeys.CORRELATION_ID, correlationId);
65: TraceParent traceParent = resolveOrGenerateTraceParent(req.getHeader(HEADER_TRACEPARENT));
66: MDC.put(MdcKeys.TRACE_ID, traceParent.traceId());
67: MDC.put(MdcKeys.SPAN_ID, traceParent.spanId());
68: res.setHeader(HEADER_TRACEPARENT, traceParent.toHeader());
91: private static String resolveOrGenerate(String inbound) {
exit=0
$ grep -n 'class RequestLoggingFilter' -B 3 adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilter.java
33- * trace_id} when a real tracer is later wired in.
34- */
35-@Component
36:public class RequestLoggingFilter extends OncePerRequestFilter {
exit=0
$ cat adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/observability/HeaderSanitizer.java | grep -vE '^import|^package'
/**
* Neutralizes inbound header values before they reach MDC / logs by stripping every {@code \r},
* {@code \n} and ASCII control char ({@code < 0x20}) and length-capping the result. See README for
* the design rationale.
*/
public final class HeaderSanitizer {
private HeaderSanitizer() {}
public static String sanitize(String raw, int maxLength) {
if (raw == null) {
return null;
}
StringBuilder sb = new StringBuilder(Math.min(raw.length(), maxLength));
for (int i = 0; i < raw.length() && sb.length() < maxLength; i++) {
char c = raw.charAt(i);
if (c >= 0x20) {
sb.append(c);
}
}
return sb.toString();
}
}
exit=0
=== 8.1 proxy 패키지 도달성 ===
TrustedProxyPolicy 1 main 1 test 1 testkit
NormalizedForwardedHeaders 2 main 2 test 1 testkit
ForwardedHeaderSanitizer 1 test 1 testkit
WebAuditPublisher
WebAccessLogger 1 main
$ grep -rn 'forward-headers-strategy' app-bootstrap/src/main/resources/application.yml
321: forward-headers-strategy: ${APP_SERVER_FORWARD_HEADERS_STRATEGY:framework}
exit=0
$ grep -n 'void ' adapter/inbound/web/src/nginxProxyTest/java/dev/caskeleton/adapter/inbound/web/testkit/proxy/NginxProxyContractIT.java | head -12
37: static void requireDocker() {
47: static void stopProxy() {
63: void attackerCannotOverrideForwardedHost() {
79: void attackerCannotDowngradeTheForwardedScheme() {
93: void attackerCannotForgeTheClientAddress() {
107: void standardForwardedHeaderIsCleared() {
123: void prefixIsAppliedExactlyOnce() {
134: void publishedUriCarriesTheIncomingPrefix() {
143: void clientCannotInjectAPrefix() {
157: void bodyWithinTheProxyLimitArrives() {
167: void oversizedBodyIsRefusedAtTheProxy() {
180: void unknownPrefixIsNotProxied() {
exit=0
$ grep -rn 'TrustedProxyPolicy.of' adapter/inbound/web/src/testkit
adapter/inbound/web/src/testkit/java/dev/caskeleton/adapter/inbound/web/testkit/proxy/ProxyFixtureController.java:53: TrustedProxyPolicy.of("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"));
exit=0
=== 8.4 ExternalRequestContext 생성 ===
$ sed -n '140,146p' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextFilter.java
private ExternalRequestContext externalRequest(ServerHttpRequest request) {
var uri = request.getURI();
String scheme = uri.getScheme() == null ? "http" : uri.getScheme();
int port = uri.getPort() > 0 ? uri.getPort() : ("https".equalsIgnoreCase(scheme) ? 443 : 80);
String host = uri.getHost() == null ? "localhost" : uri.getHost();
return new ExternalRequestContext(scheme, host, port, "");
}
exit=0