No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-02 19:11:19 +02:00
LICENSE Initial commit 2026-09-02 19:00:44 +02:00
proxyauth-dashboard-en.json Add files dahsboard fr/en 2026-09-02 19:04:19 +02:00
proxyauth-dashboard-fr.json Add files dahsboard fr/en 2026-09-02 19:04:19 +02:00
README.fr.md Update readme.md 2026-09-02 19:10:52 +02:00
README.md Update readme.md 2026-09-02 19:11:19 +02:00

ProxyAuth — Grafana dashboard for Loki

Access-log dashboard for ProxyAuth, built on Grafana + Loki. 42 panels: traffic, latency, status codes, bandwidth, authentication failures, user-agent breakdown, referrers, and raw logs.

Version française de ce document : README.fr.md.

Files

File Language
proxyauth-dashboard-en.json English
proxyauth-dashboard.json French

Both are identical apart from panel titles, legends and variable labels. The LogQL queries are the same. Import only one — they use different UIDs, so importing both gives you two independent dashboards.

Requirements

  • Grafana 10.2 or later. The queries use the $__auto range variable.
  • Loki, reachable as a Grafana datasource.
  • Your ProxyAuth logs shipped to Loki with the stream label app="proxyauth".

If you use a different label, see Adapting the stream selector.

Expected log format

The dashboard parses this access-log line, wrapped in ProxyAuth's JSON envelope (the message field of an entry whose _target is proxyauth::access):

2026-09-02 11:36:32 [vhost] [ip] - METHOD HTTP/2.0 200 9075 /path [tid:-] 'user-agent' 'referer' 141620268 0.7% 177.7MB

Fields, in order: timestamp, vhost, client IP, method, protocol, status, response length, path, token id, user-agent, referrer, request time in nanoseconds, CPU usage, memory usage.

Parsing is done at query time by four independent regexp stages. If one stage fails to match, the others still produce their fields — a malformed tail will not blank out the whole dashboard.

Import

  1. In Grafana, go to Dashboards → New → Import.
  2. Click Upload dashboard JSON file and pick your file.
  3. Select your Loki datasource in the Loki dropdown.
  4. Click Import.

Configure the vhost list

vhost is parsed out of the log line, not a Loki stream label, so Grafana cannot discover its values automatically. The list is hard-coded in the variable and ships with the author's own vhosts.

Go to Dashboard settings → Variables → vhost and replace the comma-separated list under Custom options with your own hostnames.

If you would rather not maintain a list, change the variable Type to Textbox and set the default value to .+. You then type a regex instead of picking from a dropdown — for example .*\.example\.com.

Global filters

Four selectors at the top apply to every panel:

  • Vhost — multi-select.
  • Status — All, 2xx, 3xx, 4xx, 5xx, 401/403, 429. Not applied to panels that already target one class, such as 5xx over range, so that picking 2xx does not blank them.
  • Method — multi-select.
  • Search — free-text regex on the raw line, applied before parsing. Leave at .* to disable. This is the cheapest filter: Loki drops lines before running any regex.

Extracted fields

All fields are extracted at query time; none of them is a Loki stream label. That matters in two ways: they cost nothing at ingestion and do not inflate your index, but they also cannot be listed by label_values() and are subject to the series limit when grouped on.

Field Type Example Extracted by
vhost string git.proxyauth.app stage 1
ip string 192.168.1.254 stage 1
method string POST stage 1
protocol string HTTP/2.0 stage 1
status number 200 stage 1
length bytes 9075 stage 1
path string /api/ds/query stage 1
tid string - when unauthenticated stage 2
rt nanoseconds 141620268 stage 3
cpu percent 0.7 stage 3
mem bytes with unit 177.7MB stage 3
ua string full user-agent UA parser
bot string Googlebot, curl UA parser
browser string Firefox, Chrome UA parser
device string Android, X11 UA parser
referer string - when direct referrer parser
ref_host string grafana.proxyauth.app referrer parser

bot, browser, device and ref_host are derived: the regex captures a short token rather than the whole string, which keeps cardinality low enough to group on. ua and referer keep the full value and are the two fields most likely to trip the series limit.

mem is read with unwrap bytes(mem), so the MB / GB suffix is converted to real bytes rather than compared as a bare number. rt is in nanoseconds and rendered with Grafana's ns unit, which rescales to µs / ms / s on display.

Panel reference

Key metrics

Panel What it shows
Requests / sec Access-log line rate. The only panel that does not parse.
Parsed lines Lines where stage 1 matched. Compare with your total to gauge parser coverage.
Tail parsed (rt/cpu/mem) Lines where stage 3 matched. Diverging from the above means the line tail differs.
5xx over range Server error count over the selected range.
P95 latency 95th percentile of rt.
Total transferred Sum of length over the range.
Requests under 100 ms Share of requests below 100 ms — a rough Apdex.
Authenticated traffic Share of requests carrying a real tid. Stays at 0 % while all tokens are -.
Median response size P50 of length.

Charts

