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  }