VictoriaLogs 클러스터 + OpenTelemetry 폐쇄망 트러블슈팅: 포트·vmauth·k8s 필드·로그 레벨 총정리

공식 문서대로 했는데 안 되는 지점이 유독 많은 조합이 VictoriaLogs 클러스터 + OpenTelemetry입니다. 대부분은 공식 예시가 single-node 기준이라 cluster에서 포트·경로·라우팅이 어긋나거나, OTel의 processor를 정의만 하고 pipeline에 등록하지 않아 조용히 무시되는 데서 옵니다. 이 글은 폐쇄망 멀티클러스터(dev/stg/prod + 중앙 mgmt)에 로그 스택을 세우며 실제로 막혔던 문제들을 증상 → 원인 → 해결 형태로 모았습니다. 이 시리즈의 백엔드편·설치편을 먼저 세운 뒤, 실제 운영에서 부딪히는 함정을 정리하는 마지막 편입니다.

전제 환경: K8s 멀티클러스터(노드 20+), VictoriaLogs는 cluster 모드로 mgmt에 구축, 수집은 OTel Collector(Agent DaemonSet + Gateway Deployment), 전송은 otlphttp(HTTP), env 라벨로 클러스터 구분, 폐쇄망(이미지·차트 버전 고정).


🔌 1. 포트 혼란: single은 9428, cluster는 9481·9471·8427 #

증상: 공식 OpenTelemetry 적재 가이드대로 http://<host>:9428/insert/opentelemetry/v1/logs로 보냈는데 전송이 안 되거나 연결이 거부됩니다.

원인: 그 9428single-node 예시입니다. single-node는 모든 기능이 포트 하나(9428)에 통합돼 있지만, cluster 모드는 컴포넌트별로 포트가 분리됩니다. single 문서를 보고 cluster에 9428로 적재를 시도하면 닿지 않습니다.

배포 모드적재조회진입점
single-node942894289428 (전부 통합)
clustervlinsert 9481vlselect 9471vmauth 8427

해결: 자기 구성이 cluster면 적재는 vmauth(8427) 경유가 표준이고, vmauth 없이 vlinsert(9481) 직결도 가능합니다. vlinsert는 OpenTelemetry·Elasticsearch·Loki·Syslog 등 모든 적재 프로토콜을 이 포트에서 받습니다.

endpoint vs logs_endpoint — 경로 자동완성 차이 #

포트를 맞춰도 경로에서 또 막힙니다. otlphttp exporter는 어떤 키를 쓰느냐에 따라 /v1/logs를 자동으로 붙이기도, 안 붙이기도 합니다.

자동 경로 완성적어야 하는 값
endpoint✅ OTLP 표준대로 /v1/logs 자동 부착http://<host>:9481/insert/opentelemetry 까지만
logs_endpoint❌ 자동으로 안 붙음전체 경로 http://<host>:9481/insert/opentelemetry/v1/logs

💡 otlphttp는 HTTP, otlp는 gRPC입니다. exporter 이름 뒤의 /victorialogs 같은 건 별칭일 뿐 타입과 무관합니다(타입은 앞부분 otlphttp/otlp로 갈립니다).


🚦 2. vmauth 400: url_prefix 경로 중복 #

증상: vlinsert(9481)로 직접 보내면 되는데, vmauth(8427) 경유로 보내면 400이 납니다.

원인: vmauth는 프록시이고, url_map의 url_prefix들어온 경로를 prefix 뒤에 그대로 이어 붙입니다. 그래서 url_prefix 끝에 /insert를 넣으면 경로가 중복됩니다.

1url_prefix: http://<vlinsert>:9481/insert
2들어온 경로:                      /insert/opentelemetry/v1/logs
3실제 요청:  http://<vlinsert>:9481/insert/insert/opentelemetry/v1/logs  → 400

조회도 마찬가지로 .../select/select/logsql/query가 되어 실패합니다.

해결: url_prefix에는 포트까지만 적고, 끝에 /insert·/select를 붙이지 않습니다. 경로 구분은 src_paths로 합니다.

