Documentation
¶
Index ¶
- Variables
- func Backup(dbPath, dest string) error
- func HaveCert(cfg Config) bool
- func LoadTLS(cfg Config) (*tls.Config, error)
- func NewLogger(cfg Config) *slog.Logger
- func NewRouteID() (string, error)
- func RegisterMigrations(extra ...Migration)
- func SchemaVersion() int
- func VerifyBackup(path string) (schemaVersion int, tenants int, err error)
- func WriteSample(path string) error
- type Admin
- func (a *Admin) Routes() http.Handler
- func (a *Admin) Serve(addr string) error
- func (a *Admin) Shutdown()
- func (a *Admin) WithHealth(h *Health) *Admin
- func (a *Admin) WithLogger(l *slog.Logger) *Admin
- func (a *Admin) WithProbes(p *ProbeRecorder) *Admin
- func (a *Admin) WithRateLimiter(r *RateLimiter) *Admin
- type Blocklist
- type Cache
- type Check
- type Config
- type Health
- type HealthReport
- type Listeners
- func (l *Listeners) ServeDoH(addr string) error
- func (l *Listeners) ServeDoT(addr string) error
- func (l *Listeners) ServeDoTListener(ln net.Listener) error
- func (l *Listeners) ServePlain(addr string) error
- func (l *Listeners) Shutdown()
- func (l *Listeners) WithLogger(lg *slog.Logger) *Listeners
- func (l *Listeners) WithRateLimiter(r *RateLimiter) *Listeners
- type Metrics
- type Migration
- type OverrideRow
- type ProbeRecorder
- type ProbeResult
- type RateLimiter
- type Resolver
- func (r *Resolver) Probes() *ProbeRecorder
- func (r *Resolver) Resolve(req *dns.Msg, id identity) *dns.Msg
- func (r *Resolver) WithLogger(l *slog.Logger) *Resolver
- func (r *Resolver) WithProbes(p *ProbeRecorder) *Resolver
- func (r *Resolver) WithRateLimiter(l *RateLimiter) *Resolver
- func (r *Resolver) WithUsage(u *UsageCollector) *Resolver
- type SQLiteStore
- func (s *SQLiteStore) AddAllow(routeID, domain string) error
- func (s *SQLiteStore) Allowed(routeID, name string) bool
- func (s *SQLiteStore) Close() error
- func (s *SQLiteStore) CreateTenant(routeID, label string, expiresAt int64) error
- func (s *SQLiteStore) Extend(routeID string, expiresAt int64) error
- func (s *SQLiteStore) ListAllow(routeID string) ([]string, error)
- func (s *SQLiteStore) ListIPs(routeID string) ([]string, error)
- func (s *SQLiteStore) ListOverrides() ([]OverrideRow, error)
- func (s *SQLiteStore) Override(routeID, name string) (netip.Addr, bool)
- func (s *SQLiteStore) PauseFiltering(routeID string, until int64) error
- func (s *SQLiteStore) Ping(ctx context.Context) error
- func (s *SQLiteStore) RecordUsage(counts map[string]UsageDelta) error
- func (s *SQLiteStore) RegisterIP(routeID, ip string) error
- func (s *SQLiteStore) ReleaseIP(ip string) error
- func (s *SQLiteStore) Reload() error
- func (s *SQLiteStore) RemoveAllow(routeID, domain string) error
- func (s *SQLiteStore) RemoveOverride(routeID, domain string) error
- func (s *SQLiteStore) SchemaVersion() (int, error)
- func (s *SQLiteStore) SetOverride(routeID, domain, answer string) error
- func (s *SQLiteStore) SetStatus(routeID, status string) error
- func (s *SQLiteStore) Tenant(routeID string) *Tenant
- func (s *SQLiteStore) TenantByIP(ip string) *Tenant
- func (s *SQLiteStore) TenantCount() int
- func (s *SQLiteStore) Usage(routeID string) (Usage, bool)
- func (s *SQLiteStore) WatchReload(every time.Duration, onErr func(error))
- type Server
- type Store
- type Tenant
- type Usage
- type UsageCollector
- type UsageDelta
Constants ¶
This section is empty.
Variables ¶
var Version = "dev"
Version is the build version, set by the command wrapper.
Functions ¶
func Backup ¶
Backup writes a consistent snapshot of the policy database to dest.
This exists as a Go function rather than a shell one-liner because copying the file is wrong. The database runs in WAL mode, so at any moment the committed state is split between the main file and the write-ahead log — `cp policy.db backup.db` captures the first without the second and produces a backup that restores to a torn, sometimes unreadable, database.
VACUUM INTO takes a read transaction and writes a complete, defragmented copy, safely while the service is still serving. Nothing has to be stopped.
func HaveCert ¶ added in v1.0.6
HaveCert reports whether a usable certificate and key are on disk right now.
Used to decide what to log at startup and what readiness should say, so the difference between "waiting for a certificate" and "serving" is visible without reading a handshake failure.
func LoadTLS ¶
LoadTLS builds the TLS configuration shared by the DoT and DoH listeners.
The certificate must cover *.<base_domain>, because every tenant connects to its own hostname beneath that base. A wildcard can only be issued over the ACME DNS-01 challenge — HTTP-01 cannot produce one.
Certificates are re-read from disk rather than pinned at startup, so a renewal takes effect without restarting the service or dropping connections.
A missing certificate is NOT an error here. On a fresh install there is no certificate until the operator has pointed DNS at the host and run the ACME client, and refusing to start until then took the whole service down -- including the admin endpoint needed to check on it, and the plain listener. The process would exit, systemd would restart it, and the log filled with hundreds of identical failures while the operator worked through the setup steps. Instead the listeners come up and handshakes fail until a certificate appears, at which point they start succeeding on their own within certReloadInterval. HaveCert reports which state you are in.
func NewLogger ¶
NewLogger builds the structured logger from configuration.
Text is the default because an operator reading journalctl wants readable lines; JSON suits a log shipper. Either way the resolver logs events and aggregate counts, never the domains a customer looked up.
func NewRouteID ¶
NewRouteID returns a short, unambiguous tenant identifier. The alphabet omits look-alike characters because customers read these off a screen and type them into a phone.
func RegisterMigrations ¶
func RegisterMigrations(extra ...Migration)
RegisterMigrations adds schema steps owned by another package.
The resolver and the backend share one SQLite database, and one database can only have one ordered migration history -- two independent trackers against the same file would race and diverge. So each package owns its own migrations and registers them here, where they are merged into a single ordered sequence.
Version ranges are partitioned by owner to keep them from colliding: 1-99 belongs to the resolver, 100+ to the backend.
func SchemaVersion ¶
func SchemaVersion() int
SchemaVersion is the version a freshly migrated database ends at.
func VerifyBackup ¶
VerifyBackup checks that a file is a usable policy database rather than a truncated or corrupt one.
Worth doing at the moment a backup is taken. A backup nobody has opened is an assumption, and the moment it matters is the worst time to find out.
func WriteSample ¶
WriteSample writes a starter configuration so a fresh install has something to edit. The format follows the file extension.
Types ¶
type Admin ¶
type Admin struct {
// contains filtered or unexported fields
}
Admin is the provisioning and metrics surface. The billing system calls it when an order is paid; the dashboard calls it for "update my IP".
It binds to localhost by default. Expose it only behind a reverse proxy on an internal network, never directly to the internet.
func (*Admin) Routes ¶ added in v1.0.10
Routes builds the admin mux.
Separate from Serve so the route table can be asserted in a test. A client asking for a path that was never registered gets a 404, which curl -f reports the same way it reports an unreachable host -- so a missing route reads as a dead service.
func (*Admin) Shutdown ¶
func (a *Admin) Shutdown()
Shutdown stops the admin listener, allowing in-flight requests to finish.
func (*Admin) WithHealth ¶
func (*Admin) WithProbes ¶
func (a *Admin) WithProbes(p *ProbeRecorder) *Admin
func (*Admin) WithRateLimiter ¶
func (a *Admin) WithRateLimiter(r *RateLimiter) *Admin
type Blocklist ¶
type Blocklist struct {
// contains filtered or unexported fields
}
Blocklist holds the compiled set of blocked names. Lists are parsed once into a hash set and swapped atomically, so a reload never blocks queries.
func NewBlocklist ¶
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is a shared, tenant-agnostic answer cache sitting in front of the upstreams. It holds only upstream answers — blocked, allowlisted and overridden results are decided per tenant and never cached here.
func (*Cache) StartSweeper ¶
StartSweeper drops expired entries so an idle cache does not grow forever.
type Check ¶
type Check struct {
Name string `json:"name"`
OK bool `json:"ok"`
Detail string `json:"detail,omitempty"`
Latency string `json:"latency,omitempty"`
}
Check is one component's health.
type Config ¶
type Config struct {
// BaseDomain is the suffix that per-tenant hostnames hang off. A client
// connecting to "a1b2c3.dns.example.com" with BaseDomain "dns.example.com"
// is identified as tenant "a1b2c3".
BaseDomain string `yaml:"base_domain" json:"base_domain"`
CertFile string `yaml:"cert_file" json:"cert_file"`
KeyFile string `yaml:"key_file" json:"key_file"`
ListenDoT string `yaml:"listen_dot" json:"listen_dot"` // ":853", empty disables
ListenDoH string `yaml:"listen_doh" json:"listen_doh"` // ":443", empty disables
ListenPlain string `yaml:"listen_plain" json:"listen_plain"` // ":53", empty disables
ListenAdmin string `yaml:"listen_admin" json:"listen_admin"` // metrics + provisioning
// Upstreams are the recursive resolvers queries are forwarded to. Point
// these at a local Unbound in production; a public resolver works for
// testing but sees every query your customers make.
Upstreams []string `yaml:"upstreams" json:"upstreams"`
DBPath string `yaml:"db_path" json:"db_path"`
BlocklistDir string `yaml:"blocklist_dir" json:"blocklist_dir"`
AdminTokens []string `yaml:"admin_tokens" json:"admin_tokens"`
// OpenPlain allows unauthenticated queries on the plain :53 listener.
// Leave false. An open resolver is a DNS amplification vector and will
// get the host onto abuse lists.
OpenPlain bool `yaml:"open_plain" json:"open_plain"`
// RateLimitQPS caps sustained queries per second for a single tenant.
// Zero disables rate limiting entirely.
RateLimitQPS float64 `yaml:"rate_limit_qps" json:"rate_limit_qps"`
// RateLimitBurst is how far above the sustained rate a tenant may spike.
// Page loads arrive in bursts, so this needs headroom or normal browsing
// gets throttled.
RateLimitBurst int `yaml:"rate_limit_burst" json:"rate_limit_burst"`
// MaxConnsPerTenant bounds concurrent DoT connections from one tenant.
// Zero means unlimited.
MaxConnsPerTenant int `yaml:"max_conns_per_tenant" json:"max_conns_per_tenant"`
// StripECS removes EDNS Client Subnet from forwarded queries so upstream
// resolvers cannot learn which subnet a customer is on. On by default:
// privacy is the product.
StripECS bool `yaml:"strip_ecs" json:"strip_ecs"`
// BlockRebind strips private-space addresses from upstream answers.
//
// On by default. A public name resolving to 192.168.x has no legitimate
// use and is how a browser gets turned into a proxy into the network it
// sits on.
BlockRebind bool `yaml:"block_rebind" json:"block_rebind"`
// RebindAllowDomains are names permitted to resolve into private space.
//
// Split-horizon DNS is a real arrangement -- an internal name resolving to
// 10.x is correct on a corporate network -- so it needs an exemption rather
// than forcing the protection off entirely.
RebindAllowDomains []string `yaml:"rebind_allow_domains" json:"rebind_allow_domains"`
// LogLevel is one of debug, info, warn, error.
LogLevel string `yaml:"log_level" json:"log_level"`
// LogFormat is "text" or "json". JSON suits log shipping; text is
// readable in a terminal.
LogFormat string `yaml:"log_format" json:"log_format"`
}
Config is the resolver's configuration. Every environment-specific value lives here rather than in source: no domain, address, path or credential is compiled into the binary.
func DefaultConfig ¶
func DefaultConfig() Config
func LoadConfig ¶
LoadConfig reads configuration from path, then applies environment overrides. The format is chosen by file extension: .yaml/.yml parse as YAML, anything else as JSON.
type Health ¶
type Health struct {
// contains filtered or unexported fields
}
Health probes the resolver's real dependencies.
The point of these checks is that they exercise the actual serving path. A handler that returns 200 because the process is running tells an operator nothing: the process can be up while the upstream is unreachable, the database is locked, or the certificate expired last night.
func (*Health) Live ¶
Live reports process liveness. It is deliberately trivial: a liveness probe that fails on a dependency outage causes an orchestrator to kill a process that would have recovered on its own.
type HealthReport ¶
type HealthReport struct {
OK bool `json:"ok"`
Version string `json:"version"`
Uptime string `json:"uptime"`
Checks []Check `json:"checks"`
}
HealthReport is the aggregate result returned by /ready.
type Listeners ¶
type Listeners struct {
// contains filtered or unexported fields
}
Listeners wires the transports to the resolver. Each transport differs only in how it establishes the tenant identity.
func NewListeners ¶
func (*Listeners) ServeDoH ¶
ServeDoH serves RFC 8484 DoH. The tenant comes from the SNI exactly as it does for DoT, so one wildcard certificate covers both transports.
func (*Listeners) ServeDoT ¶
ServeDoT accepts TLS connections and reads the tenant from the SNI in the ClientHello. This is the whole per-customer identification mechanism: the customer sets Private DNS to "<routeID>.<base_domain>", and the routeID arrives in the handshake before a single query is sent.
func (*Listeners) ServeDoTListener ¶
ServeDoTListener serves DoT on an already-established TLS listener.
func (*Listeners) ServePlain ¶
ServePlain serves unencrypted DNS, where the only available identity is the source address. Needed because iOS profiles, routers and older Android cannot all speak DoT.
func (*Listeners) Shutdown ¶
func (l *Listeners) Shutdown()
Shutdown closes every listener. In-flight queries finish because each connection has its own deadline; new connections are refused immediately.
func (*Listeners) WithRateLimiter ¶
func (l *Listeners) WithRateLimiter(r *RateLimiter) *Listeners
type Metrics ¶
type Metrics struct {
Queries atomic.Uint64
Blocked atomic.Uint64
Overridden atomic.Uint64
Allowed atomic.Uint64
Refused atomic.Uint64
Throttled atomic.Uint64
Malformed atomic.Uint64
Rebind atomic.Uint64
CacheHits atomic.Uint64
Upstream atomic.Uint64
UpstreamNG atomic.Uint64
}
Metrics are exposed in Prometheus text format on the admin listener.
type Migration ¶
Migration is one forward schema step. Migrations are applied in Version order inside a transaction and recorded, so a given version runs exactly once against a database.
Never edit a migration that has shipped. Anyone already running it will not re-apply the changed version, so their schema silently diverges. Add a new migration instead.
type OverrideRow ¶
OverrideRow is one answer-override rule as stored.
type ProbeRecorder ¶
type ProbeRecorder struct {
// contains filtered or unexported fields
}
ProbeRecorder answers the question the diagnostic page needs: did this device's DNS query actually reach us, and as which tenant?
The mechanism is the only one that genuinely works from a browser. A page cannot inspect the system resolver, but it can ask for a hostname nobody has ever looked up before. If that lookup arrives here, the device is using this resolver; if it does not, it is not. The tenant comes free, because the request is already identified by SNI.
Probes are held in memory rather than the database. They are worthless after a couple of minutes, and writing one per diagnostic would put the query path on a disk write for a feature used once per customer.
func NewProbeRecorder ¶
func NewProbeRecorder(baseDomain string) *ProbeRecorder
func (*ProbeRecorder) Lookup ¶
func (p *ProbeRecorder) Lookup(nonce string) ProbeResult
Lookup reads a probe back, once. Consuming the entry means a nonce cannot be replayed to make a second device look configured.
func (*ProbeRecorder) Pending ¶
func (p *ProbeRecorder) Pending() int
func (*ProbeRecorder) Record ¶
func (p *ProbeRecorder) Record(nonce, routeID, proto string)
Record notes that a probe was seen. Older entries are swept opportunistically so the map cannot grow without bound.
type ProbeResult ¶
ProbeResult is what a diagnostic lookup produced.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter caps how much traffic one tenant may generate.
This matters because a tenant hostname is not a secret: it travels in the SNI in cleartext and a customer may share it, deliberately or not. Without a limit, one leaked hostname can be used to flood the resolver on that tenant's behalf.
func NewRateLimiter ¶
func NewRateLimiter(qps float64, burst, maxConns int) *RateLimiter
NewRateLimiter returns a limiter, or nil if qps is zero or negative, in which case rate limiting is disabled entirely. A nil *RateLimiter is safe to call — every method allows.
func (*RateLimiter) AcquireConn ¶
func (r *RateLimiter) AcquireConn(routeID string) (release func(), ok bool)
AcquireConn reserves a concurrent-connection slot for a tenant. The returned release function must be called when the connection closes; it is safe to call on the failure path too.
func (*RateLimiter) Allow ¶
func (r *RateLimiter) Allow(routeID string) bool
Allow reports whether a query from this tenant may proceed, consuming one token if so.
func (*RateLimiter) StartSweeper ¶
func (r *RateLimiter) StartSweeper(every time.Duration, stop <-chan struct{})
StartSweeper drops buckets for tenants that have gone quiet, so memory tracks active tenants rather than every tenant ever seen.
func (*RateLimiter) Tracked ¶
func (r *RateLimiter) Tracked() int
Tracked reports how many tenant buckets are currently held.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
func NewResolver ¶
func (*Resolver) Probes ¶
func (r *Resolver) Probes() *ProbeRecorder
Probes exposes the recorder so the portal can read results back.
func (*Resolver) Resolve ¶
Resolve runs one query through the policy pipeline and returns the reply.
func (*Resolver) WithLogger ¶
WithLogger attaches a structured logger.
func (*Resolver) WithProbes ¶
func (r *Resolver) WithProbes(p *ProbeRecorder) *Resolver
WithProbes attaches the diagnostic probe recorder.
func (*Resolver) WithRateLimiter ¶
func (r *Resolver) WithRateLimiter(l *RateLimiter) *Resolver
WithRateLimiter attaches a limiter. A nil limiter disables rate limiting.
func (*Resolver) WithUsage ¶
func (r *Resolver) WithUsage(u *UsageCollector) *Resolver
WithUsage attaches the usage collector.
type SQLiteStore ¶
type SQLiteStore struct {
// contains filtered or unexported fields
}
SQLiteStore keeps policy in SQLite and serves reads from an in-memory snapshot that is rebuilt on a timer. Queries never touch the database.
func OpenStore ¶
func OpenStore(path string) (*SQLiteStore, error)
func (*SQLiteStore) AddAllow ¶
func (s *SQLiteStore) AddAllow(routeID, domain string) error
func (*SQLiteStore) Allowed ¶
func (s *SQLiteStore) Allowed(routeID, name string) bool
Allowed reports whether the name (or a parent of it) is on the tenant's allowlist or the global one. Allowlist beats both blocklist and override.
func (*SQLiteStore) Close ¶
func (s *SQLiteStore) Close() error
func (*SQLiteStore) CreateTenant ¶
func (s *SQLiteStore) CreateTenant(routeID, label string, expiresAt int64) error
func (*SQLiteStore) ListAllow ¶
func (s *SQLiteStore) ListAllow(routeID string) ([]string, error)
ListAllow returns a tenant's allowlist entries.
func (*SQLiteStore) ListIPs ¶
func (s *SQLiteStore) ListIPs(routeID string) ([]string, error)
ListIPs returns the source addresses bound to a tenant.
func (*SQLiteStore) ListOverrides ¶
func (s *SQLiteStore) ListOverrides() ([]OverrideRow, error)
ListOverrides returns every override rule, global ones first.
func (*SQLiteStore) Override ¶
func (s *SQLiteStore) Override(routeID, name string) (netip.Addr, bool)
Override returns the address to answer with, if this name or any parent of it has an override. Tenant-specific rules beat global ones.
func (*SQLiteStore) PauseFiltering ¶
func (s *SQLiteStore) PauseFiltering(routeID string, until int64) error
func (*SQLiteStore) RecordUsage ¶
func (s *SQLiteStore) RecordUsage(counts map[string]UsageDelta) error
RecordUsage applies a batch of counter increments in one transaction. Counters are flushed periodically rather than written per query, so a busy resolver does not turn every lookup into a database write.
func (*SQLiteStore) RegisterIP ¶
func (s *SQLiteStore) RegisterIP(routeID, ip string) error
RegisterIP binds a source address to a tenant. This is what the "update my IP" control in the dashboard calls, and it is the most-used action in the product for customers on mobile networks.
func (*SQLiteStore) ReleaseIP ¶
func (s *SQLiteStore) ReleaseIP(ip string) error
func (*SQLiteStore) Reload ¶
func (s *SQLiteStore) Reload() error
Reload rebuilds the in-memory snapshot. Called on a one-second timer, which is what bounds revocation latency.
func (*SQLiteStore) RemoveAllow ¶
func (s *SQLiteStore) RemoveAllow(routeID, domain string) error
func (*SQLiteStore) RemoveOverride ¶
func (s *SQLiteStore) RemoveOverride(routeID, domain string) error
func (*SQLiteStore) SchemaVersion ¶
func (s *SQLiteStore) SchemaVersion() (int, error)
func (*SQLiteStore) SetOverride ¶
func (s *SQLiteStore) SetOverride(routeID, domain, answer string) error
func (*SQLiteStore) SetStatus ¶
func (s *SQLiteStore) SetStatus(routeID, status string) error
func (*SQLiteStore) Tenant ¶
func (s *SQLiteStore) Tenant(routeID string) *Tenant
func (*SQLiteStore) TenantByIP ¶
func (s *SQLiteStore) TenantByIP(ip string) *Tenant
func (*SQLiteStore) TenantCount ¶
func (s *SQLiteStore) TenantCount() int
func (*SQLiteStore) WatchReload ¶
func (s *SQLiteStore) WatchReload(every time.Duration, onErr func(error))
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server owns the resolver's long-lived state.
func New ¶
New assembles a Server from configuration, opening the policy store and loading blocklists. The caller owns shutdown via Close.
func (*Server) Probes ¶
func (s *Server) Probes() *ProbeRecorder
type Store ¶
type Store interface {
// Read path — called on every query, must be cheap and non-blocking.
Tenant(routeID string) *Tenant
TenantByIP(ip string) *Tenant
Allowed(routeID, name string) bool
Override(routeID, name string) (netip.Addr, bool)
TenantCount() int
// Write path — provisioning, called by the admin API.
CreateTenant(routeID, label string, expiresAt int64) error
SetStatus(routeID, status string) error
Extend(routeID string, expiresAt int64) error
PauseFiltering(routeID string, until int64) error
RegisterIP(routeID, ip string) error
ReleaseIP(ip string) error
AddAllow(routeID, domain string) error
RemoveAllow(routeID, domain string) error
SetOverride(routeID, domain, answer string) error
RemoveOverride(routeID, domain string) error
// Usage accounting — aggregates only, never per-query history.
RecordUsage(counts map[string]UsageDelta) error
Usage(routeID string) (Usage, bool)
// Listing, for the operator dashboard. These read the database directly
// rather than the serving snapshot: an operator wants the committed truth,
// not what the resolver happens to have cached this second.
ListAllow(routeID string) ([]string, error)
ListIPs(routeID string) ([]string, error)
ListOverrides() ([]OverrideRow, error)
// Lifecycle.
Reload() error
WatchReload(every time.Duration, onErr func(error))
Ping(ctx context.Context) error
SchemaVersion() (int, error)
Close() error
}
Store is the policy interface the resolver depends on. SQLiteStore is the only implementation today; the interface exists so a PostgreSQL backend can be added later without touching the query path.
type Tenant ¶
type UsageCollector ¶
type UsageCollector struct {
// contains filtered or unexported fields
}
UsageCollector accumulates per-tenant counters in memory and flushes them to the store periodically.
Counting in the query path and writing in a background flush is deliberate: a per-query database write would turn a cheap in-memory lookup into a disk operation, and DNS query rates are high enough that it would dominate the cost of resolution.
Note what is counted and what is not. These are aggregate totals per tenant. The names a customer looked up are never recorded — that is the strongest privacy claim the product has, and it also keeps storage flat regardless of traffic.
func NewUsageCollector ¶
func NewUsageCollector(store Store) *UsageCollector
func (*UsageCollector) Flush ¶
func (u *UsageCollector) Flush() error
Flush writes pending counters to the store and clears them. Counters are taken under the lock and written outside it, so the query path is never blocked by a database write.
func (*UsageCollector) PendingTenants ¶
func (u *UsageCollector) PendingTenants() int
PendingTenants reports how many tenants have unflushed counters.
func (*UsageCollector) Record ¶
func (u *UsageCollector) Record(routeID string, blocked, overridden, throttled bool)
Record adds one query's outcome to the pending totals.
func (*UsageCollector) StartFlusher ¶
func (u *UsageCollector) StartFlusher(every time.Duration, stop <-chan struct{}, onErr func(error))
StartFlusher flushes on a timer until stop is closed, then flushes once more so a clean shutdown does not lose the final interval's counts.