Add build log file storage and download functionality

- Write deployment logs to files when deployment finishes (success or failure)
- Log files stored in DataDir/logs/<hostname>/<appname>/<appname>_<sha>_<timestamp>.log.txt
- Capture commit SHA for manual deploys by parsing git rev-parse HEAD after clone
- Add download endpoint for log files at /apps/{id}/deployments/{deploymentID}/download
- Add download link in deployment history view for finished deployments
This commit is contained in:
2026-01-01 06:08:00 -08:00
parent c4362c3143
commit 2cbcd3d72a
5 changed files with 226 additions and 23 deletions

View File

@@ -5,6 +5,8 @@ import (
"database/sql"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -438,6 +440,66 @@ func (h *Handlers) HandleDeploymentLogsAPI() http.HandlerFunc {
}
}
// HandleDeploymentLogDownload serves the log file for download.
func (h *Handlers) HandleDeploymentLogDownload() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
appID := chi.URLParam(request, "id")
deploymentIDStr := chi.URLParam(request, "deploymentID")
application, findErr := models.FindApp(request.Context(), h.db, appID)
if findErr != nil || application == nil {
http.NotFound(writer, request)
return
}
deploymentID, parseErr := strconv.ParseInt(deploymentIDStr, 10, 64)
if parseErr != nil {
http.NotFound(writer, request)
return
}
deployment, deployErr := models.FindDeployment(request.Context(), h.db, deploymentID)
if deployErr != nil || deployment == nil || deployment.AppID != appID {
http.NotFound(writer, request)
return
}
// Get the log file path from deploy service
logPath := h.deploy.GetLogFilePath(application, deployment)
if logPath == "" {
http.NotFound(writer, request)
return
}
// Check if file exists
_, err := os.Stat(logPath)
if os.IsNotExist(err) {
http.NotFound(writer, request)
return
}
if err != nil {
h.log.Error("failed to stat log file", "error", err, "path", logPath)
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
return
}
// Extract filename for Content-Disposition header
filename := filepath.Base(logPath)
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
http.ServeFile(writer, request, logPath)
}
}
// containerLogsAPITail is the default number of log lines for the container logs API.
const containerLogsAPITail = "100"