netbis: NPM Vector _msg 재합성 (proxy_v2 → nginx combined) — child-nginx-logs unparsed 84% 해결

This commit is contained in:
heimdall
2026-04-25 15:15:54 +09:00
parent 46cb3236d3
commit f733dd574a
2 changed files with 126 additions and 1 deletions

View File

@@ -0,0 +1,119 @@
---
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 결정

View File

@@ -89,7 +89,13 @@ NPM-1..6 (Linode Tokyo public) Vector 0.55 file→http
Netbis 오리진(NPM) nginx access/error 로그 수집. VL 은 LAN-only(192.168.9.53) 이므로 public NPM이 도달할 수 없어 zlambda 를 HTTP 중계로 투입. 상세: [[../../services/netbis#로그-수집-vector-→-zlambda-→-victorialogs|netbis]], [[../../history/2026-04-23-netbis-npm-vl-collection|history]]
**2026-04-25 CrowdSec 연동 완료.** 통합 acquisition `/etc/crowdsec/acquis.d/victorialogs-nginx.yaml` 의 query에 `(program:npm AND log_type:access)` 추가. nginx-logs parser가 NPM proxy format도 호환 처리 (grok이 충분히 lenient하여 핵심 필드 추출). `cscli explain --type nginx` 로 한 줄 시뮬 결과 s00-raw → s01-parse(nginx-logs +22 ~2) → s02-enrich → 시나리오(`custom/apisix-high-rate-per-ip`, `custom/apisix-single-path-flood`) 매칭 확인.
**2026-04-25 CrowdSec 연동 완료.** 통합 acquisition `/etc/crowdsec/acquis.d/victorialogs-nginx.yaml` 의 query에 `(program:npm AND log_type:access)` 추가. **단**: 초기 `cscli explain` 결과로 "nginx-logs parser가 NPM proxy format도 lenient 호환"이라 적었던 부분은 잘못된 검증이었음 — 실제로는 `child-crowdsec/nginx-logs` unparsed 84%로 grok 미매칭. 정정 사항은 아래 `_msg` 재합성 서브섹션 참조.
#### `_msg` 재합성 (proxy_v2 → nginx combined, 2026-04-25)
CrowdSec `crowdsecurity/nginx-logs` Hub 파서는 표준 `NGINXACCESS` grok 만 매칭한다. NPM 의 `proxy_v2` raw 포맷(`[Client x] [Real y] ...`)은 grok 미지원이라 `child-crowdsec/nginx-logs` unparsed 84% 발생. **해결**: NPM Vector remap에 `_msg` 재작성 블록 추가 — 이미 추출된 메타필드(client_ip/method/path/status/bytes/user_agent/referer/log_time)로 표준 nginx combined 포맷을 합성해 `.message` 에 덮어쓰기. 원본은 `.original_message` 에 보존. 검증: `cscli explain --type nginx` 결과 `s01-parse: crowdsecurity/nginx-logs (+23 ~2)` 통과 + 시나리오(`crowdsecurity/http-crawl-non_statics`, `custom/apisix-high-rate-per-ip`, `custom/apisix-single-path-flood`) 매칭 확인. 상세: [[../../history/2026-04-25-netbis-npm-vector-msg-rewrite|history]]
`_msg` 가 이제 표준 nginx combined 포맷이라 hub 파서 그대로 동작 — 통합 acquisition의 `(program:npm AND log_type:access)` 분기에서 LAPI decision까지 자연스럽게 흐름.
### SafeLine → CrowdSec (실시간, PG LISTEN/NOTIFY)