feat(doctor): add --server flag for Dolt server mode health checks (bd-dolt.2.3)
Adds bd doctor --server to diagnose Dolt server mode connections: - Server reachability (TCP connect to host:port) - Dolt version check (verifies it is Dolt, not vanilla MySQL) - Database exists and is accessible - Schema compatible (can query beads tables) - Connection pool health metrics Supports --json for machine-readable output. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
433115725b
commit
66d994264b
@@ -58,6 +58,7 @@ var (
|
|||||||
doctorDeep bool // full graph integrity validation
|
doctorDeep bool // full graph integrity validation
|
||||||
doctorGastown bool // running in gastown multi-workspace mode
|
doctorGastown bool // running in gastown multi-workspace mode
|
||||||
gastownDuplicatesThreshold int // duplicate tolerance threshold for gastown mode
|
gastownDuplicatesThreshold int // duplicate tolerance threshold for gastown mode
|
||||||
|
doctorServer bool // run server mode health checks
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigKeyHintsDoctor is the config key for suppressing doctor hints
|
// ConfigKeyHintsDoctor is the config key for suppressing doctor hints
|
||||||
@@ -114,6 +115,14 @@ Deep Validation Mode (--deep):
|
|||||||
- Mail thread integrity: Thread IDs reference existing issues
|
- Mail thread integrity: Thread IDs reference existing issues
|
||||||
- Molecule integrity: Molecules have valid parent-child structures
|
- Molecule integrity: Molecules have valid parent-child structures
|
||||||
|
|
||||||
|
Server Mode (--server):
|
||||||
|
Run health checks for Dolt server mode connections (bd-dolt.2.3):
|
||||||
|
- Server reachable: Can connect to configured host:port?
|
||||||
|
- Dolt version: Is it a Dolt server (not vanilla MySQL)?
|
||||||
|
- Database exists: Does the 'beads' database exist?
|
||||||
|
- Schema compatible: Can query beads tables?
|
||||||
|
- Connection pool: Pool health metrics
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bd doctor # Check current directory
|
bd doctor # Check current directory
|
||||||
bd doctor /path/to/repo # Check specific repository
|
bd doctor /path/to/repo # Check specific repository
|
||||||
@@ -129,7 +138,8 @@ Examples:
|
|||||||
bd doctor --output diagnostics.json # Export diagnostics to file
|
bd doctor --output diagnostics.json # Export diagnostics to file
|
||||||
bd doctor --check=pollution # Show potential test issues
|
bd doctor --check=pollution # Show potential test issues
|
||||||
bd doctor --check=pollution --clean # Delete test issues (with confirmation)
|
bd doctor --check=pollution --clean # Delete test issues (with confirmation)
|
||||||
bd doctor --deep # Full graph integrity validation`,
|
bd doctor --deep # Full graph integrity validation
|
||||||
|
bd doctor --server # Dolt server mode health checks`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Use global jsonOutput set by PersistentPreRun
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
@@ -177,6 +187,12 @@ Examples:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run server mode health checks if --server flag is set
|
||||||
|
if doctorServer {
|
||||||
|
runServerHealth(absPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Run diagnostics
|
// Run diagnostics
|
||||||
result := runDiagnostics(absPath)
|
result := runDiagnostics(absPath)
|
||||||
|
|
||||||
@@ -230,6 +246,7 @@ func init() {
|
|||||||
doctorCmd.Flags().StringVar(&doctorSource, "source", "auto", "Choose source of truth for recovery: auto (detect), jsonl (prefer JSONL), db (prefer database)")
|
doctorCmd.Flags().StringVar(&doctorSource, "source", "auto", "Choose source of truth for recovery: auto (detect), jsonl (prefer JSONL), db (prefer database)")
|
||||||
doctorCmd.Flags().BoolVar(&doctorGastown, "gastown", false, "Running in gastown multi-workspace mode (routes.jsonl is expected, higher duplicate tolerance)")
|
doctorCmd.Flags().BoolVar(&doctorGastown, "gastown", false, "Running in gastown multi-workspace mode (routes.jsonl is expected, higher duplicate tolerance)")
|
||||||
doctorCmd.Flags().IntVar(&gastownDuplicatesThreshold, "gastown-duplicates-threshold", 1000, "Duplicate tolerance threshold for gastown mode (wisps are ephemeral)")
|
doctorCmd.Flags().IntVar(&gastownDuplicatesThreshold, "gastown-duplicates-threshold", 1000, "Duplicate tolerance threshold for gastown mode (wisps are ephemeral)")
|
||||||
|
doctorCmd.Flags().BoolVar(&doctorServer, "server", false, "Run Dolt server mode health checks (connectivity, version, schema)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDiagnostics(path string) doctorResult {
|
func runDiagnostics(path string) doctorResult {
|
||||||
|
|||||||
371
cmd/bd/doctor/server.go
Normal file
371
cmd/bd/doctor/server.go
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// Import MySQL driver for server mode connections
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/configfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerHealthResult holds the results of all server health checks
|
||||||
|
type ServerHealthResult struct {
|
||||||
|
Checks []DoctorCheck `json:"checks"`
|
||||||
|
OverallOK bool `json:"overall_ok"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunServerHealthChecks runs all server-mode health checks and returns the result.
|
||||||
|
// This is called when `bd doctor --server` is used.
|
||||||
|
func RunServerHealthChecks(path string) ServerHealthResult {
|
||||||
|
result := ServerHealthResult{
|
||||||
|
OverallOK: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config to check if server mode is configured
|
||||||
|
_, beadsDir := getBackendAndBeadsDir(path)
|
||||||
|
cfg, err := configfile.Load(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
result.Checks = append(result.Checks, DoctorCheck{
|
||||||
|
Name: "Server Config",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "Failed to load config",
|
||||||
|
Detail: err.Error(),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
})
|
||||||
|
result.OverallOK = false
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg == nil {
|
||||||
|
result.Checks = append(result.Checks, DoctorCheck{
|
||||||
|
Name: "Server Config",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "No metadata.json found",
|
||||||
|
Fix: "Run 'bd init' to initialize beads",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
})
|
||||||
|
result.OverallOK = false
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Dolt backend is configured
|
||||||
|
if cfg.GetBackend() != configfile.BackendDolt {
|
||||||
|
result.Checks = append(result.Checks, DoctorCheck{
|
||||||
|
Name: "Server Config",
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("Backend is '%s', not Dolt", cfg.GetBackend()),
|
||||||
|
Detail: "Server mode health checks are only relevant for Dolt backend",
|
||||||
|
Fix: "Set backend: dolt in metadata.json to use Dolt server mode",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
})
|
||||||
|
result.OverallOK = false
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if server mode is configured
|
||||||
|
if !cfg.IsDoltServerMode() {
|
||||||
|
result.Checks = append(result.Checks, DoctorCheck{
|
||||||
|
Name: "Server Config",
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("Dolt mode is '%s' (not server)", cfg.GetDoltMode()),
|
||||||
|
Detail: "Server health checks require dolt_mode: server in metadata.json",
|
||||||
|
Fix: "Set dolt_mode: server in metadata.json and start dolt sql-server",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
})
|
||||||
|
result.OverallOK = false
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server mode is configured - run health checks
|
||||||
|
host := cfg.GetDoltServerHost()
|
||||||
|
port := cfg.GetDoltServerPort()
|
||||||
|
|
||||||
|
// Check 1: Server reachability (TCP connect)
|
||||||
|
reachCheck := checkServerReachable(host, port)
|
||||||
|
result.Checks = append(result.Checks, reachCheck)
|
||||||
|
if reachCheck.Status == StatusError {
|
||||||
|
result.OverallOK = false
|
||||||
|
// Can't continue without connectivity
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 2: Connect and verify it's Dolt (get version)
|
||||||
|
versionCheck, db := checkDoltVersion(cfg)
|
||||||
|
result.Checks = append(result.Checks, versionCheck)
|
||||||
|
if versionCheck.Status == StatusError {
|
||||||
|
result.OverallOK = false
|
||||||
|
if db != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if db != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Check 3: Database exists and is queryable
|
||||||
|
dbExistsCheck := checkDatabaseExists(db, "beads")
|
||||||
|
result.Checks = append(result.Checks, dbExistsCheck)
|
||||||
|
if dbExistsCheck.Status == StatusError {
|
||||||
|
result.OverallOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 4: Schema compatible (can query beads tables)
|
||||||
|
schemaCheck := checkSchemaCompatible(db)
|
||||||
|
result.Checks = append(result.Checks, schemaCheck)
|
||||||
|
if schemaCheck.Status == StatusError {
|
||||||
|
result.OverallOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 5: Connection pool health
|
||||||
|
poolCheck := checkConnectionPool(db)
|
||||||
|
result.Checks = append(result.Checks, poolCheck)
|
||||||
|
if poolCheck.Status == StatusError {
|
||||||
|
result.OverallOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkServerReachable checks if the server is reachable via TCP
|
||||||
|
func checkServerReachable(host string, port int) DoctorCheck {
|
||||||
|
addr := fmt.Sprintf("%s:%d", host, port)
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Server Reachable",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: fmt.Sprintf("Cannot connect to %s", addr),
|
||||||
|
Detail: err.Error(),
|
||||||
|
Fix: "Ensure dolt sql-server is running and accessible",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = conn.Close()
|
||||||
|
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Server Reachable",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: fmt.Sprintf("Connected to %s", addr),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDoltVersion connects to the server and checks if it's a Dolt server
|
||||||
|
// Returns the DoctorCheck and an open database connection (caller must close)
|
||||||
|
func checkDoltVersion(cfg *configfile.Config) (DoctorCheck, *sql.DB) {
|
||||||
|
host := cfg.GetDoltServerHost()
|
||||||
|
port := cfg.GetDoltServerPort()
|
||||||
|
user := cfg.GetDoltServerUser()
|
||||||
|
|
||||||
|
// Build DSN without database (just to test server connectivity)
|
||||||
|
var connStr string
|
||||||
|
connStr = fmt.Sprintf("%s@tcp(%s:%d)/?parseTime=true&timeout=5s",
|
||||||
|
user, host, port)
|
||||||
|
|
||||||
|
db, err := sql.Open("mysql", connStr)
|
||||||
|
if err != nil {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Dolt Version",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "Failed to open connection",
|
||||||
|
Detail: err.Error(),
|
||||||
|
Fix: "Check MySQL driver and connection settings",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set connection pool limits
|
||||||
|
db.SetMaxOpenConns(2)
|
||||||
|
db.SetMaxIdleConns(1)
|
||||||
|
db.SetConnMaxLifetime(30 * time.Second)
|
||||||
|
|
||||||
|
// Test connectivity
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Dolt Version",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "Server not responding",
|
||||||
|
Detail: err.Error(),
|
||||||
|
Fix: "Ensure dolt sql-server is running",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query Dolt version
|
||||||
|
var version string
|
||||||
|
err = db.QueryRowContext(ctx, "SELECT dolt_version()").Scan(&version)
|
||||||
|
if err != nil {
|
||||||
|
// If dolt_version() doesn't exist, it's not a Dolt server
|
||||||
|
if strings.Contains(err.Error(), "Unknown") || strings.Contains(err.Error(), "doesn't exist") {
|
||||||
|
_ = db.Close()
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Dolt Version",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "Server is not Dolt",
|
||||||
|
Detail: "dolt_version() function not found - this may be a MySQL server, not Dolt",
|
||||||
|
Fix: "Ensure you're connecting to a Dolt sql-server, not vanilla MySQL",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
_ = db.Close()
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Dolt Version",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "Failed to query version",
|
||||||
|
Detail: err.Error(),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Dolt Version",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: fmt.Sprintf("Dolt %s", version),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}, db
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDatabaseExists checks if the beads database exists
|
||||||
|
func checkDatabaseExists(db *sql.DB, database string) DoctorCheck {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Check if database exists
|
||||||
|
var exists int
|
||||||
|
query := fmt.Sprintf("SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '%s'", database)
|
||||||
|
err := db.QueryRowContext(ctx, query).Scan(&exists)
|
||||||
|
if err != nil {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Database Exists",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "Failed to query databases",
|
||||||
|
Detail: err.Error(),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists == 0 {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Database Exists",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: fmt.Sprintf("Database '%s' not found", database),
|
||||||
|
Fix: fmt.Sprintf("Run 'bd init --backend dolt' to create the '%s' database", database),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to the database
|
||||||
|
_, err = db.ExecContext(ctx, fmt.Sprintf("USE %s", database))
|
||||||
|
if err != nil {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Database Exists",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: fmt.Sprintf("Cannot access database '%s'", database),
|
||||||
|
Detail: err.Error(),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Database Exists",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: fmt.Sprintf("Database '%s' accessible", database),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSchemaCompatible checks if the beads tables are queryable
|
||||||
|
func checkSchemaCompatible(db *sql.DB) DoctorCheck {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Try to query the issues table
|
||||||
|
var count int
|
||||||
|
err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM issues").Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "doesn't exist") || strings.Contains(err.Error(), "Unknown table") {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Schema Compatible",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "Issues table not found",
|
||||||
|
Fix: "Run 'bd init --backend dolt' to create schema",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Schema Compatible",
|
||||||
|
Status: StatusError,
|
||||||
|
Message: "Cannot query issues table",
|
||||||
|
Detail: err.Error(),
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query metadata table for bd_version
|
||||||
|
var bdVersion string
|
||||||
|
err = db.QueryRowContext(ctx, "SELECT value FROM metadata WHERE `key` = 'bd_version'").Scan(&bdVersion)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
if strings.Contains(err.Error(), "doesn't exist") || strings.Contains(err.Error(), "Unknown table") {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Schema Compatible",
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("%d issues found (no metadata table)", count),
|
||||||
|
Fix: "Run 'bd migrate' to update schema",
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detail := fmt.Sprintf("%d issues", count)
|
||||||
|
if bdVersion != "" {
|
||||||
|
detail = fmt.Sprintf("%d issues (bd %s)", count, bdVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Schema Compatible",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: detail,
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConnectionPool checks the connection pool health
|
||||||
|
func checkConnectionPool(db *sql.DB) DoctorCheck {
|
||||||
|
stats := db.Stats()
|
||||||
|
|
||||||
|
// Report pool statistics
|
||||||
|
detail := fmt.Sprintf("open: %d, in_use: %d, idle: %d",
|
||||||
|
stats.OpenConnections,
|
||||||
|
stats.InUse,
|
||||||
|
stats.Idle,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check for connection errors
|
||||||
|
if stats.MaxIdleClosed > 0 || stats.MaxLifetimeClosed > 0 {
|
||||||
|
detail += fmt.Sprintf("\nclosed: idle=%d, lifetime=%d",
|
||||||
|
stats.MaxIdleClosed,
|
||||||
|
stats.MaxLifetimeClosed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Connection Pool",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "Pool healthy",
|
||||||
|
Detail: detail,
|
||||||
|
Category: CategoryFederation,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -106,6 +107,83 @@ func runDeepValidation(path string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runServerHealth runs Dolt server mode health checks
|
||||||
|
func runServerHealth(path string) {
|
||||||
|
result := doctor.RunServerHealthChecks(path)
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
jsonBytes, _ := json.Marshal(result)
|
||||||
|
fmt.Println(string(jsonBytes))
|
||||||
|
} else {
|
||||||
|
fmt.Println("Dolt Server Mode Health Check")
|
||||||
|
fmt.Println()
|
||||||
|
printServerHealthResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.OverallOK {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printServerHealthResult prints the server health check results
|
||||||
|
func printServerHealthResult(result doctor.ServerHealthResult) {
|
||||||
|
var passCount, warnCount, failCount int
|
||||||
|
|
||||||
|
for _, check := range result.Checks {
|
||||||
|
var statusIcon string
|
||||||
|
switch check.Status {
|
||||||
|
case statusOK:
|
||||||
|
statusIcon = "✓"
|
||||||
|
passCount++
|
||||||
|
case statusWarning:
|
||||||
|
statusIcon = "⚠"
|
||||||
|
warnCount++
|
||||||
|
case statusError:
|
||||||
|
statusIcon = "✗"
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" %s %s", statusIcon, check.Name)
|
||||||
|
if check.Message != "" {
|
||||||
|
fmt.Printf(" %s", check.Message)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if check.Detail != "" {
|
||||||
|
// Indent detail lines
|
||||||
|
for _, line := range strings.Split(check.Detail, "\n") {
|
||||||
|
fmt.Printf(" └─ %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Summary line
|
||||||
|
fmt.Printf("─────────────────────────────────────────\n")
|
||||||
|
fmt.Printf("✓ %d passed ⚠ %d warnings ✗ %d failed\n", passCount, warnCount, failCount)
|
||||||
|
|
||||||
|
// Print fixes for any errors/warnings
|
||||||
|
var fixes []doctor.DoctorCheck
|
||||||
|
for _, check := range result.Checks {
|
||||||
|
if check.Fix != "" && (check.Status == statusError || check.Status == statusWarning) {
|
||||||
|
fixes = append(fixes, check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fixes) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("⚠ FIXES NEEDED")
|
||||||
|
for i, check := range fixes {
|
||||||
|
fmt.Printf(" %d. %s: %s\n", i+1, check.Name, check.Message)
|
||||||
|
fmt.Printf(" └─ %s\n", check.Fix)
|
||||||
|
}
|
||||||
|
} else if result.OverallOK {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("✓ All server health checks passed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// printCheckHealthHint prints the health check hint and exits with error.
|
// printCheckHealthHint prints the health check hint and exits with error.
|
||||||
func printCheckHealthHint(issues []string) {
|
func printCheckHealthHint(issues []string) {
|
||||||
fmt.Fprintf(os.Stderr, "💡 bd doctor recommends a health check:\n")
|
fmt.Fprintf(os.Stderr, "💡 bd doctor recommends a health check:\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user