AppRunner: Automatically annotate logs

Change-Id: I5e614fc1e841e183ac649758972428ae55162a67
diff --git a/apps/app-runner/log.go b/apps/app-runner/log.go
index 2e5f524..084e3d1 100644
--- a/apps/app-runner/log.go
+++ b/apps/app-runner/log.go
@@ -1,24 +1,97 @@
 package main
 
 import (
+	"bytes"
 	"strings"
 	"sync"
+	"time"
 )
 
-type Log struct {
-	l sync.Mutex
-	d strings.Builder
+type LogItem struct {
+	RunId          string `json:"runId"`
+	TimestampMilli int64  `json:"timestampMilli"`
+	Commit         string `json:"commit,omitempty"`
+	Contents       []byte `json:"contents"`
 }
 
-func (l *Log) Write(p []byte) (n int, err error) {
+type Logger struct {
+	l          sync.Mutex
+	runId      string
+	commitHash string
+	items      []LogItem
+	curr       LogItem
+}
+
+func NewLogger(runId string) *Logger {
+	return &Logger{
+		l:     sync.Mutex{},
+		runId: runId,
+		items: []LogItem{},
+		curr: LogItem{
+			RunId:    runId,
+			Contents: []byte{},
+		},
+	}
+}
+
+func (l *Logger) Write(p []byte) (n int, err error) {
 	l.l.Lock()
 	defer l.l.Unlock()
+	cnt := 0
 	// TODO(gio): Reset s.logs periodically
-	return l.d.Write(p)
+	for len(p) > 0 {
+		pos := bytes.Index(p, []byte("\n"))
+		if pos != -1 {
+			if l.curr.TimestampMilli == 0 {
+				l.curr.TimestampMilli = time.Now().UnixMilli()
+			}
+			l.curr.Contents = append(l.curr.Contents, p[:pos]...)
+			l.items = append(l.items, l.curr)
+			l.curr = LogItem{
+				RunId:    l.runId,
+				Contents: []byte{},
+			}
+			p = p[pos+len([]byte("\n")):]
+			cnt += pos + len([]byte("\n"))
+		} else {
+			if l.curr.TimestampMilli == 0 {
+				l.curr.TimestampMilli = time.Now().UnixMilli()
+			}
+			l.curr.Contents = append(l.curr.Contents, p...)
+			cnt += len(p)
+			p = []byte{}
+		}
+	}
+	return cnt, nil
 }
 
-func (l *Log) Contents() string {
+func (l *Logger) Items() []LogItem {
 	l.l.Lock()
 	defer l.l.Unlock()
-	return l.d.String()
+	ret := []LogItem{}
+	for _, i := range l.items {
+		ret = append(ret, i)
+	}
+	return ret
+}
+
+func (l *Logger) Trim(n int) {
+	l.l.Lock()
+	defer l.l.Unlock()
+	l.items = l.items[n:]
+}
+
+func (l *Logger) Contents() (string, error) {
+	l.l.Lock()
+	defer l.l.Unlock()
+	var ret strings.Builder
+	for _, i := range l.items {
+		if _, err := ret.Write(i.Contents); err != nil {
+			return "", err
+		}
+		if _, err := ret.WriteString("\n"); err != nil {
+			return "", err
+		}
+	}
+	return ret.String(), nil
 }