# grpc-advanced-diagnostics 완전 해부 > 상태: COMPLETE > 재오픈 게이트: cycle 2 — `src/main` production 4파일 277줄, test 1파일 229줄과 픽스처 1개 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음. > 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916` > 분석 범위: `src/grpc-advanced/grpc-advanced-diagnostics` > SSOT owner: `grpc-advanced-diagnostics` > integration/family document: `analysis/20-grpc-platform.md` (secondary, INTEGRATION_ONLY) --- ## 0. SSOT identity / 커버리지 - `allowed_dependencies`: `["grpc-core-api", "grpc-client", "grpc-advanced-bootstrap"]` - `runtime_memberships`: **`[]`** — build-only (Advanced 가족 전체가 그렇다) | 파일 | LOC | |---|---:| | `GrpcAdvancedInfrastructureTestkit` | 87 | | `GrpcDiagnosticsRedactor` | 72 | | `GrpcChannelDiagnosticsSnapshot` | 63 | | `GrpcChannelDiagnosticsPolicy` | 55 | | **main 합계** | **277** | | `GrpcChannelDiagnosticsPolicyTest` | 229 | | `xds/control-plane-snapshot.json` | 24 | ### Coverage ledger | scope | count | disposition | reason | |---|---:|---|---| | `main/java/**` | 4 | `FULL_READ` | 전 본문 축자 확인 | | `test/java/**` | 1 | `FULL_READ` | 229줄 · 테스트 11개 | | `test/resources/xds/*.json` | 1 | `FULL_READ` | 24줄 픽스처 | | `build.gradle` | 1 | `FULL_READ` | 10줄 | | `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 | `UNCLASSIFIED` 0. --- ## 1. 모듈의 정체 진단 표면(Channelz·CSDS)과 그것을 게시 가능하게 만드는 편집기, 그리고 고급 능력이 무엇을 상대로 검증되어야 하는지를 이름 짓는 테스트킷 계약을 담는다. 편집기 javadoc 이 왜 이것이 필요한지 적는다. > "Channelz is unusually dangerous to expose because it is genuinely useful: it holds every socket's > local and remote address, the security details of each connection, and per-call state… once the > endpoint exists the whole of it is one authorization mistake away from being readable." ## 2. 두 겹의 게이트 `GrpcChannelDiagnosticsPolicy` 는 네트워크와 역할 두 게이트를 모두 요구하고, 하나라도 비면 생성자가 거부한다. > "diagnostics need both a network and a role gate; Channelz holds every socket's peer and security > detail, so either gate alone is the whole surface" 그리고 등록 판정이 능력 깃발에 걸려 있다. CSDS 는 Channelz 가 켜져 있고 xDS 도 켜져 있을 때만 등록된다. > "A CSDS service on a deployment that does not use xDS answers every query with nothing, which is > harmless, and advertises a control-plane surface that does not exist, which is not." ## 3. 스냅숏이 스스로를 검사한다 `GrpcChannelDiagnosticsSnapshot` 정규 생성자가 두 가지를 거부한다. ```java maskedSocketAddresses.stream() .filter(address -> !address.equals(GrpcDiagnosticsRedactor.maskAddress(address))) … // 마스킹되지 않은 주소 xdsResourceVersions.keySet().stream() .filter(GrpcDiagnosticsRedactor::forbiddenField) … // 금지된 필드 이름 ``` 즉 편집을 거치지 않은 값으로는 스냅숏을 만들 수 없다. §17.1 이 그 검사의 범위를 다룬다. ## 4. 마스킹의 형태 주소는 버리지 않고 가린다. > "An operator has to be able to tell two subchannels apart, and a stable mask does that without > publishing where they point… The last two octets go; the first two stay, because 'which subnet' > is a real diagnostic question and 'which host' is not one the diagnostics endpoint should answer." ## 5. 인프라 없는 증거를 거부하는 계약 `GrpcAdvancedInfrastructureTestkit` 이 능력별로 필요한 실제 인프라를 이름 짓는다. | 능력 | 필요 인프라 | |---|---| | `GRPC_WEB` | gRPC-Web 프록시 | | `SERVLET_COMPAT` | 서블릿 컨테이너 | | `XDS` | 멈출 수 있는 xDS 통제 평면 | | `KOTLIN` | 코틀린 툴체인 | | 나머지 11종 | 없음 | 근거가 javadoc 에 있다. > "gRPC-Web without a proxy tests a code path no browser will take; a Servlet profile without a > container tests the profile object; xDS without a control plane cannot exercise the case that > matters, which is the control plane going away. In all three, a suite that runs without the > infrastructure passes and establishes nothing, which is worse than not having one." ## 10. 테스트 레인 11개 테스트 229줄. 두 게이트, CSDS 조건부 등록, 금지 필드 제거, 마스킹, 스냅숏 거부와 수용, 커밋된 xDS 픽스처의 편집, 능력별 인프라 목록을 확인한다. ## 12. negative-space probes **12.1 도달성.** Advanced 가족이므로 배선 경로가 없다. 리프 밖 참조도 없고, 이 리프를 의존 선언한 모듈도 없다. ``` $ grep -rn "advanced.diagnostics" --include=*.java src/ | grep -v grpc-advanced-diagnostics/ grpc-core-api/…/GrpcStableModuleCatalog.java:42: "grpc-advanced-diagnostics"); ← 목록 안의 문자열 $ grep -rn "grpc-advanced-diagnostics" --include=*.gradle src/ (매치 없음) ``` 방향을 뒤집으면 이 리프는 `grpc-advanced-bootstrap` 의 실제 소비자 둘 중 하나다 — `GrpcChannelDiagnosticsPolicy` 가 `GrpcAdvancedModuleGuard.available` 을 두 번 부른다(그쪽 §12.1). 이 가족에서 리프끼리 실제로 코드가 닿는 몇 안 되는 자리다. **12.2 선언된 의존 셋 중 둘이 쓰이지 않는다.** ```groovy api project(':grpc:grpc-core-api') // import 0 api project(':grpc:grpc-client') // import 0 api project(':grpc-advanced:grpc-advanced-bootstrap') // import 4줄 ``` 리프의 자바 4파일이 갖는 `dev.caskeleton` import 는 넷뿐이고 전부 bootstrap 것이다. ``` GrpcAdvancedInfrastructureTestkit.java:3 GrpcAdvancedCapability GrpcChannelDiagnosticsPolicy.java:3,4,5 GrpcAdvancedCapability · GrpcAdvancedFeatureFlags · GrpcAdvancedModuleGuard ``` `grpc-client` 는 특히 눈에 띈다 — Channelz 진단이 채널을 다루는 주제이므로 의존 선언은 자연스럽게 읽히는데, 이 리프의 스냅숏은 채널 타입을 쓰지 않고 `String channelProfile` 과 `String connectivityState` 로 받는다. 진단 값 객체가 채널 타입에서 독립적인 것 자체는 설계로 읽히고, 그렇다면 남은 것은 쓰이지 않는 의존 선언이다. 같은 형태를 세 리프에서 기록했다 — `grpc-advanced-edition` §12.2(셋 다 미사용), `grpc-spring-boot-starter` §12.3(셋 미사용), `grpc-observability` §12.1(두 모듈이 이 리프를 `api` 로 노출하면서 쓰지 않음). **12.2 대조군.** 이 저장소의 다른 편집기와 비교하면 방향이 같다 — `grpc-observability` 의 태그 정책은 허용 목록으로, 이쪽은 금지 패턴 + 마스킹으로 같은 문제(내용이 관측 표면으로 새는 것)를 푼다. **12.3 대조군 — 같은 두 리터럴이 두 리프에 있다.** ```java // grpc-advanced-diagnostics: GrpcChannelDiagnosticsPolicy.standard() new GrpcChannelDiagnosticsPolicy(Set.of("admin"), Set.of("ROLE_PLATFORM_ADMIN")); // grpc-spring-boot-starter: GrpcPlatformAutoConfiguration.grpcReflectionPolicy(...) new GrpcReflectionPolicy(properties.getReflectionMode(), java.util.Set.of("admin"), java.util.Set.of("ROLE_PLATFORM_ADMIN")); ``` 관리 네트워크 이름과 관리 역할 이름이 같은 값으로 두 곳에 손으로 적혀 있고, 둘을 묶는 상수가 없다. 하나를 바꾸면 다른 하나가 남는다. 스타터 쪽은 그 리터럴이 설정 표면에 노출되지 않는다는 별도 문제도 있다(그쪽 §17.4). **12.4 드리프트.** build.gradle 이 서술한 세 요소(Channelz/CSDS 진단, 편집기, 인프라 테스트킷 계약)가 전부 존재한다. 드리프트 없음. ## 16. 확인하지 못한 것 - 실제 Channelz 서비스를 띄워 스냅숏을 만들지 않았다. 배선 경로가 없다. - IPv6 주소로 스냅숏을 만들어 §17.1 을 실행으로 재현하지 않았다. 정규식과 생성자 검사로 판정했다. - 테스트를 실행하지 않았다. 11개 전부 본문으로만 확인했다. - 두 의존이 쓰이지 않는다는 것(§12.2)은 `^import dev.caskeleton` grep 으로 판정했다. ## 17. 손볼 것 ### 17.1 P2 — 마스킹이 IPv4 만 알고, 그 결과 "마스킹되지 않은 주소" 검사가 나머지 형태를 전부 통과시킨다 ```java private static final Pattern IPV4_WITH_PORT = Pattern.compile("\\b(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})(:\\d{1,5})?\\b"); public static String maskAddress(String address) { if (address == null || address.isBlank()) { return "unknown"; } return IPV4_WITH_PORT.matcher(address) .replaceAll(m -> m.group(1) + "." + m.group(2) + ".x.x"); } ``` IPv4 가 아닌 주소는 패턴에 맞지 않아 **입력 그대로 반환된다.** 그리고 스냅숏 생성자의 검사는 이렇게 되어 있다. ```java .filter(address -> !address.equals(GrpcDiagnosticsRedactor.maskAddress(address))) ``` 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. 그러므로 IPv4 가 아닌 주소는 전부 이 검사를 통과한다. | 입력 | `maskAddress` 결과 | 생성자 판정 | |---|---|---| | `10.4.13.201:9090` | `10.4.x.x` | 거부(마스킹 필요) | | `10.4.x.x` | `10.4.x.x` | 수용 | | `[2001:db8::4:13:201]:9090` | 입력 그대로 | **수용** | | `pod-3.svc.cluster.local:8080` | 입력 그대로 | **수용** | | `unix:/var/run/grpc.sock` | 입력 그대로 | **수용** | 세 번째와 네 번째가 문제다. 이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고(`grpc-discovery` 전체가 그 주제다), 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 클러스터에서는 IPv6 주소다. 편집기가 막으려 한 것이 정확히 그것이다 — "a diagnostics endpoint that publishes peer addresses publishes every tenant's connection." `unix` 소켓 경로도 통과한다. 그것은 호스트 파일 시스템 경로다. **테스트가 이것을 볼 수 없다.** 테스트의 주소 리터럴이 전부 IPv4 다 — `10.4.13.201:9090` · `10.9.13.201` · `10.4.x.x` · `10.5.x.x`. IPv6 도 호스트 이름도 없다. **수정.** 마스킹을 형태별로 나눈다. IPv6 는 앞 두 그룹만 남기고 나머지를 `:x:x` 로, 호스트 이름은 최상위 라벨 몇 개만 남기고, 그 밖의 형태는 `unknown` 으로 접는다. 그리고 검사를 "결과가 입력과 같으면 통과" 가 아니라 "알려진 마스킹 형태와 일치해야 통과" 로 뒤집는다. 지금 형태는 마스킹이 모르는 입력을 전부 안전하다고 판정한다. ### 17.2 P3 — 금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다 ```java xdsResourceVersions.keySet().stream().filter(GrpcDiagnosticsRedactor::forbiddenField)… ``` `redact(...)` 도 같다 — 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. 값 자체가 자격증명 형태인지는 보지 않는다. `grpc-observability` 의 태그 정책은 값도 본다(UUID·`sha256:`·`bearer ` 패턴). 같은 저장소의 두 관측 편집기가 값 검사에서 갈린다. xDS 자원 버전은 보통 짧은 숫자나 해시라 도달성이 낮다. 기록하는 이유는 두 편집기의 규율이 다르다는 점이다. ### 17.3 P3 — "실환경 증거" 가 두 리프에 반씩 있고 서로 만나지 않는다 이 리프가 능력별로 무엇이 실환경인지 정의한다. ```java public static Set requiredFor(GrpcAdvancedCapability capability) { … } public static List missingInfrastructure(GrpcAdvancedCapability capability, Set available) { … } ``` 그리고 `grpc-advanced-bootstrap` 이 승격 증거로 그것을 요구한다. ```java public record GrpcAdvancedPromotionEvidence( GrpcAdvancedCapability capability, …, boolean realEnvironmentTest) { … } // ^^^^^^^^^^^^^^^^^^^^^^^^^^ 불리언 하나 ``` `GrpcAdvancedPromotionGate.evaluate` 는 그 불리언이 거짓이면 "xds has no real environment test" 를 차단 사유로 낸다. 그 불리언을 무엇으로 채워야 하는지는 그쪽에서 답하지 않고, 답하는 코드가 이 리프에 있는데 두 쪽이 서로를 부르지 않는다. 결과: `GrpcAdvancedPromotionEvidence.complete(XDS, 7일)` 은 `realEnvironmentTest = true` 를 그냥 넣는다. xDS 통제 평면이 실제로 있었는지와 무관하다. 이 리프의 javadoc 이 경계한 상태 — "a suite that runs without the infrastructure passes and establishes nothing" — 를 승격 게이트가 그대로 통과시킬 수 있다. **왜 P3 인가.** 두 리프 모두 배선되지 않았고 승격은 사람이 수행한다. 다만 이 두 조각이 존재하는 이유가 "그 판단을 코드로 적어 두는 것" 이므로, 판단의 절반이 다른 절반을 부르지 않는 것은 그 목적에 어긋난다. `grpc-advanced-edition` §17.2 가 같은 가족에서 같은 모양을 기록했다 — 두 승격 게이트가 서로를 부르지 않는다. **수정.** `GrpcAdvancedPromotionEvidence.realEnvironmentTest` 를 불리언 대신 `Set availableInfrastructure` 로 바꾸고, 게이트가 `missingInfrastructure(capability, available)` 를 불러 그 결과를 차단 사유에 합친다. 그러면 "실환경 테스트를 했다" 가 선언이 아니라 능력별 목록에 대한 대조가 된다. 의존 방향도 맞는다 — 이 리프가 이미 bootstrap 을 의존하므로, 게이트가 이쪽을 부르려면 방향을 뒤집거나 `Infrastructure` 열거형을 bootstrap 으로 옮겨야 한다는 점은 함께 정해야 한다. ### 확인된 설계(문제 아님) - **두 게이트를 모두 요구하고 하나만 있으면 생성자가 거부하는 것.** - **CSDS 를 xDS 사용 시에만 등록하는 것과 그 근거** — 존재하지 않는 통제 평면 표면을 광고하지 않는다. - **주소를 버리지 않고 가리는 판단** — 두 서브채널을 구별할 수 있어야 한다. - **스냅숏이 스스로 편집 여부를 검사하는 것** — 편집을 우회한 값으로는 만들 수 없다(형태 범위는 §17.1). - **능력별로 필요한 실제 인프라를 이름 지은 것** — 인프라 없이 통과하는 묶음은 없는 것보다 나쁘다. (승격 게이트와의 연결 없음은 §17.3.) - **`requiredFor` 의 switch 가 15개 능력을 전부 나열하고 `default` 를 두지 않은 것** — 능력이 하나 늘면 이 파일이 컴파일되지 않는다. 새 능력이 조용히 "인프라 불필요" 로 분류되지 않는다. - **픽스처가 금지 대상 셋을 일부러 담고 있는 것** — 통제 평면 토큰·피어 인증서·원시 소켓 주소. 파일 안 주석이 그 의도를 적고("so the redactor is tested against data shaped like the real thing rather than against a string somebody invented for the assertion"), 테스트가 편집 전에 그 셋이 실제로 들어 있는지부터 단언한 뒤 편집 결과를 본다. --- ## Source anchors ``` src/grpc-advanced/grpc-advanced-diagnostics/build.gradle:1-10 main/java/…/diagnostics/GrpcAdvancedInfrastructureTestkit.java:1-87 main/java/…/diagnostics/GrpcDiagnosticsRedactor.java:1-72 main/java/…/diagnostics/GrpcChannelDiagnosticsSnapshot.java:1-63 main/java/…/diagnostics/GrpcChannelDiagnosticsPolicy.java:1-55 test/java/…/diagnostics/GrpcChannelDiagnosticsPolicyTest.java:1-229 test/resources/xds/control-plane-snapshot.json:1-24 ```