github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/logstream.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver
     5  
     6  import (
     7  	"net/http"
     8  	"time"
     9  
    10  	"github.com/gorilla/schema"
    11  	"github.com/juju/clock"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/featureflag"
    14  
    15  	"github.com/juju/juju/apiserver/websocket"
    16  	corelogger "github.com/juju/juju/core/logger"
    17  	"github.com/juju/juju/feature"
    18  	"github.com/juju/juju/rpc/params"
    19  	"github.com/juju/juju/state"
    20  )
    21  
    22  type logStreamSource interface {
    23  	getStart(sink string) (time.Time, error)
    24  	newTailer(corelogger.LogTailerParams) (corelogger.LogTailer, error)
    25  }
    26  
    27  type messageWriter interface {
    28  	WriteJSON(v interface{}) error
    29  }
    30  
    31  // logStreamEndpointHandler takes requests to stream logs from the DB.
    32  type logStreamEndpointHandler struct {
    33  	stopCh    <-chan struct{}
    34  	newSource func(*http.Request) (logStreamSource, state.PoolHelper, error)
    35  }
    36  
    37  func newLogStreamEndpointHandler(ctxt httpContext) *logStreamEndpointHandler {
    38  	newSource := func(req *http.Request) (logStreamSource, state.PoolHelper, error) {
    39  		st, _, err := ctxt.stateForRequestAuthenticatedAgent(req)
    40  		if err != nil {
    41  			return nil, nil, errors.Trace(err)
    42  		}
    43  		return &logStreamState{st}, st, nil
    44  	}
    45  	return &logStreamEndpointHandler{
    46  		stopCh:    ctxt.stop(),
    47  		newSource: newSource,
    48  	}
    49  }
    50  
    51  // ServeHTTP will serve up connections as a websocket for the logstream API.
    52  //
    53  // Args for the HTTP request are as follows:
    54  //
    55  //	all -> string - one of [true, false], if true, include records from all models
    56  //	sink -> string - the name of the log forwarding target
    57  func (h *logStreamEndpointHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    58  	logger.Infof("log stream request handler starting")
    59  	handler := func(conn *websocket.Conn) {
    60  		defer conn.Close()
    61  		reqHandler, err := h.newLogStreamRequestHandler(conn, req, clock.WallClock)
    62  		if err != nil {
    63  			h.sendError(conn, req, err)
    64  			return
    65  		}
    66  		defer reqHandler.close()
    67  
    68  		// If we get to here, no more errors to report, so we report a nil
    69  		// error.  This way the first line of the connection is always a json
    70  		// formatted simple error.
    71  		h.sendError(conn, req, nil)
    72  		reqHandler.serveWebsocket(h.stopCh)
    73  	}
    74  	websocket.Serve(w, req, handler)
    75  }
    76  
    77  func (h *logStreamEndpointHandler) newLogStreamRequestHandler(conn messageWriter, req *http.Request, clock clock.Clock) (rh *logStreamRequestHandler, err error) {
    78  	// Validate before authenticate because the authentication is
    79  	// dependent on the state connection that is determined during the
    80  	// validation.
    81  	source, ph, err := h.newSource(req)
    82  	if err != nil {
    83  		return nil, errors.Trace(err)
    84  	}
    85  	defer func() {
    86  		if err != nil {
    87  			ph.Release()
    88  		}
    89  	}()
    90  
    91  	var cfg params.LogStreamConfig
    92  	query := req.URL.Query()
    93  	query.Del(":modeluuid")
    94  	if err := schema.NewDecoder().Decode(&cfg, query); err != nil {
    95  		return nil, errors.Annotate(err, "decoding schema")
    96  	}
    97  
    98  	tailer, err := h.newTailer(source, cfg, clock)
    99  	if err != nil {
   100  		return nil, errors.Annotate(err, "creating new tailer")
   101  	}
   102  
   103  	reqHandler := &logStreamRequestHandler{
   104  		conn:       conn,
   105  		req:        req,
   106  		tailer:     tailer,
   107  		poolHelper: ph,
   108  	}
   109  	return reqHandler, nil
   110  }
   111  
   112  func (h *logStreamEndpointHandler) newTailer(
   113  	source logStreamSource, cfg params.LogStreamConfig, clock clock.Clock,
   114  ) (corelogger.LogTailer, error) {
   115  	start, err := source.getStart(cfg.Sink)
   116  	if err != nil {
   117  		return nil, errors.Annotate(err, "getting log start position")
   118  	}
   119  	if cfg.MaxLookbackDuration != "" {
   120  		d, err := time.ParseDuration(cfg.MaxLookbackDuration)
   121  		if err != nil {
   122  			return nil, errors.Annotatef(err, "invalid lookback duration")
   123  		}
   124  		now := clock.Now()
   125  		if now.Sub(start) > d {
   126  			start = now.Add(-1 * d)
   127  		}
   128  	}
   129  
   130  	tailerArgs := corelogger.LogTailerParams{
   131  		StartTime:    start,
   132  		InitialLines: cfg.MaxLookbackRecords,
   133  	}
   134  	tailer, err := source.newTailer(tailerArgs)
   135  	if err != nil {
   136  		return nil, errors.Annotate(err, "tailing logs")
   137  	}
   138  	return tailer, nil
   139  }
   140  
   141  // sendError sends a JSON-encoded error response.
   142  func (h *logStreamEndpointHandler) sendError(ws *websocket.Conn, req *http.Request, err error) {
   143  	// There is no need to log the error for normal operators as there is nothing
   144  	// they can action. This is for developers.
   145  	if err != nil && featureflag.Enabled(feature.DeveloperMode) {
   146  		logger.Errorf("returning error from %s %s: %s", req.Method, req.URL.Path, errors.Details(err))
   147  	}
   148  	if sendErr := ws.SendInitialErrorV0(err); sendErr != nil {
   149  		logger.Errorf("closing websocket, %v", err)
   150  		ws.Close()
   151  	}
   152  }
   153  
   154  // logStreamState is an implementation of logStreamSource.
   155  type logStreamState struct {
   156  	state.LogTailerState
   157  }
   158  
   159  func (st logStreamState) getStart(sink string) (time.Time, error) {
   160  	tracker := state.NewLastSentLogTracker(st, st.ModelUUID(), sink)
   161  	defer tracker.Close()
   162  
   163  	// Resume for the sink...
   164  	_, lastSentTimestamp, err := tracker.Get()
   165  	if errors.Cause(err) == state.ErrNeverForwarded {
   166  		// If we've never forwarded a message, we start from
   167  		// position zero.
   168  		lastSentTimestamp = 0
   169  	} else if err != nil {
   170  		return time.Time{}, errors.Trace(err)
   171  	}
   172  
   173  	// Using the same timestamp will cause at least 1 duplicate
   174  	// entry, but that is better than dropping records.
   175  	// TODO(ericsnow) Add 1 to start once we track by sequential int ID
   176  	// instead of by timestamp.
   177  	return time.Unix(0, lastSentTimestamp), nil
   178  }
   179  
   180  func (st logStreamState) newTailer(args corelogger.LogTailerParams) (corelogger.LogTailer, error) {
   181  	tailer, err := state.NewLogTailer(st, args, nil)
   182  	if err != nil {
   183  		return nil, errors.Trace(err)
   184  	}
   185  	return tailer, nil
   186  }
   187  
   188  type logStreamRequestHandler struct {
   189  	conn       messageWriter
   190  	req        *http.Request
   191  	tailer     corelogger.LogTailer
   192  	poolHelper state.PoolHelper
   193  }
   194  
   195  func (h *logStreamRequestHandler) serveWebsocket(stop <-chan struct{}) {
   196  	logger.Infof("log stream request handler starting")
   197  
   198  	// TODO(wallyworld) - we currently only send one record at a time, but the API allows for
   199  	// sending batches of records, so we need to batch up the output from tailer.Logs().
   200  	for {
   201  		select {
   202  		case <-stop:
   203  			return
   204  		case rec, ok := <-h.tailer.Logs():
   205  			if !ok {
   206  				logger.Errorf("tailer stopped: %v", h.tailer.Err())
   207  				return
   208  			}
   209  			if err := h.sendRecords([]*corelogger.LogRecord{rec}); err != nil {
   210  				if isBrokenPipe(err) {
   211  					logger.Tracef("logstream handler stopped (client disconnected)")
   212  				} else {
   213  					logger.Errorf("logstream handler error: %v", err)
   214  				}
   215  			}
   216  		}
   217  	}
   218  }
   219  
   220  func (h *logStreamRequestHandler) close() {
   221  	_ = h.tailer.Stop()
   222  	h.poolHelper.Release()
   223  }
   224  
   225  func (h *logStreamRequestHandler) sendRecords(rec []*corelogger.LogRecord) error {
   226  	apiRec := h.apiFromRecords(rec)
   227  	return errors.Trace(h.conn.WriteJSON(apiRec))
   228  }
   229  
   230  func (h *logStreamRequestHandler) apiFromRecords(records []*corelogger.LogRecord) params.LogStreamRecords {
   231  	var result params.LogStreamRecords
   232  	result.Records = make([]params.LogStreamRecord, len(records))
   233  	for i, rec := range records {
   234  		apiRec := params.LogStreamRecord{
   235  			ID:        rec.ID,
   236  			ModelUUID: rec.ModelUUID,
   237  			Version:   rec.Version.String(),
   238  			Entity:    rec.Entity,
   239  			Timestamp: rec.Time,
   240  			Module:    rec.Module,
   241  			Location:  rec.Location,
   242  			Level:     rec.Level.String(),
   243  			Message:   rec.Message,
   244  			Labels:    rec.Labels,
   245  		}
   246  		result.Records[i] = apiRec
   247  	}
   248  	return result
   249  }