From 604d7ebcc8fb5a917dc12551d6af070dd713fa18 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 19 Jul 2025 18:59:58 +0200 Subject: [PATCH] Implement instance log management --- server/pkg/handlers.go | 37 +++++++++++++ server/pkg/instance.go | 123 ++++++++++++++++++++++++++++++++--------- server/pkg/routes.go | 2 +- 3 files changed, 135 insertions(+), 27 deletions(-) diff --git a/server/pkg/handlers.go b/server/pkg/handlers.go index 5a00350..fcb8727 100644 --- a/server/pkg/handlers.go +++ b/server/pkg/handlers.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "os/exec" + "strconv" "strings" "github.com/go-chi/chi/v5" @@ -347,6 +348,42 @@ func (h *Handler) DeleteInstance() http.HandlerFunc { } } +func (h *Handler) GetInstanceLogs() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + if name == "" { + http.Error(w, "Instance name cannot be empty", http.StatusBadRequest) + return + } + + lines := r.URL.Query().Get("lines") + if lines == "" { + lines = "-1" + } + + num_lines, err := strconv.Atoi(lines) + if err != nil { + http.Error(w, "Invalid lines parameter: "+err.Error(), http.StatusBadRequest) + return + } + + instance, err := h.InstanceManager.GetInstance(name) + if err != nil { + http.Error(w, "Failed to get instance: "+err.Error(), http.StatusInternalServerError) + return + } + + logs, err := instance.GetLogs(num_lines) + if err != nil { + http.Error(w, "Failed to get logs: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte(logs)) + } +} + func (h *Handler) ProxyToInstance() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") diff --git a/server/pkg/instance.go b/server/pkg/instance.go index 9360ec9..4e70b52 100644 --- a/server/pkg/instance.go +++ b/server/pkg/instance.go @@ -9,8 +9,10 @@ import ( "log" "net/http/httputil" "net/url" + "os" "os/exec" "runtime" + "strings" "sync" "syscall" "time" @@ -23,9 +25,9 @@ type Instance struct { // Status Running bool `json:"running"` - // Output channels - StdOutChan chan string `json:"-"` // Channel for sending output messages - StdErrChan chan string `json:"-"` // Channel for sending error messages + // Log files + logPath string `json:"-"` + logFile *os.File `json:"-"` // internal cmd *exec.Cmd `json:"-"` // Command to run the instance @@ -44,9 +46,37 @@ func NewInstance(name string, options *InstanceOptions) *Instance { options: options, Running: false, + } +} - StdOutChan: make(chan string, 100), - StdErrChan: make(chan string, 100), +// createLogFiles creates and opens the log files for stdout and stderr +func (i *Instance) createLogFiles() error { + if i.logPath == "" { + return fmt.Errorf("log file path not set") + } + + // Create stdout log file + logFile, err := os.OpenFile(i.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("failed to create stdout log file: %w", err) + } + + i.logFile = logFile + + // Write a startup marker to both files + timestamp := time.Now().Format("2006-01-02 15:04:05") + fmt.Fprintf(i.logFile, "\n=== Instance %s started at %s ===\n", i.Name, timestamp) + + return nil +} + +// closeLogFiles closes the log files +func (i *Instance) closeLogFiles() { + if i.logFile != nil { + timestamp := time.Now().Format("2006-01-02 15:04:05") + fmt.Fprintf(i.logFile, "=== Instance %s stopped at %s ===\n\n", i.Name, timestamp) + i.logFile.Close() + i.logFile = nil } } @@ -97,6 +127,11 @@ func (i *Instance) Start() error { return fmt.Errorf("instance %s is already running", i.Name) } + // Create log files + if err := i.createLogFiles(); err != nil { + return fmt.Errorf("failed to create log files: %w", err) + } + args := i.options.BuildCommandArgs() i.ctx, i.cancel = context.WithCancel(context.Background()) @@ -112,10 +147,13 @@ func (i *Instance) Start() error { var err error i.stdout, err = i.cmd.StdoutPipe() if err != nil { + i.closeLogFiles() // Ensure log files are closed on error return fmt.Errorf("failed to get stdout pipe: %w", err) } i.stderr, err = i.cmd.StderrPipe() if err != nil { + i.stdout.Close() // Ensure stdout is closed on error + i.closeLogFiles() // Ensure log files are closed on error return fmt.Errorf("failed to get stderr pipe: %w", err) } @@ -125,8 +163,8 @@ func (i *Instance) Start() error { i.Running = true - go i.readOutput(i.stdout, i.StdOutChan, "stdout") - go i.readOutput(i.stderr, i.StdErrChan, "stderr") + go i.readOutput(i.stdout, i.logFile) + go i.readOutput(i.stderr, i.logFile) go i.monitorProcess() @@ -166,25 +204,65 @@ func (i *Instance) Stop() error { i.Running = false - // Close channels when process is stopped - close(i.StdOutChan) - close(i.StdErrChan) + i.closeLogFiles() // Close log files after stopping return nil } -// readOutput reads from the given reader and sends lines to the channel -func (i *Instance) readOutput(reader io.ReadCloser, ch chan string, streamType string) { +// GetLogs retrieves the last n lines of logs from the instance +func (i *Instance) GetLogs(num_lines int) (string, error) { + i.mu.Lock() + defer i.mu.Unlock() + + if i.logFile == nil { + return "", fmt.Errorf("log file not created for instance %s", i.Name) + } + + file, err := os.Open(i.logFile.Name()) + if err != nil { + return "", fmt.Errorf("failed to open log file: %w", err) + } + defer file.Close() + + if num_lines <= 0 { + content, err := io.ReadAll(file) + if err != nil { + return "", fmt.Errorf("failed to read log file: %w", err) + } + return string(content), nil + } + + var lines []string + scanner := bufio.NewScanner(file) + + // Read all lines into a slice + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error reading file: %w", err) + } + + // Return the last N lines + start := len(lines) - num_lines + if start < 0 { + start = 0 + } + + return strings.Join(lines[start:], "\n"), nil +} + +// readOutput reads from the given reader and writes lines to the log file +func (i *Instance) readOutput(reader io.ReadCloser, logFile *os.File) { defer reader.Close() scanner := bufio.NewScanner(reader) for scanner.Scan() { line := scanner.Text() - select { - case ch <- line: - default: - // Channel is full, drop the line - log.Printf("Dropped %s line for instance %s: %s", streamType, i.Name, line) + if logFile != nil { + fmt.Fprintln(logFile, line) + logFile.Sync() // Ensure data is written to disk } } } @@ -198,9 +276,10 @@ func (i *Instance) monitorProcess() { if !i.Running { return } - i.Running = false + i.closeLogFiles() + // Log the exit if err != nil { log.Printf("Instance %s crashed with error: %v", i.Name, err) @@ -272,13 +351,5 @@ func (i *Instance) UnmarshalJSON(data []byte) error { i.options = temp.Options } - // Initialize channels if they don't exist - if i.StdOutChan == nil { - i.StdOutChan = make(chan string, 100) - } - if i.StdErrChan == nil { - i.StdErrChan = make(chan string, 100) - } - return nil } diff --git a/server/pkg/routes.go b/server/pkg/routes.go index 0570913..707d527 100644 --- a/server/pkg/routes.go +++ b/server/pkg/routes.go @@ -37,7 +37,7 @@ func SetupRouter(handler *Handler) *chi.Mux { r.Post("/start", handler.StartInstance()) // Start stopped instance r.Post("/stop", handler.StopInstance()) // Stop running instance r.Post("/restart", handler.RestartInstance()) // Restart instance - // r.Get("/logs", handler.GetInstanceLogs()) // Get instance logs + r.Get("/logs", handler.GetInstanceLogs()) // Get instance logs // Llama.cpp server proxy endpoints (proxied to the actual llama.cpp server) r.Route("/proxy", func(r chi.Router) {