github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/logsink.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver
     5  
     6  import (
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  	"github.com/juju/utils"
    17  	"github.com/juju/version"
    18  	"golang.org/x/net/websocket"
    19  	"gopkg.in/natefinch/lumberjack.v2"
    20  
    21  	"github.com/juju/juju/apiserver/common"
    22  	"github.com/juju/juju/apiserver/params"
    23  	"github.com/juju/juju/state"
    24  )
    25  
    26  func newLogSinkHandler(h httpContext, logDir string) http.Handler {
    27  
    28  	logPath := filepath.Join(logDir, "logsink.log")
    29  	if err := primeLogFile(logPath); err != nil {
    30  		// This isn't a fatal error so log and continue if priming
    31  		// fails.
    32  		logger.Errorf("Unable to prime %s (proceeding anyway): %v", logPath, err)
    33  	}
    34  
    35  	return &logSinkHandler{
    36  		ctxt: h,
    37  		fileLogger: &lumberjack.Logger{
    38  			Filename:   logPath,
    39  			MaxSize:    300, // MB
    40  			MaxBackups: 2,
    41  		},
    42  	}
    43  }
    44  
    45  // primeLogFile ensures the logsink log file is created with the
    46  // correct mode and ownership.
    47  func primeLogFile(path string) error {
    48  	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
    49  	if err != nil {
    50  		return errors.Trace(err)
    51  	}
    52  	f.Close()
    53  	err = utils.ChownPath(path, "syslog")
    54  	return errors.Trace(err)
    55  }
    56  
    57  type logSinkHandler struct {
    58  	ctxt       httpContext
    59  	fileLogger io.WriteCloser
    60  }
    61  
    62  // ServeHTTP implements the http.Handler interface.
    63  func (h *logSinkHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    64  	server := websocket.Server{
    65  		Handler: func(socket *websocket.Conn) {
    66  			defer socket.Close()
    67  
    68  			st, entity, err := h.ctxt.stateForRequestAuthenticatedAgent(req)
    69  			if err != nil {
    70  				h.sendError(socket, req, err)
    71  				return
    72  			}
    73  			tag := entity.Tag()
    74  
    75  			// Note that this endpoint is agent-only. Thus the only
    76  			// callers will necessarily provide their Juju version.
    77  			//
    78  			// This would be a problem if non-Juju clients (e.g. the
    79  			// GUI) could use this endpoint since we require that the
    80  			// *Juju* version be provided as part of the request. Any
    81  			// attempt to open this endpoint to broader access must
    82  			// address this caveat appropriately.
    83  			ver, err := jujuClientVersionFromReq(req)
    84  			if err != nil {
    85  				h.sendError(socket, req, err)
    86  				return
    87  			}
    88  
    89  			filePrefix := st.ModelUUID() + " " + tag.String() + ":"
    90  			dbLogger := state.NewDbLogger(st, tag, ver)
    91  			defer dbLogger.Close()
    92  
    93  			// If we get to here, no more errors to report, so we report a nil
    94  			// error.  This way the first line of the socket is always a json
    95  			// formatted simple error.
    96  			h.sendError(socket, req, nil)
    97  
    98  			logCh := h.receiveLogs(socket)
    99  			for {
   100  				select {
   101  				case <-h.ctxt.stop():
   102  					return
   103  				case m := <-logCh:
   104  					fileErr := h.logToFile(filePrefix, m)
   105  					if fileErr != nil {
   106  						logger.Errorf("logging to logsink.log failed: %v", fileErr)
   107  					}
   108  					level, _ := loggo.ParseLevel(m.Level)
   109  					dbErr := dbLogger.Log(m.Time, m.Module, m.Location, level, m.Message)
   110  					if dbErr != nil {
   111  						logger.Errorf("logging to DB failed: %v", err)
   112  					}
   113  					if fileErr != nil || dbErr != nil {
   114  						return
   115  					}
   116  				}
   117  			}
   118  		},
   119  	}
   120  	server.ServeHTTP(w, req)
   121  }
   122  
   123  func jujuClientVersionFromReq(req *http.Request) (version.Number, error) {
   124  	verStr := req.URL.Query().Get("jujuclientversion")
   125  	if verStr == "" {
   126  		return version.Zero, errors.New(`missing "jujuclientversion" in URL query`)
   127  	}
   128  	ver, err := version.Parse(verStr)
   129  	if err != nil {
   130  		return version.Zero, errors.Annotatef(err, "invalid jujuclientversion %q", verStr)
   131  	}
   132  	return ver, nil
   133  }
   134  
   135  func (h *logSinkHandler) receiveLogs(socket *websocket.Conn) <-chan params.LogRecord {
   136  	logCh := make(chan params.LogRecord)
   137  
   138  	go func() {
   139  		var m params.LogRecord
   140  		for {
   141  			// Receive() blocks until data arrives but will also be
   142  			// unblocked when the API handler calls socket.Close as it
   143  			// finishes.
   144  			if err := websocket.JSON.Receive(socket, &m); err != nil {
   145  				logger.Debugf("logsink receive error: %v", err)
   146  				return
   147  			}
   148  
   149  			// Send the log message.
   150  			select {
   151  			case <-h.ctxt.stop():
   152  				return
   153  			case logCh <- m:
   154  			}
   155  		}
   156  	}()
   157  
   158  	return logCh
   159  }
   160  
   161  // sendError sends a JSON-encoded error response.
   162  func (h *logSinkHandler) sendError(w io.Writer, req *http.Request, err error) {
   163  	if err != nil {
   164  		logger.Errorf("returning error from %s %s: %s", req.Method, req.URL.Path, errors.Details(err))
   165  	}
   166  	sendJSON(w, &params.ErrorResult{
   167  		Error: common.ServerError(err),
   168  	})
   169  }
   170  
   171  // logToFile writes a single log message to the logsink log file.
   172  func (h *logSinkHandler) logToFile(prefix string, m params.LogRecord) error {
   173  	_, err := h.fileLogger.Write([]byte(strings.Join([]string{
   174  		prefix,
   175  		m.Time.In(time.UTC).Format("2006-01-02 15:04:05"),
   176  		m.Level,
   177  		m.Module,
   178  		m.Location,
   179  		m.Message,
   180  	}, " ") + "\n"))
   181  	return err
   182  }