VictoriaLogs + Vector: replacing EFK with a lightweight log stack

Published: 2026-03-09

Elasticsearch handles application logs. VictoriaLogs handles cluster-level observability logs — errors and warnings from every namespace, with 30-day retention and 2-node high availability. Vector is the single collector that feeds both.


Architecture

Pod stdout/stderr
       │
       ▼
  Vector DaemonSet
       │
  ┌────┴─────────────┐
  │                  │
  ▼                  ▼
Elasticsearch    VictoriaLogs
(app/aurora      (error + warn
 namespaces)      from all ns)

Vector runs as a DaemonSet, reads from the Kubernetes log API, parses JSON logs, and routes based on namespace and log level.


VictoriaLogs cluster

Deployed via victoria-logs-cluster chart:

yamlvlstorage:
  replicaCount: 2
  retentionPeriod: 30d
  persistentVolume:
    size: 10Gi
  resources:
    requests:
      cpu: 200m
      memory: 512Mi
    limits:
      cpu: 1
      memory: 2Gi

vlinsert:
  replicaCount: 1
  resources:
    requests:
      cpu: 100m
      memory: 128Mi

vlselect:
  replicaCount: 1
  resources:
    requests:
      cpu: 100m
      memory: 128Mi

Three components: insert, select, storage. Two storage replicas for redundancy. The vlinsert and vlselect components are stateless — scale them independently.


Vector configuration

The Vector config lives inside the victoria-logs HelmRelease values under vector.customConfig. It's structured as a pipeline: sources → transforms → sinks.

Sources:

yamlsources:
  k8s:
    type: kubernetes_logs
    exclude_paths_glob_patterns:
      - "/var/log/pods/observability_vector-*/**"  # don't collect Vector's own logs
  internal_metrics:
    type: internal_metrics

Transforms — label enrichment:

yamltransforms:
  dedot_labels:
    type: remap
    inputs: [k8s]
    source: |
      # Kubernetes labels use dots in keys, which break JSON.
      # Convert dots to underscores in all kubernetes.pod_labels keys.
      .kubernetes.pod_labels = map_keys(.kubernetes.pod_labels) -> |k| {
        replace(k, ".", "_")
      }

Transforms — JSON parsing:

yaml  parser:
    type: remap
    inputs: [dedot_labels]
    source: |
      .log = parse_json(.message) ?? .message
      ._msg = string(.log.message) ?? string(.log.msg) ?? string(.message) ?? ""
      del(.message)

Transforms — routing to Elasticsearch (app namespaces):

yaml  aurora_filter:
    type: filter
    inputs: [parser]
    condition: |
      includes(["app", "aurora"], .kubernetes.pod_namespace)

Transforms — routing to VictoriaLogs (errors/warnings from all namespaces):

yaml  error_filter:
    type: filter
    inputs: [parser]
    condition: |
      lvl = downcase(string(.log.level ?? .log.lvl ?? .log.severity ?? ""))
      includes(["error", "warn", "warning", "fatal", "panic", "err"], lvl)

Sinks:

yamlsinks:
  elasticsearch:
    type: elasticsearch
    inputs: [aurora_filter]
    endpoints:
      - "http://dev-master.elasticsearch.svc.cluster.local:9200"
    index: "vector-%Y-%m-%d"
    auth:
      strategy: basic
      user: elastic
      password: "${ES_PASSWORD}"

  victorialogs:
    type: elasticsearch
    inputs: [error_filter]
    endpoints:
      - "http://victoria-logs-vlinsert.observability.svc.cluster.local:9428/insert/elasticsearch"
    mode: bulk

VictoriaLogs exposes an Elasticsearch-compatible bulk insert API, so Vector's elasticsearch sink works without modification.


Why Vector instead of Fluentd or Filebeat

Feature Vector Fluentd Filebeat
Memory (DaemonSet) ~50 MiB ~200 MiB ~100 MiB
Config language VRL (functional) Ruby DSL YAML
Transform power High Medium Low
Multi-output Native Via copy plugin Limited
Hot reload Yes Partial Yes

VRL (Vector Remap Language) is more readable than Fluentd's record_transformer. The elasticsearch sink works for both Elasticsearch and VictoriaLogs — one config type covers both.


VictoriaLogs query

VictoriaLogs has a web UI and a LogsQL query language:

bashkubectl port-forward -n observability svc/victoria-logs-vlselect 9471:9471
# Open http://localhost:9471/select/vmui/

LogsQL examples:

# All errors from app namespace in last hour
kubernetes.pod_namespace: app AND level: error

# Specific pod
kubernetes.pod_name: my-app-xxx AND level: error

# Text search
"database connection refused"

# Combined
kubernetes.pod_namespace: app AND "connection refused" AND _time: 1h

Excluding Vector's own logs

The Vector DaemonSet pod has a label vector.dev/exclude: "true". Configure the source to skip it:

yamlsources:
  k8s:
    type: kubernetes_logs
    pod_annotation_fields:
      pod_labels: "kubernetes.pod_labels"
    # Exclude pods with this label
    extra_label_selector: "vector.dev/exclude != true"

Without this, Vector logs its own log ingestion, which creates a feedback loop that fills the log store with Vector's own debug output.


Resource sizing

VictoriaLogs is significantly lighter than Elasticsearch for the same data volume:

Stack RAM (1M logs/day) Disk (30 days)
Elasticsearch 4–8 GB ~100 GB
VictoriaLogs 512 MB–1 GB ~10 GB

The disk savings come from VictoriaLogs' compression (~10x vs Elasticsearch ~3x).