Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 120 additions & 1 deletion cmd/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/docker/docker/api/types/filters"
dockerimage "github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/libops/sitectl/internal/debugreport"
"github.com/libops/sitectl/pkg/config"
"github.com/libops/sitectl/pkg/docker"
"github.com/libops/sitectl/pkg/plugin"
Expand Down Expand Up @@ -187,7 +188,40 @@ func renderCoreDebug(runCtx context.Context, ctx config.Context) string {
"",
formatDebugRows(meta),
}
logDiagnostics, logErr, imageDiagnostics, imageErr := collectCoreDockerDiagnostics(runCtx, &ctx)
var sharedSession *debugreport.Session
if ctx.DockerHostType == config.ContextRemote {
if session, err := debugreport.NewSession(&ctx); err != nil {
slog.Debug("shared debug session setup failed", "context", ctx.Name, "error", err)
} else {
sharedSession = session
defer sharedSession.Close()
}
}

var hostDiagnostics debugreport.HostDiagnostics
var composeDiagnostics debugreport.ComposeDiagnostics
var logDiagnostics logDiagnostics
var imageDiagnostics imageDiagnostics
var logErr error
var imageErr error

if sharedSession != nil {
hostDiagnostics = debugreport.CollectHostDiagnosticsWithSession(runCtx, &ctx, sharedSession)
composeDiagnostics = debugreport.CollectComposeDiagnosticsWithSession(runCtx, &ctx, sharedSession)
if cli, err := sharedSession.DockerClient(); err != nil {
logErr = err
imageErr = err
} else {
logDiagnostics, logErr = collectLogDiagnosticsWithClient(runCtx, &ctx, cli)
imageDiagnostics, imageErr = collectImageDiagnosticsWithClient(runCtx, &ctx, cli)
}
} else {
hostDiagnostics = debugreport.CollectHostDiagnostics(runCtx, &ctx)
composeDiagnostics = debugreport.CollectComposeDiagnostics(runCtx, &ctx)
logDiagnostics, logErr, imageDiagnostics, imageErr = collectCoreDockerDiagnostics(runCtx, &ctx)
}
coreBody = append(coreBody, "", debugDivider(), "", debugTitleStyle.Render("Host Resources"), "", formatDebugRows(hostSummaryRows(hostDiagnostics, ctx.ProjectDir)))
coreBody = append(coreBody, "", debugDivider(), "", debugTitleStyle.Render("Compose Services"), "", formatDebugRows(composeSummaryRows(composeDiagnostics)))
if logErr == nil {
slog.Debug("collected log diagnostics", "context", ctx.Name, "containers", len(logDiagnostics.Containers))
coreBody = append(coreBody, "", debugDivider(), "", debugTitleStyle.Render("Log Summary"), "", formatDebugRows(logSummaryRows(logDiagnostics)))
Expand Down Expand Up @@ -430,6 +464,91 @@ func imageSummaryRows(diagnostics imageDiagnostics) []debugRow {
return rows
}

func hostSummaryRows(diagnostics debugreport.HostDiagnostics, projectDir string) []debugRow {
status := "ok"
if len(diagnostics.Issues) > 0 {
status = "warning"
}

rows := []debugRow{
{Label: "Host status", Value: renderStatus(status)},
{Label: "CPUs", Value: renderDebugValue(intValueOrUnknown(diagnostics.CPUCount))},
{Label: "Memory", Value: renderDebugValue(bytesValueOrUnknown(diagnostics.MemoryBytes))},
{Label: "Swap", Value: renderDebugValue(bytesValueOrUnknown(diagnostics.SwapBytes))},
{Label: "Available disk", Value: renderDebugValue(diskValueOrUnknown(diagnostics.DiskAvailableBytes, projectDir))},
{Label: "OS version", Value: renderDebugValue(diagnostics.OSVersion)},
}
if len(diagnostics.Issues) > 0 {
rows = append(rows, debugRow{Label: "Diagnostics", Value: strings.Join(diagnostics.Issues, "\n")})
}
return rows
}

func composeSummaryRows(diagnostics debugreport.ComposeDiagnostics) []debugRow {
status := "ok"
if len(diagnostics.Issues) > 0 {
status = "warning"
}

rows := []debugRow{
{Label: "Compose status", Value: renderStatus(status)},
{Label: "Compose file", Value: renderDebugValue(diagnostics.ComposePath)},
}
if len(diagnostics.Services) == 0 {
rows = append(rows, debugRow{Label: "Services", Value: "none found"})
} else {
for _, service := range diagnostics.Services {
rows = append(rows, debugRow{Label: service.Service, Value: renderDebugValue(service.Image)})
}
}
if len(diagnostics.BindMounts) > 0 {
for _, mount := range diagnostics.BindMounts {
value := mount.Target + " <- " + mount.Source
if mount.Issue != "" {
value += " (" + mount.Issue + ")"
} else {
value += ": " + humanBytes(mount.AvailableBytes) + " available"
}
rows = append(rows, debugRow{Label: "Bind mount", Value: value})
}
}
if len(diagnostics.Issues) > 0 {
rows = append(rows, debugRow{Label: "Diagnostics", Value: strings.Join(diagnostics.Issues, "\n")})
}
return rows
}

func renderDebugValue(value string) string {
if strings.TrimSpace(value) == "" {
return "unknown"
}
return value
}

func intValueOrUnknown(value int) string {
if value <= 0 {
return ""
}
return strconv.Itoa(value)
}

func bytesValueOrUnknown(value int64) string {
if value < 0 {
return ""
}
return humanBytes(value)
}

func diskValueOrUnknown(value int64, projectDir string) string {
if value < 0 {
return ""
}
if strings.TrimSpace(projectDir) == "" {
return humanBytes(value)
}
return fmt.Sprintf("%s at %s", humanBytes(value), projectDir)
}

func describeContainerLogs(service, containerName string, inspect dockercontainer.InspectResponse) containerLogDiagnostics {
item := containerLogDiagnostics{
Service: service,
Expand Down
18 changes: 18 additions & 0 deletions cmd/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd
import (
"strings"
"testing"

"github.com/libops/sitectl/internal/debugreport"
)

func TestEvaluateLogConfigDetectsUnboundedJSONFileLogs(t *testing.T) {
Expand Down Expand Up @@ -78,3 +80,19 @@ func TestImageSummaryRowsWarnWhenThresholdExceeded(t *testing.T) {
t.Fatalf("expected prune docs link, got:\n%s", rendered)
}
}

func TestHostSummaryRowsIncludeRequestedStats(t *testing.T) {
rendered := formatDebugRows(hostSummaryRows(debugreport.HostDiagnostics{
CPUCount: 8,
MemoryBytes: 16 * 1024 * 1024 * 1024,
SwapBytes: 2 * 1024 * 1024 * 1024,
DiskAvailableBytes: 50 * 1024 * 1024 * 1024,
OSVersion: "Debian GNU/Linux 12 (bookworm)",
}, "/srv/project"))

for _, expected := range []string{"CPUs", "Memory", "Swap", "Available disk", "OS version", "Debian GNU/Linux 12 (bookworm)", "/srv/project"} {
if !strings.Contains(rendered, expected) {
t.Fatalf("expected %q in rendered rows, got:\n%s", expected, rendered)
}
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
golang.org/x/crypto v0.46.0
golang.org/x/sys v0.42.0
golang.org/x/term v0.38.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -81,7 +82,6 @@ require (
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
Expand Down
Loading
Loading