- Created internal/checks package for disk space and error classification - CheckDiskSpace(): Real-time disk usage detection (80% warning, 95% critical) - CheckDiskSpaceForRestore(): 4x archive size requirement calculation - ClassifyError(): Smart error classification (ignorable/warning/critical/fatal) - FormatErrorWithHint(): User-friendly error messages with actionable solutions - Integrated disk checks into backup/restore workflows with pre-flight validation - Error hints for: lock exhaustion, disk full, syntax errors, permissions, connections - Blocks operations at 95% disk usage, warns at 80%
222 lines
6.3 KiB
Go
222 lines
6.3 KiB
Go
package checks
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// ErrorClassification represents the severity and type of error
|
||
type ErrorClassification struct {
|
||
Type string // "ignorable", "warning", "critical", "fatal"
|
||
Category string // "disk_space", "locks", "corruption", "permissions", "network", "syntax"
|
||
Message string
|
||
Hint string
|
||
Action string // Suggested command or action
|
||
Severity int // 0=info, 1=warning, 2=error, 3=fatal
|
||
}
|
||
|
||
// ClassifyError analyzes an error message and provides actionable hints
|
||
func ClassifyError(errorMsg string) *ErrorClassification {
|
||
lowerMsg := strings.ToLower(errorMsg)
|
||
|
||
// Ignorable errors (objects already exist)
|
||
if strings.Contains(lowerMsg, "already exists") {
|
||
return &ErrorClassification{
|
||
Type: "ignorable",
|
||
Category: "duplicate",
|
||
Message: errorMsg,
|
||
Hint: "Object already exists in target database - this is normal during restore",
|
||
Action: "No action needed - restore will continue",
|
||
Severity: 0,
|
||
}
|
||
}
|
||
|
||
// Disk space errors
|
||
if strings.Contains(lowerMsg, "no space left") || strings.Contains(lowerMsg, "disk full") {
|
||
return &ErrorClassification{
|
||
Type: "critical",
|
||
Category: "disk_space",
|
||
Message: errorMsg,
|
||
Hint: "Insufficient disk space to complete operation",
|
||
Action: "Free up disk space: rm old_backups/* or increase storage",
|
||
Severity: 3,
|
||
}
|
||
}
|
||
|
||
// Lock exhaustion errors
|
||
if strings.Contains(lowerMsg, "max_locks_per_transaction") ||
|
||
strings.Contains(lowerMsg, "out of shared memory") ||
|
||
strings.Contains(lowerMsg, "could not open large object") {
|
||
return &ErrorClassification{
|
||
Type: "critical",
|
||
Category: "locks",
|
||
Message: errorMsg,
|
||
Hint: "Lock table exhausted - typically caused by large objects in parallel restore",
|
||
Action: "Increase max_locks_per_transaction in postgresql.conf to 512 or higher",
|
||
Severity: 2,
|
||
}
|
||
}
|
||
|
||
// Syntax errors (corrupted dump)
|
||
if strings.Contains(lowerMsg, "syntax error") {
|
||
return &ErrorClassification{
|
||
Type: "critical",
|
||
Category: "corruption",
|
||
Message: errorMsg,
|
||
Hint: "Syntax error in dump file - backup may be corrupted or incomplete",
|
||
Action: "Re-create backup with: dbbackup backup single <database>",
|
||
Severity: 3,
|
||
}
|
||
}
|
||
|
||
// Permission errors
|
||
if strings.Contains(lowerMsg, "permission denied") || strings.Contains(lowerMsg, "must be owner") {
|
||
return &ErrorClassification{
|
||
Type: "critical",
|
||
Category: "permissions",
|
||
Message: errorMsg,
|
||
Hint: "Insufficient permissions to perform operation",
|
||
Action: "Run as superuser or use --no-owner flag for restore",
|
||
Severity: 2,
|
||
}
|
||
}
|
||
|
||
// Connection errors
|
||
if strings.Contains(lowerMsg, "connection refused") ||
|
||
strings.Contains(lowerMsg, "could not connect") ||
|
||
strings.Contains(lowerMsg, "no pg_hba.conf entry") {
|
||
return &ErrorClassification{
|
||
Type: "critical",
|
||
Category: "network",
|
||
Message: errorMsg,
|
||
Hint: "Cannot connect to database server",
|
||
Action: "Check database is running and pg_hba.conf allows connection",
|
||
Severity: 2,
|
||
}
|
||
}
|
||
|
||
// Version compatibility warnings
|
||
if strings.Contains(lowerMsg, "version mismatch") || strings.Contains(lowerMsg, "incompatible") {
|
||
return &ErrorClassification{
|
||
Type: "warning",
|
||
Category: "version",
|
||
Message: errorMsg,
|
||
Hint: "PostgreSQL version mismatch between backup and restore target",
|
||
Action: "Review release notes for compatibility: https://www.postgresql.org/docs/",
|
||
Severity: 1,
|
||
}
|
||
}
|
||
|
||
// Excessive errors (corrupted dump)
|
||
if strings.Contains(errorMsg, "total errors:") {
|
||
parts := strings.Split(errorMsg, "total errors:")
|
||
if len(parts) > 1 {
|
||
var count int
|
||
if _, err := fmt.Sscanf(parts[1], "%d", &count); err == nil && count > 100000 {
|
||
return &ErrorClassification{
|
||
Type: "fatal",
|
||
Category: "corruption",
|
||
Message: errorMsg,
|
||
Hint: fmt.Sprintf("Excessive errors (%d) indicate severely corrupted dump file", count),
|
||
Action: "Re-create backup from source database",
|
||
Severity: 3,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Default: unclassified error
|
||
return &ErrorClassification{
|
||
Type: "error",
|
||
Category: "unknown",
|
||
Message: errorMsg,
|
||
Hint: "An error occurred during operation",
|
||
Action: "Check logs for details or contact support",
|
||
Severity: 2,
|
||
}
|
||
}
|
||
|
||
// FormatErrorWithHint creates a user-friendly error message with hints
|
||
func FormatErrorWithHint(errorMsg string) string {
|
||
classification := ClassifyError(errorMsg)
|
||
|
||
var icon string
|
||
switch classification.Type {
|
||
case "ignorable":
|
||
icon = "ℹ️ "
|
||
case "warning":
|
||
icon = "⚠️ "
|
||
case "critical":
|
||
icon = "❌"
|
||
case "fatal":
|
||
icon = "🛑"
|
||
default:
|
||
icon = "⚠️ "
|
||
}
|
||
|
||
output := fmt.Sprintf("%s %s Error\n\n", icon, strings.ToUpper(classification.Type))
|
||
output += fmt.Sprintf("Category: %s\n", classification.Category)
|
||
output += fmt.Sprintf("Message: %s\n\n", classification.Message)
|
||
output += fmt.Sprintf("💡 Hint: %s\n\n", classification.Hint)
|
||
output += fmt.Sprintf("🔧 Action: %s\n", classification.Action)
|
||
|
||
return output
|
||
}
|
||
|
||
// FormatMultipleErrors formats multiple errors with classification
|
||
func FormatMultipleErrors(errors []string) string {
|
||
if len(errors) == 0 {
|
||
return "✓ No errors"
|
||
}
|
||
|
||
ignorable := 0
|
||
warnings := 0
|
||
critical := 0
|
||
fatal := 0
|
||
|
||
var criticalErrors []string
|
||
|
||
for _, err := range errors {
|
||
class := ClassifyError(err)
|
||
switch class.Type {
|
||
case "ignorable":
|
||
ignorable++
|
||
case "warning":
|
||
warnings++
|
||
case "critical":
|
||
critical++
|
||
if len(criticalErrors) < 3 { // Keep first 3 critical errors
|
||
criticalErrors = append(criticalErrors, err)
|
||
}
|
||
case "fatal":
|
||
fatal++
|
||
criticalErrors = append(criticalErrors, err)
|
||
}
|
||
}
|
||
|
||
output := "📊 Error Summary:\n\n"
|
||
if ignorable > 0 {
|
||
output += fmt.Sprintf(" ℹ️ %d ignorable (objects already exist)\n", ignorable)
|
||
}
|
||
if warnings > 0 {
|
||
output += fmt.Sprintf(" ⚠️ %d warnings\n", warnings)
|
||
}
|
||
if critical > 0 {
|
||
output += fmt.Sprintf(" ❌ %d critical errors\n", critical)
|
||
}
|
||
if fatal > 0 {
|
||
output += fmt.Sprintf(" 🛑 %d fatal errors\n", fatal)
|
||
}
|
||
|
||
if len(criticalErrors) > 0 {
|
||
output += "\n📝 Critical Issues:\n\n"
|
||
for i, err := range criticalErrors {
|
||
class := ClassifyError(err)
|
||
output += fmt.Sprintf("%d. %s\n", i+1, class.Hint)
|
||
output += fmt.Sprintf(" Action: %s\n\n", class.Action)
|
||
}
|
||
}
|
||
|
||
return output
|
||
}
|