After a reconfig that changed named's views, the cache and resolver counters (summed over views) went down, which was taken for a restart: every counter then counted its total since named started for one minute. Only a new boot time means a restart now; a counter that goes down counts 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
package stats
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Snapshot is named's cumulative counters at one moment, flattened to the
|
|
// keys Jarl stores (see Counters in docs/stats.md).
|
|
type Snapshot struct {
|
|
BootTime string
|
|
Values map[string]int64
|
|
}
|
|
|
|
// serverJSON is the part of /json/v1/server Jarl uses.
|
|
type serverJSON struct {
|
|
BootTime string `json:"boot-time"`
|
|
Opcodes map[string]int64 `json:"opcodes"`
|
|
RCodes map[string]int64 `json:"rcodes"`
|
|
QTypes map[string]int64 `json:"qtypes"`
|
|
NSStats map[string]int64 `json:"nsstats"`
|
|
Views map[string]struct {
|
|
Resolver struct {
|
|
Stats map[string]int64 `json:"stats"`
|
|
CacheStats map[string]int64 `json:"cachestats"`
|
|
} `json:"resolver"`
|
|
} `json:"views"`
|
|
}
|
|
|
|
// nsstatsKeys maps named's server counters to Jarl's counter names.
|
|
var nsstatsKeys = map[string]string{
|
|
"QryAuthAns": "answer:authoritative",
|
|
"QryNoauthAns": "answer:recursive",
|
|
"QryRecursion": "recursion",
|
|
"QryDropped": "dropped",
|
|
"RateDropped": "ratelimit:dropped",
|
|
"RateSlipped": "ratelimit:slipped",
|
|
"QryTCP": "transport:tcp",
|
|
"QryUDP": "transport:udp",
|
|
"QryDuplicate": "duplicate",
|
|
}
|
|
|
|
// rttKeys maps named's upstream round-trip buckets to readable names.
|
|
var rttKeys = map[string]string{
|
|
"QryRTT10": "rtt:<10ms",
|
|
"QryRTT100": "rtt:10-100ms",
|
|
"QryRTT500": "rtt:100-500ms",
|
|
"QryRTT800": "rtt:500-800ms",
|
|
"QryRTT1600": "rtt:800-1600ms",
|
|
"QryRTT1600+": "rtt:>1600ms",
|
|
}
|
|
|
|
var dnssecKeys = map[string]string{"ValOk": "dnssec:valid", "ValNegOk": "dnssec:valid-negative", "ValFail": "dnssec:failed"}
|
|
|
|
// Flatten turns the statistics channel's JSON into a snapshot.
|
|
func Flatten(body []byte) (*Snapshot, error) {
|
|
var j serverJSON
|
|
if err := json.Unmarshal(body, &j); err != nil {
|
|
return nil, fmt.Errorf("unexpected statistics data: %w", err)
|
|
}
|
|
v := map[string]int64{"queries": j.Opcodes["QUERY"]}
|
|
for k, n := range j.RCodes {
|
|
if n != 0 && !strings.HasPrefix(k, "RESERVED") && (k[0] < '0' || k[0] > '9') {
|
|
v["rcode:"+k] = n
|
|
}
|
|
}
|
|
for k, n := range j.QTypes {
|
|
v["qtype:"+k] = n
|
|
}
|
|
for k, name := range nsstatsKeys {
|
|
if n := j.NSStats[k]; n != 0 {
|
|
v[name] = n
|
|
}
|
|
}
|
|
for _, view := range j.Views {
|
|
for k, name := range rttKeys {
|
|
v[name] += view.Resolver.Stats[k]
|
|
}
|
|
for k, name := range dnssecKeys {
|
|
v[name] += view.Resolver.Stats[k]
|
|
}
|
|
v["cache:hits"] += view.Resolver.CacheStats["QueryHits"]
|
|
v["cache:misses"] += view.Resolver.CacheStats["QueryMisses"]
|
|
}
|
|
for k, n := range v {
|
|
if n == 0 && k != "queries" {
|
|
delete(v, k)
|
|
}
|
|
}
|
|
return &Snapshot{BootTime: j.BootTime, Values: v}, nil
|
|
}
|
|
|
|
// Delta returns the change from prev to cur. When named restarted (its
|
|
// boot time changed), the counters started again from zero, so the
|
|
// current values are the change. Without a restart, a counter can still
|
|
// go down: the per-view ones (cache, upstream times, validation) are
|
|
// summed over named's views, and a reconfig that adds, removes or
|
|
// recreates views changes that sum. Such a counter counts 0 for the
|
|
// minute; it must never make every counter count from zero, which would
|
|
// add everything since named started (millions of "queries" in a minute).
|
|
func Delta(prev, cur *Snapshot) map[string]int64 {
|
|
out := map[string]int64{}
|
|
restarted := prev.BootTime != cur.BootTime
|
|
for k, n := range cur.Values {
|
|
d := n
|
|
if !restarted {
|
|
d = max(n-prev.Values[k], 0)
|
|
}
|
|
if d != 0 || k == "queries" {
|
|
out[k] = d
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Poller reads named's statistics channel.
|
|
type Poller struct {
|
|
URL string // e.g. http://127.0.0.1:8058/json/v1/server
|
|
Client *http.Client
|
|
}
|
|
|
|
// NewPoller returns a poller for a statistics channel address.
|
|
func NewPoller(addr string) *Poller {
|
|
return &Poller{URL: "http://" + addr + "/json/v1/server", Client: &http.Client{Timeout: 10 * time.Second}}
|
|
}
|
|
|
|
// Poll fetches a snapshot.
|
|
func (p *Poller) Poll(ctx context.Context) (*Snapshot, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.URL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := p.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("named's statistics channel is not reachable (%v); is the configuration with statistics applied and named running?", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("named's statistics channel answered %s", resp.Status)
|
|
}
|
|
return Flatten(body)
|
|
}
|