--- date: 2026-04-25 topic: NPM Vector `_msg` 재합성 — proxy_v2 → 표준 nginx combined areas: [services/netbis.md, infra/security/crowdsec-safeline.md, infra/platform/victorialogs.md] tags: [netbis, vector, vrl, crowdsec, nginx-logs, observability] --- ## 배경 [[2026-04-23-netbis-npm-vl-collection|NPM → zlambda → VL 파이프라인]] + [[2026-04-23-netbis-npm-client-ip-fix|client_ip 정비]] 완료 후 CrowdSec 파서 통과율 확인 결과 `child-crowdsec/nginx-logs` unparsed 84%. 원인: NPM `proxy_v2` raw 포맷(`[Client x] [Real y] [XFF z] ...`)이 hub 파서의 `NGINXACCESS` grok과 매칭되지 않음. cscli explain으로 100% 실패 확인. 이전 정본에 `nginx-logs parser가 NPM proxy format도 호환 처리 (lenient)` 라고 잘못 기록되어 있던 부분 정정 — 호환 안 됨, Vector 측에서 표준 포맷으로 재합성하는 방식 채택. ## 결정 NPM Vector `parse_npm_access` remap 끝부분에 `_msg` 재작성 블록 추가. 이미 추출된 메타필드를 활용해 표준 nginx combined 포맷을 합성하여 `.message` 에 덮어쓰기. 원본은 `.original_message` 에 백업. 대안 비교: - (A) **Vector 측 재합성** ← 선택. 모든 다운스트림(VL, CrowdSec, anomaly-detect 등)에 표준 포맷 일괄 공급 - (B) CrowdSec 측 custom 파서 작성 — child-nginx-logs를 NPM proxy_v2용으로 분기. 분기 추가시마다 유지보수 부담 - (C) NPM nginx 측 log_format을 표준 combined로 변경 — Real/XFF 메타정보 손실, IP 추적 불가 ## 변환 로직 ```vrl lf = .log_format if lf != "raw" { ip_v = .client_ip if ip_v == null { ip_v = .remote_addr } ip_s = to_string(ip_v) ?? "-" if ip_s == "" { ip_s = "-" } m_s = to_string(.method) ?? "-" ... ts_s = to_string(.log_time) ?? "" # NPM $time_local if ts_s == "" { ts_v = .timestamp # fallback: Vector ingest time if is_nullish(ts_v) { ts_v = now() } fmt, fmt_err = format_timestamp(ts_v, "%d/%b/%Y:%H:%M:%S %z") if fmt_err == null { ts_s = fmt } else { ts_s = format_timestamp!(now(), "%d/%b/%Y:%H:%M:%S %z") } } .original_message = .message .message = ip_s + " - - [" + ts_s + "] \"" + m_s + " " + p_s + " HTTP/1.1\" " + st_s + " " + b_s + " \"" + r_s + "\" \"" + u_s + "\"" } ``` 각 parse 분기에서 `.log_time = m.ts` 도 추가해 NPM 의 `$time_local` 보존 (재합성 시 `[$time_local]` 자리에 사용). raw 포맷(파싱 실패)은 재작성 안 함 — `_msg` 그대로 둬서 디버깅 단서 보존. ## Vector 검증 시 함정 1. **YAML 주석 내 `$variable` 표기** → Vector env-var interpolation 트리거 → 검증 실패. `$$variable` 로 escape 필요. `$time_local`, `$body_bytes_sent` 등 nginx 변수 표기 모두 영향 2. **infallible 표현식에 `??` 사용** → VRL 컴파일 에러 (`E651: unnecessary error coalescing operation`). 예: `to_string(.log_format)` 가 모든 분기에서 string으로 설정되었음을 컴파일러가 추론 → `??` 가 dead code → 직접 `.log_format` 참조로 변경 ## npm-1 단독 검증 합성 라인 주입: ``` [25/Apr/2026:06:10:56 +0000] - 200 200 - GET http heimdall.test "/transform-verify" [Client 203.0.113.55] [Real 172.71.24.1] [XFF 203.0.113.55] [Length 1234] [Gzip 1.5] [Sent-to 42.0.0.1] "heimdall-transform-test/1.0" "https://heimdall.test/ref" ``` VL 도달 후 `_msg`: ``` 203.0.113.55 - - [25/Apr/2026:06:10:56 +0000] "GET /transform-verify HTTP/1.1" 200 1234 "https://heimdall.test/ref" "heimdall-transform-test/1.0" ``` `cscli explain --type nginx` (jp1:crowdsec): ``` ├ s01-parse | └ 🟢 crowdsecurity/nginx-logs (+23 ~2) ├ s02-enrich | ├ 🟢 crowdsecurity/dateparse-enrich (+2 ~2) | ├ 🟢 crowdsecurity/geoip-enrich (+9) | ├ 🟢 crowdsecurity/http-logs (+7) ├-------- parser success 🟢 ├ Scenarios ├ 🟢 crowdsecurity/http-crawl-non_statics ├ 🟢 custom/apisix-high-rate-per-ip └ 🟢 custom/apisix-single-path-flood ``` s01-parse 통과 + 3개 시나리오 매칭. ## 6대 롤링 배포 npm-1 → 5대(npm-2..6) 순차. 각 호스트 `vector validate` 통과 후 `systemctl kill -SIGKILL && reset-failed && start`. VL 샘플 확인 (host별): | Host | 샘플 _msg (앞 150자) | |---|---| | npm-1 | `203.0.113.55 - - [25/Apr/2026:06:10:56 +0000] "GET /transform-verify HTTP/1.1" 200 1234 ...` | | npm-2 | `255.95.21.9 - - [25/Apr/2026:06:13:12 +0000] "POST /main/betIt HTTP/1.1" 200 56 ...` | | npm-3 | `170.187.231.160 - - [25/Apr/2026:06:13:12 +0000] "POST /vinus/cback?bet_..." 200 85 "-" "-"` | | npm-4 | `254.5.9.221 - - [25/Apr/2026:06:13:14 +0000] "POST /wel/login.html HTTP/1.1" 200 90 "https://fall-vip.com/wel.html" ...` | 전부 표준 nginx combined 포맷. ## 30분 후 검증 baseline 캡처 (06:13:40Z): ``` victorialogs source: 977.24k read / 514.72k parsed / 462.52k unparsed (47.3% unparsed) child-crowdsec/nginx-logs: 3.29M hits / 514.72k parsed / 2.78M unparsed (84.5% unparsed) ``` 30분 후 cscli metrics 재캡처 → Δ window 비율 계산 → Discord webhook 푸시 (백그라운드 스크립트 예약). ## 운영 주의 - **`.original_message` 보존** — 디버깅·재처리 용도. 재합성 결과가 이상하면 original 로 진단 가능 - **로그 회전 안전성** — VRL 변경은 새 라인부터 적용. 기존 unparsed 누적은 그대로 (cscli metrics는 cumulative). delta 비교에서만 효과 보임 - **Vector restart 전략** — 기존 inflight ES sink 때문에 graceful shutdown 60s 대기. 빠른 전환 필요 시 SIGKILL → reset-failed → start - **추가 영향** — VL 인덱스에 `_msg` 가 표준 포맷으로 들어가서 LogsQL 쿼리 패턴이 단순해짐. 동시에 `.original_message` 가 별도 필드로 추가되어 retention 비용 약간 증가 (~5-10%) ## 후속 - [ ] 30분 metrics 비교 결과 (백그라운드 스크립트 자동 푸시) - [ ] 안정화 후 CrowdSec acquisition에 VL `service:npm log_type:access` 추가 → APISIX 파이프라인과 동일하게 시나리오 매칭 → bouncer 결정