feat: relaybox v0.2.0 — 프로젝트 리네이밍, 파서 파이프라인, CEL/Expr 표현식 엔진, TCP 어댑터#21
Conversation
Structural-only rename: go.mod module path, all import paths, cobra Use/version strings, binary name, API doc titles, config example storage path, README rewrite for generic relay concept, CLAUDE.md build command update.
- Update README API paths from /sources/{sourceId}/alerts to /inputs/{inputId}/messages
- Update config keys: sources→inputs, channels→outputs, routes→rules, sourceId→inputId, channelIds→outputIds
- Update prose: channels/routes/per-source → outputs/rules/per-input
- Add comment to TemplateData.Source explaining intentional retention until Phase 4
Register JSON/Form/XML/Logfmt parsers in InMemoryParserRegistry and pass parserTypes map to NewMessageService so the parser pipeline is active in production (previously both args were nil).
…emeral ParsedData
…s (Phase 4) - Add ExpressionEngine and ExpressionEngineRegistry port interfaces - Implement CEL engine (google/cel-go) with sync.Map program cache - Implement Expr engine (expr-lang/expr) with sync.Map program cache - Implement InMemoryExpressionEngineRegistry with Register/SetDefault/Get/Default - Update domain.Rule with Filter, Mapping, Routing, Engine fields - Change domain.Output.Template from string to map[string]string - Delete domain/template.go and domain/template_test.go (Go templates removed) - Update RuleConfigReader interface: GetRule() replaces GetOutputs() - Update OutputSender interface: Send() takes []byte payload instead of domain.Message - Update RelayWorker with expression-based filter/mapping/routing/template pipeline - Update webhook Sender to accept pre-rendered payload bytes - Update config structs: OutputConfig.Template is map[string]string, RuleConfig has filter/mapping/routing/engine, new ExpressionConfig with defaultEngine - Remove ValidateTemplate call from config validation - Update InMemoryRuleConfigReader to build domain.Rule with new fields - Wire expression engine registry in cmd/server/main.go - Add comprehensive tests for CEL engine, Expr engine, registry, and RelayWorker (filter pass/reject, mapping enrichment, routing conditions, template expressions) - Update E2E test and config example for new template format
… safety, routing validation - CEL engine: build single env at construction with data as map(string,dyn), eliminating cache collisions from different runtime data schemas - Both CEL and Expr engines now wrap data under "data" key; expressions use data.field syntax for consistent, schema-independent evaluation - Add nil guard in getEngine() when no default expression engine is registered - Validate routing condition output IDs in config validateIDs() - Deduplicate routed outputs to prevent double-delivery - Document parallel evaluation semantics of mapping expressions - Use LoadOrStore in caches to avoid double-compile races
…it, config validation - Fix goroutine leak in handleConn: use connection-scoped connCtx so the ctx.Done watcher goroutine exits when the connection closes naturally, not only when the parent context is cancelled (Fix 1) - Add scanner.Buffer call to raise max token size to 1 MiB, matching the HTTP handler's 1 MB body limit (Fix 2) - Add config validation for TCP inputs: check delimiter is a single byte and address is a resolvable TCP address (Fix 3) - Extend parserToContentType to handle logfmt and regex cases and emit a slog.Warn for unknown parser types (Fix 4) - Clarify example.yaml TCP secret comment: secret is unused for TCP inputs (Fix 5)
ppzxc
left a comment
There was a problem hiding this comment.
Code Review Summary
Overall quality is high. Hexagonal architecture is maintained throughout all 5 phases, all 15 test packages pass with the race detector, and go vet is clean.
Important (should fix before or shortly after merge)
1. buildEvalData ParsedData key shadowing built-ins
internal/application/service/relay_worker.go:215-228
ParsedData keys are merged into the flat data map alongside built-ins (id, input, payload, createdAt, status). A JSON body with {"id": "attacker-controlled"} silently overwrites the built-in id. Document priority order explicitly or enforce built-in precedence (iterate ParsedData first, then overwrite with built-ins).
2. Deduplication aliases stored outputs slice backing array
internal/application/service/relay_worker.go:116-150
When len(rule.Routing) == 0, routedOutputs = outputs aliases the slice returned by GetRule. The subsequent deduped := routedOutputs[:0] trick then aliases the stored slice's backing array. Safe today (no duplicates in practice) but fragile — use slices.Clone(outputs) or an explicit copy.
3. time.Sleep-based test assertions
internal/application/service/relay_worker_test.go
Several tests use time.Sleep(150ms) to wait for async processing. Replace with a poll-with-timeout pattern (similar to the waitForCalls helper already used in listener_test.go) for robustness.
4. Config validation missing inputId reference check
internal/config/config.go — validateIDs()
Rules with an inputId that doesn't match any declared input silently have no effect. Add a check alongside the existing outputId validation.
Suggestions
internal/adapter/output/webhook/sender.go— newhttp.Clientallocated on everySend()call, preventing connection reuse. Consider a shared client with per-output TLS config.internal/adapter/input/http/router.go:57—GET /inputs/{inputId}/messages/{messageId}placeholder returns200 OK(healthz). Should return501 Not Implementedto avoid confusing API consumers.internal/domain/input_type.go—IsValid()hard-codes 3 types (BESZEL,DOZZLE,GENERIC). Inconsistent with the generic relay concept — either remove or adopt an open-world assumption.- Missing tests: TCP 1 MiB size limit boundary, ParsedData key collision priority order.
What Was Done Well
- CEL engine single
cel.Envwithmap(string, dyn)wrapping correctly eliminates schema-keyed cache collision.TestCELEngine_DifferentSchemas_SameExpressiondirectly validates the fix. - TCP
StartWithListenersplit is excellent for testability. - Parallel-semantics mapping step is documented clearly in a comment.
- Retry backoff in
relay_worker.go:deliveruses exponential backoff with context-aware cancellation. - Graceful shutdown sequence in
main.go(cancel → worker.Wait → srv.Shutdown) is correctly ordered.
ppzxc
left a comment
There was a problem hiding this comment.
코드 리뷰 요약
전체적으로 높은 품질입니다. 5개 Phase 전반에 걸쳐 헥사고날 아키텍처가 유지되었고, 15개 테스트 패키지 모두 레이스 디텍터 포함 통과, go vet 경고 없음.
(이전 영문 리뷰의 한글 버전입니다.)
중요 (머지 전후 수정 권장)
1. buildEvalData에서 ParsedData 키가 내장 키를 덮어씀
internal/application/service/relay_worker.go:215-228
ParsedData 키가 내장 키(id, input, payload, createdAt, status)와 동일한 flat map에 병합됩니다. 예를 들어 {"id": "공격자-제어값"}이 포함된 JSON 바디가 내장 id를 조용히 덮어씁니다. 우선순위를 명시적으로 문서화하거나, 내장 키가 항상 우선하도록 순서를 보장해야 합니다(ParsedData 먼저 순회 후 내장 키로 덮어쓰기).
2. 중복 제거 로직이 stored outputs slice의 backing array를 alias함
internal/application/service/relay_worker.go:116-150
len(rule.Routing) == 0일 때 routedOutputs = outputs가 GetRule이 반환한 슬라이스를 alias합니다. 이후 deduped := routedOutputs[:0] 트릭이 stored slice의 backing array를 alias하게 됩니다. 현재는 중복이 없어 안전하지만 잠재적으로 취약합니다 — slices.Clone(outputs) 또는 명시적 복사 사용을 권장합니다.
3. time.Sleep 기반 테스트 assertion
internal/application/service/relay_worker_test.go
일부 테스트가 비동기 처리를 기다리기 위해 time.Sleep(150ms)를 사용합니다. listener_test.go에서 이미 사용 중인 waitForCalls 헬퍼와 같이 poll-with-timeout 패턴으로 교체하면 더 견고해집니다.
4. 설정 검증에서 inputId 참조 확인 누락
internal/config/config.go — validateIDs()
선언된 input을 참조하지 않는 inputId를 가진 룰이 조용히 no-op 됩니다. 기존 outputId 검증과 동일하게 inputId 참조 확인을 추가해야 합니다.
제안 (선택 사항)
internal/adapter/output/webhook/sender.go—Send()호출마다 새http.Client를 생성해 커넥션 재사용이 불가합니다. 공유 클라이언트에 output별 TLS 설정 방식을 고려해 보세요.internal/adapter/input/http/router.go:57—GET /inputs/{inputId}/messages/{messageId}placeholder가200 OK를 반환합니다. API 사용자 혼란 방지를 위해501 Not Implemented를 반환하는 것이 좋습니다.internal/domain/input_type.go—IsValid()가 3개 타입(BESZEL,DOZZLE,GENERIC)만 하드코딩되어 있어 범용 릴레이 컨셉과 불일치합니다. 제거하거나 개방형 가정(open-world assumption)을 채택하는 것을 권장합니다.- 추가 테스트 권장: TCP 1MiB 크기 제한 경계값, ParsedData 키 충돌 우선순위.
잘 된 점
- CEL 엔진의 단일
cel.Env+map(string, dyn)래핑이 이전 스키마 키 기반 캐시 충돌 문제를 올바르게 해결했습니다.TestCELEngine_DifferentSchemas_SameExpression테스트가 이를 직접 검증합니다. - TCP
StartWithListener분리가 테스트 용이성을 크게 향상시켰습니다. - 매핑 단계의 병렬 평가 시맨틱이 주석으로 명확히 문서화되어 있습니다.
relay_worker.go:deliver의 재시도 백오프가 context 인식 지수 백오프를 사용합니다.main.go의 우아한 종료 순서(cancel → worker.Wait → srv.Shutdown)가 올바릅니다.
요약
webhook-relay→relaybox변경, 바이너리/cobra/버전 문자열 업데이트, README 범용 릴레이 컨셉으로 재작성/sources/{sourceId}/alerts→/inputs/{inputId}/messages; SQLite 스키마 alerts→messages 테이블 재생성ParserRegistry;Message.ParsedData map[string]any필드 추가; 파서를 YAML 설정으로 인바운드별 지정 가능RelayWorker에 filter→mapping→routing→template 파이프라인 추가;Output.Template map[string]string;RuleConfig에 Engine/Filter/Mapping/Routing 필드 추가InputConfig에 Address/Delimiter 필드 추가; TCP 설정 검증 강화테스트 계획
CGO_ENABLED=1 go build -o relaybox ./cmd/server/빌드 성공go test -race ./... -timeout 60s— 15개 패키지 전체 통과go vet ./...— 경고 없음config.yaml룰 수정 → 재시작 없이 반영 확인