github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/state/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  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"code.google.com/p/go.net/websocket"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/utils/tailer"
    20  	"launchpad.net/tomb"
    21  
    22  	"github.com/juju/juju/state/api/params"
    23  )
    24  
    25  // debugLogHandler takes requests to watch the debug log.
    26  type debugLogHandler struct {
    27  	httpHandler
    28  	logDir string
    29  }
    30  
    31  var maxLinesReached = fmt.Errorf("max lines reached")
    32  
    33  // ServeHTTP will serve up connections as a websocket.
    34  // Args for the HTTP request are as follows:
    35  //   includeEntity -> []string - lists entity tags to include in the response
    36  //      - tags may finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2
    37  //      - if none are set, then all lines are considered included
    38  //   includeModule -> []string - lists logging modules to include in the response
    39  //      - if none are set, then all lines are considered included
    40  //   excludeEntity -> []string - lists entity tags to exclude from the response
    41  //      - as with include, it may finish with a '*'
    42  //   excludeModule -> []string - lists logging modules to exclude from the response
    43  //   limit -> uint - show *at most* this many lines
    44  //   backlog -> uint
    45  //      - go back this many lines from the end before starting to filter
    46  //      - has no meaning if 'replay' is true
    47  //   level -> string one of [TRACE, DEBUG, INFO, WARNING, ERROR]
    48  //   replay -> string - one of [true, false], if true, start the file from the start
    49  func (h *debugLogHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    50  	server := websocket.Server{
    51  		Handler: func(socket *websocket.Conn) {
    52  			logger.Infof("debug log handler starting")
    53  			if err := h.authenticate(req); err != nil {
    54  				h.sendError(socket, fmt.Errorf("auth failed: %v", err))
    55  				socket.Close()
    56  				return
    57  			}
    58  			if err := h.validateEnvironUUID(req); err != nil {
    59  				h.sendError(socket, err)
    60  				socket.Close()
    61  				return
    62  			}
    63  			stream, err := newLogStream(req.URL.Query())
    64  			if err != nil {
    65  				h.sendError(socket, err)
    66  				socket.Close()
    67  				return
    68  			}
    69  			// Open log file.
    70  			logLocation := filepath.Join(h.logDir, "all-machines.log")
    71  			logFile, err := os.Open(logLocation)
    72  			if err != nil {
    73  				h.sendError(socket, fmt.Errorf("cannot open log file: %v", err))
    74  				socket.Close()
    75  				return
    76  			}
    77  			defer logFile.Close()
    78  			if err := stream.positionLogFile(logFile); err != nil {
    79  				h.sendError(socket, fmt.Errorf("cannot position log file: %v", err))
    80  				socket.Close()
    81  				return
    82  			}
    83  
    84  			// If we get to here, no more errors to report, so we report a nil
    85  			// error.  This way the first line of the socket is always a json
    86  			// formatted simple error.
    87  			if err := h.sendError(socket, nil); err != nil {
    88  				logger.Errorf("could not send good log stream start")
    89  				socket.Close()
    90  				return
    91  			}
    92  
    93  			stream.start(logFile, socket)
    94  			go func() {
    95  				defer stream.tomb.Done()
    96  				defer socket.Close()
    97  				stream.tomb.Kill(stream.loop())
    98  			}()
    99  			if err := stream.tomb.Wait(); err != nil {
   100  				if err != maxLinesReached {
   101  					logger.Errorf("debug-log handler error: %v", err)
   102  				}
   103  			}
   104  		}}
   105  	server.ServeHTTP(w, req)
   106  }
   107  
   108  func newLogStream(queryMap url.Values) (*logStream, error) {
   109  	maxLines := uint(0)
   110  	if value := queryMap.Get("maxLines"); value != "" {
   111  		num, err := strconv.ParseUint(value, 10, 64)
   112  		if err != nil {
   113  			return nil, fmt.Errorf("maxLines value %q is not a valid unsigned number", value)
   114  		}
   115  		maxLines = uint(num)
   116  	}
   117  
   118  	fromTheStart := false
   119  	if value := queryMap.Get("replay"); value != "" {
   120  		replay, err := strconv.ParseBool(value)
   121  		if err != nil {
   122  			return nil, fmt.Errorf("replay value %q is not a valid boolean", value)
   123  		}
   124  		fromTheStart = replay
   125  	}
   126  
   127  	backlog := uint(0)
   128  	if value := queryMap.Get("backlog"); value != "" {
   129  		num, err := strconv.ParseUint(value, 10, 64)
   130  		if err != nil {
   131  			return nil, fmt.Errorf("backlog value %q is not a valid unsigned number", value)
   132  		}
   133  		backlog = uint(num)
   134  	}
   135  
   136  	level := loggo.UNSPECIFIED
   137  	if value := queryMap.Get("level"); value != "" {
   138  		var ok bool
   139  		level, ok = loggo.ParseLevel(value)
   140  		if !ok || level < loggo.TRACE || level > loggo.ERROR {
   141  			return nil, fmt.Errorf("level value %q is not one of %q, %q, %q, %q, %q",
   142  				value, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR)
   143  		}
   144  	}
   145  
   146  	return &logStream{
   147  		includeEntity: queryMap["includeEntity"],
   148  		includeModule: queryMap["includeModule"],
   149  		excludeEntity: queryMap["excludeEntity"],
   150  		excludeModule: queryMap["excludeModule"],
   151  		maxLines:      maxLines,
   152  		fromTheStart:  fromTheStart,
   153  		backlog:       backlog,
   154  		filterLevel:   level,
   155  	}, nil
   156  }
   157  
   158  // sendError sends a JSON-encoded error response.
   159  func (h *debugLogHandler) sendError(w io.Writer, err error) error {
   160  	response := &params.ErrorResult{}
   161  	if err != nil {
   162  		response.Error = &params.Error{Message: fmt.Sprint(err)}
   163  	}
   164  	message, err := json.Marshal(response)
   165  	if err != nil {
   166  		// If we are having trouble marshalling the error, we are in big trouble.
   167  		logger.Errorf("failure to marshal SimpleError: %v", err)
   168  		return err
   169  	}
   170  	message = append(message, []byte("\n")...)
   171  	_, err = w.Write(message)
   172  	return err
   173  }
   174  
   175  type logLine struct {
   176  	line   string
   177  	agent  string
   178  	level  loggo.Level
   179  	module string
   180  }
   181  
   182  func parseLogLine(line string) *logLine {
   183  	const (
   184  		agentField  = 0
   185  		levelField  = 3
   186  		moduleField = 4
   187  	)
   188  	fields := strings.Fields(line)
   189  	result := &logLine{
   190  		line: line,
   191  	}
   192  	if len(fields) > agentField {
   193  		agent := fields[agentField]
   194  		if strings.HasSuffix(agent, ":") {
   195  			result.agent = agent[:len(agent)-1]
   196  		}
   197  	}
   198  	if len(fields) > moduleField {
   199  		if level, valid := loggo.ParseLevel(fields[levelField]); valid {
   200  			result.level = level
   201  			result.module = fields[moduleField]
   202  		}
   203  	}
   204  
   205  	return result
   206  }
   207  
   208  // logStream runs the tailer to read a log file and stream
   209  // it via a web socket.
   210  type logStream struct {
   211  	tomb          tomb.Tomb
   212  	logTailer     *tailer.Tailer
   213  	filterLevel   loggo.Level
   214  	includeEntity []string
   215  	includeModule []string
   216  	excludeEntity []string
   217  	excludeModule []string
   218  	backlog       uint
   219  	maxLines      uint
   220  	lineCount     uint
   221  	fromTheStart  bool
   222  }
   223  
   224  // positionLogFile will update the internal read position of the logFile to be
   225  // at the end of the file or somewhere in the middle if backlog has been specified.
   226  func (stream *logStream) positionLogFile(logFile io.ReadSeeker) error {
   227  	// Seek to the end, or lines back from the end if we need to.
   228  	if !stream.fromTheStart {
   229  		return tailer.SeekLastLines(logFile, stream.backlog, stream.filterLine)
   230  	}
   231  	return nil
   232  }
   233  
   234  // start the tailer listening to the logFile, and sending the matching
   235  // lines to the writer.
   236  func (stream *logStream) start(logFile io.ReadSeeker, writer io.Writer) {
   237  	stream.logTailer = tailer.NewTailer(logFile, writer, stream.countedFilterLine)
   238  }
   239  
   240  // loop starts the tailer with the log file and the web socket.
   241  func (stream *logStream) loop() error {
   242  	select {
   243  	case <-stream.logTailer.Dead():
   244  		return stream.logTailer.Err()
   245  	case <-stream.tomb.Dying():
   246  		stream.logTailer.Stop()
   247  	}
   248  	return nil
   249  }
   250  
   251  // filterLine checks the received line for one of the confgured tags.
   252  func (stream *logStream) filterLine(line []byte) bool {
   253  	log := parseLogLine(string(line))
   254  	return stream.checkIncludeEntity(log) &&
   255  		stream.checkIncludeModule(log) &&
   256  		!stream.exclude(log) &&
   257  		stream.checkLevel(log)
   258  }
   259  
   260  // countedFilterLine checks the received line for one of the confgured tags,
   261  // and also checks to make sure the stream doesn't send more than the
   262  // specified number of lines.
   263  func (stream *logStream) countedFilterLine(line []byte) bool {
   264  	result := stream.filterLine(line)
   265  	if result && stream.maxLines > 0 {
   266  		stream.lineCount++
   267  		result = stream.lineCount <= stream.maxLines
   268  		if stream.lineCount == stream.maxLines {
   269  			stream.tomb.Kill(maxLinesReached)
   270  		}
   271  	}
   272  	return result
   273  }
   274  
   275  func (stream *logStream) checkIncludeEntity(line *logLine) bool {
   276  	if len(stream.includeEntity) == 0 {
   277  		return true
   278  	}
   279  	for _, value := range stream.includeEntity {
   280  		// special handling, if ends with '*', check prefix
   281  		if strings.HasSuffix(value, "*") {
   282  			if strings.HasPrefix(line.agent, value[:len(value)-1]) {
   283  				return true
   284  			}
   285  		} else if line.agent == value {
   286  			return true
   287  		}
   288  	}
   289  	return false
   290  }
   291  
   292  func (stream *logStream) checkIncludeModule(line *logLine) bool {
   293  	if len(stream.includeModule) == 0 {
   294  		return true
   295  	}
   296  	for _, value := range stream.includeModule {
   297  		if strings.HasPrefix(line.module, value) {
   298  			return true
   299  		}
   300  	}
   301  	return false
   302  }
   303  
   304  func (stream *logStream) exclude(line *logLine) bool {
   305  	for _, value := range stream.excludeEntity {
   306  		// special handling, if ends with '*', check prefix
   307  		if strings.HasSuffix(value, "*") {
   308  			if strings.HasPrefix(line.agent, value[:len(value)-1]) {
   309  				return true
   310  			}
   311  		} else if line.agent == value {
   312  			return true
   313  		}
   314  	}
   315  	for _, value := range stream.excludeModule {
   316  		if strings.HasPrefix(line.module, value) {
   317  			return true
   318  		}
   319  	}
   320  	return false
   321  }
   322  
   323  func (stream *logStream) checkLevel(line *logLine) bool {
   324  	return line.level >= stream.filterLevel
   325  }