github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/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, resources []string, p *hud.IncrementalPrinter) *WebsocketReader { 35 ls := NewLogStreamer(resources, 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 resources model.ManifestNameSet // if present, resource(s) to stream logs for 63 printer *hud.IncrementalPrinter 64 } 65 66 func NewLogStreamer(resources []string, p *hud.IncrementalPrinter) *LogStreamer { 67 mnSet := make(map[model.ManifestName]bool, len(resources)) 68 for _, r := range resources { 69 mnSet[model.ManifestName(r)] = true 70 } 71 72 return &LogStreamer{ 73 resources: mnSet, 74 logstore: logstore.NewLogStore(), 75 printer: p, 76 } 77 } 78 79 func (ls *LogStreamer) Handle(v *proto_webview.View) error { 80 if v == nil || v.LogList == nil || v.LogList.FromCheckpoint == -1 { 81 // Server has no new logs to send 82 return nil 83 } 84 85 // if printing logs for only one resource, don't need resource name prefix 86 suppressPrefix := len(ls.resources) == 1 87 88 segments := v.LogList.Segments 89 if v.LogList.FromCheckpoint < ls.serverWatermark { 90 // The server is re-sending some logs we already have, so slice them off. 91 deleteCount := ls.serverWatermark - v.LogList.FromCheckpoint 92 segments = segments[deleteCount:] 93 } 94 95 for _, seg := range segments { 96 // TODO(maia): secrets??? 97 ls.logstore.Append(webview.LogSegmentToEvent(seg, v.LogList.Spans), model.SecretSet{}) 98 } 99 100 ls.printer.Print(ls.logstore.ContinuingLinesWithOptions(ls.checkpoint, logstore.LineOptions{ 101 ManifestNames: ls.resources, 102 SuppressPrefix: suppressPrefix, 103 })) 104 105 ls.checkpoint = ls.logstore.Checkpoint() 106 ls.serverWatermark = v.LogList.ToCheckpoint 107 108 return nil 109 } 110 func StreamLogs(ctx context.Context, follow bool, url model.WebURL, resources []string, printer *hud.IncrementalPrinter) error { 111 url.Scheme = "ws" 112 url.Path = "/ws/view" 113 logger.Get(ctx).Debugf("connecting to %s", url.String()) 114 115 conn, _, err := websocket.DefaultDialer.Dial(url.String(), nil) 116 if err != nil { 117 return errors.Wrapf(err, "dialing websocket %s", url.String()) 118 } 119 defer conn.Close() 120 121 wsr := newWebsocketReaderForLogs(conn, follow, resources, printer) 122 return wsr.Listen(ctx) 123 } 124 125 func (wsr *WebsocketReader) Listen(ctx context.Context) error { 126 done := make(chan struct{}) 127 go func() { 128 defer close(done) 129 for { 130 messageType, reader, err := wsr.conn.NextReader() 131 if err != nil { 132 return 133 } 134 135 if messageType == websocket.TextMessage { 136 err = wsr.handleTextMessage(ctx, reader) 137 if err != nil { 138 logger.Get(ctx).Errorf("Error streaming logs: %v", err) 139 } 140 if !wsr.persistent { 141 return 142 } 143 } 144 } 145 }() 146 147 for { 148 select { 149 case <-done: 150 return nil 151 case <-ctx.Done(): 152 err := ctx.Err() 153 if err != context.Canceled { 154 return err 155 } 156 157 return wsr.conn.Close() 158 } 159 } 160 } 161 162 func (wsr *WebsocketReader) handleTextMessage(_ context.Context, reader io.Reader) error { 163 v := &proto_webview.View{} 164 err := wsr.unmarshaller.Unmarshal(reader, v) 165 if err != nil { 166 return errors.Wrap(err, "parsing") 167 } 168 169 err = wsr.handler.Handle(v) 170 if err != nil { 171 return errors.Wrap(err, "handling") 172 } 173 174 return nil 175 }