API / Packages
Import path: github.com/lenny-ts/caddy-analyzer/pkg/*. Go 1.25+. For the data flow across these packages, see Architecture.
parser — Caddy JSON → Go structs
Parses one JSON line into *LogEntry (handled request) or *OperationalEntry (log without request).
import "github.com/lenny-ts/caddy-analyzer/pkg/parser"
entry, err := parser.Parse(line)
switch v := entry.(type) {
case *parser.LogEntry: // v.Method, v.URI, v.Status, v.RemoteIP, v.UserAgent
case *parser.OperationalEntry: // v.Level, v.Logger, v.Msg, v.Extra
}
| Key type | Fields |
|---|---|
LogEntry | Method, URI, Host, RemoteIP, Status, Size, Duration, UserAgent, Referer, ForwardedFor |
OperationalEntry | Level, Logger, Msg, Extra map[string]json.RawMessage |
analysis — filtering + 26-category detection
import "github.com/lenny-ts/caddy-analyzer/pkg/analysis"
import "github.com/lenny-ts/caddy-analyzer/pkg/types"
eng := analysis.New(types.Filters{From: time.Now().Add(-1*time.Hour), Status: []string{"500"}})
det := analysis.NewDetector(10) // uaRotation
eng.SetDetector(det)
// per line:
if eng.MatchEntry(entry) { // respects all Filters (ip, country, asn, grep, bot, etc.)
eng.Process(entry) // counters + detector + histogram
}
stats := eng.Finalize() // computes P50/P95/RPS
topIPs := eng.TopN(10, types.TopFieldIP)
suspicious := stats.SuspiciousIPs // grouped by IP with MITRE tags
Behavioral heuristics: ua_rotation ≥10, object_enumeration ≥10 IDs/template, beaconing CV<0.25. Export Sigma: det.ExportSigmaInfo().
enrich — GeoIP
import "github.com/lenny-ts/caddy-analyzer/pkg/enrich"
geo, err := enrich.NewGeoIP("") // auto-discovers mmdb in 7 paths + auto-downloads GeoLite2
if err == nil { defer geo.Close() }
info := geo.Lookup("203.0.113.18") // -> CountryCode, CountryName, ASN
code, _ := enrich.CountryCodeFromName("Italy") // "IT"
Cache: 50K LRU, 24h TTL (tunable via Configuration). Async variant: NewGeoIPAsync.
blocklist — CIDR trie
import "github.com/lenny-ts/caddy-analyzer/pkg/blocklist"
mgr := blocklist.NewManager("~/.cache/caddy-analyzer/blocklists")
mgr.Refresh(context.Background()) // download 8 feeds
hit := mgr.Contains("198.51.100.23") // O(1) via cidranger.PCTrie
sources := mgr.ListSources() // name, URL, count, age, status
Add custom: mgr.AddSource(blocklist.Source{Name:"My",URL:"https://…/list.txt"}). Cache TTL 7d.
guard — firewall
import "github.com/lenny-ts/caddy-analyzer/pkg/guard"
import "github.com/lenny-ts/caddy-analyzer/pkg/guard/firewall"
backend, _ := firewall.Detect(guard.LevelInfo) // auto → hybrid/docker/iptables/nftables
g := guard.New(guard.Config{
Limit: 100, Window: time.Minute, Duration: 10*time.Minute,
Blocker: backend, GeoIP: geo, BlocklistMgr: mgr,
NeverBlock: []string{"10.0.0.0/8"},
OnAudit: func(a guard.AuditEntry){ /* log */ },
})
// per log line:
if g.Evaluate(entry) == guard.BlockNow { /* iptables BLOCK */ }
// periodic:
blocked := g.Tick() // expires, subnet/cred-stuffing/rps checks
State persists to /var/lib/caddy-analyzer/blocked.json. See Architecture → data flow for where guard sits.
reader — multi-source ingestion
import "github.com/lenny-ts/caddy-analyzer/pkg/reader"
src, _ := reader.ParseSource("docker://caddy")
r, _ := reader.FromSource(src, true /*follow*/) // → LogReader (file/docker/k8s/journalctl/stdin)
for line := range r.Lines() { entry, _ := parser.Parse(line) }
Merges Docker stdout+stderr, follows file rotation via inode check, globs via expandPaths.
output — table / json / csv / html
import "github.com/lenny-ts/caddy-analyzer/pkg/output"
output.Report(stats, output.FormatTable, false /*defang*/, nil)
output.Report(stats, output.FormatHTML, true, os.Stdout) // standalone dark HTML
types — shared structs
types.GeoInfo includes optional City, Latitude, Longitude, and Timezone. City MMDBs are discovered locally via enrich.FindCityDB; they are not auto-downloaded.
import "github.com/lenny-ts/caddy-analyzer/pkg/types"
var f types.Filters // --ip, --country, --asn, --grep, --level, --ops-only, Top, Format, ...
stats := &types.Stats{} // histogram 1000 buckets, TopN, SuspiciousIPs
entry.EffectiveClientIP(trustForwarded) // last public XFF or RemoteIP