added more detailed debug logging

This commit is contained in:
2026-02-14 17:33:33 +01:00
parent 85aa518208
commit 9d7a36a143
5 changed files with 272 additions and 14 deletions
+13 -1
View File
@@ -178,9 +178,16 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
}
m.logger.Printf("[INFO] Deploying stack %s with %d env vars: [%s]", req.StackName, len(env), strings.Join(envKeys, ", "))
// Check which images are available locally before pulling
if m.isDebug() {
m.checkLocalImages(req.StackName, stackDir)
}
// Run docker compose up -d
start := time.Now()
_, composeErr := m.composeExecWithEnv(stackDir, env, "up", "-d")
if composeErr != nil {
m.logger.Printf("[ERROR] Stack %s deploy failed after %.1fs: %v", req.StackName, time.Since(start).Seconds(), composeErr)
// Deployment failed — keep app.yaml for debugging but mark as not deployed
appCfg.Deployed = false
_ = SaveAppConfig(stackDir, appCfg)
@@ -196,7 +203,12 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
}
m.mu.Unlock()
m.logger.Printf("[INFO] Stack %s deployed successfully", req.StackName)
m.logger.Printf("[INFO] Stack %s deployed successfully (took %.1fs)", req.StackName, time.Since(start).Seconds())
// Post-deploy container state check (async, non-blocking)
deployEnv := m.stackEnv(stackDir)
m.logPostStartStatus(req.StackName, stackDir, deployEnv)
return deployWarning, m.RefreshStatus()
}
+160 -7
View File
@@ -166,7 +166,14 @@ func (m *Manager) ScanStacks() error {
}
}
m.logger.Printf("[INFO] Scanned stacks: %d found", len(m.stacks))
deployedCount := 0
for _, s := range m.stacks {
if s.Deployed {
deployedCount++
}
}
m.logger.Printf("[INFO] Scanned stacks: %d found (%d deployed, %d available)",
len(m.stacks), deployedCount, len(m.stacks)-deployedCount)
return m.refreshStatusLocked()
}
@@ -357,15 +364,18 @@ func (m *Manager) StartStack(name string) error {
}
m.logger.Printf("[INFO] Starting stack: %s", name)
start := time.Now()
dir := filepath.Dir(stack.ComposePath)
env := m.stackEnv(dir)
if _, err := m.composeExecCustomEnv(dir, env, "up", "-d"); err != nil {
m.logger.Printf("[ERROR] Stack %s start failed after %.1fs: %v", name, time.Since(start).Seconds(), err)
return fmt.Errorf("starting stack %s: %w", name, err)
}
m.logger.Printf("[INFO] Stack %s started", name)
m.logger.Printf("[INFO] Stack %s started successfully (took %.1fs)", name, time.Since(start).Seconds())
m.logPostStartStatus(name, dir, env)
return m.RefreshStatus()
}
@@ -380,13 +390,15 @@ func (m *Manager) StopStack(name string) error {
}
m.logger.Printf("[INFO] Stopping stack: %s", name)
start := time.Now()
dir := filepath.Dir(stack.ComposePath)
if _, err := m.composeExec(dir, "down"); err != nil {
m.logger.Printf("[ERROR] Stack %s stop failed after %.1fs: %v", name, time.Since(start).Seconds(), err)
return fmt.Errorf("stopping stack %s: %w", name, err)
}
m.logger.Printf("[INFO] Stack %s stopped", name)
m.logger.Printf("[INFO] Stack %s stopped successfully (took %.1fs)", name, time.Since(start).Seconds())
return m.RefreshStatus()
}
@@ -397,13 +409,17 @@ func (m *Manager) RestartStack(name string) error {
}
m.logger.Printf("[INFO] Restarting stack: %s", name)
start := time.Now()
dir := filepath.Dir(stack.ComposePath)
if _, err := m.composeExec(dir, "restart"); err != nil {
m.logger.Printf("[ERROR] Stack %s restart failed after %.1fs: %v", name, time.Since(start).Seconds(), err)
return fmt.Errorf("restarting stack %s: %w", name, err)
}
m.logger.Printf("[INFO] Stack %s restarted", name)
m.logger.Printf("[INFO] Stack %s restarted successfully (took %.1fs)", name, time.Since(start).Seconds())
env := m.stackEnv(dir)
m.logPostStartStatus(name, dir, env)
return m.RefreshStatus()
}
@@ -414,18 +430,26 @@ func (m *Manager) UpdateStack(name string) error {
}
m.logger.Printf("[INFO] Updating stack: %s", name)
start := time.Now()
dir := filepath.Dir(stack.ComposePath)
env := m.stackEnv(dir)
if m.isDebug() {
m.checkLocalImages(name, dir)
}
if _, err := m.composeExecCustomEnv(dir, env, "pull"); err != nil {
m.logger.Printf("[ERROR] Stack %s update (pull) failed after %.1fs: %v", name, time.Since(start).Seconds(), err)
return fmt.Errorf("pulling images for %s: %w", name, err)
}
if _, err := m.composeExecCustomEnv(dir, env, "up", "-d", "--remove-orphans"); err != nil {
m.logger.Printf("[ERROR] Stack %s update (up) failed after %.1fs: %v", name, time.Since(start).Seconds(), err)
return fmt.Errorf("recreating %s: %w", name, err)
}
m.logger.Printf("[INFO] Stack %s updated", name)
m.logger.Printf("[INFO] Stack %s updated successfully (took %.1fs)", name, time.Since(start).Seconds())
m.logPostStartStatus(name, dir, env)
return m.RefreshStatus()
}
@@ -442,11 +466,20 @@ func (m *Manager) GetLogs(name string, lines int) (string, error) {
lines = 1000
}
m.logger.Printf("[DEBUG] Fetching logs for %s (tail %d)", name, lines)
dir := filepath.Dir(stack.ComposePath)
output, err := m.composeExec(dir, "logs", "--tail", fmt.Sprintf("%d", lines), "--no-color")
if err != nil {
m.logger.Printf("[WARN] Failed to fetch logs for %s: %v", name, err)
return "", fmt.Errorf("getting logs for %s: %w", name, err)
}
if len(output) == 0 {
m.logger.Printf("[DEBUG] Logs result for %s: 0 bytes returned (empty)", name)
} else {
m.logger.Printf("[DEBUG] Logs result for %s: %d bytes returned", name, len(output))
}
return output, nil
}
@@ -489,7 +522,30 @@ func (m *Manager) composeExecCustomEnv(dir string, env []string, args ...string)
if env != nil {
cmd.Env = env
} else {
cmd.Env = m.stackEnv(dir)
env = m.stackEnv(dir)
cmd.Env = env
}
// Log env var keys at debug level
if m.isDebug() {
var appKeys []string
sysCount := 0
for _, e := range env {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
key := parts[0]
// Only log non-system env vars (skip PATH, HOME, etc.)
if strings.ToUpper(key) == key && !strings.HasPrefix(key, "_") {
appKeys = append(appKeys, key)
} else {
sysCount++
}
}
}
if len(appKeys) > 0 {
m.logger.Printf("[DEBUG] Env vars for compose: [%s] (%d app + %d system)",
strings.Join(appKeys, ", "), len(appKeys), sysCount)
}
}
var stdout, stderr bytes.Buffer
@@ -498,10 +554,25 @@ func (m *Manager) composeExecCustomEnv(dir string, env []string, args ...string)
m.logger.Printf("[DEBUG] Running: %s %s (in %s)", m.composeCmd, strings.Join(args, " "), dir)
start := time.Now()
if err := cmd.Run(); err != nil {
return stdout.String(), fmt.Errorf("%w\nstderr: %s", err, stderr.String())
elapsed := time.Since(start)
exitCode := -1
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
}
m.logger.Printf("[ERROR] Command failed: %s %s (in %s) — exit code %d (took %.1fs)",
m.composeCmd, strings.Join(args, " "), dir, exitCode, elapsed.Seconds())
if stdoutStr := truncateStr(stdout.String(), 500); stdoutStr != "" {
m.logger.Printf("[ERROR] stdout: %s", stdoutStr)
}
if stderrStr := truncateStr(stderr.String(), 500); stderrStr != "" {
m.logger.Printf("[ERROR] stderr: %s", stderrStr)
}
return stdout.String(), fmt.Errorf("exit code %d\nstderr: %s", exitCode, truncateStr(stderr.String(), 500))
}
m.logger.Printf("[DEBUG] Command completed: %s %s (took %.1fs)", m.composeCmd, strings.Join(args, " "), time.Since(start).Seconds())
return stdout.String(), nil
}
@@ -519,6 +590,88 @@ func (m *Manager) execCommand(name string, args ...string) (string, error) {
return stdout.String(), nil
}
// isDebug returns true if logging level is "debug".
func (m *Manager) isDebug() bool {
return m.cfg.Logging.Level == "debug"
}
// truncateStr truncates a string to maxLen characters, appending "..." if truncated.
func truncateStr(s string, maxLen int) string {
s = strings.TrimSpace(s)
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
// logPostStartStatus queries container states after a start/deploy operation
// and logs them. This runs asynchronously to avoid blocking the HTTP response.
func (m *Manager) logPostStartStatus(name, stackDir string, env []string) {
go func() {
time.Sleep(3 * time.Second)
output, err := m.composeExecCustomEnv(stackDir, env, "ps", "-a", "--format", "table {{.Name}}\t{{.Image}}\t{{.State}}\t{{.Status}}")
if err != nil {
m.logger.Printf("[WARN] Post-start status check failed for %s: %v", name, err)
return
}
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) <= 1 {
m.logger.Printf("[WARN] Post-start status for %s: no containers found", name)
return
}
m.logger.Printf("[INFO] Stack %s post-start status:", name)
// Skip header line
for _, line := range lines[1:] {
m.logger.Printf("[INFO] %s", line)
}
}()
}
// checkLocalImages parses docker-compose.yml for image: lines and checks which
// are available locally. Informational only — logs results but never fails.
func (m *Manager) checkLocalImages(name, stackDir string) {
composePath := filepath.Join(stackDir, "docker-compose.yml")
data, err := os.ReadFile(composePath)
if err != nil {
composePath = filepath.Join(stackDir, "docker-compose.yaml")
data, err = os.ReadFile(composePath)
if err != nil {
m.logger.Printf("[DEBUG] Could not read compose file for image check: %v", err)
return
}
}
var images []string
for _, line := range strings.Split(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "image:") {
img := strings.TrimSpace(strings.TrimPrefix(trimmed, "image:"))
img = strings.Trim(img, "\"'")
if img != "" && !strings.Contains(img, "${") {
images = append(images, img)
}
}
}
if len(images) == 0 {
m.logger.Printf("[DEBUG] No static image references found in %s compose file", name)
return
}
m.logger.Printf("[INFO] Deploying stack %s — checking %d images...", name, len(images))
for _, img := range images {
cmd := exec.Command("docker", "image", "inspect", img)
if err := cmd.Run(); err != nil {
m.logger.Printf("[DEBUG] %s — not found locally, will pull", img)
} else {
m.logger.Printf("[DEBUG] %s — found locally", img)
}
}
}
// --- Memory helpers ---
// ParseMemoryMB parses a memory string like "500M", "1G", "1.5G", "1024M", "768"