runlog.go

  1// Package runlog reads and writes a run's events.ndjson: one
  2// run.rtw.ci.event per line, the single observable format shared by the
  3// terminal renderer, `ci view`, the runner's streams and the appview.
  4package runlog
  5
  6import (
  7	"bufio"
  8	"bytes"
  9	"encoding/json"
 10	"fmt"
 11	"io"
 12	"os"
 13	"sync"
 14	"time"
 15
 16	"git.kilimanjaro.io/ci/internal/lex"
 17)
 18
 19// Writer appends events to an events.ndjson file. It is safe for concurrent
 20// use (an action's stdout and stderr are written from two goroutines).
 21type Writer struct {
 22	mu    sync.Mutex
 23	f     *os.File
 24	run   string
 25	seq   int
 26	sinks []func(lex.RunEvent)
 27	now   func() time.Time
 28}
 29
 30// Open opens (or creates) path for appending, continuing the sequence of
 31// any events already in it, so a resumed run extends its own log.
 32func Open(path, run string) (*Writer, error) {
 33	n, err := countLines(path)
 34	if err != nil {
 35		return nil, err
 36	}
 37	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
 38	if err != nil {
 39		return nil, err
 40	}
 41	return &Writer{f: f, run: run, seq: n, now: time.Now}, nil
 42}
 43
 44func countLines(path string) (int, error) {
 45	f, err := os.Open(path)
 46	if os.IsNotExist(err) {
 47		return 0, nil
 48	}
 49	if err != nil {
 50		return 0, err
 51	}
 52	defer f.Close()
 53	n := 0
 54	buf := make([]byte, 64*1024)
 55	for {
 56		c, err := f.Read(buf)
 57		n += bytes.Count(buf[:c], []byte{'\n'})
 58		if err == io.EOF {
 59			return n, nil
 60		}
 61		if err != nil {
 62			return 0, err
 63		}
 64	}
 65}
 66
 67// AddSink registers a function called with every event after it is written.
 68func (w *Writer) AddSink(f func(lex.RunEvent)) {
 69	w.mu.Lock()
 70	defer w.mu.Unlock()
 71	w.sinks = append(w.sinks, f)
 72}
 73
 74// Emit stamps ev with the next sequence number, the time and the run id, and
 75// appends it.
 76func (w *Writer) Emit(ev lex.RunEvent) error {
 77	w.mu.Lock()
 78	defer w.mu.Unlock()
 79	w.seq++
 80	ev.Seq = w.seq
 81	ev.Time = w.now().UTC().Format(time.RFC3339Nano)
 82	ev.Run = w.run
 83	b, err := json.Marshal(ev)
 84	if err != nil {
 85		return err
 86	}
 87	if _, err := w.f.Write(append(b, '\n')); err != nil {
 88		return err
 89	}
 90	for _, s := range w.sinks {
 91		s(ev)
 92	}
 93	return nil
 94}
 95
 96// Close closes the file.
 97func (w *Writer) Close() error { return w.f.Close() }
 98
 99// Lines returns an io.WriteCloser that emits one log event per line written
100// to it, for an action's stdout or stderr. Close flushes a final partial line.
101func (w *Writer) Lines(action string, attempt int, stream string) io.WriteCloser {
102	return &lineWriter{w: w, action: action, attempt: attempt, stream: stream}
103}
104
105type lineWriter struct {
106	w       *Writer
107	action  string
108	attempt int
109	stream  string
110	buf     []byte
111}
112
113func (l *lineWriter) Write(p []byte) (int, error) {
114	l.buf = append(l.buf, p...)
115	for {
116		i := bytes.IndexByte(l.buf, '\n')
117		if i < 0 {
118			break
119		}
120		if err := l.emit(l.buf[:i]); err != nil {
121			return 0, err
122		}
123		l.buf = l.buf[i+1:]
124	}
125	return len(p), nil
126}
127
128func (l *lineWriter) emit(line []byte) error {
129	return l.w.Emit(lex.RunEvent{
130		Kind:    lex.KindLog,
131		Action:  l.action,
132		Attempt: l.attempt,
133		Stream:  l.stream,
134		Content: string(bytes.TrimSuffix(line, []byte{'\r'})),
135	})
136}
137
138func (l *lineWriter) Close() error {
139	if len(l.buf) == 0 {
140		return nil
141	}
142	err := l.emit(l.buf)
143	l.buf = nil
144	return err
145}
146
147// Read decodes every event in r.
148func Read(r io.Reader) ([]lex.RunEvent, error) {
149	var out []lex.RunEvent
150	err := Scan(r, func(ev lex.RunEvent) error { out = append(out, ev); return nil })
151	return out, err
152}
153
154// Scan calls f for each event in r, in order.
155func Scan(r io.Reader, f func(lex.RunEvent) error) error {
156	sc := bufio.NewScanner(r)
157	sc.Buffer(make([]byte, 64*1024), 16*1024*1024)
158	line := 0
159	for sc.Scan() {
160		line++
161		if len(bytes.TrimSpace(sc.Bytes())) == 0 {
162			continue
163		}
164		var ev lex.RunEvent
165		if err := json.Unmarshal(sc.Bytes(), &ev); err != nil {
166			return fmt.Errorf("line %d: %w", line, err)
167		}
168		if err := f(ev); err != nil {
169			return err
170		}
171	}
172	return sc.Err()
173}