Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

spindle: stream logs from disk

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

authored by anirudh.fi and committed by

Tangled 3a3a98e0 ad84c0f8

+94 -30
+1
spindle/server.go
··· 148 148 w.Write([]byte(s.cfg.Server.Owner)) 149 149 }) 150 150 mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs) 151 + mux.HandleFunc("/logs/{knot}/{rkey}/{name}/{idx}", s.StepLogs) 151 152 return mux 152 153 } 153 154
+93 -30
spindle/stream.go
··· 1 1 package spindle 2 2 3 3 import ( 4 + "bufio" 4 5 "context" 5 6 "encoding/json" 6 7 "fmt" 7 8 "net/http" 8 9 "strconv" 10 + "strings" 9 11 "time" 10 12 13 + "tangled.sh/tangled.sh/core/spindle/engine" 11 14 "tangled.sh/tangled.sh/core/spindle/models" 12 15 13 16 "github.com/go-chi/chi/v5" ··· 91 88 } 92 89 93 90 func (s *Spindle) Logs(w http.ResponseWriter, r *http.Request) { 91 + wid, err := getWorkflowID(r) 92 + if err != nil { 93 + http.Error(w, err.Error(), http.StatusBadRequest) 94 + return 95 + } 96 + 97 + s.handleLogStream(w, r, func(ctx context.Context, conn *websocket.Conn) error { 98 + return s.streamLogs(ctx, conn, wid) 99 + }) 100 + } 101 + 102 + func (s *Spindle) StepLogs(w http.ResponseWriter, r *http.Request) { 103 + wid, err := getWorkflowID(r) 104 + if err != nil { 105 + http.Error(w, err.Error(), http.StatusBadRequest) 106 + return 107 + } 108 + 109 + idxStr := chi.URLParam(r, "idx") 110 + if idxStr == "" { 111 + http.Error(w, "step index required", http.StatusBadRequest) 112 + return 113 + } 114 + idx, err := strconv.Atoi(idxStr) 115 + if err != nil { 116 + http.Error(w, "bad step index", http.StatusBadRequest) 117 + return 118 + } 119 + 120 + s.handleLogStream(w, r, func(ctx context.Context, conn *websocket.Conn) error { 121 + return s.streamLogFromDisk(ctx, conn, wid, idx) 122 + }) 123 + } 124 + 125 + func (s *Spindle) handleLogStream(w http.ResponseWriter, r *http.Request, streamFn func(ctx context.Context, conn *websocket.Conn) error) { 94 126 l := s.l.With("handler", "Logs") 95 - 96 - knot := chi.URLParam(r, "knot") 97 - if knot == "" { 98 - http.Error(w, "knot required", http.StatusBadRequest) 99 - return 100 - } 101 - 102 - rkey := chi.URLParam(r, "rkey") 103 - if rkey == "" { 104 - http.Error(w, "rkey required", http.StatusBadRequest) 105 - return 106 - } 107 - 108 - name := chi.URLParam(r, "name") 109 - if name == "" { 110 - http.Error(w, "name required", http.StatusBadRequest) 111 - return 112 - } 113 - 114 - wid := models.WorkflowId{ 115 - PipelineId: models.PipelineId{ 116 - Knot: knot, 117 - Rkey: rkey, 118 - }, 119 - Name: name, 120 - } 121 - 122 - l = l.With("knot", knot, "rkey", rkey, "name", name) 123 127 124 128 conn, err := upgrader.Upgrade(w, r, nil) 125 129 if err != nil { ··· 150 140 } 151 141 }() 152 142 153 - if err := s.streamLogs(ctx, conn, wid); err != nil { 154 - l.Error("streamLogs failed", "err", err) 143 + if err := streamFn(ctx, conn); err != nil { 144 + l.Error("log stream failed", "err", err) 155 145 } 156 146 l.Debug("logs connection closed") 157 147 } ··· 216 206 return nil 217 207 } 218 208 209 + func (s *Spindle) streamLogFromDisk(ctx context.Context, conn *websocket.Conn, wid models.WorkflowId, stepIdx int) error { 210 + streams := []string{"stdout", "stderr"} 211 + 212 + for _, stream := range streams { 213 + data, err := engine.ReadStepLog(s.cfg.Pipelines.LogDir, wid.String(), stream, stepIdx) 214 + if err != nil { 215 + // log but continue to next stream 216 + s.l.Error("failed to read step log", "stream", stream, "step", stepIdx, "wid", wid.String(), "err", err) 217 + continue 218 + } 219 + 220 + scanner := bufio.NewScanner(strings.NewReader(data)) 221 + for scanner.Scan() { 222 + select { 223 + case <-ctx.Done(): 224 + return ctx.Err() 225 + default: 226 + msg := map[string]string{ 227 + "type": stream, 228 + "data": scanner.Text(), 229 + } 230 + if err := conn.WriteJSON(msg); err != nil { 231 + return err 232 + } 233 + } 234 + } 235 + 236 + if err := scanner.Err(); err != nil { 237 + return fmt.Errorf("error scanning %s log: %w", stream, err) 238 + } 239 + } 240 + 241 + return nil 242 + } 243 + 219 244 func (s *Spindle) streamPipelines(conn *websocket.Conn, cursor *int64) error { 220 245 events, err := s.db.GetEvents(*cursor) 221 246 if err != nil { ··· 286 241 } 287 242 288 243 return nil 244 + } 245 + 246 + func getWorkflowID(r *http.Request) (models.WorkflowId, error) { 247 + knot := chi.URLParam(r, "knot") 248 + rkey := chi.URLParam(r, "rkey") 249 + name := chi.URLParam(r, "name") 250 + 251 + if knot == "" || rkey == "" || name == "" { 252 + return models.WorkflowId{}, fmt.Errorf("missing required parameters") 253 + } 254 + 255 + return models.WorkflowId{ 256 + PipelineId: models.PipelineId{ 257 + Knot: knot, 258 + Rkey: rkey, 259 + }, 260 + Name: name, 261 + }, nil 289 262 }