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, ¶ms.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 }