github.com/drone/runner-go@v1.12.0/livelog/livelog.go (about) 1 // Copyright 2019 Drone.IO Inc. All rights reserved. 2 // Use of this source code is governed by the Polyform License 3 // that can be found in the LICENSE file. 4 5 // Package livelog provides a Writer that collects pipeline 6 // output and streams to the central server. 7 package livelog 8 9 import ( 10 "context" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/drone/drone-go/drone" 16 "github.com/drone/runner-go/client" 17 ) 18 19 // defaultLimit is the default maximum log size in bytes. 20 const defaultLimit = 5242880 // 5MB 21 22 // Writer is an io.Writer that sends logs to the server. 23 type Writer struct { 24 sync.Mutex 25 26 client client.Client 27 28 id int64 29 num int 30 now time.Time 31 size int 32 limit int 33 34 interval time.Duration 35 pending []*drone.Line 36 history []*drone.Line 37 38 closed bool 39 close chan struct{} 40 ready chan struct{} 41 } 42 43 // New returns a new Wrtier. 44 func New(client client.Client, id int64) *Writer { 45 b := &Writer{ 46 client: client, 47 id: id, 48 now: time.Now(), 49 limit: defaultLimit, 50 interval: time.Second, 51 close: make(chan struct{}), 52 ready: make(chan struct{}, 1), 53 } 54 go b.start() 55 return b 56 } 57 58 // SetLimit sets the Writer limit. 59 func (b *Writer) SetLimit(limit int) { 60 b.limit = limit 61 } 62 63 // SetInterval sets the Writer flusher interval. 64 func (b *Writer) SetInterval(interval time.Duration) { 65 b.interval = interval 66 } 67 68 // Write uploads the live log stream to the server. 69 func (b *Writer) Write(p []byte) (n int, err error) { 70 for _, part := range split(p) { 71 line := &drone.Line{ 72 Number: b.num, 73 Message: part, 74 Timestamp: int64(time.Since(b.now).Seconds()), 75 } 76 77 for b.size+len(p) > b.limit { 78 b.stop() // buffer is full, step streaming data 79 b.size -= len(b.history[0].Message) 80 b.history = b.history[1:] 81 } 82 83 b.size = b.size + len(part) 84 b.num++ 85 86 if b.stopped() == false { 87 b.Lock() 88 b.pending = append(b.pending, line) 89 b.Unlock() 90 } 91 92 b.Lock() 93 b.history = append(b.history, line) 94 b.Unlock() 95 } 96 97 select { 98 case b.ready <- struct{}{}: 99 default: 100 } 101 102 return len(p), nil 103 } 104 105 // Close closes the writer and uploads the full contents to 106 // the server. 107 func (b *Writer) Close() error { 108 if b.stop() { 109 b.flush() 110 } 111 return b.upload() 112 } 113 114 // upload uploads the full log history to the server. 115 func (b *Writer) upload() error { 116 return b.client.Upload( 117 context.Background(), b.id, b.history) 118 } 119 120 // flush batch uploads all buffered logs to the server. 121 func (b *Writer) flush() error { 122 b.Lock() 123 lines := b.copy() 124 b.clear() 125 b.Unlock() 126 if len(lines) == 0 { 127 return nil 128 } 129 return b.client.Batch( 130 context.Background(), b.id, lines) 131 } 132 133 // copy returns a copy of the buffered lines. 134 func (b *Writer) copy() []*drone.Line { 135 return append(b.pending[:0:0], b.pending...) 136 } 137 138 // clear clears the buffer. 139 func (b *Writer) clear() { 140 b.pending = b.pending[:0] 141 } 142 143 func (b *Writer) stop() bool { 144 b.Lock() 145 var closed bool 146 if b.closed == false { 147 close(b.close) 148 closed = true 149 b.closed = true 150 } 151 b.Unlock() 152 return closed 153 } 154 155 func (b *Writer) stopped() bool { 156 b.Lock() 157 closed := b.closed 158 b.Unlock() 159 return closed 160 } 161 162 func (b *Writer) start() error { 163 for { 164 select { 165 case <-b.close: 166 return nil 167 case <-b.ready: 168 select { 169 case <-b.close: 170 return nil 171 case <-time.After(b.interval): 172 // we intentionally ignore errors. log streams 173 // are ephemeral and are considered low prioirty 174 // because they are not required for drone to 175 // operator, and the impact of failure is minimal 176 b.flush() 177 } 178 } 179 } 180 } 181 182 func split(p []byte) []string { 183 s := string(p) 184 v := []string{s} 185 // kubernetes buffers the output and may combine 186 // multiple lines into a single block of output. 187 // Split into multiple lines. 188 // 189 // note that docker output always inclines a line 190 // feed marker. This needs to be accounted for when 191 // splitting the output into multiple lines. 192 if strings.Contains(strings.TrimSuffix(s, "\n"), "\n") { 193 v = strings.SplitAfter(s, "\n") 194 } 195 return v 196 }