feat: comprehensive debug logging across all controller modules

Add detailed [DEBUG] logging to every controller module when
logging.level is set to "debug". Each module with stateful debug
uses SetDebug(bool) wired from main.go. Covers stacks, backup,
cloudflare, integrations, system, monitor, settings, scheduler,
web handlers, storage, metrics, API, selfupdate, and assets.

Also includes the app export/import (.fab bundles) feature from
v0.32.0 and its debug page integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 18:14:43 +01:00
parent f6caea8067
commit 95c821deb2
54 changed files with 5015 additions and 82 deletions
+81 -1
View File
@@ -23,6 +23,7 @@ type GeoSyncManager struct {
domain string
stacks StackLister
logger *log.Logger
debug bool
mu sync.Mutex
running bool
@@ -39,6 +40,11 @@ func NewGeoSyncManager(client *Client, sett *settings.Settings, domain string, s
}
}
// SetDebug enables or disables debug logging for the geo sync manager.
func (g *GeoSyncManager) SetDebug(debug bool) {
g.debug = debug
}
// IsRunning returns true if a sync operation is in progress.
func (g *GeoSyncManager) IsRunning() bool {
g.mu.Lock()
@@ -75,17 +81,28 @@ func (g *GeoSyncManager) Sync(ctx context.Context) error {
// 1. Resolve zone ID (use cached value if available)
zoneID := geo.ZoneID
if zoneID == "" {
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] Zone ID not cached, resolving via API for domain %s", g.domain)
}
var err error
zoneID, err = g.client.GetZoneID(ctx, g.domain)
if err != nil {
g.saveError(zoneID, "", err.Error())
return fmt.Errorf("resolve zone: %w", err)
}
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] Resolved zone ID: %s", zoneID)
}
} else if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] Using cached zone ID: %s", zoneID)
}
// 2. Get or create the custom WAF ruleset
rulesetID := geo.RulesetID
if rulesetID == "" {
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] Ruleset ID not cached, looking up for zone %s", zoneID)
}
var err error
rulesetID, err = g.client.GetCustomRulesetID(ctx, zoneID)
if err != nil {
@@ -93,12 +110,20 @@ func (g *GeoSyncManager) Sync(ctx context.Context) error {
return fmt.Errorf("get ruleset: %w", err)
}
if rulesetID == "" {
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] No existing custom ruleset found, creating new one")
}
rulesetID, err = g.client.CreateCustomRuleset(ctx, zoneID)
if err != nil {
g.saveError(zoneID, "", err.Error())
return fmt.Errorf("create ruleset: %w", err)
}
}
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] Using ruleset ID: %s", rulesetID)
}
} else if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] Using cached ruleset ID: %s", rulesetID)
}
// 3. List existing felhom-managed rules
@@ -107,9 +132,21 @@ func (g *GeoSyncManager) Sync(ctx context.Context) error {
g.saveError(zoneID, rulesetID, err.Error())
return fmt.Errorf("list existing rules: %w", err)
}
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] Found %d existing felhom rules", len(existing))
for _, r := range existing {
g.logger.Printf("[DEBUG] [cloudflare] existing: %s (id=%s)", r.Description, r.ID)
}
}
// 4. Build desired rules
desired := g.buildDesiredRules(geo)
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] Built %d desired rules", len(desired))
for _, d := range desired {
g.logger.Printf("[DEBUG] [cloudflare] desired: %s", d.description)
}
}
// 5. Diff and apply
if err := g.applyDiff(ctx, zoneID, rulesetID, existing, desired); err != nil {
@@ -134,24 +171,41 @@ func (g *GeoSyncManager) deleteAllRules(ctx context.Context, geo *settings.GeoRe
}
if zoneID == "" || rulesetID == "" {
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] deleteAllRules: no cached zone/ruleset IDs, nothing to clean up")
}
// No cached IDs — nothing to clean up
return nil
}
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] deleteAllRules: listing rules for zone=%s ruleset=%s", zoneID, rulesetID)
}
existing, err := g.client.GetFelhomRules(ctx, zoneID, rulesetID)
if err != nil {
g.logger.Printf("[GEO] Warning: could not list rules for cleanup: %v", err)
return nil
}
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] deleteAllRules: found %d felhom rules to delete", len(existing))
}
deleted := 0
for _, r := range existing {
if err := g.client.DeleteRule(ctx, zoneID, rulesetID, r.ID); err != nil {
g.logger.Printf("[GEO] Warning: could not delete rule %s: %v", r.ID, err)
} else {
deleted++
}
}
if len(existing) > 0 {
g.logger.Printf("[GEO] Deleted %d felhom-geo rules (feature disabled)", len(existing))
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] deleteAllRules: successfully deleted %d/%d rules", deleted, len(existing))
}
}
g.saveError(zoneID, rulesetID, "")
@@ -169,6 +223,9 @@ func (g *GeoSyncManager) buildDesiredRules(geo *settings.GeoRestriction) []desir
var rules []desiredRule
hostnames := g.stacks.GetDeployedHostnames()
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] buildDesiredRules: %d deployed hostnames from stacks", len(hostnames))
}
// Collect app hostnames that have overrides (to exclude from global rule)
var excludeHostnames []string
@@ -177,6 +234,9 @@ func (g *GeoSyncManager) buildDesiredRules(geo *settings.GeoRestriction) []desir
for appName, override := range geo.AppOverrides {
hostname, ok := hostnames[appName]
if !ok {
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] buildDesiredRules: skipping override for %q (not deployed)", appName)
}
continue // app not deployed, skip
}
overrideApps[appName] = true
@@ -189,15 +249,24 @@ func (g *GeoSyncManager) buildDesiredRules(geo *settings.GeoRestriction) []desir
})
}
if g.debug && len(overrideApps) > 0 {
g.logger.Printf("[DEBUG] [cloudflare] buildDesiredRules: %d app overrides active (deployed)", len(overrideApps))
}
// Sort exclude hostnames for deterministic expression
sort.Strings(excludeHostnames)
// Global rule (excludes apps with their own rules)
globalExpr := BuildGlobalExpression(geo.AllowedCountries, excludeHostnames)
rules = append(rules, desiredRule{
description: globalRuleDesc,
expression: BuildGlobalExpression(geo.AllowedCountries, excludeHostnames),
expression: globalExpr,
})
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] buildDesiredRules: global rule expression: %s", globalExpr)
}
return rules
}
@@ -220,13 +289,21 @@ func (g *GeoSyncManager) applyDiff(ctx context.Context, zoneID, rulesetID string
if ex, ok := existingByDesc[d.description]; ok {
// Rule exists — check if expression changed
if ex.Expression != d.expression {
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] applyDiff: updating rule %q (id=%s) — expression changed", d.description, ex.ID)
}
r := newBlockRule(d.description, d.expression)
if err := g.client.UpdateRule(ctx, zoneID, rulesetID, ex.ID, r); err != nil {
return fmt.Errorf("update rule %q: %w", d.description, err)
}
} else if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] applyDiff: rule %q unchanged, skipping", d.description)
}
} else {
// New rule — create
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] applyDiff: creating new rule %q", d.description)
}
r := newBlockRule(d.description, d.expression)
if _, err := g.client.CreateRule(ctx, zoneID, rulesetID, r); err != nil {
return fmt.Errorf("create rule %q: %w", d.description, err)
@@ -237,6 +314,9 @@ func (g *GeoSyncManager) applyDiff(ctx context.Context, zoneID, rulesetID string
// Delete rules that are no longer desired
for _, ex := range existing {
if _, ok := desiredByDesc[ex.Description]; !ok {
if g.debug {
g.logger.Printf("[DEBUG] [cloudflare] applyDiff: deleting obsolete rule %q (id=%s)", ex.Description, ex.ID)
}
if err := g.client.DeleteRule(ctx, zoneID, rulesetID, ex.ID); err != nil {
return fmt.Errorf("delete rule %q: %w", ex.Description, err)
}
+33
View File
@@ -71,12 +71,23 @@ func (c *Client) GetCustomRulesetID(ctx context.Context, zoneID string) (string,
return "", fmt.Errorf("decode rulesets: %w", err)
}
if c.debug {
c.logger.Printf("[CF-DEBUG] GetCustomRulesetID: found %d rulesets for zone %s", len(rulesets), zoneID)
}
for _, rs := range rulesets {
if rs.Phase == wafPhase {
if c.debug {
c.logger.Printf("[CF-DEBUG] GetCustomRulesetID: matched ruleset %s (phase=%s)", rs.ID, wafPhase)
}
return rs.ID, nil
}
}
if c.debug {
c.logger.Printf("[CF-DEBUG] GetCustomRulesetID: no ruleset with phase %s found", wafPhase)
}
return "", nil
}
@@ -119,6 +130,10 @@ func (c *Client) GetRules(ctx context.Context, zoneID, rulesetID string) ([]rule
return nil, fmt.Errorf("decode rules: %w", err)
}
if c.debug {
c.logger.Printf("[CF-DEBUG] GetRules: %d total rules in ruleset %s", len(rs.Rules), rulesetID)
}
return rs.Rules, nil
}
@@ -141,6 +156,10 @@ func (c *Client) GetFelhomRules(ctx context.Context, zoneID, rulesetID string) (
}
}
if c.debug {
c.logger.Printf("[CF-DEBUG] GetFelhomRules: %d felhom rules out of %d total", len(result), len(rules))
}
return result, nil
}
@@ -163,6 +182,13 @@ func (c *Client) CreateRule(ctx context.Context, zoneID, rulesetID string, r rul
for _, created := range rs.Rules {
if created.Description == r.Description {
c.logger.Printf("[CF] Created rule %q → %s", r.Description, created.ID)
if c.debug {
expr := r.Expression
if len(expr) > 120 {
expr = expr[:120] + "..."
}
c.logger.Printf("[CF-DEBUG] CreateRule: expression: %s", expr)
}
return created.ID, nil
}
}
@@ -178,6 +204,13 @@ func (c *Client) UpdateRule(ctx context.Context, zoneID, rulesetID, ruleID strin
return fmt.Errorf("update rule %s: %w", ruleID, err)
}
c.logger.Printf("[CF] Updated rule %q (%s)", r.Description, ruleID)
if c.debug {
expr := r.Expression
if len(expr) > 120 {
expr = expr[:120] + "..."
}
c.logger.Printf("[CF-DEBUG] UpdateRule: expression: %s", expr)
}
return nil
}
+10
View File
@@ -16,7 +16,14 @@ type zone struct {
// GetZoneID resolves the Cloudflare zone ID for a domain.
// It tries the exact domain first, then strips subdomains progressively.
func (c *Client) GetZoneID(ctx context.Context, domain string) (string, error) {
if c.debug {
c.logger.Printf("[CF-DEBUG] GetZoneID: looking up zone for domain %q", domain)
}
// Try exact domain first (e.g., "demo-felhom.eu")
if c.debug {
c.logger.Printf("[CF-DEBUG] GetZoneID: trying exact domain %q", domain)
}
id, err := c.lookupZone(ctx, domain)
if err != nil {
return "", err
@@ -32,6 +39,9 @@ func (c *Client) GetZoneID(ctx context.Context, domain string) (string, error) {
if parent == "" {
break
}
if c.debug {
c.logger.Printf("[CF-DEBUG] GetZoneID: trying parent domain %q", parent)
}
id, err = c.lookupZone(ctx, parent)
if err != nil {
return "", err