Implement instance log management

This commit is contained in:
2025-07-19 18:59:58 +02:00
parent 7c2192a48d
commit 604d7ebcc8
3 changed files with 135 additions and 27 deletions

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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) {