- dns.TTLCache: stop preallocating every shard map to maxShard; this allocated the full theoretical cache capacity up front (hundreds of MB of empty buckets) and pushed the container over its memory limit, causing an OOM restart loop under Docker's default 1g cap. - pipeline: write direct provider matches immediately without a blocking DNS enrichment step, so they can no longer be starved by the optional CNAME queue. Add TestDirectNotStarvedByDNSBacklog. - config: add DEDUP_SIZE, lower DNS_CACHE_SIZE/DEDUP_SIZE defaults to 256k. - .env.example / README: document DEDUP_SIZE and cache sizing. Live: stable at ~250-300 MiB with no restarts, direct queue draining to 0.
164 lines
4.3 KiB
Go
164 lines
4.3 KiB
Go
// Package config loads all ctcloud configuration from environment variables.
|
|
// There are no configuration files at runtime.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
DatabaseURL string
|
|
DBMaxConns int
|
|
|
|
DNSServers []string
|
|
DNSWorkers int
|
|
DNSTimeout time.Duration
|
|
DNSCacheSize int
|
|
DNSMaxCNAMEdepth int
|
|
DNSPositiveTTL time.Duration
|
|
DNSNegativeTTL time.Duration
|
|
|
|
DedupSize int
|
|
|
|
DirectQueueSize int
|
|
DNSQueueSize int
|
|
DBQueueSize int
|
|
|
|
DBBatchSize int
|
|
DBFlushInterval time.Duration
|
|
|
|
ActiveProbing bool
|
|
ProbeWorkers int
|
|
ProbeTimeout time.Duration
|
|
|
|
MetricsAddr string
|
|
LogLevel string
|
|
StatsInterval time.Duration
|
|
|
|
LogListSource string
|
|
StartAtEnd bool
|
|
ShutdownTimeout time.Duration
|
|
DedupeTTL time.Duration
|
|
}
|
|
|
|
// DefaultLogListSource is the public CT log list used by certspotter.
|
|
const DefaultLogListSource = "https://www.gstatic.com/ct/log_list/v3/log_list.json"
|
|
|
|
// DefaultDNSServers are load-balanced across. Set DNS_SERVERS=system to use the
|
|
// host resolver instead, or provide your own comma-separated list.
|
|
var DefaultDNSServers = []string{"1.1.1.1:53", "8.8.8.8:53", "1.0.0.1:53"}
|
|
|
|
func Load() (*Config, error) {
|
|
c := &Config{
|
|
DatabaseURL: os.Getenv("DATABASE_URL"),
|
|
DBMaxConns: envInt("DB_MAX_CONNS", 8),
|
|
DNSServers: envList("DNS_SERVERS", DefaultDNSServers),
|
|
DNSWorkers: envInt("DNS_WORKERS", 16),
|
|
DNSTimeout: envDuration("DNS_TIMEOUT", 5*time.Second),
|
|
DNSCacheSize: envInt("DNS_CACHE_SIZE", 1<<18),
|
|
DedupSize: envInt("DEDUP_SIZE", 1<<18),
|
|
DNSMaxCNAMEdepth: envInt("DNS_MAX_CNAME_DEPTH", 8),
|
|
DNSPositiveTTL: envDuration("DNS_POSITIVE_TTL", 10*time.Minute),
|
|
DNSNegativeTTL: envDuration("DNS_NEGATIVE_TTL", 1*time.Minute),
|
|
DirectQueueSize: envInt("DIRECT_QUEUE_SIZE", 8192),
|
|
DNSQueueSize: envInt("DNS_QUEUE_SIZE", 65536),
|
|
DBQueueSize: envInt("DB_QUEUE_SIZE", 8192),
|
|
DBBatchSize: envInt("DB_BATCH_SIZE", 500),
|
|
DBFlushInterval: envDuration("DB_FLUSH_INTERVAL", 2*time.Second),
|
|
ActiveProbing: envBool("ACTIVE_PROBING", false),
|
|
ProbeWorkers: envInt("PROBE_WORKERS", 4),
|
|
ProbeTimeout: envDuration("PROBE_TIMEOUT", 5*time.Second),
|
|
MetricsAddr: envString("METRICS_ADDR", ":9090"),
|
|
LogLevel: envString("LOG_LEVEL", "info"),
|
|
StatsInterval: envDuration("STATS_INTERVAL", 60*time.Second),
|
|
LogListSource: envString("LOG_LIST_SOURCE", DefaultLogListSource),
|
|
StartAtEnd: envBool("START_AT_END", false),
|
|
ShutdownTimeout: envDuration("SHUTDOWN_TIMEOUT", 20*time.Second),
|
|
DedupeTTL: envDuration("DEDUP_TTL", time.Hour),
|
|
}
|
|
if err := c.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (c *Config) validate() error {
|
|
if c.DatabaseURL == "" {
|
|
return fmt.Errorf("DATABASE_URL is required")
|
|
}
|
|
if c.DNSWorkers < 1 {
|
|
return fmt.Errorf("DNS_WORKERS must be >= 1")
|
|
}
|
|
if c.DNSMaxCNAMEdepth < 1 {
|
|
return fmt.Errorf("DNS_MAX_CNAME_DEPTH must be >= 1")
|
|
}
|
|
if c.DirectQueueSize < 1 || c.DNSQueueSize < 1 || c.DBQueueSize < 1 {
|
|
return fmt.Errorf("queue sizes must be >= 1")
|
|
}
|
|
if c.DBBatchSize < 1 {
|
|
return fmt.Errorf("DB_BATCH_SIZE must be >= 1")
|
|
}
|
|
switch c.LogLevel {
|
|
case "debug", "info", "warn", "error":
|
|
default:
|
|
return fmt.Errorf("LOG_LEVEL must be one of debug, info, warn, error")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func envList(key string, def []string) []string {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
return def
|
|
}
|
|
parts := strings.Split(v, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return def
|
|
}
|
|
return out
|
|
}
|
|
|
|
func envString(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envInt(key string, def int) int {
|
|
if v := os.Getenv(key); v != "" {
|
|
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
|
|
return n
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envBool(key string, def bool) bool {
|
|
if v := os.Getenv(key); v != "" {
|
|
if b, err := strconv.ParseBool(strings.TrimSpace(v)); err == nil {
|
|
return b
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envDuration(key string, def time.Duration) time.Duration {
|
|
if v := os.Getenv(key); v != "" {
|
|
if d, err := time.ParseDuration(strings.TrimSpace(v)); err == nil {
|
|
return d
|
|
}
|
|
}
|
|
return def
|
|
}
|