feat: comprehensive INFO/WARN/ERROR logging across all controller modules
Add structured operational logging at INFO, WARN, and ERROR levels to every controller module. Standardize custom prefixes ([GEO], [SCHED], [SYNC]) to use [INFO/WARN/ERROR] [module] format. Fix misleveled logs (WARN->ERROR for data loss scenarios, WARN->INFO for routine operations). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1145,9 +1145,14 @@ When `logging.level: "debug"` is set in `controller.yaml`, the controller expose
|
||||
- **DebugCallbacks**: 7 closures wired from main.go for operations needing modules not on Server struct (hub push, infra backup, connectivity tests, telemetry preview).
|
||||
- **Telemetry debug**: `GetTelemetryPreview` callback calls `report.BuildAppTelemetryForDebug()` (exported wrapper around the private `buildAppTelemetrySection()`). Result renders as a table with collapsible raw JSON. Available regardless of hub configuration.
|
||||
|
||||
#### Per-Module Debug Logging
|
||||
#### Per-Module Logging
|
||||
|
||||
When `logging.level: "debug"`, every module emits detailed `[DEBUG] [module]` prefixed log lines. Each module with stateful debug (struct-based) exposes a `SetDebug(bool)` method, wired from `main.go`. Modules without a struct use package-level `DebugLogger` variables (e.g., `system.DebugLogger`).
|
||||
All modules emit structured log lines at `[INFO]`, `[WARN]`, and `[ERROR]` levels for operational events (state changes, completions, failures). When `logging.level: "debug"`, additional detailed `[DEBUG] [module]` prefixed log lines are emitted. Each module with stateful debug (struct-based) exposes a `SetDebug(bool)` method, wired from `main.go`. Modules without a struct use package-level `DebugLogger` variables (e.g., `system.DebugLogger`).
|
||||
|
||||
**Standard-level logging** (always active):
|
||||
- `[INFO]` — Operational events: stack deploy/start/stop, backup completion, config changes, disk operations, sync results
|
||||
- `[WARN]` — Degraded states: health threshold breaches, unsafe backup destinations, retryable failures, best-effort operation failures
|
||||
- `[ERROR]` — Hard failures: data restore errors, integration apply failures, compose file update errors, disk format failures
|
||||
|
||||
| Module | Debug Field | Prefix | Key Areas |
|
||||
|--------|------------|--------|-----------|
|
||||
|
||||
@@ -456,6 +456,7 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
r.logger.Printf("[ERROR] [api] %s failed for %s: %v", action, name, err)
|
||||
status := http.StatusInternalServerError
|
||||
if strings.Contains(err.Error(), "protected") {
|
||||
status = http.StatusForbidden
|
||||
@@ -796,6 +797,7 @@ func (r *Router) backupSnapshots(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
snapshots, err := r.backupMgr.ListAllSnapshots(50)
|
||||
if err != nil {
|
||||
r.logger.Printf("[ERROR] [api] Failed to list backup snapshots: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -833,6 +835,7 @@ func (r *Router) metricsSystem(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
samples, err := r.metricsStore.QuerySystemMetrics(from, to, resolution)
|
||||
if err != nil {
|
||||
r.logger.Printf("[ERROR] [api] Failed to query system metrics: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -869,6 +872,7 @@ func (r *Router) metricsContainerSummary(w http.ResponseWriter, _ *http.Request)
|
||||
|
||||
summary, err := r.metricsStore.QueryContainerSummary()
|
||||
if err != nil {
|
||||
r.logger.Printf("[ERROR] [api] Failed to query container summary: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -887,6 +891,7 @@ func (r *Router) metricsContainer(w http.ResponseWriter, req *http.Request, name
|
||||
|
||||
samples, err := r.metricsStore.QueryContainerMetrics(name, from, to, resolution)
|
||||
if err != nil {
|
||||
r.logger.Printf("[ERROR] [api] Failed to query container metrics for %s: %v", name, err)
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -218,6 +218,7 @@ func (e *Exporter) StartImport(req ImportRequest) error {
|
||||
|
||||
func (e *Exporter) executeImport(req ImportRequest, job *Job) {
|
||||
importStart := time.Now()
|
||||
e.logger.Printf("[INFO] [appexport] Import started for %s", filepath.Base(req.FABPath))
|
||||
e.debugf("=== IMPORT START: path=%s encrypted=%v ===", req.FABPath, req.Password != "")
|
||||
|
||||
defer func() {
|
||||
|
||||
@@ -218,7 +218,7 @@ func (s *Syncer) setError(err error) {
|
||||
TotalBytes: s.status.TotalBytes,
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.logger.Printf("[WARN] Asset sync failed: %v", err)
|
||||
s.logger.Printf("[ERROR] [assets] Asset sync failed: %v", err)
|
||||
}
|
||||
|
||||
func (s *Syncer) fetchManifest(ctx context.Context) (*HubManifest, error) {
|
||||
@@ -316,14 +316,18 @@ func (s *Syncer) downloadFile(ctx context.Context, filename string) error {
|
||||
func (s *Syncer) saveLocalManifest(manifest *HubManifest) {
|
||||
data, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [assets] Failed to save local manifest: %v", err)
|
||||
return
|
||||
}
|
||||
path := filepath.Join(s.assetsDir, "manifest.json")
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
||||
s.logger.Printf("[ERROR] [assets] Failed to save local manifest: %v", err)
|
||||
return
|
||||
}
|
||||
os.Rename(tmp, path)
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
s.logger.Printf("[ERROR] [assets] Failed to save local manifest: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func fileSHA256(path string) (string, error) {
|
||||
|
||||
@@ -856,6 +856,7 @@ func (m *Manager) perDriveRepoStats() []DriveRepoInfo {
|
||||
}
|
||||
stats, err := m.restic.Stats(repoPath)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] [backup] perDriveRepoStats: failed to get stats for %s: %v", drive, err)
|
||||
continue
|
||||
}
|
||||
infos = append(infos, DriveRepoInfo{
|
||||
@@ -866,6 +867,7 @@ func (m *Manager) perDriveRepoStats() []DriveRepoInfo {
|
||||
LatestSnapshot: stats.LatestSnapshot,
|
||||
})
|
||||
}
|
||||
m.logger.Printf("[INFO] [backup] perDriveRepoStats: collected stats for %d drives", len(infos))
|
||||
return infos
|
||||
}
|
||||
|
||||
@@ -918,6 +920,7 @@ func (m *Manager) aggregateRepoStats() *RepoStats {
|
||||
if totalBytes > 0 {
|
||||
agg.TotalSize = humanizeBytes(totalBytes)
|
||||
}
|
||||
m.logger.Printf("[INFO] [backup] Aggregated repo stats: %d snapshots, total size %s", agg.SnapshotCount, agg.TotalSize)
|
||||
return agg
|
||||
}
|
||||
|
||||
@@ -932,6 +935,7 @@ func (m *Manager) listAllDumpFiles() []DumpFileInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
m.logger.Printf("[INFO] [backup] Found %d DB dump files across drives", len(allFiles))
|
||||
return allFiles
|
||||
}
|
||||
|
||||
@@ -979,7 +983,9 @@ func (m *Manager) saveSnapshotHistory() {
|
||||
}
|
||||
if err := os.Rename(tmp, m.snapshotHistoryFile); err != nil {
|
||||
m.logger.Printf("[WARN] Could not rename snapshot history file: %v", err)
|
||||
return
|
||||
}
|
||||
m.logger.Printf("[INFO] [backup] Saved snapshot history (%d entries)", len(m.snapshotHistory))
|
||||
}
|
||||
|
||||
// loadSnapshotHistoryFromFile reads the persisted snapshot history from disk.
|
||||
|
||||
@@ -203,6 +203,7 @@ func (r *CrossDriveRunner) RunAllScheduled(ctx context.Context, schedule string)
|
||||
|
||||
var errs []string
|
||||
var scheduled, skippedDisabled, skippedWrongSchedule int
|
||||
r.logger.Printf("[INFO] [backup] Cross-drive backup: starting scheduled run for %d configured app(s), schedule=%s", len(configs), schedule)
|
||||
for stackName, cfg := range configs {
|
||||
if !cfg.Enabled {
|
||||
if r.debug {
|
||||
@@ -240,6 +241,8 @@ func (r *CrossDriveRunner) RunAllScheduled(ctx context.Context, schedule string)
|
||||
scheduled, skippedDisabled, skippedWrongSchedule, len(errs))
|
||||
}
|
||||
|
||||
r.logger.Printf("[INFO] [backup] Cross-drive backup complete: %d succeeded, %d failed", scheduled-len(errs), len(errs))
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("cross-drive backup errors: %s", strings.Join(errs, "; "))
|
||||
}
|
||||
@@ -263,6 +266,7 @@ func (r *CrossDriveRunner) RunAllConfigured(ctx context.Context) error {
|
||||
|
||||
var errs []string
|
||||
var ran int
|
||||
r.logger.Printf("[INFO] [backup] Cross-drive backup: starting all configured app(s), %d total", len(configs))
|
||||
for stackName, cfg := range configs {
|
||||
if !cfg.Enabled {
|
||||
continue
|
||||
@@ -281,6 +285,7 @@ func (r *CrossDriveRunner) RunAllConfigured(ctx context.Context) error {
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] RunAllConfigured: done — %d ran, %d errors", ran, len(errs))
|
||||
}
|
||||
r.logger.Printf("[INFO] [backup] Cross-drive backup complete: %d succeeded, %d failed", ran-len(errs), len(errs))
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("cross-drive errors: %s", strings.Join(errs, "; "))
|
||||
}
|
||||
@@ -418,6 +423,7 @@ func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBa
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] runRsyncBackup: rsync failed for %s: %s", srcMount, util.TruncateStr(strings.TrimSpace(string(out)), 500))
|
||||
}
|
||||
r.logger.Printf("[ERROR] [backup] Rsync backup for %s failed: %v", stackName, err)
|
||||
return fmt.Errorf("rsync failed for %s: %v (%s)", srcMount, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
if r.debug {
|
||||
@@ -452,6 +458,7 @@ func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBa
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.Printf("[INFO] [backup] Rsync backup for %s to %s complete", stackName, destDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -484,6 +491,7 @@ func (r *CrossDriveRunner) copyStackDBDumps(stackName, destDir string) error {
|
||||
if copied > 0 {
|
||||
r.logger.Printf("[DEBUG] Copied %d DB dump file(s) to %s", copied, destDir)
|
||||
}
|
||||
r.logger.Printf("[INFO] [backup] Copied %d DB dumps for %s", copied, stackName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ func DiscoverDatabases(ctx context.Context, logger *log.Logger, debug bool) ([]D
|
||||
logger.Printf("[DEBUG] DiscoverDatabases: found %d database(s), skipped %d non-DB container(s)", len(dbs), skipped)
|
||||
}
|
||||
|
||||
logger.Printf("[INFO] [backup] Discovered %d databases", len(dbs))
|
||||
return dbs, nil
|
||||
}
|
||||
|
||||
@@ -143,11 +144,17 @@ func DumpAll(ctx context.Context, dbs []DiscoveredDB, dumpDir string, logger *lo
|
||||
// Clean up old .tmp files (older than 1 hour)
|
||||
cleanupTmpFiles(dumpDir, logger)
|
||||
|
||||
logger.Printf("[INFO] [backup] Starting DB dump for %d databases", len(dbs))
|
||||
var results []DumpResult
|
||||
var failed int
|
||||
for _, db := range dbs {
|
||||
result := DumpOne(ctx, db, dumpDir, logger, debug)
|
||||
results = append(results, result)
|
||||
if result.Error != nil {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
logger.Printf("[INFO] [backup] DB dump complete: %d succeeded, %d failed", len(results)-failed, failed)
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,9 @@ func pruneLocalHistory(histDir string, maxKeep int, logger *log.Logger) {
|
||||
logger.Printf("[DEBUG] Local infra history: pruned old version %s", ts)
|
||||
}
|
||||
}
|
||||
if logger != nil && toDelete > 0 {
|
||||
logger.Printf("[INFO] [backup] Pruning old backup versions: kept %d, removed %d", maxKeep, toDelete)
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeTimestamp converts an RFC3339 timestamp to a filename-safe format.
|
||||
|
||||
@@ -207,6 +207,7 @@ func (r *ResticManager) Snapshot(repoPath string, paths []string, tags []string)
|
||||
result.Duration, result.SnapshotID, result.FilesNew, result.FilesChanged, result.DataAdded)
|
||||
}
|
||||
|
||||
r.logger.Printf("[INFO] [backup] Restic snapshot complete for %s", repoPath)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -264,6 +265,7 @@ func (r *ResticManager) Check(repoPath string) error {
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Check: repo=%s OK, completed in %s", repoPath, time.Since(start))
|
||||
}
|
||||
r.logger.Printf("[INFO] [backup] Restic check passed for repo %s", repoPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ func (m *Manager) RestoreApp(stackName, snapshotID string) error {
|
||||
m.logger.Printf("[DEBUG] RestoreApp: using repo=%s, %d restore path(s)", repoPath, len(restorePaths))
|
||||
}
|
||||
|
||||
m.logger.Printf("[WARN] RESTORE starting: stack=%s, snapshot=%s, repo=%s, paths=%v, hasHDD=%v",
|
||||
m.logger.Printf("[INFO] [backup] Starting restore for %s (snapshot=%s, repo=%s, paths=%v, hasHDD=%v)",
|
||||
stackName, snapshotID, repoPath, restorePaths, hasHDD)
|
||||
|
||||
// Stop the app before restore
|
||||
|
||||
@@ -156,14 +156,14 @@ func restoreUserData(ctx context.Context, app *RestorableApp, logger *log.Logger
|
||||
if e.IsDir() {
|
||||
src = strings.TrimRight(src, "/") + "/"
|
||||
if err := os.MkdirAll(dst, 0755); err != nil {
|
||||
logger.Printf("[WARN] Cannot create %s: %v", dst, err)
|
||||
logger.Printf("[ERROR] [backup] Cannot create %s: %v", dst, err)
|
||||
continue
|
||||
}
|
||||
dst = strings.TrimRight(dst, "/") + "/"
|
||||
logger.Printf("[DEBUG] [backup] restoreUserData: %s — rsync dir %s → %s", app.Name, src, dst)
|
||||
cmd := exec.CommandContext(ctx, "rsync", "-a", src, dst)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
logger.Printf("[WARN] rsync data %s: %v (%s)", name, err, strings.TrimSpace(string(out)))
|
||||
logger.Printf("[ERROR] [backup] rsync data %s: %v (%s)", name, err, strings.TrimSpace(string(out)))
|
||||
} else {
|
||||
restored++
|
||||
}
|
||||
@@ -171,12 +171,12 @@ func restoreUserData(ctx context.Context, app *RestorableApp, logger *log.Logger
|
||||
// Single file — copy directly
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
logger.Printf("[WARN] Cannot read %s: %v", src, err)
|
||||
logger.Printf("[ERROR] [backup] Cannot read %s: %v", src, err)
|
||||
continue
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] restoreUserData: %s — copying file %s (%d bytes)", app.Name, name, len(data))
|
||||
if err := os.WriteFile(dst, data, 0644); err != nil {
|
||||
logger.Printf("[WARN] Cannot write %s: %v", dst, err)
|
||||
logger.Printf("[ERROR] [backup] Cannot write %s: %v", dst, err)
|
||||
} else {
|
||||
restored++
|
||||
}
|
||||
@@ -224,12 +224,12 @@ func restoreDBDumps(app *RestorableApp, logger *log.Logger) error {
|
||||
dst := filepath.Join(destDir, e.Name())
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
logger.Printf("[WARN] Cannot read dump %s: %v", e.Name(), err)
|
||||
logger.Printf("[ERROR] [backup] Cannot read dump %s: %v", e.Name(), err)
|
||||
continue
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] restoreDBDumps: %s — copying %s (%d bytes)", app.Name, e.Name(), len(data))
|
||||
if err := os.WriteFile(dst, data, 0644); err != nil {
|
||||
logger.Printf("[WARN] Cannot write dump %s: %v", e.Name(), err)
|
||||
logger.Printf("[ERROR] [backup] Cannot write dump %s: %v", e.Name(), err)
|
||||
} else {
|
||||
copied++
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ func mountRawAndBind(ctx context.Context, device string, dm DiskMount, logger *l
|
||||
func addDRFstabEntries(dm DiskMount, logger *log.Logger) error {
|
||||
const fstabPath = "/host-fstab"
|
||||
|
||||
logger.Printf("[INFO] [backup] Adding fstab entries for disaster recovery (%s, UUID=%s)", dm.Label, dm.UUID)
|
||||
logger.Printf("[DEBUG] [backup] addDRFstabEntries: checking fstab for %s (UUID=%s)", dm.Label, dm.UUID)
|
||||
|
||||
data, err := os.ReadFile(fstabPath)
|
||||
@@ -257,20 +258,24 @@ func addDRFstabEntries(dm DiskMount, logger *log.Logger) error {
|
||||
var additions strings.Builder
|
||||
additions.WriteString("\n# Restored by felhom-controller DR\n")
|
||||
|
||||
entryCount := 0
|
||||
if dm.RawMount != "" {
|
||||
// Raw mount entry
|
||||
additions.WriteString(fmt.Sprintf("UUID=%s\t%s\t%s\t%s\t0 2\n",
|
||||
dm.UUID, dm.RawMount, dm.FSType, dm.FstabOptions))
|
||||
entryCount++
|
||||
}
|
||||
|
||||
if dm.BindSubdir != "" && dm.RawMount != "" {
|
||||
// Bind mount entry
|
||||
additions.WriteString(fmt.Sprintf("%s/%s\t%s\tnone\tbind,nofail\t0 0\n",
|
||||
dm.RawMount, dm.BindSubdir, dm.MountPoint))
|
||||
entryCount++
|
||||
} else if dm.RawMount == "" {
|
||||
// Direct mount entry (no bind)
|
||||
additions.WriteString(fmt.Sprintf("UUID=%s\t%s\t%s\t%s\t0 2\n",
|
||||
dm.UUID, dm.MountPoint, dm.FSType, dm.FstabOptions))
|
||||
entryCount++
|
||||
}
|
||||
|
||||
newContent := content + additions.String()
|
||||
@@ -288,6 +293,7 @@ func addDRFstabEntries(dm DiskMount, logger *log.Logger) error {
|
||||
}
|
||||
}
|
||||
|
||||
logger.Printf("[INFO] [backup] Added %d fstab entries for %s", entryCount, dm.Label)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ func (g *GeoSyncManager) Sync(ctx context.Context) error {
|
||||
g.mu.Unlock()
|
||||
}()
|
||||
|
||||
g.logger.Printf("[INFO] [cloudflare] Geo-restriction sync starting")
|
||||
|
||||
geo := g.settings.GetGeoRestriction()
|
||||
|
||||
// If geo is nil or disabled, delete all felhom rules and return.
|
||||
@@ -75,7 +77,7 @@ func (g *GeoSyncManager) Sync(ctx context.Context) error {
|
||||
return g.deleteAllRules(ctx, geo)
|
||||
}
|
||||
|
||||
g.logger.Printf("[GEO] Starting sync for domain %s (%d allowed countries, %d app overrides)",
|
||||
g.logger.Printf("[INFO] [cloudflare] Starting sync for domain %s (%d allowed countries, %d app overrides)",
|
||||
g.domain, len(geo.AllowedCountries), len(geo.AppOverrides))
|
||||
|
||||
// 1. Resolve zone ID (use cached value if available)
|
||||
@@ -156,7 +158,9 @@ func (g *GeoSyncManager) Sync(ctx context.Context) error {
|
||||
|
||||
// 6. Save success state
|
||||
g.saveError(zoneID, rulesetID, "")
|
||||
g.logger.Printf("[GEO] Sync completed successfully")
|
||||
// Count rules for summary
|
||||
finalRules, _ := g.client.GetFelhomRules(ctx, zoneID, rulesetID)
|
||||
g.logger.Printf("[INFO] [cloudflare] Geo-restriction sync complete (%d active rules)", len(finalRules))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -184,7 +188,7 @@ func (g *GeoSyncManager) deleteAllRules(ctx context.Context, geo *settings.GeoRe
|
||||
|
||||
existing, err := g.client.GetFelhomRules(ctx, zoneID, rulesetID)
|
||||
if err != nil {
|
||||
g.logger.Printf("[GEO] Warning: could not list rules for cleanup: %v", err)
|
||||
g.logger.Printf("[WARN] [cloudflare] Could not list rules for cleanup: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -195,14 +199,14 @@ func (g *GeoSyncManager) deleteAllRules(ctx context.Context, geo *settings.GeoRe
|
||||
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)
|
||||
g.logger.Printf("[ERROR] [cloudflare] Failed to delete WAF 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))
|
||||
g.logger.Printf("[INFO] [cloudflare] 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))
|
||||
}
|
||||
@@ -296,6 +300,7 @@ func (g *GeoSyncManager) applyDiff(ctx context.Context, zoneID, rulesetID string
|
||||
if err := g.client.UpdateRule(ctx, zoneID, rulesetID, ex.ID, r); err != nil {
|
||||
return fmt.Errorf("update rule %q: %w", d.description, err)
|
||||
}
|
||||
g.logger.Printf("[INFO] [cloudflare] Updated WAF rule %s", d.description)
|
||||
} else if g.debug {
|
||||
g.logger.Printf("[DEBUG] [cloudflare] applyDiff: rule %q unchanged, skipping", d.description)
|
||||
}
|
||||
@@ -308,6 +313,7 @@ func (g *GeoSyncManager) applyDiff(ctx context.Context, zoneID, rulesetID string
|
||||
if _, err := g.client.CreateRule(ctx, zoneID, rulesetID, r); err != nil {
|
||||
return fmt.Errorf("create rule %q: %w", d.description, err)
|
||||
}
|
||||
g.logger.Printf("[INFO] [cloudflare] Created WAF rule %s", d.description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +326,7 @@ func (g *GeoSyncManager) applyDiff(ctx context.Context, zoneID, rulesetID string
|
||||
if err := g.client.DeleteRule(ctx, zoneID, rulesetID, ex.ID); err != nil {
|
||||
return fmt.Errorf("delete rule %q: %w", ex.Description, err)
|
||||
}
|
||||
g.logger.Printf("[INFO] [cloudflare] Deleted WAF rule %s", ex.Description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +335,10 @@ func (g *GeoSyncManager) applyDiff(ctx context.Context, zoneID, rulesetID string
|
||||
|
||||
// saveError updates the sync state in settings.
|
||||
func (g *GeoSyncManager) saveError(zoneID, rulesetID, errMsg string) {
|
||||
if errMsg != "" {
|
||||
g.logger.Printf("[ERROR] [cloudflare] Geo-sync error: %v", errMsg)
|
||||
}
|
||||
if err := g.settings.SetGeoSyncState(zoneID, rulesetID, errMsg); err != nil {
|
||||
g.logger.Printf("[GEO] Warning: failed to save sync state: %v", err)
|
||||
g.logger.Printf("[WARN] [cloudflare] Failed to save sync state: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ func (c *Client) GetCustomRulesetID(ctx context.Context, zoneID string) (string,
|
||||
path := fmt.Sprintf("/zones/%s/rulesets", zoneID)
|
||||
resp, err := c.do(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
c.logger.Printf("[ERROR] [cloudflare] Failed to get custom ruleset: %v", err)
|
||||
return "", fmt.Errorf("list rulesets: %w", err)
|
||||
}
|
||||
|
||||
@@ -120,6 +121,7 @@ func (c *Client) GetRules(ctx context.Context, zoneID, rulesetID string) ([]rule
|
||||
path := fmt.Sprintf("/zones/%s/rulesets/%s", zoneID, rulesetID)
|
||||
resp, err := c.do(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
c.logger.Printf("[WARN] [cloudflare] Failed to get WAF rules: %v", err)
|
||||
return nil, fmt.Errorf("get ruleset: %w", err)
|
||||
}
|
||||
|
||||
@@ -165,9 +167,11 @@ func (c *Client) GetFelhomRules(ctx context.Context, zoneID, rulesetID string) (
|
||||
|
||||
// CreateRule adds a new rule to the ruleset.
|
||||
func (c *Client) CreateRule(ctx context.Context, zoneID, rulesetID string, r rule) (string, error) {
|
||||
c.logger.Printf("[INFO] [cloudflare] Creating WAF rule: %s", r.Description)
|
||||
path := fmt.Sprintf("/zones/%s/rulesets/%s/rules", zoneID, rulesetID)
|
||||
resp, err := c.do(ctx, "POST", path, r)
|
||||
if err != nil {
|
||||
c.logger.Printf("[ERROR] [cloudflare] Failed to create WAF rule: %v", err)
|
||||
return "", fmt.Errorf("create rule: %w", err)
|
||||
}
|
||||
|
||||
@@ -198,9 +202,11 @@ func (c *Client) CreateRule(ctx context.Context, zoneID, rulesetID string, r rul
|
||||
|
||||
// UpdateRule updates an existing rule in the ruleset.
|
||||
func (c *Client) UpdateRule(ctx context.Context, zoneID, rulesetID, ruleID string, r rule) error {
|
||||
c.logger.Printf("[INFO] [cloudflare] Updating WAF rule %s", ruleID)
|
||||
path := fmt.Sprintf("/zones/%s/rulesets/%s/rules/%s", zoneID, rulesetID, ruleID)
|
||||
_, err := c.do(ctx, "PATCH", path, r)
|
||||
if err != nil {
|
||||
c.logger.Printf("[ERROR] [cloudflare] Failed to update WAF rule %s: %v", ruleID, err)
|
||||
return fmt.Errorf("update rule %s: %w", ruleID, err)
|
||||
}
|
||||
c.logger.Printf("[CF] Updated rule %q (%s)", r.Description, ruleID)
|
||||
@@ -216,9 +222,11 @@ func (c *Client) UpdateRule(ctx context.Context, zoneID, rulesetID, ruleID strin
|
||||
|
||||
// DeleteRule removes a rule from the ruleset.
|
||||
func (c *Client) DeleteRule(ctx context.Context, zoneID, rulesetID, ruleID string) error {
|
||||
c.logger.Printf("[INFO] [cloudflare] Deleting WAF rule %s", ruleID)
|
||||
path := fmt.Sprintf("/zones/%s/rulesets/%s/rules/%s", zoneID, rulesetID, ruleID)
|
||||
_, err := c.do(ctx, "DELETE", path, nil)
|
||||
if err != nil {
|
||||
c.logger.Printf("[ERROR] [cloudflare] Failed to delete WAF rule %s: %v", ruleID, err)
|
||||
return fmt.Errorf("delete rule %s: %w", ruleID, err)
|
||||
}
|
||||
c.logger.Printf("[CF] Deleted rule %s", ruleID)
|
||||
|
||||
@@ -52,6 +52,7 @@ func (c *Client) GetZoneID(ctx context.Context, domain string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Printf("[WARN] [cloudflare] No zone found for domain %s", domain)
|
||||
return "", fmt.Errorf("no Cloudflare zone found for domain %q", domain)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ func (m *Manager) OnStackStop(_ context.Context, stackName string) {
|
||||
|
||||
ac, err := m.buildApplyContext(provider, target)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Cannot build context for integration %s revoke: %v", key, err)
|
||||
m.logger.Printf("[WARN] [integrations] Cannot build context for integration %s revoke: %v", key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (m *Manager) OnStackStop(_ context.Context, stackName string) {
|
||||
m.logger.Printf("[DEBUG] [integrations] OnStackStop: revoking %s", key)
|
||||
}
|
||||
if err := handler.Revoke(ac); err != nil {
|
||||
m.logger.Printf("[WARN] Integration revoke on stop failed for %s: %v", key, err)
|
||||
m.logger.Printf("[WARN] [integrations] Integration revoke on stop failed for %s: %v", key, err)
|
||||
}
|
||||
|
||||
if provider == stackName {
|
||||
@@ -58,7 +58,7 @@ func (m *Manager) OnStackStop(_ context.Context, stackName string) {
|
||||
state.Status = "target_unavailable"
|
||||
}
|
||||
_ = m.sett.SetIntegrationState(key, state)
|
||||
m.logger.Printf("[INFO] Integration %s suspended (stack %s stopped)", key, stackName)
|
||||
m.logger.Printf("[INFO] [integrations] Integration %s suspended (stack %s stopped)", key, stackName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +126,12 @@ func (m *Manager) OnStackStart(_ context.Context, stackName string) {
|
||||
|
||||
ac, err := m.buildApplyContext(provider, target)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Cannot re-apply integration %s on start: %v", key, err)
|
||||
m.logger.Printf("[WARN] [integrations] Cannot re-apply integration %s on start: %v", key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := handler.Apply(ac); err != nil {
|
||||
m.logger.Printf("[WARN] Integration re-apply on start failed for %s: %v", key, err)
|
||||
m.logger.Printf("[ERROR] [integrations] Integration re-apply on start failed for %s: %v", key, err)
|
||||
state.Status = "error"
|
||||
state.LastError = err.Error()
|
||||
_ = m.sett.SetIntegrationState(key, state)
|
||||
@@ -141,7 +141,7 @@ func (m *Manager) OnStackStart(_ context.Context, stackName string) {
|
||||
state.Status = "active"
|
||||
state.LastError = ""
|
||||
_ = m.sett.SetIntegrationState(key, state)
|
||||
m.logger.Printf("[INFO] Integration %s re-activated (stack %s started)", key, stackName)
|
||||
m.logger.Printf("[INFO] [integrations] Integration %s re-activated (stack %s started)", key, stackName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,6 @@ func (m *Manager) OnStackRemove(_ context.Context, stackName string) {
|
||||
}
|
||||
|
||||
_ = m.sett.RemoveIntegrationState(key)
|
||||
m.logger.Printf("[INFO] Integration %s removed (stack %s removed)", key, stackName)
|
||||
m.logger.Printf("[INFO] [integrations] Integration %s removed (stack %s removed)", key, stackName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ func (m *Manager) Toggle(ctx context.Context, provider, target string, enable bo
|
||||
|
||||
start := time.Now()
|
||||
if err := handler.Apply(ac); err != nil {
|
||||
m.logger.Printf("[ERROR] [integrations] Integration %s apply failed: %v", key, err)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [integrations] Toggle: Apply failed for %s in %v: %v", key, time.Since(start), err)
|
||||
}
|
||||
@@ -210,6 +211,7 @@ func (m *Manager) ListForProvider(providerSlug string) []StatusInfo {
|
||||
func (m *Manager) buildApplyContext(provider, target string) (*ApplyContext, error) {
|
||||
provStack, ok := m.stacks.GetStack(provider)
|
||||
if !ok {
|
||||
m.logger.Printf("[WARN] [integrations] Failed to build context for %s:%s: provider stack not found", provider, target)
|
||||
return nil, fmt.Errorf("szolgáltató stack %q nem található", provider)
|
||||
}
|
||||
|
||||
@@ -299,6 +301,7 @@ func (m *Manager) loadDecryptedEnv(s *stacks.Stack) map[string]string {
|
||||
stackDir := filepath.Dir(s.ComposePath)
|
||||
cfg := stacks.LoadAppConfig(stackDir)
|
||||
if cfg == nil {
|
||||
m.logger.Printf("[WARN] [integrations] Failed to load env for %s: app config is nil", s.Name)
|
||||
return nil
|
||||
}
|
||||
if m.encKey != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ type OnlyOfficeFileBrowserHandler struct{}
|
||||
func (h *OnlyOfficeFileBrowserHandler) Apply(ac *ApplyContext) error {
|
||||
jwtSecret := ac.ProviderEnv["JWT_SECRET"]
|
||||
if jwtSecret == "" {
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-FileBrowser apply: JWT_SECRET not set")
|
||||
return fmt.Errorf("OnlyOffice JWT_SECRET nincs beállítva — telepítsd újra az alkalmazást")
|
||||
}
|
||||
|
||||
@@ -21,6 +22,7 @@ func (h *OnlyOfficeFileBrowserHandler) Apply(ac *ApplyContext) error {
|
||||
subdomain = ac.ProviderMeta.Subdomain
|
||||
}
|
||||
if subdomain == "" {
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-FileBrowser apply: subdomain unknown")
|
||||
return fmt.Errorf("OnlyOffice aldomain nem ismert")
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ func (h *OnlyOfficeFileBrowserHandler) Apply(ac *ApplyContext) error {
|
||||
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-FileBrowser apply: %v", err)
|
||||
return fmt.Errorf("FileBrowser config olvasási hiba: %w", err)
|
||||
}
|
||||
|
||||
@@ -47,10 +50,12 @@ func (h *OnlyOfficeFileBrowserHandler) Apply(ac *ApplyContext) error {
|
||||
// Atomic write
|
||||
tmpPath := configPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(configStr), 0644); err != nil {
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-FileBrowser apply: %v", err)
|
||||
return fmt.Errorf("config írási hiba: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, configPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-FileBrowser apply: %v", err)
|
||||
return fmt.Errorf("config átnevezési hiba: %w", err)
|
||||
}
|
||||
|
||||
@@ -67,6 +72,7 @@ func (h *OnlyOfficeFileBrowserHandler) Revoke(ac *ApplyContext) error {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-FileBrowser revoke: %v", err)
|
||||
return fmt.Errorf("config olvasási hiba: %w", err)
|
||||
}
|
||||
|
||||
@@ -79,10 +85,12 @@ func (h *OnlyOfficeFileBrowserHandler) Revoke(ac *ApplyContext) error {
|
||||
|
||||
tmpPath := configPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(cleaned), 0644); err != nil {
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-FileBrowser revoke: %v", err)
|
||||
return fmt.Errorf("config írási hiba: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, configPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-FileBrowser revoke: %v", err)
|
||||
return fmt.Errorf("config átnevezési hiba: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type OnlyOfficeNextcloudHandler struct{}
|
||||
func (h *OnlyOfficeNextcloudHandler) Apply(ac *ApplyContext) error {
|
||||
jwtSecret := ac.ProviderEnv["JWT_SECRET"]
|
||||
if jwtSecret == "" {
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-Nextcloud apply: JWT_SECRET not set")
|
||||
return fmt.Errorf("OnlyOffice JWT_SECRET nincs beállítva")
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@ func (h *OnlyOfficeNextcloudHandler) Apply(ac *ApplyContext) error {
|
||||
subdomain = ac.ProviderMeta.Subdomain
|
||||
}
|
||||
if subdomain == "" {
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-Nextcloud apply: subdomain unknown")
|
||||
return fmt.Errorf("OnlyOffice aldomain nem ismert")
|
||||
}
|
||||
|
||||
@@ -72,6 +74,7 @@ func (h *OnlyOfficeNextcloudHandler) Apply(ac *ApplyContext) error {
|
||||
ac.Logger.Printf("[DEBUG] Nextcloud occ: tolerated — %s", strings.TrimSpace(string(out)))
|
||||
continue
|
||||
}
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-Nextcloud apply: occ %s failed: %v", cmd.args[len(cmd.args)-1], err)
|
||||
return fmt.Errorf("occ parancs sikertelen (%s): %v (kimenet: %s)", cmd.args[len(cmd.args)-1], err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
ac.Logger.Printf("[DEBUG] Nextcloud occ %s: ok", strings.Join(cmd.args[7:], " "))
|
||||
@@ -96,6 +99,7 @@ func (h *OnlyOfficeNextcloudHandler) Revoke(ac *ApplyContext) error {
|
||||
ac.Logger.Printf("[DEBUG] Nextcloud occ app:disable skipped — %s", strings.TrimSpace(outStr))
|
||||
return nil
|
||||
}
|
||||
ac.Logger.Printf("[ERROR] [integrations] OnlyOffice-Nextcloud revoke: occ app:disable failed: %v", err)
|
||||
return fmt.Errorf("occ app:disable sikertelen: %v (kimenet: %s)", err, strings.TrimSpace(outStr))
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,17 @@ func RunHealthCheck(cfg *config.Config, cpuCollector *system.CPUCollector, stora
|
||||
if sysInfo.DiskPercent > 0 {
|
||||
if sysInfo.DiskPercent >= float64(cfg.Monitoring.Thresholds.DiskCritPercent) {
|
||||
report.Issues = append(report.Issues, fmt.Sprintf("SSD disk usage critical: %.0f%%", sysInfo.DiskPercent))
|
||||
if logger != nil {
|
||||
logger.Printf("[WARN] [monitor] Disk (SSD) threshold breached: %.0f%% (limit: %d%%)", sysInfo.DiskPercent, cfg.Monitoring.Thresholds.DiskCritPercent)
|
||||
}
|
||||
if debug {
|
||||
logger.Printf("[DEBUG] [HEALTH] SSD disk: CRITICAL (%.0f%% >= %d%%)", sysInfo.DiskPercent, cfg.Monitoring.Thresholds.DiskCritPercent)
|
||||
}
|
||||
} else if sysInfo.DiskPercent >= float64(cfg.Monitoring.Thresholds.DiskWarnPercent) {
|
||||
report.Warnings = append(report.Warnings, fmt.Sprintf("SSD disk usage high: %.0f%%", sysInfo.DiskPercent))
|
||||
if logger != nil {
|
||||
logger.Printf("[WARN] [monitor] Disk (SSD) threshold breached: %.0f%% (limit: %d%%)", sysInfo.DiskPercent, cfg.Monitoring.Thresholds.DiskWarnPercent)
|
||||
}
|
||||
if debug {
|
||||
logger.Printf("[DEBUG] [HEALTH] SSD disk: WARN (%.0f%% >= %d%%)", sysInfo.DiskPercent, cfg.Monitoring.Thresholds.DiskWarnPercent)
|
||||
}
|
||||
@@ -68,8 +74,14 @@ func RunHealthCheck(cfg *config.Config, cpuCollector *system.CPUCollector, stora
|
||||
if sysInfo.HDDConfigured && sysInfo.HDDPercent > 0 {
|
||||
if sysInfo.HDDPercent >= float64(cfg.Monitoring.Thresholds.DiskCritPercent) {
|
||||
report.Issues = append(report.Issues, fmt.Sprintf("HDD disk usage critical: %.0f%%", sysInfo.HDDPercent))
|
||||
if logger != nil {
|
||||
logger.Printf("[WARN] [monitor] Disk (HDD) threshold breached: %.0f%% (limit: %d%%)", sysInfo.HDDPercent, cfg.Monitoring.Thresholds.DiskCritPercent)
|
||||
}
|
||||
} else if sysInfo.HDDPercent >= float64(cfg.Monitoring.Thresholds.DiskWarnPercent) {
|
||||
report.Warnings = append(report.Warnings, fmt.Sprintf("HDD disk usage high: %.0f%%", sysInfo.HDDPercent))
|
||||
if logger != nil {
|
||||
logger.Printf("[WARN] [monitor] Disk (HDD) threshold breached: %.0f%% (limit: %d%%)", sysInfo.HDDPercent, cfg.Monitoring.Thresholds.DiskWarnPercent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +89,9 @@ func RunHealthCheck(cfg *config.Config, cpuCollector *system.CPUCollector, stora
|
||||
if sysInfo.MemPercent > 0 {
|
||||
if sysInfo.MemPercent >= float64(cfg.Monitoring.Thresholds.MemoryWarnPercent) {
|
||||
report.Warnings = append(report.Warnings, fmt.Sprintf("Memory usage high: %.0f%%", sysInfo.MemPercent))
|
||||
if logger != nil {
|
||||
logger.Printf("[WARN] [monitor] Memory threshold breached: %.0f%% (limit: %d%%)", sysInfo.MemPercent, cfg.Monitoring.Thresholds.MemoryWarnPercent)
|
||||
}
|
||||
if debug {
|
||||
logger.Printf("[DEBUG] [HEALTH] Memory: WARN (%.0f%% >= %d%%)", sysInfo.MemPercent, cfg.Monitoring.Thresholds.MemoryWarnPercent)
|
||||
}
|
||||
@@ -92,6 +107,9 @@ func RunHealthCheck(cfg *config.Config, cpuCollector *system.CPUCollector, stora
|
||||
if sysInfo.CPUPercent > 0 {
|
||||
if sysInfo.CPUPercent >= float64(cfg.Monitoring.Thresholds.CPUWarnPercent) {
|
||||
report.Warnings = append(report.Warnings, fmt.Sprintf("CPU usage high: %.0f%%", sysInfo.CPUPercent))
|
||||
if logger != nil {
|
||||
logger.Printf("[WARN] [monitor] CPU threshold breached: %.0f%% (limit: %d%%)", sysInfo.CPUPercent, cfg.Monitoring.Thresholds.CPUWarnPercent)
|
||||
}
|
||||
if debug {
|
||||
logger.Printf("[DEBUG] [HEALTH] CPU: WARN (%.0f%% >= %d%%)", sysInfo.CPUPercent, cfg.Monitoring.Thresholds.CPUWarnPercent)
|
||||
}
|
||||
@@ -107,6 +125,9 @@ func RunHealthCheck(cfg *config.Config, cpuCollector *system.CPUCollector, stora
|
||||
if sysInfo.TemperatureCelsius > 0 {
|
||||
if sysInfo.TemperatureCelsius >= float64(cfg.Monitoring.Thresholds.TemperatureWarnCelsius) {
|
||||
report.Warnings = append(report.Warnings, fmt.Sprintf("Temperature high: %.0f°C (%s)", sysInfo.TemperatureCelsius, sysInfo.TemperatureSource))
|
||||
if logger != nil {
|
||||
logger.Printf("[WARN] [monitor] Temperature threshold breached: %.0f°C (limit: %d°C)", sysInfo.TemperatureCelsius, cfg.Monitoring.Thresholds.TemperatureWarnCelsius)
|
||||
}
|
||||
if debug {
|
||||
logger.Printf("[DEBUG] [HEALTH] Temperature: WARN (%.0f°C >= %d°C)", sysInfo.TemperatureCelsius, cfg.Monitoring.Thresholds.TemperatureWarnCelsius)
|
||||
}
|
||||
@@ -159,6 +180,10 @@ func RunHealthCheck(cfg *config.Config, cpuCollector *system.CPUCollector, stora
|
||||
report.Status = "warn"
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Printf("[INFO] [monitor] Health check: status=%s", report.Status)
|
||||
}
|
||||
|
||||
if debug {
|
||||
logger.Printf("[DEBUG] [HEALTH] Final status: %s (issues=%d, warnings=%d, info=%d)",
|
||||
report.Status, len(report.Issues), len(report.Warnings), len(report.Info))
|
||||
|
||||
@@ -34,6 +34,9 @@ func BuildReport(
|
||||
logger *log.Logger,
|
||||
) *Report {
|
||||
debug := cfg.Logging.Level == "debug"
|
||||
if logger != nil {
|
||||
logger.Printf("[INFO] [report] Building system report")
|
||||
}
|
||||
if debug && logger != nil {
|
||||
logger.Printf("[DEBUG] BuildReport: starting — version=%s, storagePaths=%d", version, len(storagePaths))
|
||||
}
|
||||
|
||||
@@ -125,6 +125,8 @@ func (p *Pusher) Push(report *Report) error {
|
||||
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
p.logger.Printf("[WARN] [report] Push failed: %v", lastErr)
|
||||
|
||||
p.statusMu.Lock()
|
||||
p.status.LastError = lastErr.Error()
|
||||
p.status.Consecutive++
|
||||
@@ -186,6 +188,7 @@ func (p *Pusher) PushInfraBackup(data []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
p.logger.Printf("[WARN] [report] InfraBackup push failed: %v", lastErr)
|
||||
return fmt.Errorf("infra backup push failed after 3 attempts: %w", lastErr)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func New(logger *log.Logger) *Scheduler {
|
||||
// If the scheduler is already started, the job's goroutine is launched immediately.
|
||||
func (s *Scheduler) Every(name string, interval time.Duration, fn JobFunc) {
|
||||
if interval <= 0 {
|
||||
s.logger.Printf("[ERROR] Periodic job %s has invalid interval %s — job not registered", name, interval)
|
||||
s.logger.Printf("[ERROR] [scheduler] Periodic job %s has invalid interval %s — job not registered", name, interval)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (s *Scheduler) Every(name string, interval time.Duration, fn JobFunc) {
|
||||
Interval: interval,
|
||||
}
|
||||
s.jobs = append(s.jobs, job)
|
||||
s.logger.Printf("[SCHED] Registered periodic job: %s (every %s)", name, interval)
|
||||
s.logger.Printf("[INFO] [scheduler] Registered periodic job: %s (every %s)", name, interval)
|
||||
s.dbg("periodic job registered: name=%q interval=%s totalJobs=%d", name, interval, len(s.jobs))
|
||||
|
||||
if s.started {
|
||||
@@ -105,7 +105,7 @@ func (s *Scheduler) Daily(name string, timeStr string, fn JobFunc) {
|
||||
|
||||
// Validate time format
|
||||
if _, _, err := parseDailyTime(timeStr); err != nil {
|
||||
s.logger.Printf("[ERROR] Daily job %s has invalid schedule %q: %v — job not started", name, timeStr, err)
|
||||
s.logger.Printf("[ERROR] [scheduler] Daily job %s has invalid schedule %q: %v — job not started", name, timeStr, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (s *Scheduler) Daily(name string, timeStr string, fn JobFunc) {
|
||||
s.jobs = append(s.jobs, job)
|
||||
|
||||
nextRun := nextDailyRun(timeStr)
|
||||
s.logger.Printf("[SCHED] Daily job %s scheduled for %s", name, nextRun.Format("2006-01-02 15:04 MST"))
|
||||
s.logger.Printf("[INFO] [scheduler] Daily job %s scheduled for %s", name, nextRun.Format("2006-01-02 15:04 MST"))
|
||||
s.dbg("daily job registered: name=%q schedule=%q nextRun=%s totalJobs=%d", name, timeStr, nextRun.Format(time.RFC3339), len(s.jobs))
|
||||
|
||||
if s.started {
|
||||
@@ -131,7 +131,7 @@ func (s *Scheduler) Start(ctx context.Context) {
|
||||
s.mu.Lock()
|
||||
if s.cancel != nil {
|
||||
s.mu.Unlock()
|
||||
s.logger.Println("[WARN] Scheduler already started — ignoring duplicate Start()")
|
||||
s.logger.Println("[WARN] [scheduler] Scheduler already started — ignoring duplicate Start()")
|
||||
return
|
||||
}
|
||||
s.ctx, s.cancel = context.WithCancel(ctx)
|
||||
@@ -147,7 +147,7 @@ func (s *Scheduler) Start(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Printf("[SCHED] Scheduler started with %d jobs", len(s.jobs))
|
||||
s.logger.Printf("[INFO] [scheduler] Starting scheduler with %d jobs", len(s.jobs))
|
||||
s.dbg("scheduler started: periodic=%d daily=%d", func() int {
|
||||
n := 0
|
||||
for _, j := range s.jobs {
|
||||
@@ -173,6 +173,7 @@ func (s *Scheduler) Stop() {
|
||||
s.mu.Lock()
|
||||
cancel := s.cancel
|
||||
s.mu.Unlock()
|
||||
s.logger.Printf("[INFO] [scheduler] Stopping scheduler")
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
@@ -185,9 +186,9 @@ func (s *Scheduler) Stop() {
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
s.logger.Println("[SCHED] All jobs stopped")
|
||||
s.logger.Println("[INFO] [scheduler] All jobs stopped")
|
||||
case <-time.After(30 * time.Second):
|
||||
s.logger.Println("[WARN] Scheduler stop timed out after 30s — some jobs may still be running")
|
||||
s.logger.Println("[WARN] [scheduler] Scheduler stop timed out after 30s — some jobs may still be running")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +252,7 @@ func (s *Scheduler) executeJob(job *Job, quiet bool) {
|
||||
s.mu.Lock()
|
||||
if job.Running {
|
||||
s.mu.Unlock()
|
||||
s.logger.Printf("[WARN] Job %s still running, skipping", job.Name)
|
||||
s.logger.Printf("[WARN] [scheduler] Job %s still running, skipping", job.Name)
|
||||
return
|
||||
}
|
||||
job.Running = true
|
||||
@@ -270,12 +271,12 @@ func (s *Scheduler) executeJob(job *Job, quiet bool) {
|
||||
job.LastErr = fmt.Errorf("panic: %v", r)
|
||||
job.LastRun = time.Now()
|
||||
s.mu.Unlock()
|
||||
s.logger.Printf("[ERROR] Job %s panicked: %v", job.Name, r)
|
||||
s.logger.Printf("[ERROR] [scheduler] Job %s panicked: %v", job.Name, r)
|
||||
}
|
||||
}()
|
||||
|
||||
if !quiet {
|
||||
s.logger.Printf("[SCHED] Running job: %s", job.Name)
|
||||
s.logger.Printf("[INFO] [scheduler] Running job: %s", job.Name)
|
||||
}
|
||||
s.dbg("job %s: execution starting", job.Name)
|
||||
|
||||
@@ -289,10 +290,10 @@ func (s *Scheduler) executeJob(job *Job, quiet bool) {
|
||||
s.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
s.logger.Printf("[WARN] Job %s failed: %v (took %s)", job.Name, err, elapsed.Round(time.Millisecond))
|
||||
s.logger.Printf("[ERROR] [scheduler] Job %s failed: %v (took %s)", job.Name, err, elapsed.Round(time.Millisecond))
|
||||
s.dbg("job %s: failed after %s: %v", job.Name, elapsed.Round(time.Millisecond), err)
|
||||
} else if !quiet {
|
||||
s.logger.Printf("[SCHED] Job %s completed (took %s)", job.Name, elapsed.Round(time.Millisecond))
|
||||
s.logger.Printf("[INFO] [scheduler] Job %s completed (took %s)", job.Name, elapsed.Round(time.Millisecond))
|
||||
}
|
||||
s.dbg("job %s: finished in %s (err=%v)", job.Name, elapsed.Round(time.Millisecond), err)
|
||||
}
|
||||
|
||||
@@ -67,5 +67,7 @@ func ClearState(dataDir string, logger *log.Logger) {
|
||||
path := filepath.Join(dataDir, stateFileName)
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
logger.Printf("[WARN] Failed to clear update state file: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Printf("[INFO] [selfupdate] Update state cleared")
|
||||
}
|
||||
|
||||
@@ -144,6 +144,9 @@ func (u *Updater) CheckForUpdate() CheckResult {
|
||||
cmp := latestVer.Compare(currentVer)
|
||||
if cmp > 0 {
|
||||
result.UpdateAvailable = true
|
||||
u.logger.Printf("[INFO] [selfupdate] Update available: %s → %s", u.currentVer, latestStr)
|
||||
} else {
|
||||
u.logger.Printf("[INFO] [selfupdate] Current version %s is up to date", u.currentVer)
|
||||
}
|
||||
|
||||
u.dbg("version comparison: current=%s (%d.%d.%d), latest=%s (%d.%d.%d), cmp=%d, updateAvailable=%v",
|
||||
@@ -422,8 +425,10 @@ func (u *Updater) performUpdate(targetVersion, targetImage, previousImage, initi
|
||||
|
||||
// updateComposeFile reads the compose file, replaces the image tag, and writes it back atomically.
|
||||
func (u *Updater) updateComposeFile(newImage string) error {
|
||||
u.logger.Printf("[INFO] [selfupdate] Updating compose file")
|
||||
data, err := os.ReadFile(u.composePath)
|
||||
if err != nil {
|
||||
u.logger.Printf("[ERROR] [selfupdate] Failed to update compose file: %v", err)
|
||||
return fmt.Errorf("reading compose file: %w", err)
|
||||
}
|
||||
|
||||
@@ -441,15 +446,18 @@ func (u *Updater) updateComposeFile(newImage string) error {
|
||||
newData := re.ReplaceAll(data, []byte("${1}"+newImage))
|
||||
|
||||
if bytes.Equal(data, newData) {
|
||||
u.logger.Printf("[ERROR] [selfupdate] Failed to update compose file: no image line found to replace")
|
||||
return fmt.Errorf("no image line found to replace in compose file")
|
||||
}
|
||||
|
||||
// Atomic write: write to .tmp, then rename
|
||||
tmpPath := u.composePath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, newData, 0644); err != nil {
|
||||
u.logger.Printf("[ERROR] [selfupdate] Failed to update compose file: %v", err)
|
||||
return fmt.Errorf("writing temp compose file: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, u.composePath); err != nil {
|
||||
u.logger.Printf("[ERROR] [selfupdate] Failed to update compose file: %v", err)
|
||||
return fmt.Errorf("renaming compose file: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ func Load(path string, logger *log.Logger) (*Settings, error) {
|
||||
return nil, fmt.Errorf("parsing settings file: %w", err)
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] Settings loaded from %s", path)
|
||||
logger.Printf("[INFO] [settings] Loaded settings from %s", path)
|
||||
if s.debug {
|
||||
s.log.Printf("[DEBUG] [settings] loaded: storage_paths=%d integrations=%d pending_events=%d",
|
||||
len(s.StoragePaths), len(s.Integrations), len(s.PendingEvents))
|
||||
@@ -218,27 +218,42 @@ func (s *Settings) migrateResticToRsync() {
|
||||
func (s *Settings) save() error {
|
||||
data, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
if s.log != nil {
|
||||
s.log.Printf("[ERROR] [settings] Failed to save: %v", err)
|
||||
}
|
||||
return fmt.Errorf("marshaling settings: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := s.path + ".tmp"
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
|
||||
if s.log != nil {
|
||||
s.log.Printf("[ERROR] [settings] Failed to save: %v", err)
|
||||
}
|
||||
return fmt.Errorf("creating settings dir: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tmpPath, data, 0644); err != nil {
|
||||
os.Remove(tmpPath) // clean up partial file
|
||||
if s.log != nil {
|
||||
s.log.Printf("[ERROR] [settings] Failed to save: %v", err)
|
||||
}
|
||||
return fmt.Errorf("writing tmp settings: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, s.path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
if s.log != nil {
|
||||
s.log.Printf("[ERROR] [settings] Failed to save: %v", err)
|
||||
}
|
||||
return fmt.Errorf("renaming settings file: %w", err)
|
||||
}
|
||||
|
||||
if s.debug {
|
||||
s.log.Printf("[DEBUG] [settings] saved to %s (%d bytes)", s.path, len(data))
|
||||
}
|
||||
if s.log != nil {
|
||||
s.log.Printf("[INFO] [settings] Settings saved")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -461,6 +476,9 @@ func (s *Settings) AddStoragePath(sp StoragePath) error {
|
||||
}
|
||||
}
|
||||
s.StoragePaths = append(s.StoragePaths, sp)
|
||||
if s.log != nil {
|
||||
s.log.Printf("[INFO] [settings] Added storage path: %s", sp.Path)
|
||||
}
|
||||
return s.save()
|
||||
}
|
||||
|
||||
@@ -478,6 +496,9 @@ func (s *Settings) RemoveStoragePath(path string) error {
|
||||
}
|
||||
}
|
||||
s.StoragePaths = kept
|
||||
if s.log != nil {
|
||||
s.log.Printf("[INFO] [settings] Removed storage path: %s", path)
|
||||
}
|
||||
return s.save()
|
||||
}
|
||||
|
||||
@@ -597,6 +618,9 @@ func (s *Settings) SetDisconnected(path string, disconnected bool, stoppedStacks
|
||||
if s.debug {
|
||||
s.log.Printf("[DEBUG] [settings] SetDisconnected path=%q disconnected=%v stopped_stacks=%d", path, disconnected, len(stoppedStacks))
|
||||
}
|
||||
if s.log != nil {
|
||||
s.log.Printf("[INFO] [settings] Node disconnected: %v", disconnected)
|
||||
}
|
||||
for i := range s.StoragePaths {
|
||||
if s.StoragePaths[i].Path == path {
|
||||
s.StoragePaths[i].Disconnected = disconnected
|
||||
@@ -707,6 +731,9 @@ func (s *Settings) SetDecommissioned(path, migratedTo string) error {
|
||||
if s.debug {
|
||||
s.log.Printf("[DEBUG] [settings] SetDecommissioned path=%q migrated_to=%q", path, migratedTo)
|
||||
}
|
||||
if s.log != nil {
|
||||
s.log.Printf("[INFO] [settings] Node decommissioned")
|
||||
}
|
||||
for i := range s.StoragePaths {
|
||||
if s.StoragePaths[i].Path == path {
|
||||
s.StoragePaths[i].Decommissioned = true
|
||||
@@ -842,6 +869,9 @@ func (s *Settings) AddPendingEvent(event PendingEvent) error {
|
||||
if s.debug {
|
||||
s.log.Printf("[DEBUG] [settings] AddPendingEvent type=%q severity=%q", event.EventType, event.Severity)
|
||||
}
|
||||
if s.log != nil {
|
||||
s.log.Printf("[INFO] [settings] Added pending event: %s", event.EventType)
|
||||
}
|
||||
s.PendingEvents = append(s.PendingEvents, event)
|
||||
return s.save()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -557,6 +558,7 @@ func ParseComposeHDDMounts(composePath, hddPath string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[INFO] [stacks] ParseComposeHDDMounts: found %d HDD mounts for %s", len(mounts), composePath)
|
||||
return mounts
|
||||
}
|
||||
|
||||
|
||||
@@ -375,6 +375,7 @@ func (m *Manager) runComposeDeploy(name, stackDir string, env map[string]string,
|
||||
|
||||
// UpdateStackConfig updates non-locked fields for a deployed stack.
|
||||
func (m *Manager) UpdateStackConfig(name string, values map[string]string) error {
|
||||
m.logger.Printf("[INFO] [stacks] Updating config for stack %s", name)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] UpdateStackConfig called: name=%q, %d values to update", name, len(values))
|
||||
}
|
||||
@@ -457,6 +458,7 @@ func (m *Manager) GetDeployFields(name string) (*Metadata, *AppConfig, error) {
|
||||
// UpdateOptionalConfig updates optional env vars in app.yaml and restarts the stack if deployed.
|
||||
// Only updates env vars that are listed in the metadata's optional_config sections.
|
||||
func (m *Manager) UpdateOptionalConfig(stackName string, values map[string]string) error {
|
||||
m.logger.Printf("[INFO] [stacks] Updating optional config for stack %s", stackName)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] UpdateOptionalConfig called: stack=%q, %d values provided", stackName, len(values))
|
||||
}
|
||||
@@ -588,6 +590,7 @@ func LoadAppConfig(stackDir string) *AppConfig {
|
||||
}
|
||||
cfg := &AppConfig{}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
log.Printf("[WARN] [stacks] LoadAppConfig: %v", err)
|
||||
log.Printf("[DEBUG] [stacks] LoadAppConfig: failed to parse %s: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
@@ -626,6 +629,7 @@ func SaveAppConfig(stackDir string, cfg *AppConfig, encKey []byte, sensitiveVars
|
||||
|
||||
data, err := yaml.Marshal(saveCfg)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [stacks] SaveAppConfig: failed to marshal config for %s: %v", stackDir, err)
|
||||
return fmt.Errorf("marshaling app config: %w", err)
|
||||
}
|
||||
path := filepath.Join(stackDir, "app.yaml")
|
||||
@@ -635,12 +639,15 @@ func SaveAppConfig(stackDir string, cfg *AppConfig, encKey []byte, sensitiveVars
|
||||
// Atomic write: write to .tmp then rename (H04 fix)
|
||||
tmpPath := path + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(content), 0600); err != nil {
|
||||
log.Printf("[ERROR] [stacks] SaveAppConfig: failed to save %s: %v", path, err)
|
||||
return fmt.Errorf("writing %s: %w", tmpPath, err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
log.Printf("[ERROR] [stacks] SaveAppConfig: failed to save %s: %v", path, err)
|
||||
return fmt.Errorf("renaming %s to %s: %w", tmpPath, path, err)
|
||||
}
|
||||
log.Printf("[INFO] [stacks] SaveAppConfig: saved config for %s", filepath.Base(stackDir))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -724,11 +731,13 @@ func (m *Manager) InjectMissingFields(stackNames []string) {
|
||||
m.logger.Printf("[DEBUG] [stacks] InjectMissingFields: checking %d stacks", len(stackNames))
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, name := range stackNames {
|
||||
stack, ok := m.GetStack(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
|
||||
stackDir := filepath.Dir(stack.ComposePath)
|
||||
meta := LoadMetadata(stackDir)
|
||||
@@ -804,6 +813,7 @@ func (m *Manager) InjectMissingFields(stackNames []string) {
|
||||
m.logger.Printf("[SYNC] Stack %s: injected missing fields: %s", name, strings.Join(injected, ", "))
|
||||
}
|
||||
}
|
||||
m.logger.Printf("[INFO] [stacks] InjectMissingFields: processed %d stacks", count)
|
||||
}
|
||||
|
||||
func containsStr(slice []string, s string) bool {
|
||||
|
||||
@@ -126,9 +126,9 @@ func (m *Manager) RunHealthProbes() error {
|
||||
|
||||
// Summary log
|
||||
if failCount > 0 {
|
||||
m.logger.Printf("[INFO] Health probes: %d ok, %d unhealthy (of %d probed)", okCount, failCount, len(targets))
|
||||
} else if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] Health probes: %d ok (of %d probed)", okCount, len(targets))
|
||||
m.logger.Printf("[WARN] Health probes: %d ok, %d unhealthy (of %d probed)", okCount, failCount, len(targets))
|
||||
} else {
|
||||
m.logger.Printf("[INFO] Health probes: %d ok (of %d probed)", okCount, len(targets))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -184,6 +184,8 @@ func (m *Manager) MigrateEncryption() {
|
||||
}
|
||||
if migrated > 0 {
|
||||
m.logger.Printf("[INFO] Encrypted sensitive values in %d app.yaml file(s)", migrated)
|
||||
} else {
|
||||
m.logger.Printf("[INFO] [stacks] Encryption migration: no stacks needed migration")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +328,7 @@ func (m *Manager) ScanStacks() error {
|
||||
}
|
||||
m.logger.Printf("[INFO] Scanned stacks: %d found (%d deployed, %d available)",
|
||||
len(m.stacks), deployedCount, len(m.stacks)-deployedCount)
|
||||
m.logger.Printf("[INFO] [stacks] ScanStacks complete: %d stacks found", len(m.stacks))
|
||||
return m.refreshStatusLocked()
|
||||
}
|
||||
|
||||
@@ -370,6 +373,8 @@ func (m *Manager) refreshStatusLocked() error {
|
||||
m.logger.Printf("[DEBUG] [stacks] refreshStatusLocked: docker ps returned %d containers across %d projects", totalContainers, len(projectContainers))
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] [stacks] Status refresh: %d containers across %d stacks", totalContainers, len(m.stacks))
|
||||
|
||||
for name, stack := range m.stacks {
|
||||
containers, exists := projectContainers[name]
|
||||
if !exists {
|
||||
@@ -759,6 +764,7 @@ func (m *Manager) GetLogs(name string, lines int) (string, error) {
|
||||
lines = 1000
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] [stacks] Fetching logs for stack %s (tail=%d)", name, lines)
|
||||
m.logger.Printf("[DEBUG] Fetching logs for %s (tail %d)", name, lines)
|
||||
|
||||
dir := filepath.Dir(stack.ComposePath)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -126,7 +126,7 @@ func LoadMetadata(stackDir string) Metadata {
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &meta); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Failed to parse .felhom.yml in %s: %v\n", stackDir, err)
|
||||
log.Printf("[ERROR] [stacks] Failed to parse .felhom.yml in %s: %v", stackDir, err)
|
||||
dirName := filepath.Base(stackDir)
|
||||
meta.DisplayName = toTitleCase(strings.ReplaceAll(dirName, "-", " "))
|
||||
meta.Slug = dirName
|
||||
|
||||
@@ -184,6 +184,9 @@ func FinalizeAttach(req AttachRequest, progress chan<- FormatProgress) (string,
|
||||
errStr = err.Error()
|
||||
}
|
||||
progress <- FormatProgress{Step: "error", Message: msg, Error: errStr, Percent: 0}
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[ERROR] [storage] Failed to attach disk: %v", err)
|
||||
}
|
||||
return fmt.Errorf("%s: %w", msg, err)
|
||||
}
|
||||
dbg := func(format string, args ...interface{}) {
|
||||
@@ -193,6 +196,9 @@ func FinalizeAttach(req AttachRequest, progress chan<- FormatProgress) (string,
|
||||
}
|
||||
|
||||
mountPath := "/mnt/" + req.MountName
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[INFO] [storage] Attaching disk %s at %s", req.DevicePath, mountPath)
|
||||
}
|
||||
dbg("starting: device=%s mountName=%s subPath=%s", req.DevicePath, req.MountName, req.SubPath)
|
||||
|
||||
// --- Step 1: Validate ---
|
||||
@@ -322,6 +328,9 @@ func FinalizeAttach(req AttachRequest, progress chan<- FormatProgress) (string,
|
||||
}
|
||||
|
||||
dbg("attach completed successfully: %s", mountPath)
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[INFO] [storage] Disk attached successfully")
|
||||
}
|
||||
send("done", "Meghajtó sikeresen csatolva: "+mountPath, 100)
|
||||
|
||||
return mountPath, nil
|
||||
|
||||
@@ -27,6 +27,9 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string,
|
||||
errStr = err.Error()
|
||||
}
|
||||
progress <- FormatProgress{Step: "error", Message: msg, Error: errStr, Percent: 0}
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[ERROR] [storage] Format failed: %v", err)
|
||||
}
|
||||
return fmt.Errorf("%s: %w", msg, err)
|
||||
}
|
||||
dbg := func(format string, args ...interface{}) {
|
||||
@@ -36,6 +39,9 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string,
|
||||
}
|
||||
|
||||
mountPath := "/mnt/" + req.MountName
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[INFO] [storage] Formatting disk %s as ext4", req.DevicePath)
|
||||
}
|
||||
dbg("starting: device=%s mountName=%s createPartition=%v", req.DevicePath, req.MountName, req.CreatePartition)
|
||||
|
||||
// --- Step 1: Validate ---
|
||||
@@ -216,6 +222,9 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string,
|
||||
}
|
||||
|
||||
dbg("format and mount completed successfully: %s", mountPath)
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[INFO] [storage] Disk formatted and mounted at %s", mountPath)
|
||||
}
|
||||
send("done", "Meghajtó sikeresen inicializálva: "+mountPath, 100)
|
||||
|
||||
return mountPath, nil
|
||||
|
||||
@@ -82,6 +82,9 @@ func MigrateAppData(
|
||||
Error: errStr,
|
||||
ElapsedSeconds: int(time.Since(start).Seconds()),
|
||||
}
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[ERROR] [storage] Migration %s failed at %s: %v", req.StackName, step, err)
|
||||
}
|
||||
return fmt.Errorf("%s: %w", msg, err)
|
||||
}
|
||||
|
||||
@@ -91,6 +94,9 @@ func MigrateAppData(
|
||||
}
|
||||
}
|
||||
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[INFO] [storage] Starting migration: stack=%s from=%s to=%s", req.StackName, req.CurrentHDDPath, req.TargetPath)
|
||||
}
|
||||
dbg("starting migration: stack=%s from=%s to=%s mounts=%d", req.StackName, req.CurrentHDDPath, req.TargetPath, len(req.HDDMounts))
|
||||
|
||||
// --- Step 1: Validate ---
|
||||
@@ -196,6 +202,9 @@ func MigrateAppData(
|
||||
return fail("starting", "Alkalmazás indítása sikertelen az új tárolóról", err)
|
||||
}
|
||||
|
||||
if req.Logger != nil {
|
||||
req.Logger.Printf("[INFO] [storage] Migration completed: stack=%s elapsed=%ds", req.StackName, int(time.Since(start).Seconds()))
|
||||
}
|
||||
dbg("migration completed: stack=%s bytesCopied=%d elapsed=%ds", req.StackName, bytesCopied, int(time.Since(start).Seconds()))
|
||||
send("done",
|
||||
fmt.Sprintf("Áthelyezés kész! Az alkalmazás az új tárolóról fut. (Régi adat: %s, idő: %ds)",
|
||||
|
||||
@@ -246,6 +246,9 @@ func ScanDisks(logger *log.Logger, debug bool) (*ScanResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Printf("[INFO] [storage] Scanning disks")
|
||||
}
|
||||
dbg("starting disk scan")
|
||||
scanStart := time.Now()
|
||||
|
||||
@@ -341,6 +344,9 @@ func ScanDisks(logger *log.Logger, debug bool) (*ScanResult, error) {
|
||||
enrichWithBlkid(result.AvailableDisks, logger, debug)
|
||||
enrichWithBlkid(result.SystemDisks, logger, debug)
|
||||
|
||||
if logger != nil {
|
||||
logger.Printf("[INFO] [storage] Found %d disks", len(result.AvailableDisks)+len(result.SystemDisks))
|
||||
}
|
||||
dbg("disk scan completed in %s", time.Since(scanStart).Round(time.Millisecond))
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -178,8 +178,11 @@ func (s *Syncer) doSync() SyncResult {
|
||||
|
||||
result := SyncResult{OK: true}
|
||||
|
||||
s.logger.Printf("[INFO] [sync] Starting catalog sync")
|
||||
|
||||
// Step 1: Clone or pull
|
||||
if err := s.gitCloneOrPull(); err != nil {
|
||||
s.logger.Printf("[ERROR] [sync] Catalog sync failed: %v", err)
|
||||
s.mu.Lock()
|
||||
s.lastErr = err
|
||||
s.lastSync = time.Now()
|
||||
@@ -190,6 +193,7 @@ func (s *Syncer) doSync() SyncResult {
|
||||
// Step 2: Copy templates to stacks dir
|
||||
newApps, updated, err := s.copyTemplates()
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [sync] Catalog sync failed: %v", err)
|
||||
s.mu.Lock()
|
||||
s.lastErr = err
|
||||
s.lastSync = time.Now()
|
||||
@@ -229,6 +233,8 @@ func (s *Syncer) doSync() SyncResult {
|
||||
result.Message = "Sablonok frissítve — " + strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] [sync] Catalog sync complete")
|
||||
|
||||
s.mu.Lock()
|
||||
s.lastErr = nil
|
||||
s.lastSync = time.Now()
|
||||
@@ -344,7 +350,7 @@ func (s *Syncer) copyTemplates() (newApps []string, updated []string, err error)
|
||||
|
||||
changed, err := copyIfChanged(src, dst)
|
||||
if err != nil {
|
||||
s.logger.Printf("[SYNC] Failed to copy %s/%s: %v", appName, filename, err)
|
||||
s.logger.Printf("[WARN] [sync] Failed to copy catalog file %s/%s: %v", appName, filename, err)
|
||||
continue
|
||||
}
|
||||
if changed {
|
||||
|
||||
@@ -5,6 +5,7 @@ package system
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -150,6 +151,7 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
h.Warning = "A cél tárhely (" + path + ") nem létezik!"
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
log.Printf("[WARN] [system] Backup destination %s is not safe: path does not exist", path)
|
||||
debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier1 FAIL (not exists)", path)
|
||||
return h
|
||||
}
|
||||
@@ -160,6 +162,7 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
h.Warning = "A cél tárhely (" + path + ") nem írható! Ellenőrizd a jogosultságokat."
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
log.Printf("[WARN] [system] Backup destination %s is not safe: not writable", path)
|
||||
debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier2 FAIL (not writable)", path)
|
||||
return h
|
||||
}
|
||||
@@ -191,10 +194,12 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
h.Warning = fmt.Sprintf("A rendszermeghajtón csak %.1f GB szabad — legalább 10 GB szükséges a rendszer stabilitásához!", di.AvailGB)
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
log.Printf("[WARN] [system] Backup destination %s is not safe: system drive low space (%.1f GB free)", path, di.AvailGB)
|
||||
} else if di.UsedPercent >= 90 {
|
||||
h.Warning = fmt.Sprintf("A rendszermeghajtó %.0f%%-ban megtelt — maximum 90%% megengedett.", di.UsedPercent)
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
log.Printf("[WARN] [system] Backup destination %s is not safe: system drive %.0f%% full", path, di.UsedPercent)
|
||||
}
|
||||
// If neither triggers, keep the Tier 3 system-drive warning
|
||||
} else {
|
||||
@@ -203,6 +208,7 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
h.Warning = fmt.Sprintf("A mentési meghajtó megtelt (%.0f%% használt)!", di.UsedPercent)
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
log.Printf("[WARN] [system] Backup destination %s is not safe: drive %.0f%% full", path, di.UsedPercent)
|
||||
} else if di.UsedPercent >= 90 {
|
||||
h.Warning = fmt.Sprintf("A mentési meghajtó majdnem megtelt (%.0f%% használt).", di.UsedPercent)
|
||||
h.Severity = "warning"
|
||||
@@ -273,6 +279,7 @@ func ProbeStoragePath(path string) ProbeResult {
|
||||
|
||||
// Quick check: does the path exist at all?
|
||||
if _, err := os.Lstat(path); os.IsNotExist(err) {
|
||||
log.Printf("[WARN] [system] Storage path %s probe failed: %v", path, err)
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — not exists (%s)", path, time.Since(start).Round(time.Millisecond))
|
||||
return ProbeResult{Status: ProbeDisconnected, Err: err}
|
||||
}
|
||||
@@ -298,12 +305,15 @@ func ProbeStoragePath(path string) ProbeResult {
|
||||
if strings.Contains(errStr, "transport endpoint") ||
|
||||
strings.Contains(errStr, "input/output error") ||
|
||||
strings.Contains(errStr, "no such device") {
|
||||
log.Printf("[WARN] [system] Storage path %s probe failed: %v", path, res.err)
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — disconnected: %v (%s)", path, res.err, elapsed)
|
||||
return ProbeResult{Status: ProbeDisconnected, Err: res.err}
|
||||
}
|
||||
log.Printf("[WARN] [system] Storage path %s probe failed: %v", path, res.err)
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — disconnected (other error): %v (%s)", path, res.err, elapsed)
|
||||
return ProbeResult{Status: ProbeDisconnected, Err: res.err}
|
||||
case <-time.After(3 * time.Second):
|
||||
log.Printf("[WARN] [system] Storage path %s probe failed: stat timed out after 3s", path)
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — TIMEOUT (3s)", path)
|
||||
return ProbeResult{Status: ProbeTimeout, Err: fmt.Errorf("stat timed out after 3s")}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ func (s *Server) RequireAuth(next http.Handler) http.Handler {
|
||||
s.logger.Printf("[DEBUG] [web] auth: rejected %s %s from %s (%s)", r.Method, r.URL.Path, r.RemoteAddr, reason)
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
s.logger.Printf("[WARN] [api] Unauthorized request to %s from %s", r.URL.Path, r.RemoteAddr)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprint(w, `{"ok":false,"error":"authentication required"}`)
|
||||
@@ -203,6 +204,7 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
delete(s.sessions, cookie.Value)
|
||||
s.sessionsMu.Unlock()
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] User logged out from %s", r.RemoteAddr)
|
||||
http.SetCookie(w, &http.Cookie{Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1})
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
@@ -257,6 +259,7 @@ func (s *Server) invalidateAllSessions() {
|
||||
count := len(s.sessions)
|
||||
s.sessions = make(map[string]*session)
|
||||
s.sessionsMu.Unlock()
|
||||
s.logger.Printf("[INFO] [web] All sessions invalidated (cleared %d)", count)
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] invalidated all sessions (cleared %d)", count)
|
||||
}
|
||||
@@ -281,6 +284,9 @@ func (s *Server) cleanupSessions() {
|
||||
}
|
||||
remaining := len(s.sessions)
|
||||
s.sessionsMu.Unlock()
|
||||
if expired > 0 {
|
||||
s.logger.Printf("[INFO] [web] Cleaned up %d expired sessions, %d remaining", expired, remaining)
|
||||
}
|
||||
if s.isDebug() && expired > 0 {
|
||||
s.logger.Printf("[DEBUG] [web] session cleanup: expired=%d remaining=%d", expired, remaining)
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ func (s *Server) apiExportEstimate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
est, err := s.appExporter.EstimateExport(stackName, drive)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Export estimate failed for %s: %v", stackName, err)
|
||||
s.logger.Printf("[DEBUG] [handler_export] apiExportEstimate error: %v", err)
|
||||
jsonError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -162,6 +163,7 @@ func (s *Server) apiExportStart(w http.ResponseWriter, r *http.Request) {
|
||||
StopApp: req.StopApp,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Export start failed for %s: %v", req.StackName, err)
|
||||
s.logger.Printf("[DEBUG] [handler_export] apiExportStart error: %v", err)
|
||||
jsonError(w, err.Error(), http.StatusConflict)
|
||||
return
|
||||
@@ -306,6 +308,7 @@ func (s *Server) apiImportStart(w http.ResponseWriter, r *http.Request) {
|
||||
Password: req.Password,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Import start failed for %s: %v", req.Path, err)
|
||||
s.logger.Printf("[DEBUG] [handler_export] apiImportStart error: %v", err)
|
||||
jsonError(w, err.Error(), http.StatusConflict)
|
||||
return
|
||||
|
||||
@@ -89,6 +89,7 @@ func (s *Server) apiRestoreAll(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, "restore already in progress", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] Restore-all initiated from %s", r.RemoteAddr)
|
||||
go s.executeAllRestores()
|
||||
|
||||
jsonResponse(w, map[string]interface{}{
|
||||
|
||||
@@ -1568,6 +1568,7 @@ func (s *Server) settingsStorageDefaultHandler(w http.ResponseWriter, r *http.Re
|
||||
http.Redirect(w, r, "/settings", http.StatusFound)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] Default storage path set to %s", path)
|
||||
http.Redirect(w, r, "/settings?storage_msg=success&storage_detail="+url.QueryEscape("Alapértelmezett adattároló beállítva: "+path), http.StatusFound)
|
||||
}
|
||||
|
||||
@@ -1585,6 +1586,7 @@ func (s *Server) settingsStorageSchedulableHandler(w http.ResponseWriter, r *htt
|
||||
http.Redirect(w, r, "/settings", http.StatusFound)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] Storage schedulable updated: %s → %v", path, schedulable)
|
||||
http.Redirect(w, r, "/settings?storage_msg=success&storage_detail="+url.QueryEscape("Adattároló állapot módosítva: "+path), http.StatusFound)
|
||||
}
|
||||
|
||||
|
||||
@@ -345,6 +345,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
s.debugPageHandler(w, r)
|
||||
default:
|
||||
s.logger.Printf("[WARN] [web] 404 Not Found: %s %s", r.Method, path)
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1222,6 +1222,7 @@ func (s *Server) storageDisconnectHandler(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] [web] Disk disconnect completed: %s (stopped %d stacks)", req.Path, len(stoppedStacks))
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] storageDisconnect: path=%s success, stopped %d stacks", req.Path, len(stoppedStacks))
|
||||
}
|
||||
@@ -1264,6 +1265,7 @@ func (s *Server) storageReconnectHandler(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] [web] Disk reconnect completed: %s", req.Path)
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] storageReconnect: path=%s success, previously stopped stacks=%v", req.Path, stoppedStacks)
|
||||
}
|
||||
@@ -1309,6 +1311,7 @@ func (s *Server) storageRestartAppsHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
started, failed := s.storageWatchdog.RestartStoppedApps(req.Path)
|
||||
s.logger.Printf("[INFO] [web] Restart apps for %s: started=%d failed=%d", req.Path, len(started), len(failed))
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] storageRestartApps: path=%s started=%v failed=%v", req.Path, started, failed)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user