Configuration
All flags follow VictoriaMetrics naming conventions (-flagName=value).
Serverβ
| Flag | Env | Default | Description |
|---|---|---|---|
-listen | LISTEN_ADDR | :3100 | Listen address |
-backend | VL_BACKEND_URL | http://localhost:9428 | VictoriaLogs backend URL |
-proc-root | PROC_ROOT | /proc | Proc filesystem root used for CPU/memory/disk/network/PSI metrics (/proc for container scope, /host/proc for host scope) |
-host-proc-root | HOST_PROC_ROOT | /proc | Separate proc filesystem root used for host-scope reads (stat, meminfo, pressure/{cpu,memory,io}). Set to /host/proc when the chart mounts the five individual host /proc files at that path; keeps self-scope reads on /proc while host-scope reads pull from the dedicated mount. Falls back to -proc-root when unset. |
-backend-version-strict | β | false | Promote the existing soft backend version check to a hard startup failure. When true, a /health failure, non-2xx response, or sub-minimum semver causes the proxy to exit during startup instead of logging a warning. Mirrors the upstream vmselect strict-version pattern. |
-metadata-default-lookback | β | 12h | Default lookback bound applied to /labels, /label/{name}/values, and /series when the client omits both start and end. Bounds the backend time window so an unbounded Grafana label-discovery query cannot fan out across all retention. 0 disables (prior unbounded behavior). |
-ruler-backend | RULER_BACKEND_URL | β | Optional rules backend for legacy Loki YAML rules routes and Prometheus-style /prometheus/api/v1/rules passthrough |
-alerts-backend | ALERTS_BACKEND_URL | β | Optional alerts backend for Loki and Prometheus-style alerts endpoints (defaults to -ruler-backend) |
-log-level | β | info | Log level: debug, info, warn, error |
-tls-cert-file | β | β | TLS certificate file for HTTPS |
-tls-key-file | β | β | TLS key file for HTTPS |
-tls-client-ca-file | β | β | CA file for verifying HTTPS client certificates |
-tls-require-client-cert | β | false | Require and verify HTTPS client certificates |
-log-request-sample-rate | β | 0 | Log one in every N requests for access logging (0 = disabled). Superseded by adaptive sampling (see below). |
-log-buffered | β | true | Write logs in the background to avoid slowing down requests under high load |
-log-stats-interval | β | 10s | How often to print a request statistics summary (total, errors, latency, cache rate) |
-log-rate-threshold | β | 10 | When traffic exceeds this rate (req/s), replace per-request logs with periodic summaries. Errors are always logged. |
-cache-disabled | β | false | Disable the in-memory L1 cache entirely (useful for benchmarking raw translation overhead) |
Label Translationβ
See Translation Modes Guide for mode-selection profiles and exact underscore vs dotted exposure behavior across labels, field APIs, and structured metadata.
| Flag | Env | Default | Description |
|---|---|---|---|
-label-style | LABEL_STYLE | passthrough | passthrough or underscores |
-metadata-field-mode | METADATA_FIELD_MODE | hybrid | native, translated, or hybrid for detected_fields and structured metadata exposure |
-emit-structured-metadata | β | true | Enable Loki categorize-labels response encoding: requests with X-Loki-Response-Encoding-Flags: categorize-labels emit 3-tuples [timestamp, line, metadata], while default/no-flag requests stay canonical 2-tuples |
-patterns-enabled | β | true | Enable GET /loki/api/v1/patterns (Grafana Logs Drilldown patterns view). When false, the endpoint returns 404 not_found |
-patterns-autodetect-from-queries | β | false | Warm /loki/api/v1/patterns cache from successful query and query_range responses (global autodetect mode, opt-in) |
-patterns-custom | β | β | Static custom patterns prepended to every /patterns response. Accepts JSON string array or newline-separated text payload |
-patterns-custom-file | β | β | File path for static custom patterns prepended to every /patterns response. Supports JSON string array or newline-separated text with optional # comments |
-patterns-persist-path | β | β | Disk path for persisted patterns snapshot JSON file (empty disables persistence) |
-patterns-persist-interval | β | 30s | Periodic flush interval for in-memory patterns snapshot |
-patterns-startup-stale-threshold | β | 60s | Freshness threshold used by startup warm logic/peer snapshot cache |
-patterns-startup-peer-warm-timeout | β | 5s | Startup timeout for peer warm merge of pattern snapshots |
-field-mapping | FIELD_MAPPING | β | JSON custom field mappings |
-stream-fields | β | β | Comma-separated _stream_fields labels used for stream selector optimization and label-surface hints |
-extra-label-fields | EXTRA_LABEL_FIELDS | β | Comma-separated additional VL fields to expose on label-facing APIs and alias resolution paths (for example host.id,k8s.cluster.name) |
Label Style Modesβ
| Mode | When to Use | Response | Query |
|---|---|---|---|
passthrough | VL stores underscore labels (Vector/FluentBit normalize) | No translation | No translation |
underscores | VL stores OTel dotted labels (OTLP direct) | service.name β service_name | {service_name="x"} β VL "service.name":"x" |
Custom Field Mappingsβ
./loki-vl-proxy -label-style=underscores \
-field-mapping='[{"vl_field":"my_trace_id","loki_label":"traceID"}]'
Custom Drilldown Patternsβ
Examples:
# Inline JSON list
./loki-vl-proxy \
-patterns-custom='["time=\"<_>\" level=info msg=\"finished unary call\"","grpc.code=<_> grpc.method=<_>"]'
# File-backed list (JSON array or newline-separated text)
./loki-vl-proxy \
-patterns-custom-file=/etc/loki-vl-proxy/custom-patterns.json
Extra Label Fields (-extra-label-fields)β
Use this when you need explicit label exposure and alias resolution for fields that are not always discoverable from stream_field_names.
What it affects:
GET /loki/api/v1/labels: appends configured fields to the label set.GET /loki/api/v1/label/{name}/values: resolves underscoreβdot aliases for custom fields.GET /loki/api/v1/index/volumeandGET /loki/api/v1/index/volume_range: resolvestargetLabelsaliases (for examplehost_id->host.id) before backend grouping.
How values are handled:
- Accepts a comma-separated list.
- Entries are normalized to canonical VL field names by translator rules.
- Duplicates are removed.
- It extends exposure and aliasing only; it does not create or index fields in VictoriaLogs.
Caching and limits:
- It reuses existing label-path caches (
/labelsand/label/{name}/values), so repeated lookups do not repeatedly rescan backend metadata. - There is no dedicated hard cap on how many entries you can set in
-extra-label-fields; behavior is intentionally open to match VictoriaLogs field discovery. - Existing proxy safeguards still apply (endpoint TTLs, backend timeouts, and standard response-size protections).
- Practical guidance: keep this list focused on fields you want Loki users to filter on frequently; very large lists can add UI noise in Grafana.
Examples:
# Conservative Loki-facing UX + explicit custom fields
./loki-vl-proxy \
-label-style=underscores \
-metadata-field-mode=translated \
-emit-structured-metadata=true \
-extra-label-fields='host.id,k8s.cluster.name,custom.pipeline.processing'
# Equivalent via env
export EXTRA_LABEL_FIELDS='host.id,k8s.cluster.name,custom.pipeline.processing'
./loki-vl-proxy -label-style=underscores -metadata-field-mode=translated
When to also use -field-mapping:
- Use
-extra-label-fieldsto extend discovery and alias resolution. - Use
-field-mappingwhen you need a non-default alias name (for examplecustom.pipeline.processing<->pipeline_proc).
Indexed Label Values Browse Cache (Optional)β
This mode is designed for very high-cardinality labels (for example k8s_pod_name) where returning every value on first browse is expensive.
| Flag | Env | Default | Description |
|---|---|---|---|
-label-values-indexed-cache | β | false | Enable indexed hotset browsing for GET /loki/api/v1/label/{name}/values |
-label-values-hot-limit | β | 200 | Default values returned for empty-query browse when limit is not specified |
-label-values-index-max-entries | β | 200000 | Maximum indexed values retained per tenant+label in memory |
-label-values-index-persist-path | β | β | Disk path for persisted label-values index snapshot (JSON) |
-label-values-index-persist-interval | β | 30s | Periodic snapshot flush interval |
-label-values-index-startup-stale-threshold | β | 60s | Disk snapshot freshness threshold before peer warm fallback |
-label-values-index-startup-peer-warm-timeout | β | 5s | Startup timeout for peer warm fallback |
Behavior when enabled:
- Empty-query browse (
queryomitted orquery=*) serves a hot subset first. - Supports optional pagination-style parameters on the same endpoint:
limitandoffset. - Supports optional in-proxy value filtering with
search(aliasq), without forcing a full backend refetch when index is warm. - Query-scoped requests (
query={...}) keep standard behavior and still update index state. - Startup warm order: restore disk snapshot first, then warm from peer cache when disk snapshot is stale/missing.
- Rolling update safety: graceful shutdown writes a final snapshot before exit.
- Readiness behavior:
/readystays503until label-values startup warm is finished.
Example:
./loki-vl-proxy \
-label-values-indexed-cache=true \
-label-values-hot-limit=200 \
-label-values-index-max-entries=200000 \
-label-values-index-persist-path=/cache/label-values-index.json \
-label-values-index-persist-interval=30s \
-label-values-index-startup-stale-threshold=60s \
-label-values-index-startup-peer-warm-timeout=5s
Sizing guidance:
- RAM estimate per label key: `label-values-index-max-entries * ~96 bytes`.
- Disk snapshot estimate per label key: roughly `~60%` of RAM estimate.
- Total footprint scales with `(tenant_count * indexed_label_count)`.
Patterns Persistence (Optional, Recommended For Drilldown)β
Enable this to keep detected /loki/api/v1/patterns results warm across rolling restarts and fleet members.
Behavior:
- Pattern cache entries are retained long-term and updated in-place by cache key.
- Every detected pattern response is appended to the in-memory snapshot map (no periodic TTL-based pruning in snapshot state).
- Optional global autodetect (
-patterns-autodetect-from-queries=true) passively mines successfulqueryandquery_rangeresponses and pre-warms matching/patternscache keys. - Snapshot is persisted to disk periodically and on graceful shutdown.
- Startup restore order: disk snapshot first, then peer merge (newest entry wins per cache key).
- Readiness stays
503during startup warm when patterns persistence is configured.
Operational guidance:
- For restart-safe persistence, run StatefulSet + PVC and set
-patterns-persist-pathto a writable mounted path. - If
-patterns-persist-pathis set but not writable, proxy startup fails fast with a clear error. - If
-patterns-persist-pathis not set, patterns still work, but persistence is disabled (cold start after restart).
Sizing guidance:
- Endpoint clamp: max returned patterns per request is
1000. - Approximate persisted bytes:
snapshot_size ~= sum(pattern_response_payload_bytes_per_cached_query_key)- Rule of thumb per pattern entry:
~(120 bytes base + pattern length + ~24 bytes per sample bucket)
- Real footprint depends on:
- number of unique
(tenant, rawQuery)keys, - sample bucket count per pattern (
step, query range), - pattern text length distribution.
- number of unique
References:
- Loki patterns API: https://grafana.com/docs/loki/latest/reference/loki-http-api/#patterns-detection
- Grafana Logs patterns UI: https://grafana.com/docs/grafana/latest/visualizations/simplified-exploration/logs/patterns/
Metadata Field Modesβ
| Mode | When to Use | Field APIs |
|---|---|---|
native | You want only raw VictoriaLogs field names | service.name, k8s.pod.name |
translated | You want strict Loki-style field names only | service_name, k8s_pod_name |
hybrid | Default. You need Loki compatibility plus OTel-native correlation | Both native dotted names and translated aliases |
-metadata-field-mode=hybrid keeps the label surface Loki-compatible while making field-oriented APIs like detected_fields and detected_field/{name}/values expose both service.name and service_name when they differ.
Compatibility Profilesβ
The three flags below define the compatibility profile:
-label-style-metadata-field-mode-emit-structured-metadata
| Profile | Settings | Best For |
|---|---|---|
| Loki/Grafana conservative | label-style=underscores, metadata-field-mode=translated, emit-structured-metadata=true | Strict Loki-style field naming plus Explore/Drilldown event metadata |
| Drilldown/OTel mixed mode | label-style=underscores, metadata-field-mode=hybrid, emit-structured-metadata=true | Grafana + OTel correlation where both dotted and translated field names are useful |
| Native VL field surface | label-style=passthrough, metadata-field-mode=native, emit-structured-metadata=true | Consumers that prefer raw VictoriaLogs field names and structured metadata |
For tuple behavior and endpoint-level details, see API Reference. For support scope by product/version track, see Compatibility Matrix.
If you must interoperate with legacy clients that reject metadata objects in categorize-labels mode, explicitly set -emit-structured-metadata=false.
Maximum Loki Compatibility β Annotated Configurationβ
This is the recommended starting point when your priority is full compatibility with Grafana's Loki datasource and Logs Drilldown. Copy, adjust the backend URL, and run.
./loki-vl-proxy \
# ββ Backend ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-backend=http://victorialogs:9428 \ # VictoriaLogs HTTP API address
\
# ββ Label translation: maximum Loki compatibility βββββββββββββββββββββββββ
# OTel dotted labels (service.name, k8s.pod.name) are translated to Loki-style
# underscores (service_name, k8s_pod_name) in every response.
# Queries written with underscores are rewritten to dotted before hitting VL.
-label-style=underscores \
\
# ββ Metadata field exposure ββββββββββββββββββββββββββββββββββββββββββββββββ
# "translated" exposes only underscore-style labels in detected_fields and
# detected_labels. This gives Grafana a clean, Loki-identical field list
# with no dotted duplicates in the fields panel.
-metadata-field-mode=translated \
\
# ββ Structured metadata (3-tuple responses) ββββββββββββββββββββββββββββββββ
# Required for Grafana 10+ Loki datasource "categorize-labels" mode.
# Enables per-line label context in log details panel without extra queries.
# Default is true; listed here explicitly so the intent is visible.
-emit-structured-metadata=true \
\
# ββ Patterns (Logs Drilldown) ββββββββββββββββββββββββββββββββββββββββββββββ
# Enables GET /loki/api/v1/patterns used by the Patterns tab in Logs Drilldown.
# Default is true. Set false only if you do not use the Drilldown plugin.
-patterns-enabled=true
Equivalent as environment variables (for Docker Compose or Kubernetes):
VL_BACKEND_URL=http://victorialogs:9428
LABEL_STYLE=underscores
METADATA_FIELD_MODE=translated
-emit-structured-metadata and -patterns-enabled have no env-variable override; pass them as flags or Helm extraArgs.
Grafana datasource β global single-tenant (no tenant mapping needed):
datasources:
- name: Logs
type: loki
url: http://loki-vl-proxy:3100
jsonData:
httpHeaderName1: X-Scope-OrgID
secureJsonData:
# "0", "fake", and "default" all resolve to VL's built-in default tenant (0:0).
# Pick whichever your Loki configuration already uses; the proxy treats them identically.
httpHeaderValue1: "0"
What each flag does and why it matters for Loki compatibility:
| Flag | Value | Effect |
|---|---|---|
-label-style | underscores | Translates service.name β service_name so Grafana label filters work |
-metadata-field-mode | translated | No dotted duplicates in the fields panel; matches real Loki output |
-emit-structured-metadata | true | Enables 3-tuple [ts, line, metadata] required by Grafana 10+ log details |
-patterns-enabled | true | Unlocks the Patterns tab in Logs Drilldown |
Switch -metadata-field-mode to hybrid if you also need OTel correlation (traces/metrics linked by service.name); the proxy then exposes both service.name and service_name in field APIs.
Cache (L1 In-Memory)β
| Flag | Env | Default | Description |
|---|---|---|---|
-cache-ttl | β | 60s | Default cache TTL |
-cache-max | β | 10000 | Maximum cache entries |
-cache-max-bytes | β | 268435456 | Maximum in-memory L1 cache size in bytes (256 MiB by default) |
-compat-cache-enabled | β | true | Enable the Tier0 compatibility-edge response cache for safe GET read endpoints |
-compat-cache-max-percent | β | 10 | Percent of -cache-max-bytes reserved for Tier0 (0 disables, max 50) |
Tier0 Compatibility-Edge Cacheβ
Tier0 is a separate in-memory cache instance that reuses the same cache implementation as the deeper L1/L2/L3 stack, but stores only final Loki-shaped response bodies.
- It runs only after tenant validation and route classification.
- It only applies to safe
GETread endpoints such asquery,query_range,series, labels, volume, patterns, and Drilldown metadata endpoints. - It does not apply to
/tail, websocket upgrades, writes, deletes, admin/debug paths, or non-JSON responses. - Its budget is derived from
-cache-max-bytes, so-compat-cache-max-percent=10means Tier0 gets 10% of the primary L1 memory budget. - Tenant-map and field-mapping reloads invalidate Tier0 immediately.
Per-Endpoint TTLsβ
| Endpoint | TTL |
|---|---|
labels, label_values | 60s |
series, detected_fields, detected_field_values, detected_labels | 30s |
patterns | 100y (effectively persistent; update-on-write) |
query_range, query | 10s |
index_stats, volume, volume_range | 10s |
Cache (L2 On-Disk)β
| Flag | Env | Default | Description |
|---|---|---|---|
-disk-cache-path | β | β | Path to bbolt DB file (empty = disabled) |
-disk-cache-compress | β | true | Gzip compression for disk cache |
-disk-cache-flush-size | β | 100 | Flush write buffer after N entries |
-disk-cache-flush-interval | β | 5s | Write buffer flush interval |
-disk-cache-min-ttl | β | 30s | Minimum TTL required before an entry is eligible for L2 disk-cache writes |
-disk-cache-max-bytes | β | 0 | Maximum on-disk L2 cache size in bytes (0 = unlimited) |
Cold Storage Backendβ
Route queries for old data to a separate cold backend (e.g. Victoria Lakehouse or a second VictoriaLogs instance). Queries spanning both hot and cold data are split and merged transparently.
| Flag | Env | Default | Description |
|---|---|---|---|
-cold-enabled | β | false | Enable cold storage backend routing |
-cold-backend | β | β | Cold storage backend URL (e.g. http://lakehouse:9428) |
-cold-boundary | β | 168h | Data older than this age is routed to the cold backend (default 7 days) |
-cold-overlap | β | 1h | Overlap window around the cold boundary β queries spanning the boundary include this overlap on both sides to avoid gaps |
-cold-manifest-refresh | β | 5m | How often to refresh the cold backend capability manifest |
-cold-timeout | β | 30s | Timeout for cold backend requests |
./loki-vl-proxy \
-backend=http://victorialogs:9428 \
-cold-enabled \
-cold-backend=http://lakehouse:9428 \
-cold-boundary=336h # 14 days
Query Length Limitsβ
The proxy can enforce a maximum query time range, matching Loki's max_query_length behavior.
| Flag | Env | Default | Description |
|---|---|---|---|
-default-max-query-length | β | 0 | Default maximum query time range enforced for all tenants. 0 disables the limit (unlimited, matching Loki's default). Accepts Go duration strings: 30d, 7d, 24h. |
Precedenceβ
When multiple limits are configured, the most specific takes effect:
- Per-tenant β
max_query_lengthin the tenant limits config (highest priority) - Tenant defaults β
max_query_lengthintenantDefaultLimits - Global flag β
-default-max-query-length - No limit β when all of the above are
0or unset
Behaviorβ
- Applied after LogQL offset extraction (the enforced range reflects any
offsetmodifier in the query) - Rejected queries return HTTP 400 with
{"status":"error","errorType":"bad_data","error":"query length ... exceeds limit ..."} - Matches Loki's
max_query_lengthsemantics for client compatibility
Examplesβ
# Limit all queries to 30 days by default
loki-vl-proxy -default-max-query-length=720h
# Allow a specific tenant to query further back (in tenant limits config)
# max_query_length: "90d"
# No global limit (default β operator manages limits via per-tenant config)
loki-vl-proxy -default-max-query-length=0
Query Range Window Cacheβ
These flags control Loki-compatible query_range split/merge execution with per-window cache reuse.
| Flag | Env | Default | Description |
|---|---|---|---|
-query-range-windowing | β | true | Enable window split/merge path for log query_range |
-query-range-split-interval | β | 1h | Per-window time span used for split/merge |
-query-range-max-parallel | β | 2 | Static maximum window fetch parallelism (used when adaptive mode is disabled) |
-query-range-adaptive-parallel | β | true | Enable adaptive window fetch parallelism |
-query-range-adaptive-min-parallel | β | 2 | Lower bound for adaptive parallelism |
-query-range-adaptive-max-parallel | β | 8 | Upper bound for adaptive parallelism |
-query-range-latency-target | β | 1.5s | Target backend window fetch latency for safe adaptive increase |
-query-range-latency-backoff | β | 3s | Backoff threshold; adaptive mode reduces parallelism when EWMA latency exceeds this |
-query-range-adaptive-cooldown | β | 30s | Minimum interval between adaptive parallelism changes |
-query-range-error-backoff-threshold | β | 0.02 | Backoff threshold for EWMA backend error ratio (0-1) |
-query-range-freshness | β | 10m | Near-now freshness boundary |
-query-range-recent-cache-ttl | β | 0s | Cache TTL for windows newer than now-freshness (0s disables near-now cache) |
-query-range-history-cache-ttl | β | 24h | Cache TTL for historical windows older than now-freshness |
-query-range-prefilter-index-stats | β | true | Use /select/logsql/hits preflight to skip empty split windows before expensive log fanout |
-query-range-prefilter-min-windows | β | 8 | Minimum split-window count required before prefilter is enabled |
-query-range-stream-aware-batching | β | true | Reduce parallelism for windows estimated as expensive from prefilter hits |
-query-range-expensive-hit-threshold | β | 2000 | Prefilter hit threshold above which a window is treated as expensive |
-query-range-expensive-max-parallel | β | 1 | Maximum parallel fetches for expensive windows |
-query-range-align-windows | β | true | Align split windows to fixed interval boundaries for overlap-cache reuse |
-query-range-window-timeout | β | 20s | Per-window backend timeout budget (0 disables) |
-query-range-partial-responses | β | false | Allow partial query_range responses on retryable backend failures |
-query-range-background-warm | β | true | Continue warming failed windows in background after a partial response |
-query-range-background-warm-max-windows | β | 24 | Cap background warm fanout after a partial response |
-recent-tail-refresh-enabled | β | true | Enable near-now stale-cache bypass for query_range, index/volume, and index/volume_range |
-recent-tail-refresh-window | β | 2m | Treat requests ending within this window from now as near-now |
-recent-tail-refresh-max-staleness | β | 2s | Maximum acceptable cache age for near-now requests before forcing a fresh backend fetch |
Loki-Aligned Defaultsβ
split-interval=1hmirrors the common Loki split-by-interval setup.freshness=10mkeeps the latest time range uncached by default, matching typical Loki freshness posture.- bounded
max-parallel=2protects VictoriaLogs from fanout bursts while still reducing latency for long ranges. history-cache-ttl=24hkeeps older windows warm for repeated drilldowns and expanded-range queries.- adaptive mode (
min=2,max=8) raises parallelism only when EWMA latency/error remain healthy and backs off quickly when backend pressure rises.
Cache Sizing Guidanceβ
Use this approximation for disk planning:
required_bytes ~= unique_window_keys_per_day * avg_compressed_window_bytes * ttl_days
From in-repo sizing tests (internal/cache/sizing_test.go), a practical compressed average is around 56 KiB per entry, which yields:
1 GiB-> about18kcached windows10 GiB-> about189kcached windows50 GiB-> about948kcached windows
Set -disk-cache-max-bytes when you want deterministic upper bounds instead of relying only on PVC limits.
Practical Tuning Profilesβ
| Profile | Suggested Settings | When to Use |
|---|---|---|
| Conservative | adaptive-max=4, latency-target=2s, latency-backoff=4s, history-ttl=24h | Shared VictoriaLogs clusters with strict backend protection goals |
| Balanced (default) | adaptive-max=8, latency-target=1.5s, latency-backoff=3s, history-ttl=24h | Most Grafana + Drilldown workloads |
| Aggressive | adaptive-max=12, latency-target=1s, latency-backoff=2s, history-ttl=72h + larger disk cache | Large repeated long-range analytics where backend headroom is proven |
Tuning Visibility Metricsβ
Use these metrics to tune safely:
loki_vl_proxy_window_adaptive_parallel_current(gauge)loki_vl_proxy_window_adaptive_latency_ewma_seconds(gauge)loki_vl_proxy_window_adaptive_error_ewma(gauge)loki_vl_proxy_window_cache_hit_total,loki_vl_proxy_window_cache_miss_total(counters)loki_vl_proxy_window_fetch_secondsandloki_vl_proxy_window_merge_seconds(histograms)loki_vl_proxy_window_prefilter_attempt_total,loki_vl_proxy_window_prefilter_error_total(counters)loki_vl_proxy_window_prefilter_kept_total,loki_vl_proxy_window_prefilter_skipped_total(counters)loki_vl_proxy_window_prefilter_duration_seconds(histogram)
Recommended loop:
- Increase
-query-range-adaptive-max-parallelonly if latency EWMA stays below target and error EWMA stays near zero during peak load. - Increase
-query-range-history-cache-ttlonly if disk utilization and cache hit ratio justify it. - Set
-disk-cache-max-bytesbefore increasing TTL windows beyond 24h.
Metadata vs Live Query Freshnessβ
The proxy keeps faster-changing paths conservative and slower-changing metadata slightly warmer:
queryandquery_rangestay on short TTLs so new log lines remain visible quicklylabels,label_values,series,patterns,detected_fields, anddetected_labelscan cache longer because they change more slowly and are more expensive to rebuild- Drilldown now prefers native VictoriaLogs metadata (
field_names,field_values,streams) where possible, which reduces the amount of raw log rescanning needed on cache misses
Multitenancyβ
| Flag | Env | Default | Description |
|---|---|---|---|
-tenant-label | TENANT_LABEL | "" | VL field name for label-based tenant routing. When set, X-Scope-OrgID values are injected as {<tenant-label>="<orgID>"} into VL queries instead of AccountID/ProjectID headers. Use when all data is under VL default tenant (0:0). Explicit tenant-map entries take priority. |
-tenant-map | TENANT_MAP | β | JSON stringβint tenant mapping |
-tenant-map-file | TENANT_MAP_FILE | "" | Path to a YAML or JSON file mapping Loki X-Scope-OrgID strings to VictoriaLogs AccountID/ProjectID. Reloaded on SIGHUP and automatically when the file changes (see -tenant-map-reload-interval). Suitable for Kubernetes ConfigMap volumes. File entries override -tenant-map inline entries for the same key. |
-tenant-map-reload-interval | (flag only) | 30s | How often to poll -tenant-map-file for mtime changes. Set to 0 to disable polling (SIGHUP-only reload). |
-tenant-limits-allow-publish | TENANT_LIMITS_ALLOW_PUBLISH | built-in allowlist | Comma-separated limit fields exposed on /config/tenant/v1/limits and /loki/api/v1/drilldown-limits |
-tenant-default-limits | TENANT_DEFAULT_LIMITS | β | JSON map of default published-limit overrides |
-tenant-limits | TENANT_LIMITS | β | JSON map of per-tenant published-limit overrides keyed by X-Scope-OrgID |
-auth.enabled | β | false | Require X-Scope-OrgID on query requests |
-tenant.allow-global | β | false | Allow X-Scope-OrgID: * to bypass tenant scoping and use the backend default tenant |
-forward-tenant-header | FORWARD_TENANT_HEADER | true | When false, suppresses X-Scope-OrgID header forwarding to the backend |
Tenant Resolution Orderβ
- No header β when
-auth.enabled=false, requests withoutX-Scope-OrgIDuse VictoriaLogs' backend default tenant, which isAccountID=0andProjectID=0 - Tenant map lookup β if
-tenant-mapis configured and the org ID matches a key, use the mappedAccountID/ProjectID - Explicit tenant map override β if a tenant map contains an exact key such as
"0"or"fake", that explicit mapping wins - Default-tenant aliases β
X-Scope-OrgIDvalues0,fake, anddefaultmap to VictoriaLogs' built-in0:0tenant without rewriting headers - Wildcard global bypass β
X-Scope-OrgID: *is a proxy-specific convenience. It uses the backend default tenant only when-tenant.allow-global=true - Numeric passthrough β if the org ID is a number other than the default-tenant alias case (for example
"42"), pass it directly asAccountIDwithProjectID: 0 - Fail closed β unmapped non-numeric org IDs are rejected with
403 Forbidden
Multi-Tenant Query Headersβ
The proxy accepts Loki-style multi-tenant query headers on read/query endpoints by separating tenant IDs with |, for example X-Scope-OrgID: team-a|team-b.
- Supported on query-style endpoints such as
/query,/query_range,/labels,/label/.../values,/series,/index/*, and Drilldown metadata endpoints - Rejected on
/tail, delete, and write paths - Query results inject a synthetic
__tenant_id__label per tenant, matching Loki's documented query behavior __tenant_id__matchers in the leading selector narrow the tenant fanout set before backend requests are sent- multi-tenant
detected_fieldsanddetected_labelsuse exact merged value unions, so cardinality does not double-count identical values across tenants - Wildcard
*is not allowed inside a multi-tenant header; use explicit tenant IDs - fanout is safety-capped to prevent one request from exploding into an unbounded number of backend queries
- merged multi-tenant response bodies are also size-capped before they are returned to the client
Tenant Map File Formatβ
The tenant map file (YAML or JSON) maps each Loki X-Scope-OrgID string to a VictoriaLogs AccountID/ProjectID pair. Both fields are required and must be non-negative integers represented as strings.
YAML (recommended β easier to comment and diff):
# /etc/loki-vl-proxy/tenant-map.yaml
#
# Each key is the exact string sent in X-Scope-OrgID by Grafana (or Loki clients).
# account_id maps to VictoriaLogs AccountID header (tenant namespace).
# project_id maps to VictoriaLogs ProjectID header (sub-account partition).
# Both fields must be non-negative integers. Use "0" for the VL default.
# Single VictoriaLogs account, separate projects per team:
team-alpha:
account_id: "1"
project_id: "1"
team-beta:
account_id: "1"
project_id: "2"
# Different VL accounts (e.g. separate VL clusters behind a load balancer):
ops-prod:
account_id: "42"
project_id: "0"
ops-staging:
account_id: "43"
project_id: "0"
# Explicit override for the default-tenant alias "0".
# Without this, X-Scope-OrgID: "0" resolves to VL 0:0 automatically.
# Add it only when you need to redirect "0" to a different VL project:
# "0":
# account_id: "0"
# project_id: "99"
JSON (use when injecting from CI/CD tooling):
{
"team-alpha": { "account_id": "1", "project_id": "1" },
"team-beta": { "account_id": "1", "project_id": "2" },
"ops-prod": { "account_id": "42", "project_id": "0" }
}
Validation rules enforced at load time:
account_idandproject_idmust both be present and non-empty.- Both must parse as non-negative integers that fit in a
uint32(0β4294967295). - Strings that are not valid integers (for example
"prod") are rejected immediately β the proxy does not start if the tenant map is invalid. - The file extension determines the parser:
.jsonβ JSON, anything else β YAML.
Configuration Examplesβ
# Via flag (inline JSON β convenient for single-container deployments)
./loki-vl-proxy \
-tenant-map='{"team-alpha":{"account_id":"1","project_id":"1"},"team-beta":{"account_id":"1","project_id":"2"}}'
# Via environment variable (same JSON format)
export TENANT_MAP='{"ops-prod":{"account_id":"42","project_id":"0"}}'
./loki-vl-proxy -backend=http://victorialogs:9428
# Via file with hot-reload β ideal for Kubernetes ConfigMap volumes
./loki-vl-proxy \
-backend=http://victorialogs:9428 \
-tenant-map-file=/etc/loki-vl-proxy/tenant-map.yaml \
-tenant-map-reload-interval=30s
Kubernetes ConfigMap: use -tenant-map-file so the proxy can pick up tenant changes without a restart. The proxy polls the file's modification time every -tenant-map-reload-interval (default 30 s); Kubernetes atomically replaces the ConfigMap symlink, so the new mtime is visible immediately. See k8s-tenant-map-configmap.yaml for a complete Deployment snippet.
Global single-tenant (no map needed): if every Grafana datasource sends the same X-Scope-OrgID value ("0", "fake", or "default") and all data lives in VictoriaLogs' default tenant, you do not need -tenant-map at all. The proxy resolves those three aliases to VL's built-in AccountID=0, ProjectID=0 automatically.
Grafana Datasource per Tenantβ
datasources:
- name: Logs (team-alpha)
type: loki
url: http://loki-vl-proxy:3100
jsonData:
httpHeaderName1: X-Scope-OrgID
secureJsonData:
httpHeaderValue1: team-alpha
Grafana Datasource for Explicit Multi-Tenant Readsβ
datasources:
- name: Logs (team-a + team-b)
type: loki
url: http://loki-vl-proxy:3100
jsonData:
httpHeaderName1: X-Scope-OrgID
secureJsonData:
httpHeaderValue1: team-a|team-b
Use __tenant_id__ in LogQL when the datasource fans out to more than one tenant:
{app="api-gateway", __tenant_id__="team-b"}
{service_name="checkout", __tenant_id__=~"team-(a|b)"}
Grafana Datasource for Single-Tenant VictoriaLogsβ
If the backend only uses VictoriaLogs' default tenant, you can keep Grafana simple:
datasources:
- name: Logs (global tenant)
type: loki
url: http://loki-vl-proxy:3100
jsonData:
httpHeaderName1: X-Scope-OrgID
secureJsonData:
httpHeaderValue1: "0"
X-Scope-OrgID: "0", X-Scope-OrgID: "fake", and X-Scope-OrgID: "default" resolve to VL's default 0:0 tenant in Loki-compatible single-tenant mode. X-Scope-OrgID: "*" remains a proxy-specific wildcard convenience and always requires explicit -tenant.allow-global=true.
Hot Reloadβ
Tenant mappings and field mappings can be reloaded without restart via SIGHUP:
kill -HUP $(pidof loki-vl-proxy)
Published Tenant Limits Compatibility Surfaceβ
The proxy also exposes tenant-limit compatibility endpoints for Grafana and Logs Drilldown bootstrap:
GET /config/tenant/v1/limitsreturns YAMLGET /loki/api/v1/drilldown-limitsreturns JSON
The base payload is generated in-proxy and then filtered/overridden by:
-tenant-limits-allow-publish-tenant-default-limits-tenant-limits
Operational notes:
- multi-tenant
X-Scope-OrgID: a|bis rejected on/config/tenant/v1/limits X-Scope-OrgIDomitted on these endpoints uses the default published limits surface- these flags only shape the published compatibility payload; they do not enforce backend quotas
OTLP Telemetryβ
| Flag | Env | Default | Description |
|---|---|---|---|
-otlp-endpoint | OTLP_ENDPOINT | β | OTLP HTTP endpoint for proxy metrics |
-otlp-interval | β | 30s | Push interval |
-otlp-compression | OTLP_COMPRESSION | none | none, gzip, zstd |
-otlp-headers | OTLP_HEADERS | β | Comma-separated OTLP HTTP headers in key=value form |
-otlp-timeout | β | 10s | HTTP request timeout |
-otlp-tls-skip-verify | β | false | Skip TLS verification |
-otel-service-name | OTEL_SERVICE_NAME | loki-vl-proxy | service.name resource metadata for OTLP metrics/logs (not duplicated per JSON log line) |
-otel-service-namespace | OTEL_SERVICE_NAMESPACE | β | service.namespace resource metadata for OTLP metrics/logs (not duplicated per JSON log line) |
-otel-service-instance-id | OTEL_SERVICE_INSTANCE_ID | β | service.instance.id resource metadata for OTLP metrics/logs (not duplicated per JSON log line) |
-deployment-environment | DEPLOYMENT_ENVIRONMENT | β | deployment.environment.name resource metadata for OTLP metrics/logs (not duplicated per JSON log line) |
Go Runtime Tuningβ
| Flag | Env | Default | Description |
|---|---|---|---|
-go-mem-limit | β | 0 | Explicit GOMEMLIMIT in bytes. Overrides -go-mem-limit-percent when set (0 = disabled) |
-go-mem-limit-percent | β | 85 | GOMEMLIMIT as a percentage of the detected container memory limit (from /proc cgroup). Ignored when -go-mem-limit is set |
-go-gc-percent | β | 200 | GOGC target percentage. The proxy default (200) halves GC frequency versus Go's built-in default of 100, trading higher peak RSS for lower GC CPU. Set -1 to disable GC target entirely |
See Performance β Go Runtime Tuning for guidance on sizing these for your deployment.
HTTP Hardeningβ
| Flag | Env | Default | Description |
|---|---|---|---|
-http-read-timeout | β | 30s | Server read timeout |
-http-read-header-timeout | β | 10s | Server read header timeout |
-http-write-timeout | β | 120s | Server write timeout |
-http-idle-timeout | β | 120s | Server idle timeout |
-http-max-header-bytes | β | 1MB | Maximum header size |
-http-max-body-bytes | β | 10MB | Maximum request body size |
-http-conn-max-age | β | 10m | Maximum lifetime for downstream HTTP/1.x keepalive connections |
-http-conn-max-age-jitter | β | 2m | Jitter applied to downstream connection age rotation |
-http-conn-max-requests | β | 256 | Maximum requests per keepalive connection |
-http-conn-overload-max-age | β | 90s | Shorter connection lifetime during backpressure |
Grafana Compatibilityβ
| Flag | Env | Default | Description |
|---|---|---|---|
-max-lines | β | 1000 | Default max lines per query |
-manual-range-metric-row-limit | β | 1000000 | Maximum raw log rows fetched per proxy-side range-metric evaluation (rate, count_over_time, etc.); cap prevents memory spikes on high-cardinality queries |
-backend-timeout | β | 120s | Timeout for non-streaming VL backend requests |
-backend-min-version | β | v1.30.0 | Minimum VictoriaLogs version considered fully supported at startup compatibility gate |
-backend-allow-unsupported-version | β | false | Allow startup when detected backend version is lower than -backend-min-version (unsafe override) |
-backend-version-check-timeout | β | 5s | Timeout for startup backend version compatibility check (/health) |
-stream-response | β | false | Stream via chunked transfer |
-response-compression | β | auto | Response compression codec: auto, gzip, none |
-response-compression-min-bytes | β | 1024 | Minimum response size before frontend compression starts; smaller responses stay identity |
-response-gzip | β | true | Deprecated compatibility switch; false disables response compression when -response-compression is unset |
-derived-fields | β | β | JSON derived fields for trace linking |
-forward-headers | β | β | HTTP headers to forward to VL |
-forward-authorization | β | false | Forward client Authorization header to VL backend (adds Authorization to forwarded headers list) |
-forward-cookies | β | β | Cookie names to forward to VL |
-backend-basic-auth | β | β | user:password for VL basic auth |
-backend-compression | β | auto | Upstream compression preference: auto, gzip, zstd, none. auto detects loopback backends (localhost/127.x/::1) and disables compression for co-located VL, otherwise advertises zstd, gzip. |
-backend-tls-skip-verify | β | false | Skip TLS on VL connection |
-backend-read-buffer-size | β | 65536 | Per-connection read buffer size (bytes) for VL HTTP responses. Default 64 KB β reduces syscall count 7Γ for typical responses vs Go default 4 KB. Decrease (e.g. 8192) to save memory in high-pod-count environments; increase (e.g. 131072) when responses routinely exceed 64 KB. |
-backend-write-buffer-size | β | 65536 | Per-connection write buffer size (bytes) for VL HTTP requests. Default 64 KB. Tune alongside -backend-read-buffer-size for memory-constrained pods. |
-tail.allowed-origins | β | β | Comma-separated WebSocket Origin allowlist for /loki/api/v1/tail |
-tail.mode | β | auto | auto, native, or synthetic for /tail streaming mode |
-tail.mode=auto prefers native backend tailing and falls back to synthetic polling when native streaming is unavailable. native disables the fallback, and synthetic forces the polling bridge even when the backend can stream natively.
Compression notes:
-response-compression=autokeeps the optimized gzip path enabled for clients that advertisegzip.-response-compression-min-bytes=1024keeps small control-plane responses uncompressed and the proxy applies higher effective thresholds on metadata-heavy routes.- peer-cache
/_cache/getfetches follow the same preference order:zstd, thengzip, then identity. -backend-compression=autoauto-detects the backend address at startup: when the host is a loopback address (localhost,127.x.x.x,::1) it sendsAccept-Encoding: identity, eliminating 25β35% decompression CPU with no bandwidth cost (loopback has no network overhead); when the host is remote it advertiseszstd, gzipand the proxy safely decodes either on the way back. Usenoneto force identity for non-loopback local deployments (e.g. same host via a non-loopback IP).- current VictoriaLogs docs describe HTTP response compression in general terms, not a guaranteed
zstdselect-query path, so in practice many deployments will still observegzipor identity from stock VictoriaLogs today. - Grafana
12.4.2datasource proxy requests advertisedAccept-Encoding: deflate, gzip, notzstd, in local verification, so the proxy keeps the frontend surface ongzip/identity only. - The Helm chart now pins
-response-compression=gzipand-response-compression-min-bytes=1024by default.
Backend version gate notes:
- On startup, proxy probes backend
/healthand inspects response headers for a VictoriaLogs semver. - If detected version is below
-backend-min-version, startup is blocked by default. - Set
-backend-allow-unsupported-version=trueto bypass the gate at your own risk. - If version cannot be detected from headers, proxy logs a warning and continues startup.
Built-In Protection Defaultsβ
All protection controls are tunable via CLI flags:
| Flag | Env | Default | Description |
|---|---|---|---|
-max-concurrent | β | 100 | Maximum concurrent backend queries allowed globally |
-rate-limit-per-second | β | 50 | Per-client request rate limit (requests/second) |
-rate-limit-burst | β | 100 | Per-client burst allowance above the rate limit |
-cb-fail-threshold | β | 5 | Number of backend failures within the sliding window required to open the circuit breaker |
-cb-open-duration | β | 10s | How long the circuit breaker stays open before entering half-open state |
-cb-window-duration | β | 30s | Sliding window duration for failure counting; sporadic failures outside the window do not accumulate |
-coalescer-disabled | β | false | Disable request coalescing (singleflight); every concurrent request makes its own backend call β useful with -cache-disabled to measure raw translation overhead |
Shape per-client and global traffic at Grafana, ingress, or an outer proxy layer for additional control beyond these flags.
Observability and Admin Surfacesβ
BREAKING in v1.56.0 β admin/debug endpoints and
/metricsno longer share the main proxy listener by default.
-server.register-instrumentationnow defaults tofalse(wastrue). The main proxy listener stops serving/metricsunless instrumentation is explicitly re-enabled.- Admin and debug endpoints (
/admin/*,/debug/pprof/*,/debug/queries) move to a dedicated loopback listener on127.0.0.1:3101(configurable via--admin-listen) unless-server.admin-auth-tokenis set, in which case they stay on the main listener for back-compat./metricscan be hosted on a dedicated port via--metrics-listen=:9091; the Helm chart sets this by default and points the ServiceMonitor at it.Binary users who scrape
:3100/metricstoday must, after upgrade, either:
- set
-server.register-instrumentation=trueto keep/metricson the main listener (legacy behavior), OR- set
-server.register-instrumentation=true -metrics-listen=:9091for a dedicated metrics port (recommended).
| Flag | Env | Default | Description |
|---|---|---|---|
-server.register-instrumentation | β | false | Register /metrics and related instrumentation handlers. BREAKING in v1.56.0 β default changed from true to false so the main listener no longer exposes /metrics unless opted in. Pair with --metrics-listen to host /metrics on a dedicated port. |
--admin-listen | β | 127.0.0.1:3101 | Address of the dedicated admin/debug listener used when -server.admin-auth-token is empty. Bound to loopback by default so admin surfaces (/admin/*, /debug/pprof/*, /debug/queries) are unreachable from off-host. Set -server.admin-auth-token to keep these surfaces on the main listener instead. |
--metrics-listen | β | β | Address of the dedicated /metrics listener (e.g. :9091). When set together with -server.register-instrumentation=true, /metrics moves off the main proxy listener so scrape traffic never competes with queries. Empty disables the dedicated listener. |
-server.enable-pprof | β | false | Expose /debug/pprof/* |
-server.enable-query-analytics | β | false | Expose /debug/queries |
-server.admin-auth-token | β | β | Bearer token accepted on admin/debug endpoints |
-server.metrics-max-concurrency | β | 1 | Maximum concurrent /metrics scrapes served at once (0 disables the cap) |
-metrics.max-tenants | β | 256 | Max unique tenant labels retained in /metrics before using __overflow__ |
-metrics.max-clients | β | 256 | Max unique client labels retained in /metrics before using __overflow__ |
-metrics.export-sensitive-labels | β | false | Export per-tenant and per-client identity metrics on /metrics and OTLP |
-metrics.trust-proxy-headers | β | false | Trust user/proxy headers (X-Grafana-User, X-Forwarded-User, X-Webauth-User, X-Auth-Request-User, X-Forwarded-*) for client metrics/log attribution and backend context forwarding |
-debug-log-raw-queries | β | false | Include raw LogQL/LogsQL strings and backend query params in debug-level logs. By default each query is logged as sha256:<8hex>+len=<n> to keep PII out of operator log pipelines. Set to true only for ad-hoc investigation; INFO-level access logs are unaffected. |
Peer Cacheβ
| Flag | Env | Default | Description |
|---|---|---|---|
-peer-self | β | β | This instance address used for peer-cache ownership and fetches |
-peer-self-az | β | β | This instance's availability zone (e.g. us-east-1a). When set, same-AZ peers are preferred when fetching keys at startup and warmup to reduce cross-AZ traffic. Falls back to any peer with fresh data when no same-AZ peer has the key. Helm chart can inject this automatically from the pod's topology label via the Kubernetes Downward API (see peerCache.topologyLabel). |
-peer-discovery | β | β | Peer discovery mode: dns, srv, http, or static |
-peer-dns | β | β | Headless service DNS name used when -peer-discovery=dns (e.g., proxy-headless.ns.svc.cluster.local) |
-peer-srv | β | β | Full SRV record name used when -peer-discovery=srv (e.g., _loki-vl-proxy._tcp.proxy-headless.ns.svc.cluster.local). Port is read from the SRV record β no separate port flag needed. |
-peer-http-url | β | β | URL returning a JSON peer list when -peer-discovery=http. Supported formats: simple array, {"peers":[...]}, Prometheus HTTP SD, Consul catalog. The URL is polled every DiscoveryInterval. |
-peer-static | β | β | Comma-separated peer list used when -peer-discovery=static (e.g., 10.0.0.1:3100,10.0.0.2:3100) |
-peer-timeout | β | 2s | Timeout applied to peer-cache owner fetches (/_cache/get) before falling back locally |
-peer-auth-token | β | β | Shared token used on /_cache/get and /_cache/set peer-cache requests. Strongly recommended for fleets so peer auth does not depend only on transient discovery/IP membership during startup. Required by default in v1.56.0 β the proxy refuses to start when peer cache is configured (via -peer-discovery or -peer-static) and this token is empty, unless -peer-insecure-ip-allowlist=true is set. |
-peer-insecure-ip-allowlist | β | false | Explicit opt-in for the legacy IP-allowlist-only peer auth (membership in the discovered peer set is the only check). When true, the proxy boots with -peer-auth-token="" and falls back to source-IP membership for /_cache/get and /_cache/set. BREAKING in v1.56.0 β without this flag, a configured peer cache with an empty token now fails startup. |
-peer-write-through | β | true | Push eligible non-owner cache writes to the owner peer (/_cache/set) to keep owner shards warm under skewed traffic |
-peer-write-through-min-ttl | β | 30s | Minimum TTL required to push a write-through copy to the owner peer |
-peer-hot-read-ahead-enabled | β | false | Enable bounded periodic hot-read-ahead prefetch from peer hot indexes |
-peer-hot-read-ahead-interval | β | 30s | Base interval for hot-index pull cycles |
-peer-hot-read-ahead-jitter | β | 5s | Random jitter added to the read-ahead interval |
-peer-hot-read-ahead-top-n | β | 256 | Number of hot keys requested per peer hot-index pull |
-peer-hot-read-ahead-max-keys-per-interval | β | 64 | Max prefetched keys per read-ahead cycle |
-peer-hot-read-ahead-max-bytes-per-interval | β | 8388608 | Max prefetched bytes per read-ahead cycle |
-peer-hot-read-ahead-max-concurrency | β | 4 | Max concurrent hot-index and prefetch peer requests |
-peer-hot-read-ahead-min-ttl | β | 30s | Minimum remaining TTL required for a prefetch candidate |
-peer-hot-read-ahead-max-object-bytes | β | 262144 | Max object size eligible for read-ahead prefetch |
-peer-hot-read-ahead-tenant-fair-share | β | 50 | Per-tenant first-pass selection cap (% of key budget) |
-peer-hot-read-ahead-error-backoff | β | 15s | Base cooldown after read-ahead/index pull failures |
-warmup-max-jitter | β | 0 (disabled) | Maximum random delay before label cache warmup starts on startup. Spreads fleet instances across a random window so they don't all hit VL simultaneously. Recommended: 5s for β€5 pods, 10s for β€15 pods, 20s for β€30 pods, 30s for larger fleets. |
Peer-cache notes:
- the Helm chart manages
-peer-self,-peer-discovery, and-peer-dnsautomatically whenpeerCache.enabled=true; forsrvorhttpmodes useextraArgs - AZ-aware peer selection (
-peer-self-az) prefers same-AZ peers for key fetches; Helm auto-injects it frommetadata.labels['topology.kubernetes.io/zone']via Downward API whenpeerCache.topologyLabelis set (default). SetpodLabels: {topology.kubernetes.io/zone: <zone>}or rely on a platform webhook/node label syncer to populate the pod label. - for
httpdiscovery with a Prometheus HTTP SD endpoint, per-peer AZ is read automatically fromlabels.azorlabels.availability_zonein the SD response β no extra flag required GET /_cache/peersreturns the current peer ring as{"peers":[...],"self":"...","count":N}β useful to verify discovery is working- peer-cache fetches preserve owner TTL and can compress larger
/_cache/getresponses withzstdorgzip loki_vl_proxy_peer_cache_error_reason_total{reason=...}breaks opaque peer fetch failures into low-cardinality reasons liketimeout,transport,status_502,body_read, anddecode- with
-peer-write-through=true(default), non-owner writes with TTL above threshold are pushed to owners and stored locally as short-lived shadows to reduce hot-pod disk skew - set
-peer-auth-tokenfleet-wide whenever peer cache is enabled; it avoids transient startup or discovery-flap403responses caused by IP-membership-only peer auth - when
-peer-auth-tokenis set, all peers must share the same token or peer-cache reuse will fail closed /_cache/has?keys=k1,k2,...is a batch key-presence endpoint (metadata only, no value data transferred) used internally during startup warmup; callers can also use it to pick the freshest peer for a given set of keys before calling/_cache/get- startup warmup uses a two-phase peer-first strategy: one
/_cache/hasper peer to discover who has which label windows, then one/_cache/getper covered window from the freshest peer β only the first instance per window hits VL; see Fleet Cache Architecture
Current Tuning For Higher Fleet Reuseβ
Use these knobs first:
- set
-warmup-max-jitterfor any fleet with β₯3 pods to prevent thundering herd on rolling restart (see sizing table in Fleet Cache Architecture) - keep
-peer-write-through=true(default) to warm owner shards under skewed traffic - tune
-peer-write-through-min-ttlso only stable/hot entries are replicated - keep
-response-compression=gzipfor explicit Loki/Grafana-safe frontend behavior, orautoif you want the same gzip behavior through the legacy default - keep
-response-compression-min-bytesaround1-4KiBto avoid wasting CPU on small metadata/control responses - keep
-backend-compression=autofor smart upstream negotiation: loopback backends getidentity(no decompression overhead), remote backends getzstd/gzip - keep
query-range-windowingenabled with long history TTL and near-now freshness controls for mixed historical/live workloads
Bounded Hot Read-Aheadβ
A bounded peer hot-read-ahead mode is implemented and can be enabled with:
-peer-hot-read-ahead-enabled=true
It keeps traffic bounded via key/byte/concurrency caps, jitter, tenant fairness, and error backoff. See Fleet Cache Architecture.
Peer Cache Token Managementβ
The chart auto-generates a Secret named <release>-peer-auth on first install when peerCache.enabled=true and peerCache.authToken is unset. The Secret holds a single key peer-auth-token populated with a random value (randAlphaNum 48), wired into the proxy Deployment as the -peer-auth-token flag via env-from-secret. This satisfies the v1.56.0 "token required by default" startup gate without operator action.
To rotate or replace the token:
- Rotate (chart-managed) β delete the Secret and
helm upgradewill regenerate it:kubectl delete secret <release>-peer-auth && helm upgrade <release> charts/loki-vl-proxy. All pods must restart to pick up the new value, so do this in a maintenance window or under aRollingUpdatestrategy. - Externally manage β set
peerCache.authTokento a value (orpeerCache.existingSecretpointing at your own Secret) in the values file. The chart will skip auto-generation. Useful for GitOps flows that source secrets from Vault / External Secrets Operator. - Disable peer cache β set
peerCache.enabled=false; no Secret is created and the token gate is skipped because no peer cache is configured.
To restore the pre-v1.56.0 IP-allowlist-only behavior without a token, pass -peer-insecure-ip-allowlist=true via extraArgs. This is not recommended: peer auth then depends only on source-IP membership in the discovered peer set, which is brittle under discovery flap and pod IP churn.
The owner hot-index endpoint (/_cache/hot) is internal peer-cache surface area and should not be exposed publicly.
Grafana Datasource Mappingβ
These Grafana Loki datasource settings now have a direct proxy-side mapping:
| Grafana Setting | Proxy Support |
|---|---|
| URL | -listen, optional -tls-cert-file / -tls-key-file |
| No Authentication | Supported by default |
| TLS Client Authentication | -tls-client-ca-file, -tls-require-client-cert |
| Skip TLS certificate validation | Grafana-side only |
| Add self-signed certificate | Grafana-side only |
HTTP headers (X-Scope-OrgID, auth headers) | -forward-headers, -tenant-map, -auth.enabled |
Forward Authorization specifically | -forward-authorization=true (or -forward-headers=Authorization) |
| Allowed cookies | -forward-cookies |
| Timeout | -backend-timeout for proxyβVL, Grafana datasource timeout for Grafanaβproxy |
| Maximum lines | -max-lines |
When -metrics.trust-proxy-headers=true, the proxy forwards trusted user headers and proxy-chain headers to the backend, plus derived context headers X-Loki-VL-Client-ID / X-Loki-VL-Client-Source.
Datasource auth credentials are forwarded separately as X-Loki-VL-Auth-User / X-Loki-VL-Auth-Source and are not used as enduser.id client identity.
Grafana User Header Forwarding (Recommended)β
To attribute requests to real Grafana users instead of datasource service credentials:
- Enable trusted proxy headers on Loki-VL-proxy:
-metrics.trust-proxy-headers=true
- Configure Grafana server dataproxy to forward the logged-in user header:
[dataproxy]
send_user_header = true
- Keep Loki datasource in proxy mode (
access: proxy) and point it to Loki-VL-proxy:
datasources:
- name: Loki (VL Proxy)
type: loki
access: proxy
url: http://loki-vl-proxy:3100
- If Grafana sits behind an auth/reverse proxy, preserve user/proxy-chain headers end to end (
X-Forwarded-User,X-Grafana-User,X-Forwarded-*,Forwarded).
Expected request-log behavior with this setup:
enduser.idreflects the trusted Grafana/auth user.enduser.sourceindicates user-header source (for examplegrafana_userorforwarded_user).auth.principalreflects datasource auth identity when present (for example basic-auth datasource user).
Alerting datasource integration is still partial: the proxy supports legacy Loki YAML rules reads and Prometheus-style JSON rules/alerts reads against a configured backend, but it does not yet implement the full Loki ruler write API surface.