github.com/tilt-dev/tilt@v0.36.0/internal/hud/server/logs_reader.go (about) 1 package server 2 3 import ( 4 "context" 5 "io" 6 7 "github.com/golang/protobuf/jsonpb" 8 "github.com/gorilla/websocket" 9 "github.com/pkg/errors" 10 11 "github.com/tilt-dev/tilt/internal/hud" 12 "github.com/tilt-dev/tilt/internal/hud/webview" 13 "github.com/tilt-dev/tilt/pkg/logger" 14 "github.com/tilt-dev/tilt/pkg/model" 15 "github.com/tilt-dev/tilt/pkg/model/logstore" 16 proto_webview "github.com/tilt-dev/tilt/pkg/webview" 17 ) 18 19 // This file defines machinery to connect to the HUD server websocket and 20 // read logs from a running Tilt instance. 21 // In future, we can use WebsocketReader more generically to read state 22 // from a running Tilt, and do different things with that state depending 23 // on the handler provided (if we ever implement e.g. `tilt status`). 24 // (If we never use the WebsocketReader elsewhere, we might want to collapse 25 // it and the LogStreamer handler into a single struct.) 26 27 type WebsocketReader struct { 28 conn WebsocketConn 29 unmarshaller jsonpb.Unmarshaler 30 persistent bool // whether to keep listening on websocket, or close after first message 31 handler ViewHandler 32 } 33 34 func newWebsocketReaderForLogs(conn WebsocketConn, persistent bool, filter hud.LogFilter, p *hud.IncrementalPrinter) *WebsocketReader { 35 ls := NewLogStreamer(filter, p) 36 return newWebsocketReader(conn, persistent, ls) 37 } 38 39 func newWebsocketReader(conn WebsocketConn, persistent bool, handler ViewHandler) *WebsocketReader { 40 return &WebsocketReader{ 41 conn: conn, 42 unmarshaller: jsonpb.Unmarshaler{}, 43 persistent: persistent, 44 handler: handler, 45 } 46 } 47 48 type ViewHandler interface { 49 Handle(v *proto_webview.View) error 50 } 51 52 type LogStreamer struct { 53 logstore *logstore.LogStore 54 // checkpoint tracks the client's latest printed logs. 55 // 56 // WARNING: The server watermark values CANNOT be used for checkpointing within the client! 57 checkpoint logstore.Checkpoint 58 // serverWatermark ensures that we don't print any duplicate logs. 59 // 60 // This value should only be used to compare to other server values, NOT client checkpoints. 61 serverWatermark int32 62 filter hud.LogFilter 63 printer *hud.IncrementalPrinter 64 } 65 66 func NewLogStreamer(filter hud.LogFilter, p *hud.IncrementalPrinter) *LogStreamer { 67 return &LogStreamer{ 68 filter: filter, 69 logstore: logstore.NewLogStore(), 70 printer: p, 71 } 72 } 73 74 func (ls *LogStreamer) Handle(v *proto_webview.View) error { 75 if v == nil || v.LogList == nil || v.LogList.FromCheckpoint == -1 { 76 // Server has no new logs to send 77 return nil 78 } 79 80 segments := v.LogList.Segments 81 if v.LogList.FromCheckpoint < ls.serverWatermark { 82 // The server is re-sending some logs we already have, so slice them off. 83 deleteCount := ls.serverWatermark - v.LogList.FromCheckpoint 84 segments = segments[deleteCount:] 85 } 86 87 for _, seg := range segments { 88 // TODO(maia): secrets??? 89 ls.logstore.Append(webview.LogSegmentToEvent(seg, v.LogList.Spans), model.SecretSet{}) 90 } 91 92 lines := ls.logstore.ContinuingLinesWithOptions(ls.checkpoint, logstore.LineOptions{ 93 SuppressPrefix: ls.filter.SuppressPrefix(), 94 }) 95 lines = ls.filter.Apply(lines) 96 ls.printer.Print(lines) 97 98 ls.checkpoint = ls.logstore.Checkpoint() 99 ls.serverWatermark = v.LogList.ToCheckpoint 100 101 return nil 102 } 103 104 func StreamLogs(ctx context.Context, follow bool, url model.WebURL, filter hud.LogFilter, printer *hud.IncrementalPrinter) error { 105 url.Scheme = "ws" 106 url.Path = "/ws/view" 107 logger.Get(ctx).Debugf("connecting to %s", url.String()) 108 109 conn, _, err := websocket.DefaultDialer.Dial(url.String(), nil) 110 if err != nil { 111 return errors.Wrapf(err, "dialing websocket %s", url.String()) 112 } 113 defer conn.Close() 114 115 wsr := newWebsocketReaderForLogs(conn, follow, filter, printer) 116 return wsr.Listen(ctx) 117 } 118 119 func (wsr *WebsocketReader) Listen(ctx context.Context) error { 120 done := make(chan struct{}) 121 go func() { 122 defer close(done) 123 for { 124 messageType, reader, err := wsr.conn.NextReader() 125 if err != nil { 126 return 127 } 128 129 if messageType == websocket.TextMessage { 130 err = wsr.handleTextMessage(ctx, reader) 131 if err != nil { 132 logger.Get(ctx).Errorf("Error streaming logs: %v", err) 133 } 134 if !wsr.persistent { 135 return 136 } 137 } 138 } 139 }() 140 141 for { 142 select { 143 case <-done: 144 return nil 145 case <-ctx.Done(): 146 err := ctx.Err() 147 if err != context.Canceled { 148 return err 149 } 150 151 return wsr.conn.Close() 152 } 153 } 154 } 155 156 func (wsr *WebsocketReader) handleTextMessage(_ context.Context, reader io.Reader) error { 157 v := &proto_webview.View{} 158 err := wsr.unmarshaller.Unmarshal(reader, v) 159 if err != nil { 160 return errors.Wrap(err, "parsing") 161 } 162 163 err = wsr.handler.Handle(v) 164 if err != nil { 165 return errors.Wrap(err, "handling") 166 } 167 168 return nil 169 }