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 }