github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/debuglog.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" 8 "net/http" 9 "net/url" 10 "os" 11 "strconv" 12 "syscall" 13 "time" 14 15 "github.com/juju/clock" 16 "github.com/juju/errors" 17 "github.com/juju/loggo" 18 19 "github.com/juju/juju/apiserver/authentication" 20 "github.com/juju/juju/apiserver/websocket" 21 "github.com/juju/juju/rpc/params" 22 "github.com/juju/juju/state" 23 ) 24 25 // debugLogHandler takes requests to watch the debug log. 26 // 27 // It provides the underlying framework for the 2 debug-log 28 // variants. The supplied handle func allows for varied handling of 29 // requests. 30 type debugLogHandler struct { 31 ctxt httpContext 32 authenticator authentication.HTTPAuthenticator 33 authorizer authentication.Authorizer 34 handle debugLogHandlerFunc 35 } 36 37 type debugLogHandlerFunc func( 38 clock.Clock, 39 time.Duration, 40 state.LogTailerState, 41 debugLogParams, 42 debugLogSocket, 43 <-chan struct{}, 44 ) error 45 46 func newDebugLogHandler( 47 ctxt httpContext, 48 authenticator authentication.HTTPAuthenticator, 49 authorizer authentication.Authorizer, 50 handle debugLogHandlerFunc, 51 ) *debugLogHandler { 52 return &debugLogHandler{ 53 ctxt: ctxt, 54 authenticator: authenticator, 55 authorizer: authorizer, 56 handle: handle, 57 } 58 } 59 60 // ServeHTTP will serve up connections as a websocket for the 61 // debug-log API. 62 // 63 // The authentication and authorization have to be done after the http request 64 // has been upgraded to a websocket as we may be sending back a discharge 65 // required error. This error contains the macaroon that needs to be 66 // discharged by the user. In order for this error to be deserialized 67 // correctly any auth failure will come back in the initial error that is 68 // returned over the websocket. This is consumed by the ConnectStream function 69 // on the apiclient. 70 // 71 // Args for the HTTP request are as follows: 72 // 73 // includeEntity -> []string - lists entity tags to include in the response 74 // - tags may finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2 75 // - if none are set, then all lines are considered included 76 // includeModule -> []string - lists logging modules to include in the response 77 // - if none are set, then all lines are considered included 78 // excludeEntity -> []string - lists entity tags to exclude from the response 79 // - as with include, it may finish with a '*' 80 // excludeModule -> []string - lists logging modules to exclude from the response 81 // limit -> uint - show *at most* this many lines 82 // backlog -> uint 83 // - go back this many lines from the end before starting to filter 84 // - has no meaning if 'replay' is true 85 // level -> string one of [TRACE, DEBUG, INFO, WARNING, ERROR] 86 // replay -> string - one of [true, false], if true, start the file from the start 87 // noTail -> string - one of [true, false], if true, existing logs are sent back, 88 // - but the command does not wait for new ones. 89 func (h *debugLogHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 90 handler := func(conn *websocket.Conn) { 91 socket := &debugLogSocketImpl{conn} 92 defer conn.Close() 93 // Authentication and authorization has to be done after the http 94 // connection has been upgraded to a websocket. 95 96 authInfo, err := h.authenticator.Authenticate(req) 97 if err != nil { 98 socket.sendError(errors.Annotate(err, "authentication failed")) 99 return 100 } 101 if err := h.authorizer.Authorize(authInfo); err != nil { 102 socket.sendError(errors.Annotate(err, "authorization failed")) 103 return 104 } 105 106 st, err := h.ctxt.stateForRequestUnauthenticated(req) 107 if err != nil { 108 socket.sendError(err) 109 return 110 } 111 defer st.Release() 112 113 params, err := readDebugLogParams(req.URL.Query()) 114 if err != nil { 115 socket.sendError(err) 116 return 117 } 118 119 clock := h.ctxt.srv.clock 120 maxDuration := h.ctxt.srv.shared.maxDebugLogDuration() 121 122 if err := h.handle(clock, maxDuration, st, params, socket, h.ctxt.stop()); err != nil { 123 if isBrokenPipe(err) { 124 logger.Tracef("debug-log handler stopped (client disconnected)") 125 } else { 126 logger.Errorf("debug-log handler error: %v", err) 127 } 128 } 129 } 130 websocket.Serve(w, req, handler) 131 } 132 133 func isBrokenPipe(err error) bool { 134 err = errors.Cause(err) 135 if opErr, ok := err.(*net.OpError); ok { 136 if sysCallErr, ok := opErr.Err.(*os.SyscallError); ok { 137 return sysCallErr.Err == syscall.EPIPE 138 } 139 return opErr.Err == syscall.EPIPE 140 } 141 return false 142 } 143 144 // debugLogSocket describes the functionality required for the 145 // debuglog handlers to send logs to the client. 146 type debugLogSocket interface { 147 // sendOk sends a nil error response, indicating there were no errors. 148 sendOk() 149 150 // sendError sends a JSON-encoded error response. 151 sendError(err error) 152 153 // sendLogRecord sends record JSON encoded. 154 sendLogRecord(record *params.LogMessage) error 155 } 156 157 // debugLogSocketImpl implements the debugLogSocket interface. It 158 // wraps a websocket.Conn and provides a few debug-log specific helper 159 // methods. 160 type debugLogSocketImpl struct { 161 conn *websocket.Conn 162 } 163 164 // sendOk implements debugLogSocket. 165 func (s *debugLogSocketImpl) sendOk() { 166 s.sendError(nil) 167 } 168 169 // sendError implements debugLogSocket. 170 func (s *debugLogSocketImpl) sendError(err error) { 171 if sendErr := s.conn.SendInitialErrorV0(err); sendErr != nil { 172 logger.Errorf("closing websocket, %v", err) 173 s.conn.Close() 174 return 175 } 176 } 177 178 func (s *debugLogSocketImpl) sendLogRecord(record *params.LogMessage) error { 179 return s.conn.WriteJSON(record) 180 } 181 182 // debugLogParams contains the parsed debuglog API request parameters. 183 type debugLogParams struct { 184 startTime time.Time 185 maxLines uint 186 fromTheStart bool 187 noTail bool 188 backlog uint 189 filterLevel loggo.Level 190 includeEntity []string 191 excludeEntity []string 192 includeModule []string 193 excludeModule []string 194 includeLabel []string 195 excludeLabel []string 196 } 197 198 func readDebugLogParams(queryMap url.Values) (debugLogParams, error) { 199 var params debugLogParams 200 201 if value := queryMap.Get("maxLines"); value != "" { 202 num, err := strconv.ParseUint(value, 10, 64) 203 if err != nil { 204 return params, errors.Errorf("maxLines value %q is not a valid unsigned number", value) 205 } 206 params.maxLines = uint(num) 207 } 208 209 if value := queryMap.Get("replay"); value != "" { 210 replay, err := strconv.ParseBool(value) 211 if err != nil { 212 return params, errors.Errorf("replay value %q is not a valid boolean", value) 213 } 214 params.fromTheStart = replay 215 } 216 217 if value := queryMap.Get("noTail"); value != "" { 218 noTail, err := strconv.ParseBool(value) 219 if err != nil { 220 return params, errors.Errorf("noTail value %q is not a valid boolean", value) 221 } 222 params.noTail = noTail 223 } 224 225 if value := queryMap.Get("backlog"); value != "" { 226 num, err := strconv.ParseUint(value, 10, 64) 227 if err != nil { 228 return params, errors.Errorf("backlog value %q is not a valid unsigned number", value) 229 } 230 params.backlog = uint(num) 231 } 232 233 if value := queryMap.Get("level"); value != "" { 234 var ok bool 235 level, ok := loggo.ParseLevel(value) 236 if !ok || level < loggo.TRACE || level > loggo.ERROR { 237 return params, errors.Errorf("level value %q is not one of %q, %q, %q, %q, %q", 238 value, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) 239 } 240 params.filterLevel = level 241 } 242 243 if value := queryMap.Get("startTime"); value != "" { 244 startTime, err := time.Parse(time.RFC3339Nano, value) 245 if err != nil { 246 return params, errors.Errorf("start time %q is not a valid time in RFC3339 format", value) 247 } 248 params.startTime = startTime 249 } 250 251 params.includeEntity = queryMap["includeEntity"] 252 params.excludeEntity = queryMap["excludeEntity"] 253 params.includeModule = queryMap["includeModule"] 254 params.excludeModule = queryMap["excludeModule"] 255 256 if label, ok := queryMap["includeLabel"]; ok { 257 params.includeLabel = label 258 } 259 if label, ok := queryMap["excludeLabel"]; ok { 260 params.excludeLabel = label 261 } 262 263 return params, nil 264 }