netbis: NPM Vector _msg 재합성 (proxy_v2 → nginx combined) — child-nginx-logs unparsed 84% 해결
This commit is contained in:
119
history/2026-04-25-netbis-npm-vector-msg-rewrite.md
Normal file
119
history/2026-04-25-netbis-npm-vector-msg-rewrite.md
Normal 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 결정
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user