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  }