github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"strconv"
    12  	"syscall"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  	"golang.org/x/net/websocket"
    17  
    18  	"github.com/juju/juju/apiserver/common"
    19  	"github.com/juju/juju/apiserver/params"
    20  	"github.com/juju/juju/state"
    21  )
    22  
    23  // debugLogHandler takes requests to watch the debug log.
    24  //
    25  // It provides the underlying framework for the 2 debug-log
    26  // variants. The supplied handle func allows for varied handling of
    27  // requests.
    28  type debugLogHandler struct {
    29  	ctxt   httpContext
    30  	handle debugLogHandlerFunc
    31  }
    32  
    33  type debugLogHandlerFunc func(
    34  	state.LoggingState,
    35  	*debugLogParams,
    36  	debugLogSocket,
    37  	<-chan struct{},
    38  ) error
    39  
    40  func newDebugLogHandler(
    41  	ctxt httpContext,
    42  	handle debugLogHandlerFunc,
    43  ) *debugLogHandler {
    44  	return &debugLogHandler{
    45  		ctxt:   ctxt,
    46  		handle: handle,
    47  	}
    48  }
    49  
    50  // ServeHTTP will serve up connections as a websocket for the
    51  // debug-log API.
    52  //
    53  // Args for the HTTP request are as follows:
    54  //   includeEntity -> []string - lists entity tags to include in the response
    55  //      - tags may finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2
    56  //      - if none are set, then all lines are considered included
    57  //   includeModule -> []string - lists logging modules to include in the response
    58  //      - if none are set, then all lines are considered included
    59  //   excludeEntity -> []string - lists entity tags to exclude from the response
    60  //      - as with include, it may finish with a '*'
    61  //   excludeModule -> []string - lists logging modules to exclude from the response
    62  //   limit -> uint - show *at most* this many lines
    63  //   backlog -> uint
    64  //      - go back this many lines from the end before starting to filter
    65  //      - has no meaning if 'replay' is true
    66  //   level -> string one of [TRACE, DEBUG, INFO, WARNING, ERROR]
    67  //   replay -> string - one of [true, false], if true, start the file from the start
    68  //   noTail -> string - one of [true, false], if true, existing logs are sent back,
    69  //      - but the command does not wait for new ones.
    70  func (h *debugLogHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    71  	server := websocket.Server{
    72  		Handler: func(conn *websocket.Conn) {
    73  			socket := &debugLogSocketImpl{conn}
    74  			defer socket.Close()
    75  
    76  			logger.Infof("debug log handler starting")
    77  			// Validate before authenticate because the authentication is
    78  			// dependent on the state connection that is determined during the
    79  			// validation.
    80  			st, _, err := h.ctxt.stateForRequestAuthenticatedUser(req)
    81  			if err != nil {
    82  				socket.sendError(err)
    83  				return
    84  			}
    85  			params, err := readDebugLogParams(req.URL.Query())
    86  			if err != nil {
    87  				socket.sendError(err)
    88  				return
    89  			}
    90  
    91  			if err := h.handle(st, params, socket, h.ctxt.stop()); err != nil {
    92  				if isBrokenPipe(err) {
    93  					logger.Tracef("debug-log handler stopped (client disconnected)")
    94  				} else {
    95  					logger.Errorf("debug-log handler error: %v", err)
    96  				}
    97  			}
    98  		},
    99  	}
   100  	server.ServeHTTP(w, req)
   101  }
   102  
   103  func isBrokenPipe(err error) bool {
   104  	err = errors.Cause(err)
   105  	if opErr, ok := err.(*net.OpError); ok {
   106  		return opErr.Err == syscall.EPIPE
   107  	}
   108  	return false
   109  }
   110  
   111  // debugLogSocket describes the functionality required for the
   112  // debuglog handlers to send logs to the client.
   113  type debugLogSocket interface {
   114  	io.Writer
   115  
   116  	// sendOk sends a nil error response, indicating there were no errors.
   117  	sendOk()
   118  
   119  	// sendError sends a JSON-encoded error response.
   120  	sendError(err error)
   121  }
   122  
   123  // debugLogSocketImpl implements the debugLogSocket interface. It
   124  // wraps a websocket.Conn and provides a few debug-log specific helper
   125  // methods.
   126  type debugLogSocketImpl struct {
   127  	*websocket.Conn
   128  }
   129  
   130  // sendOk implements debugLogSocket.
   131  func (s *debugLogSocketImpl) sendOk() {
   132  	s.sendError(nil)
   133  }
   134  
   135  // sendError implements debugLogSocket.
   136  func (s *debugLogSocketImpl) sendError(err error) {
   137  	sendJSON(s.Conn, &params.ErrorResult{
   138  		Error: common.ServerError(err),
   139  	})
   140  }
   141  
   142  // debugLogParams contains the parsed debuglog API request parameters.
   143  type debugLogParams struct {
   144  	maxLines      uint
   145  	fromTheStart  bool
   146  	noTail        bool
   147  	backlog       uint
   148  	filterLevel   loggo.Level
   149  	includeEntity []string
   150  	excludeEntity []string
   151  	includeModule []string
   152  	excludeModule []string
   153  }
   154  
   155  func readDebugLogParams(queryMap url.Values) (*debugLogParams, error) {
   156  	params := new(debugLogParams)
   157  
   158  	if value := queryMap.Get("maxLines"); value != "" {
   159  		num, err := strconv.ParseUint(value, 10, 64)
   160  		if err != nil {
   161  			return nil, errors.Errorf("maxLines value %q is not a valid unsigned number", value)
   162  		}
   163  		params.maxLines = uint(num)
   164  	}
   165  
   166  	if value := queryMap.Get("replay"); value != "" {
   167  		replay, err := strconv.ParseBool(value)
   168  		if err != nil {
   169  			return nil, errors.Errorf("replay value %q is not a valid boolean", value)
   170  		}
   171  		params.fromTheStart = replay
   172  	}
   173  
   174  	if value := queryMap.Get("noTail"); value != "" {
   175  		noTail, err := strconv.ParseBool(value)
   176  		if err != nil {
   177  			return nil, errors.Errorf("noTail value %q is not a valid boolean", value)
   178  		}
   179  		params.noTail = noTail
   180  	}
   181  
   182  	if value := queryMap.Get("backlog"); value != "" {
   183  		num, err := strconv.ParseUint(value, 10, 64)
   184  		if err != nil {
   185  			return nil, errors.Errorf("backlog value %q is not a valid unsigned number", value)
   186  		}
   187  		params.backlog = uint(num)
   188  	}
   189  
   190  	if value := queryMap.Get("level"); value != "" {
   191  		var ok bool
   192  		level, ok := loggo.ParseLevel(value)
   193  		if !ok || level < loggo.TRACE || level > loggo.ERROR {
   194  			return nil, errors.Errorf("level value %q is not one of %q, %q, %q, %q, %q",
   195  				value, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR)
   196  		}
   197  		params.filterLevel = level
   198  	}
   199  
   200  	params.includeEntity = queryMap["includeEntity"]
   201  	params.excludeEntity = queryMap["excludeEntity"]
   202  	params.includeModule = queryMap["includeModule"]
   203  	params.excludeModule = queryMap["excludeModule"]
   204  
   205  	return params, nil
   206  }