resolver

package
v1.0.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: AGPL-3.0 Imports: 29 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Version = "dev"

Version is the build version, set by the command wrapper.

Functions

func Backup

func Backup(dbPath, dest string) error

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

func HaveCert(cfg Config) bool

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

func LoadTLS(cfg Config) (*tls.Config, error)

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

func NewLogger(cfg Config) *slog.Logger

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

func NewRouteID() (string, error)

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

func VerifyBackup(path string) (schemaVersion int, tenants int, err error)

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

func WriteSample(path string) error

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 NewAdmin

func NewAdmin(cfg Config, store Store, block *Blocklist, cache *Cache, m *Metrics) *Admin

func (*Admin) Routes added in v1.0.10

func (a *Admin) Routes() http.Handler

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) Serve

func (a *Admin) Serve(addr string) error

func (*Admin) Shutdown

func (a *Admin) Shutdown()

Shutdown stops the admin listener, allowing in-flight requests to finish.

func (*Admin) WithHealth

func (a *Admin) WithHealth(h *Health) *Admin

func (*Admin) WithLogger

func (a *Admin) WithLogger(l *slog.Logger) *Admin

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

func NewBlocklist(dir string) *Blocklist

func (*Blocklist) Blocked

func (b *Blocklist) Blocked(name string) bool

Blocked reports whether the name or any parent domain is listed.

func (*Blocklist) Load

func (b *Blocklist) Load() (int, error)

Load reads every *.txt file in the directory. Both plain domain lists and hosts-format files ("0.0.0.0 ads.example.com") are accepted, which covers the common feeds without needing a per-source parser.

func (*Blocklist) Size

func (b *Blocklist) Size() int

func (*Blocklist) WatchReload

func (b *Blocklist) WatchReload(every time.Duration, onReload func(int, error))

WatchReload re-reads the lists on a timer so updating a feed does not require a restart or drop a single connection.

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 NewCache

func NewCache() *Cache

func (*Cache) Get

func (c *Cache) Get(q dns.Question) (*dns.Msg, bool)

func (*Cache) Len

func (c *Cache) Len() int

func (*Cache) Put

func (c *Cache) Put(q dns.Question, msg *dns.Msg)

func (*Cache) StartSweeper

func (c *Cache) StartSweeper(every time.Duration)

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

func LoadConfig(path string) (Config, error)

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.

func (*Config) Validate

func (c *Config) Validate() error

Validate normalises the configuration and rejects combinations that cannot work, so a mistake surfaces at startup rather than as a puzzling runtime failure hours later.

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 NewHealth

func NewHealth(cfg Config, store Store, block *Blocklist, version string) *Health

func (*Health) Live

func (h *Health) Live() bool

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.

func (*Health) Ready

func (h *Health) Ready(ctx context.Context) HealthReport

Ready runs every dependency check and reports whether the resolver can actually serve traffic.

func (*Health) Uptime

func (h *Health) Uptime() time.Duration

func (*Health) Version

func (h *Health) Version() string

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 NewListeners(cfg Config, res *Resolver, st Store, tlsCfg *tls.Config) *Listeners

func (*Listeners) ServeDoH

func (l *Listeners) ServeDoH(addr string) error

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

func (l *Listeners) ServeDoT(addr string) error

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

func (l *Listeners) ServeDoTListener(ln net.Listener) error

ServeDoTListener serves DoT on an already-established TLS listener.

func (*Listeners) ServePlain

func (l *Listeners) ServePlain(addr string) error

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) WithLogger

func (l *Listeners) WithLogger(lg *slog.Logger) *Listeners

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

type Migration struct {
	Version int
	Name    string
	SQL     string
}

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

type OverrideRow struct {
	RouteID string
	Domain  string
	Answer  string
}

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

type ProbeResult struct {
	RouteID string
	At      time.Time
	Proto   string
	Found   bool
}

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 NewResolver(cfg Config, store Store, block *Blocklist, cache *Cache, m *Metrics) *Resolver

func (*Resolver) Probes

func (r *Resolver) Probes() *ProbeRecorder

Probes exposes the recorder so the portal can read results back.

func (*Resolver) Resolve

func (r *Resolver) Resolve(req *dns.Msg, id identity) *dns.Msg

Resolve runs one query through the policy pipeline and returns the reply.

func (*Resolver) WithLogger

func (r *Resolver) WithLogger(l *slog.Logger) *Resolver

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) Extend

func (s *SQLiteStore) Extend(routeID 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) Ping

func (s *SQLiteStore) Ping(ctx context.Context) 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) Usage

func (s *SQLiteStore) Usage(routeID string) (Usage, bool)

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

func New(cfg Config) (*Server, error)

New assembles a Server from configuration, opening the policy store and loading blocklists. The caller owns shutdown via Close.

func (*Server) Blocklist

func (s *Server) Blocklist() *Blocklist

func (*Server) Close

func (s *Server) Close() error

func (*Server) Health

func (s *Server) Health() *Health

func (*Server) Metrics

func (s *Server) Metrics() *Metrics

func (*Server) Probes

func (s *Server) Probes() *ProbeRecorder

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run starts every configured listener and blocks until one fails or ctx is cancelled. Listeners with an empty address in the config are skipped.

func (*Server) Store

func (s *Server) Store() Store

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 Tenant struct {
	RouteID     string
	Label       string
	Status      string
	ExpiresAt   int64
	BlockAds    bool
	PausedUntil int64
}

func (*Tenant) Active

func (t *Tenant) Active(now int64) bool

func (*Tenant) Filtering

func (t *Tenant) Filtering(now int64) bool

Filtering reports whether blocklists should apply right now. A tenant can pause filtering temporarily, which is how a false positive gets unblocked without a support ticket.

type Usage

type Usage struct {
	Queries    int64
	Blocked    int64
	Overridden int64
	Throttled  int64
	LastSeen   int64
}

Usage holds a tenant's cumulative counters.

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.

type UsageDelta

type UsageDelta struct {
	Queries    int64
	Blocked    int64
	Overridden int64
	Throttled  int64
	LastSeen   int64
}

UsageDelta is an increment to apply to a tenant's counters.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL