mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-11-06 17:14:28 +00:00
Implement instance log management
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"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 {
|
func (h *Handler) ProxyToInstance() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
name := chi.URLParam(r, "name")
|
name := chi.URLParam(r, "name")
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -23,9 +25,9 @@ type Instance struct {
|
|||||||
// Status
|
// Status
|
||||||
Running bool `json:"running"`
|
Running bool `json:"running"`
|
||||||
|
|
||||||
// Output channels
|
// Log files
|
||||||
StdOutChan chan string `json:"-"` // Channel for sending output messages
|
logPath string `json:"-"`
|
||||||
StdErrChan chan string `json:"-"` // Channel for sending error messages
|
logFile *os.File `json:"-"`
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
cmd *exec.Cmd `json:"-"` // Command to run the instance
|
cmd *exec.Cmd `json:"-"` // Command to run the instance
|
||||||
@@ -44,9 +46,37 @@ func NewInstance(name string, options *InstanceOptions) *Instance {
|
|||||||
options: options,
|
options: options,
|
||||||
|
|
||||||
Running: false,
|
Running: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StdOutChan: make(chan string, 100),
|
// createLogFiles creates and opens the log files for stdout and stderr
|
||||||
StdErrChan: make(chan string, 100),
|
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)
|
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()
|
args := i.options.BuildCommandArgs()
|
||||||
|
|
||||||
i.ctx, i.cancel = context.WithCancel(context.Background())
|
i.ctx, i.cancel = context.WithCancel(context.Background())
|
||||||
@@ -112,10 +147,13 @@ func (i *Instance) Start() error {
|
|||||||
var err error
|
var err error
|
||||||
i.stdout, err = i.cmd.StdoutPipe()
|
i.stdout, err = i.cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
i.closeLogFiles() // Ensure log files are closed on error
|
||||||
return fmt.Errorf("failed to get stdout pipe: %w", err)
|
return fmt.Errorf("failed to get stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
i.stderr, err = i.cmd.StderrPipe()
|
i.stderr, err = i.cmd.StderrPipe()
|
||||||
if err != nil {
|
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)
|
return fmt.Errorf("failed to get stderr pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +163,8 @@ func (i *Instance) Start() error {
|
|||||||
|
|
||||||
i.Running = true
|
i.Running = true
|
||||||
|
|
||||||
go i.readOutput(i.stdout, i.StdOutChan, "stdout")
|
go i.readOutput(i.stdout, i.logFile)
|
||||||
go i.readOutput(i.stderr, i.StdErrChan, "stderr")
|
go i.readOutput(i.stderr, i.logFile)
|
||||||
|
|
||||||
go i.monitorProcess()
|
go i.monitorProcess()
|
||||||
|
|
||||||
@@ -166,25 +204,65 @@ func (i *Instance) Stop() error {
|
|||||||
|
|
||||||
i.Running = false
|
i.Running = false
|
||||||
|
|
||||||
// Close channels when process is stopped
|
i.closeLogFiles() // Close log files after stopping
|
||||||
close(i.StdOutChan)
|
|
||||||
close(i.StdErrChan)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readOutput reads from the given reader and sends lines to the channel
|
// GetLogs retrieves the last n lines of logs from the instance
|
||||||
func (i *Instance) readOutput(reader io.ReadCloser, ch chan string, streamType string) {
|
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()
|
defer reader.Close()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
select {
|
if logFile != nil {
|
||||||
case ch <- line:
|
fmt.Fprintln(logFile, line)
|
||||||
default:
|
logFile.Sync() // Ensure data is written to disk
|
||||||
// Channel is full, drop the line
|
|
||||||
log.Printf("Dropped %s line for instance %s: %s", streamType, i.Name, line)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,9 +276,10 @@ func (i *Instance) monitorProcess() {
|
|||||||
if !i.Running {
|
if !i.Running {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
i.Running = false
|
i.Running = false
|
||||||
|
|
||||||
|
i.closeLogFiles()
|
||||||
|
|
||||||
// Log the exit
|
// Log the exit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Instance %s crashed with error: %v", i.Name, err)
|
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
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func SetupRouter(handler *Handler) *chi.Mux {
|
|||||||
r.Post("/start", handler.StartInstance()) // Start stopped instance
|
r.Post("/start", handler.StartInstance()) // Start stopped instance
|
||||||
r.Post("/stop", handler.StopInstance()) // Stop running instance
|
r.Post("/stop", handler.StopInstance()) // Stop running instance
|
||||||
r.Post("/restart", handler.RestartInstance()) // Restart 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)
|
// Llama.cpp server proxy endpoints (proxied to the actual llama.cpp server)
|
||||||
r.Route("/proxy", func(r chi.Router) {
|
r.Route("/proxy", func(r chi.Router) {
|
||||||
|
|||||||
Reference in New Issue
Block a user