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  }