Skip to content

Metrics ​

When metrics_endpoint is configured, the WAF serves a JSON metrics document at the given path. The document is emitted by handleMetricsRequest in caddywaf.go and reflects the counters maintained by the runtime.

Endpoint ​

caddyfile
waf {
    metrics_endpoint /waf_metrics
    ...
}

The path must start with /. There is no authentication on the endpoint; if you expose it externally, place authentication in front of it (basic auth, IP allow-list, mTLS at the proxy, etc.).

Response format ​

  • HTTP status: 200 OK
  • Content-Type: application/json
  • Body: a single JSON object with the fields below.

A representative response:

json
{
  "total_requests":                27004,
  "blocked_requests":              25328,
  "allowed_requests":              1509,
  "rule_hits": {
    "block-scanners":               25,
    "sql-injection":                7,
    "xss-attacks":                  8,
    "log4j-jndi":                   2
  },
  "rule_hits_by_phase": {
    "1": 1461,
    "2": 705
  },
  "geoip_blocked":                 0,
  "ip_blacklist_hits":             0,
  "dns_blacklist_hits":            0,
  "rate_limiter_requests":         27004,
  "rate_limiter_blocked_requests": 23640,
  "version":                       "v0.4.16",

  "schema_version":  2,
  "server_time_ms":  1756876543210,
  "uptime_seconds":  86400,
  "top_rules": [
    { "id": "block-scanners", "hits": 25 },
    { "id": "xss-attacks",    "hits": 8 }
  ],
  "top_ips": {
    "cap": 50,
    "distinct_seen": 318,
    "items": [ { "ip": "203.0.113.7", "blocked": 640 } ]
  },
  "by_country":        [ { "country": "RU", "blocked": 900 } ],
  "blocked_by_reason": { "rule": 1826, "ip_blacklist": 1290, "rate_limit": 640 },
  "recent": {
    "cap": 256,
    "items": [
      {
        "ts_ms": 1756876543000, "id": "aca34e69-c2ff-…",
        "ip": "203.0.113.7", "method": "POST", "path": "/wp-login.php",
        "reason": "rule", "rule_id": "941100", "status": 403, "score": 10,
        "country": "RU"
      }
    ]
  }
}

Field reference ​

FieldTypeSourceMeaning
total_requestsintMiddleware.totalRequestsTotal number of requests that entered ServeHTTP.
blocked_requestsintMiddleware.blockedRequestsTotal number of requests that resulted in a block (Phase 1 pre-checks, anomaly threshold, or mode: block).
allowed_requestsintMiddleware.allowedRequestsTotal number of requests that were forwarded to the next handler and not subsequently blocked.
rule_hitsobject<string,int>Middleware.ruleHits (atomic counters per rule ID)Number of times each rule's regex matched. The key is the rule's id.
rule_hits_by_phaseobject<string,int>Middleware.ruleHitsByPhasePer-phase total of rule hits. Keys are phase numbers as strings ("1", "2", "3", "4").
geoip_blockedintMiddleware.geoIPBlockedNumber of requests blocked by block_countries, whitelist_countries, or block_asns.
ip_blacklist_hitsintMiddleware.IPBlacklistBlockCountNumber of requests whose source IP matched the IP blacklist trie.
dns_blacklist_hitsintMiddleware.DNSBlacklistBlockCountNumber of requests whose Host matched the DNS blacklist map.
rate_limiter_requestsintRateLimiter.totalRequestsTotal requests counted by the rate limiter (under lock).
rate_limiter_blocked_requestsintRateLimiter.blockedRequestsRequests blocked by the rate limiter for exceeding the bucket limit.
versionstringwafVersion constant in caddywaf.goBuild version of the WAF.

Dashboard fields (schema 2) ​

The built-in dashboard (#143) adds the fields below, all back-compatible additions — every field above is unchanged. schema_version lets a consumer feature-detect them. Rates are not stored server-side: a client derives them by diffing successive snapshots using server_time_ms.

FieldTypeMeaning
schema_versionintPayload schema version (currently 2).
server_time_msintServer wall-clock at scrape time (Unix ms), for client-side rate math.
uptime_secondsintSeconds since Provision; a drop signals a restart (counters reset).
top_rulesarray of {id, hits}The most-hit rules, sorted, capped at 20 — a pre-sorted view of rule_hits.
top_ips{cap, distinct_seen, items:[{ip, blocked}]}The top offending client IPs by block count (capped at 50). distinct_seen is how many distinct IPs the bounded counter has tracked.
by_countryarray of {country, blocked}Blocks per ISO country, when a GeoIP database is configured.
blocked_by_reasonobject<string,int>Block counts by category: rule, ip_blacklist, dns_blacklist, rate_limit, country, asn, error.
recent{cap, items:[…]}A bounded ring (256) of the most recent blocked decisions, newest first. Each item: ts_ms, id (the WAF log id, correlates with access logs), ip, method, path (≤256 chars), reason, rule_id (for rule blocks), status, score, country.

Counter semantics ​

  • All counters are monotonic and process-local. They reset when the process restarts and they are not aggregated across multiple Caddy instances.
  • rule_hits keys appear only after the corresponding rule fires for the first time. A rule that has never matched is absent from the map (not present with value 0).
  • rule_hits_by_phase only contains keys for phases that have at least one hit.
  • total_requests may exceed blocked_requests + allowed_requests briefly during a request that has not yet finished evaluating, because each counter is incremented at a distinct point in the pipeline.
  • rate_limiter_requests includes both blocked and allowed requests, but only requests that actually pass through the limiter (see ratelimit.md for the path-matching subtleties).
  • ip_blacklist_hits, dns_blacklist_hits, and geoip_blocked count matches, not blocking decisions. In practice the two are equivalent because a match in any of these layers immediately blocks the request.

Sample queries ​

bash
# Raw JSON
curl -s http://localhost:8080/waf_metrics | jq .

# Top 10 most-hit rules
curl -s http://localhost:8080/waf_metrics | jq '.rule_hits | to_entries | sort_by(-.value) | .[:10]'

# Block ratio
curl -s http://localhost:8080/waf_metrics | jq '.blocked_requests / .total_requests'

# Hits by phase as a CSV
curl -s http://localhost:8080/waf_metrics \
  | jq -r '.rule_hits_by_phase | to_entries | .[] | "\(.key),\(.value)"'

Prometheus ​

The endpoint is plain JSON. To scrape it as Prometheus metrics, use the small exporter described in prometheus.md.

Operational notes ​

  • The metrics handler is wired through ServeHTTP; if a request matches metrics_endpoint, the WAF still runs Phase 1 and Phase 2 checks before the response is generated. A blocked request to the metrics path will return the block response, not the metrics document.
  • The endpoint serializes the in-memory state under the metrics mutex; cost is O(rules-with-hits) per request.
  • Because the response is small and uncached, exposing it on a debugging route or to an internal monitoring system is appropriate; do not put it on a high-RPS public path.