Panel What it shows
Traffic by status class Stacked 2xx / 3xx / 4xx / 5xx rates.
Latency — P50 / P95 / P99 Latency distribution over time. A P99 far above P50 points to a slow tail.
HTTP Status / Methods / Protocol Proportional breakdown over the range.
CPU usage Average cpu sampled per request.
Memory usage (RSS) Process resident memory, a slow-moving value.
Response throughput (kB/s) rate() over unwrapped length, split by vhost.
4xx / 5xx error rate Client and server errors side by side.
Bots vs browsers Three exclusive buckets: bot, browser, unidentified.
Bot vs browser traffic The same split over time — useful to spot a crawl burst.
Top user agents Eight most frequent full user-agent strings.
P95 latency by vhost Which backend is slow, rather than a global average.
Auth failures & rate limiting 401, 403 and 429 as separate series.
Response size — P50 / P95 / P99 Payload distribution; a heavy P99 explains bandwidth spikes.
Direct vs referred traffic Requests with referer set to - versus the rest.

Tables

Panel What it shows
Top Virtual Hosts Traffic per vhost.
Top Paths (2xx/3xx) Most requested legitimate paths. Restricted to successes to keep cardinality down.
Top Client IPs (2xx / 3xx / 4xx / 5xx) Busiest clients per status class.
Top scanned paths (4xx) Bot probing targets — /.env, /wp-config.php and friends.
Top authenticated token-ids Busiest tokens. Empty while traffic is unauthenticated.
Top bots / crawlers Crawlers by name, from the derived bot field.
Platforms Android, iPhone, iPad, Windows, macOS, X11.
Browsers Browser families. Edge is folded into Chrome.
User agents (detail) Full user-agent strings, readable where the pie chart truncates.
Slowest paths (avg) Mean rt grouped by vhost and path, over 2xx/3xx only.
Top IPs by auth failures IPs accumulating 401 and 403 — the bruteforce signal.
Top referring domains Referrer hostnames, domain only.

Raw logs & debug

Panel What it shows
ProxyAuth Access Logs Unparsed lines matching the current filters.
DEBUG — unparsed tail Only lines where the tail regex failed. Empty means full coverage.

Known limits

Series limit on high-cardinality panels

Loki's max_query_series defaults to 500. Panels grouping by path, ip or ua can exceed it and show maximum number of series (500) reached. Wrapping the query in topk() does not help: the limit applies during evaluation, before the top-N reduction.

Several panels are deliberately narrowed to stay under it — Top Paths only covers 2xx/3xx, Top Client IPs is split per status class. To lift the restriction, raise the limit in your Loki config:

limits_config:
  max_query_series: 5000

On Loki 3.3+, approx_topk is an alternative that never hits the limit. It requires limits_config.shard_aggregations: approx_topk and is an experimental feature.

User-agent classification

The Bots vs browsers panel uses a denylist of known crawler tokens plus a generic *bot pattern. A scanner impersonating Firefox is counted as a browser. Watch the Unidentified slice: if it grows, inspect the logs panel for recurring user-agents and extend the pattern.

Edge is counted as Chrome, because its user-agent string contains Chrome/ before Edg/ and RE2 returns the leftmost match rather than honouring the order of alternatives.

Troubleshooting

Everything says No data. Check the stream selector first. In Explore, run {app="proxyauth"}. If that is empty, the label is wrong — see below.

Tables show a single unnamed row with a large count. The regex matched no lines: sum by (label) collapsed everything into one anonymous series. Check the Parsed lines stat at the top; it should be close to your total line count.

Latency, CPU or memory are empty. The tail of the line did not parse. Open the DEBUG — unparsed tail panel at the bottom; it lists exactly the lines that failed.

A panel returns 503. Almost always a range query using a [$__range] window, which makes Loki rescan the full time range at every step. Use $__range only with queryType: instant; use $__auto for time series.

invalid char escape. A variable value landed inside a double-quoted LogQL string. ${var:regex} escapes dots as \., which Go string syntax rejects. Put the value in backticks instead: vhost=~`${vhost:regex}` .

A filter blanks every panel. Inspect the interpolated query via ⋮ → Query inspector → Query. The panel definition can look correct while the substituted value is wrong. Note that Grafana custom variables use label : value order, not the reverse.

Adapting the stream selector

Every query starts with:

{app="proxyauth"} |= "proxyauth::access"

The Docker Loki driver does not set a job label; by default it only sets container_name, plus compose_project / compose_service under Compose. Either edit the selector in each panel, or add the label at ship time:

logging:
  driver: loki
  options:
    loki-url: "http://loki:3100/loki/api/v1/push"
    loki-external-labels: "app=proxyauth,container_name={{.Name}}"

Defining loki-external-labels replaces the default, so keep container_name in the list. The container must be recreated, not just restarted.

The |= "proxyauth::access" line filter keeps only access-log entries and excludes ProxyAuth's other structured logs. Drop it if your setup ships access logs to a dedicated stream.

Last Versions

Show All Versions