1unauthorized_user:
2  url_map:
3    - src_paths:
4        - "/insert/.*"
5      url_prefix: http://<vlinsert-svc>:9481   # 끝에 /insert 붙이지 않음
6    - src_paths:
7        - "/select/.*"
8      url_prefix: http://<vlselect-svc>:9471   # 끝에 /select 붙이지 않음

💡 /insert.*처럼 슬래시 없이 써도 정규식상 동작하지만, /insert/.*가 더 엄밀하고 안전합니다.


🚫 3. -select.disable 에러: 조회가 vlinsert로 잘못 라우팅 #

증상: vmui나 Grafana에서 조회가 안 되고 다음 에러가 뜹니다.

1requests to /select/* are disabled with -select.disable command-line flag

원인: url_map에 포괄 규칙 하나(src_paths: ["/.*"])만 두고 전부 vlinsert로 보내면, 조회(/select/*) 요청까지 vlinsert로 갑니다. cluster에서는 차트가 vlinsert에 -select.disable을, vlselect에 -insert.disable기본으로 자동 주입합니다(정상 동작). 그래서 조회가 vlinsert에 도달하면 이 플래그에 걸려 거부됩니다.

해결: 앞의 2번처럼 url_map을 /insert/.* → vlinsert, /select/.* → vlselect로 분리합니다. vlinsert에 -select.disable이 있는 것 자체는 정상입니다 — 이 에러는 “vlinsert에 문제가 있다"가 아니라 “조회 요청이 엉뚱한 컴포넌트로 라우팅됐다“는 신호로 읽어야 합니다.

1# vlinsert에 select.disable이 있는지 확인 (있는 게 정상)
2kubectl -n <ns> get pod <vlinsert-pod> -o yaml | grep -A20 args

📁 4. hostPath·hostPort와 Kyverno Audit 모드 #

증상 A — hostPath 금지 경고인데 배포는 됨 #

OTel Agent(DaemonSet)가 노드 로그(/var/log/pods)를 읽으려면 hostPath 마운트(readOnly)가 필요합니다. 그런데 설치 시 이런 경고가 뜹니다.

1HostPath volumes are forbidden ... spec.volumes[*].hostPath must be unset

원인: hostPath를 금지하는 Kyverno 정책(예: disallow-host-path)입니다. 다만 정책이 Audit 모드면 경고·리포트만 남기고 배포는 막지 않습니다.

진단:

1# 정책 모드 확인 (Audit면 경고만, Enforce면 차단)
2kubectl get clusterpolicy <name> -o yaml | grep validationFailureAction
3# 예외가 등록돼 있는지 (없으면 No resources)
4kubectl get policyexception -A

해결: Audit 모드 + 예외 없음이면 기능은 정상이므로 경고는 무시해도 됩니다. 다만 PolicyReport에 위반 기록이 쌓이고, 나중에 Enforce로 바뀌면 차단됩니다. 정공법은 readOnly hostPath를 PolicyException으로 반영하는 것입니다(네임스페이스·경로·readOnly 한정). 구두 승인만으로는 정책에 반영돼 있지 않을 수 있으니 실제 리소스로 등록을 요청하세요.

증상 B — hostPort가 계속 렌더링됨 #

로그 수집에 hostPort는 불필요한데, values에서 -1/0/삭제를 시도해도 차트가 계속 hostPort를 넣습니다.

원인: 차트 버전에 따라 ports 렌더링 템플릿이 hostPort를 무조건 렌더링합니다.

해결(권장 순):

  1. hostPort를 만드는 preset을 끄기.
  2. values의 ports 섹션에서 hostPort 키 자체를 제거(값을 0으로 두는 게 아니라 키를 없앰).
  3. 출처를 확인하고, values로 안 되면 post-render(kustomize)로 제거(업그레이드에도 유지).
1# hostPort가 어디서 나오는지 출처 확인
2helm template <release> <chart> -f values.yaml | grep -B3 -A3 hostPort

⚠️ 차트 templates/_config.tpl에서 hostPort를 강제로 0/제거하는 건 차트 업그레이드 시 사라지는 임시방편입니다. 정착 방법으로는 위 (1)~(3)을 쓰세요.


🏷️ 5. k8s 필드가 하나도 안 들어온다: container operator 필요 #

증상: presets.kubernetesAttributes: true인데도 VictoriaLogs 대시보드에 k8s.namespace.name·k8s.pod.name 같은 필드가 하나도 없습니다.

원인: k8sattributes processor 혼자서는 k8s 필드를 못 만듭니다. 메타데이터는 두 단계로 붙습니다.

  1. filelog의 container operator + include_file_path: true — 로그 파일 경로(/var/log/pods/<namespace>_<pod>_<uid>/<container>/*.log)에서 namespace·pod·container 기본 필드를 추출합니다. 이게 없으면 애초에 pod 정보 자체가 안 생깁니다.
  2. k8sattributes processor — 위에서 만들어진 pod 정보를 기준으로 Kubernetes API에 질의해 labels·annotations를 보강합니다.

즉 container operator가 먼저 “무엇에 대해 물어볼지"를 만들어야 k8sattributes가 동작합니다. 순서가 핵심입니다.

해결: filelog에 두 가지를 반드시 넣습니다.

 1config:
 2  receivers:
 3    filelog:
 4      include: [/var/log/pods/*/*/*.log]
 5      include_file_path: true          # ① 경로를 로그에 부착 (없으면 파싱 불가)
 6      operators:
 7        - type: container              # ② 경로에서 namespace/pod/container 추출
 8  processors:
 9    k8sattributes:
10      auth_type: serviceAccount        # SA 토큰으로 API 인증 (차트가 RBAC 자동 생성)
11      passthrough: false               # true면 API 호출을 안 해 labels가 안 붙음
12      extract:
13        metadata:
14          - k8s.namespace.name
15          - k8s.pod.name
16          - k8s.node.name
17          - k8s.container.name
18      filter:
19        node_from_env_var: K8S_NODE_NAME   # 자기 노드 pod만 watch (API 부하 감소)

k8sattributes 옵션 요점:

  • passthrough: false가 기본이자 정답입니다. true면 API 질의를 건너뛰어 labels가 비어 옵니다.
  • filter.node_from_env_var: K8S_NODE_NAME를 빼면 노드마다 전체 클러스터 pod를 watch해 API 서버 부하가 노드 수만큼 폭증합니다. K8S_NODE_NAME은 차트가 downward API(spec.nodeName)로 자동 주입하므로 별도 정의가 필요 없습니다.
  • RBAC는 kubernetesAttributes preset이 최소 권한으로 자동 생성합니다. 수동으로 만들 필요가 없고, 프리셋 범위를 넘는 스코프를 원할 때만 추가로 부여합니다.

📊 6. VictoriaLogs는 폴더가 아니라 스트림 필드로 분류한다 #

증상: 저장 디렉터리를 열어봤더니 날짜별로만 나뉘어 있고, 네임스페이스·파드별 폴더 구성이 없습니다.

원인이자 개념: VictoriaLogs는 디렉터리 계층으로 분류하지 않습니다. 물리 저장은 날짜 파티션(day partition, YYYYMMDD) 단위이고, 네임스페이스·파드·클러스터 구분은 폴더가 아니라 스트림 필드(stream fields)로 합니다. 날짜를 물리 단위로 두는 이유는 retention(오래된 로그 삭제)을 날짜 통째로 지워 빠르게 처리하기 위해서입니다.

스트림 필드 vs 일반 필드 #

  • 스트림 필드로 지정된 값들은 독립 필드가 아니라 _stream이라는 하나의 묶음 안에 들어갑니다. 예: _stream = {env="mgmt", k8s.namespace.name="kube-system", k8s.pod.name="..."}
  • 스트림 필드가 아닌 나머지는 일반(독립) 필드로 저장되어, stats by ("필드명")으로 바로 그룹핑할 수 있습니다.
  • 그래서 스트림 필드를 stats by ("k8s.namespace.name")직접 그룹핑하려 하면 빈 결과가 납니다(묶여 있어서). _stream으로 집계한 뒤 extract로 값을 꺼내야 합니다(7번 참고).

⚠️ 기본값 주의: VictoriaLogs는 OTLP 리소스 라벨을 전부 스트림 필드로 취급합니다. 그대로 두면 trace_id처럼 고유값이 큰 필드까지 스트림에 들어가 카디널리티가 폭증합니다. 그래서 적재 시 VL-Stream-Fields스트림 필드를 제한하는 게 권장됩니다.

1exporters:
2  otlphttp/victorialogs:
3    endpoint: http://<vlinsert-svc>:9481/insert/opentelemetry   # /v1/logs 자동 부착
4    headers:
5      VL-Stream-Fields: "env,k8s.namespace.name,k8s.pod.name"    # 스트림 필드 제한

💾 7. 네임스페이스·클러스터별 로그 용량 산정 (block_stats) #

증상: _size 필드로 용량을 보려니 NaN이 나옵니다.

원인: _size 필드는 존재하지 않습니다. 실제 압축된 디스크 사용량은 block_stats pipe로 봅니다 — values_bytes(로그 값) + bloom_bytes(블룸 필터 인덱스)의 합이 물리 저장량입니다. 디스크 용량은 저장소만 아는 값이라 OTel이 아니라 VictoriaLogs 쿼리로 산정하는 게 맞습니다(OTel은 압축 전 통과 데이터만 봅니다).

기본 — 스트림별 용량:

1_time:7d | block_stats
2| stats by (_stream) sum(values_bytes) as v, sum(bloom_bytes) as b
3| math (v+b)/1024/1024/1024 as total_gb
4| sort by (total_gb desc)

⚠️ 반드시 시간 필터(_time:7d 등)로 범위를 좁히세요(안 그러면 전체 스캔). 점(.)이 든 필드명은 따옴표 필수입니다: "k8s.namespace.name".

클러스터(env) 간 네임스페이스 겹침 — 가장 자주 놓치는 함정 #

kube-system, logging 같은 네임스페이스는 dev/stg/prod/mgmt 모든 클러스터에 존재합니다. 네임스페이스 이름만으로 그룹핑하면 여러 클러스터의 같은 이름 네임스페이스가 한 덩어리로 합쳐져 클러스터 구분이 안 됩니다. 스트림 필드는 _stream에 묶여 있으므로, extract로 값을 꺼내 재집계해야 합니다.

env + 네임스페이스 조합(클러스터별로 구분 — 권장):

1_time:7d | block_stats
2| stats by (_stream) sum(values_bytes) as v, sum(bloom_bytes) as b
3| math (v+b) as total_bytes
4| extract 'env="<envval>"' from _stream
5| extract 'k8s.namespace.name="<ns>"' from _stream
6| stats by (envval, ns) sum(total_bytes) as bytes
7| math bytes/1024/1024/1024 as total_gb
8| fields envval, ns, total_gb
9| sort by (total_gb desc) | limit 30

extract '패턴="<추출변수>"' from _stream 패턴이 핵심입니다. env·namespace를 둘 다 꺼내 stats by (envval, ns)로 묶으면 결과가 prod/kube-system, dev/kube-system처럼 클러스터별로 분리됩니다.

특정 클러스터 고정 후 네임스페이스별(리포트·파이차트에 깔끔):

1_time:7d {env="prod"} | block_stats
2| stats by (_stream) sum(values_bytes) as v, sum(bloom_bytes) as b
3| math (v+b) as total_bytes
4| extract 'k8s.namespace.name="<ns>"' from _stream
5| stats by (ns) sum(total_bytes) as ns_bytes
6| math ns_bytes/1024/1024/1024 as total_gb
7| fields ns, total_gb
8| sort by (total_gb desc) | limit 15

앞에 {env="prod"} 스트림 필터로 클러스터 하나를 고정하면, 파이차트 라벨이 kube-system처럼 짧아 보기 좋습니다(클러스터별로 패널을 분리).

특정 네임스페이스 제외(스캔 전 제거라 효율적):

1_time:7d {"k8s.namespace.name"!="kyverno-system"} | block_stats
2| stats by (_stream) sum(values_bytes) as v, sum(bloom_bytes) as b
3| math (v+b) as total_bytes
4| extract 'k8s.namespace.name="<ns>"' from _stream
5| stats by (ns) sum(total_bytes) as ns_bytes
6| math ns_bytes/1024/1024/1024 as total_gb
7| fields ns, total_gb | sort by (total_gb desc) | limit 15

여러 개 제외는 {"k8s.namespace.name"!="kyverno-system", "k8s.namespace.name"!="kube-system"}처럼 쉼표(AND)로 나열합니다.

날짜별 추이(시간 범위 조정이 아니라 시간 버킷 그룹핑):

1_time:30d | block_stats
2| stats by (_stream, _time:1d) sum(values_bytes) as v, sum(bloom_bytes) as b
3| math (v+b) as total_bytes
4| extract 'k8s.namespace.name="<ns>"' from _stream
5| stats by (ns, _time:1d) sum(total_bytes) as bytes
6| math bytes/1024/1024/1024 as total_gb
7| sort by (_time)

💡 파드별로 보면 재생성 때마다 이름이 바뀌어 같은 앱이 여러 줄로 쪼개집니다. 앱 단위로 보려면 k8s.deployment.name이나 app 라벨로 그룹핑하세요.

Grafana _time override 경고 #

쿼리에 _time:7d를 넣으면 Grafana가 “time range is overridden by the query _time filter” 경고를 냅니다. 에러가 아니라, 쿼리의 _time이 대시보드 시간 선택보다 우선한다는 안내입니다. 대시보드 시간 선택을 쓰려면 쿼리에서 _time을 빼고, 고정 기간 리포트라면 _time:7d를 넣고 경고를 무시하면 됩니다.


🔤 8. 평문·klog 로그 레벨 구분: OTel에서 파싱한다 #

증상: “로그를 error/warn/info로 구분해달라"는 요청을 받았는데, 앱이 평문만 보내 레벨 정보가 없습니다.

원인: VictoriaLogs는 로그 레벨을 자동으로 알지 못합니다. OTel 수집 단계에서 파싱해 level 필드로 만들어야 Grafana에서 구분됩니다. 문제는 앱마다 로그 포맷이 다르고, 포맷별로 파싱 방식이 다르다는 점입니다.

포맷예시파싱 방식
JSON{"level":"INFO", ...}filelog json_parser (가장 정확)
logfmttime="..." level=info ...filelog key_value_parser
klogE0719 12:34:...첫 글자 E/W/I/F를 정규식으로 판정
순수 평문구조 없음본문 키워드(error/fail/실패…)로 추정

해결의 핵심은 우선순위입니다 — 구조화 파싱(앱이 명시한 level)을 먼저 쓰고, 안 되는 것만 추정합니다: 구조화 level → klog 첫 글자 → 평문 키워드 → unknown.

① filelog에서 JSON·logfmt 먼저 파싱(추측이 아니라 정확 추출):

 1receivers:
 2  filelog:
 3    include: [/var/log/pods/*/*/*.log]
 4    include_file_path: true
 5    operators:
 6      - type: container
 7      - type: json_parser
 8        if: 'body matches "^\\{"'          # { 로 시작하면 JSON
 9        parse_from: body
10      - type: key_value_parser
11        if: 'body matches "level="'         # level= 이 있으면 logfmt
12        parse_from: body
13        delimiter: "="
14        pair_delimiter: " "

② transform으로 정규화·추정:

 1processors:
 2  transform/loglevel:
 3    log_statements:
 4      - context: log
 5        statements:
 6          # 0) 다른 필드명(severity/lvl 등)을 level로 통일
 7          - set(attributes["level"], attributes["severity"]) where attributes["level"] == nil and attributes["severity"] != nil
 8          - set(attributes["level"], attributes["lvl"]) where attributes["level"] == nil and attributes["lvl"] != nil
 9          # 1) 이미 있으면 소문자로 정규화 (INFO/Info/info 통일)
10          - set(attributes["level"], ConvertCase(attributes["level"], "lower")) where attributes["level"] != nil
11          # 2) 없으면 klog 첫 글자로 추정
12          - set(attributes["level"], "fatal") where attributes["level"] == nil and IsMatch(body, "^F[0-9]")
13          - set(attributes["level"], "error") where attributes["level"] == nil and IsMatch(body, "^E[0-9]")
14          - set(attributes["level"], "warn")  where attributes["level"] == nil and IsMatch(body, "^W[0-9]")
15          - set(attributes["level"], "info")  where attributes["level"] == nil and IsMatch(body, "^I[0-9]")
16          # 3) 그래도 없으면 평문 키워드 추정 (영문 + 한글)
17          - set(attributes["level"], "error") where attributes["level"] == nil and IsMatch(body, "(?i)(error|exception|fail|panic|실패|오류|에러|예외)")
18          - set(attributes["level"], "warn")  where attributes["level"] == nil and IsMatch(body, "(?i)(warn|deprecated|경고|주의)")
19          # 4) 다 안 되면 판별 불가
20          - set(attributes["level"], "unknown") where attributes["level"] == nil

놓치기 쉬운 포인트:

  • 대소문자 정규화 필수: JSON은 INFO, logfmt는 info, 어떤 앱은 Info. ConvertCase(..., "lower")로 통일하지 않으면 Grafana에서 세 값으로 쪼개집니다.
  • 필드명 차이: level이 severity·lvl·loglevel로 올 수 있어, 여러 후보를 level로 옮기는 규칙을 먼저 둡니다.
  • OTTL nil 비교: “값이 있을 때"는 != nil, “없을 때"는 == nil명시적으로 씁니다. 값이 있으면 정규화하고, == nil일 때만 추정 규칙이 적용되도록 각 추정 규칙에 == nil을 붙입니다.
  • 한계: 단서 없는 평문은 판별 불가(unknown)이고 키워드 오탐도 가능합니다(“error 처리 정상"이 error로 분류). 근본 해결은 앱의 JSON 구조화 로깅이며, unknown 비율이 곧 “앱 로깅 개선 필요량"의 근거가 됩니다.

🚨 가장 흔한 실수: transform/loglevelprocessors: 아래 정의만 하고 pipeline에 등록하지 않으면 아무 일도 일어나지 않습니다. 다음 9번을 반드시 지키세요.


🧹 9. processor는 pipeline에 등록해야 동작한다 + 네임스페이스 제외 #

processor 정의 ≠ 동작 #

OTel에서 processors: 아래 정의는 “이런 processor가 있다"는 선언일 뿐입니다. service.pipelines.<signal>.processors 리스트에 이름을 넣어야 실제로 그 파이프라인을 통과합니다. 그리고 나열 순서 = 실행 순서입니다.

1service:
2  pipelines:
3    logs:
4      receivers: [filelog]
5      processors: [k8sattributes, transform/loglevel, batch]   # ← 여기 등록해야 동작
6      exporters: [otlphttp/victorialogs]
  • 관례: memory_limiter는 맨 앞, batch는 맨 뒤. 그 사이에 k8sattributes → transform → filter 순.
  • 순서 이유: k8sattributes(메타데이터 부착) → transform/loglevel(레벨 파싱) → batch(전송 직전 묶기).

수집 안 할 네임스페이스 제외 — 두 가지 방법 #

방법거르는 위치특징
A. filelog exclude (권장)파일을 아예 안 읽음리소스·전송량 절약. 단순 네임스페이스 제외에 최적
B. filter processork8sattributes 뒤에서 버림이미 읽고 파싱한 걸 버림(비용 큼). 라벨 등 복잡한 조건용

방법 A — filelog exclude (로그 경로가 <namespace>_* 형태라 네임스페이스별 제외 가능):

1receivers:
2  filelog:
3    include: [/var/log/pods/*/*/*.log]
4    exclude:
5      - /var/log/pods/kube-system_*/*/*.log
6      - /var/log/pods/kyverno-system_*/*/*.log
7    include_file_path: true
8    operators:
9      - type: container

방법 B — filter processor (pipeline 등록 필수, k8sattributes 뒤에):

1processors:
2  filter/exclude_ns:
3    logs:
4      exclude:
5        match_type: strict
6        resource_attributes:
7          - key: k8s.namespace.name
8            value: kube-system

이때 pipeline은 [k8sattributes, filter/exclude_ns, transform/loglevel, batch] 순서가 됩니다.


🧩 10. VL-* 적재 헤더 종류 #

VL-Stream-Fields 외에도 적재를 제어하는 HTTP 헤더가 여럿 있습니다. 모든 HTTP 기반 적재 프로토콜이 지원하며, 각 헤더는 HTTP query arg 형태로도 쓸 수 있습니다(query arg가 헤더보다 우선).

헤더query arg역할
VL-Msg-Field_msg_field로그 메시지 필드 이름. 미지정 시 _msg. 쉼표 나열 시 첫 비어있지 않은 값
VL-Time-Field_time_field타임스탬프 필드. 미지정/0/-이면 적재 시각 사용
VL-Stream-Fields_stream_fields스트림 필드로 취급할 필드 목록(쉼표 구분)
VL-Ignore-Fieldsignore_fields적재 시 무시할 필드
VL-Extra-Fields모든 로그에 강제로 추가할 필드(key=value)
VL-Decolorize-Fieldsdecolorize_fieldsANSI 색상 코드 제거 대상 필드
VL-Preserve-JSON-Keyspreserve_json_keys원본 값을 보존할 JSON 키
AccountID / ProjectID멀티테넌시 테넌트 지정

⚠️ VL-Stream-Fields빈 항목(foo,,bar처럼 연속 쉼표)이 있으면, 예기치 않게 _msg가 스트림 필드에 추가되어 스트림 수가 폭증할 수 있습니다. 값에 빈 항목이 없도록 주의하세요.


🔎 11. 디버깅: 필드·스트림 확인 한 줄 #

무엇이 스트림 필드(_stream에 묶임)이고 무엇이 독립 필드인지 헷갈릴 때는, 로그 한 건을 통째로 봅니다.

1_time:5m | limit 1

_stream 안에 들어간 값들이 스트림 필드, 바깥에 있는 것이 일반 필드입니다. 앞선 stats by (...)가 빈 결과를 낸다면 대상이 스트림 필드였을 가능성이 큽니다(7번의 extract 패턴으로 해결).

폐쇄망에서 Grafana VictoriaLogs 플러그인을 수동 설치하다 “permission denied plugin backend"를 만난다면, 백엔드 플러그인 바이너리에 실행 권한(+x)이 없어서입니다. 커스텀 이미지로 플러그인을 포함하거나 init container로 +x를 부여하세요. 자세한 절차는 폐쇄망 VictoriaLogs Grafana 플러그인 설치편에 정리돼 있습니다.


❓ 자주 묻는 질문 #

Q. cluster인데 9428로 보내도 되나요? 안 됩니다. 9428은 single-node 전용입니다. cluster는 적재 vlinsert 9481 또는 vmauth 8427, 조회 vlselect 9471 또는 vmauth 8427을 씁니다.

Q. -select.disable 에러는 vlinsert를 고쳐야 하나요? 아닙니다. vlinsert에 이 플래그가 있는 건 정상입니다. 조회 요청이 vlinsert로 잘못 라우팅된 것이니 vmauth url_map을 /select/.* → vlselect로 분리하면 됩니다.

Q. k8sattributes만 켰는데 왜 네임스페이스가 안 붙나요? filelog의 container operator + include_file_path: true먼저 경로에서 기본 필드를 만들어야 합니다. k8sattributes는 그 위에 API로 labels를 보강할 뿐입니다.

Q. 로그 레벨 규칙을 만들었는데 반영이 안 돼요. transform/loglevelservice.pipelines.logs.processors 리스트에 등록했는지 확인하세요. processors: 아래 정의만으로는 동작하지 않습니다.

Q. 네임스페이스별 용량이 여러 클러스터에서 합쳐져요. kube-system 같은 네임스페이스는 모든 클러스터에 있습니다. env를 함께 extractstats by (envval, ns)로 조합하거나, {env="prod"}로 클러스터를 고정하세요.


📚 참고 #