github.com/smithx10/nomad@v0.9.1-rc1/client/logmon/logmon.go (about) 1 package logmon 2 3 import ( 4 "fmt" 5 "io" 6 "strings" 7 "sync" 8 "time" 9 10 hclog "github.com/hashicorp/go-hclog" 11 "github.com/hashicorp/nomad/client/lib/fifo" 12 "github.com/hashicorp/nomad/client/logmon/logging" 13 ) 14 15 const ( 16 // processOutputCloseTolerance is the length of time we will wait for the 17 // launched process to close its stdout/stderr before we force close it. If 18 // data is written after this tolerance, we will not capture it. 19 processOutputCloseTolerance = 2 * time.Second 20 ) 21 22 type LogConfig struct { 23 // LogDir is the host path where logs are to be written to 24 LogDir string 25 26 // StdoutLogFile is the path relative to LogDir for stdout logging 27 StdoutLogFile string 28 29 // StderrLogFile is the path relative to LogDir for stderr logging 30 StderrLogFile string 31 32 // StdoutFifo is the path on the host to the stdout pipe 33 StdoutFifo string 34 35 // StderrFifo is the path on the host to the stderr pipe 36 StderrFifo string 37 38 // MaxFiles is the max rotated files allowed 39 MaxFiles int 40 41 // MaxFileSizeMB is the max log file size in MB allowed before rotation occures 42 MaxFileSizeMB int 43 } 44 45 type LogMon interface { 46 Start(*LogConfig) error 47 Stop() error 48 } 49 50 func NewLogMon(logger hclog.Logger) LogMon { 51 return &logmonImpl{ 52 logger: logger, 53 } 54 } 55 56 type logmonImpl struct { 57 logger hclog.Logger 58 tl *TaskLogger 59 lock sync.Mutex 60 } 61 62 func (l *logmonImpl) Start(cfg *LogConfig) error { 63 l.lock.Lock() 64 defer l.lock.Unlock() 65 66 // first time Start has been called 67 if l.tl == nil { 68 return l.start(cfg) 69 } 70 71 // stdout and stderr have been closed, this happens during task restarts 72 // restart the TaskLogger 73 if !l.tl.IsRunning() { 74 l.tl.Close() 75 return l.start(cfg) 76 } 77 78 // if the TaskLogger has been created and is currently running, noop 79 return nil 80 } 81 82 func (l *logmonImpl) start(cfg *LogConfig) error { 83 tl, err := NewTaskLogger(cfg, l.logger) 84 if err != nil { 85 return err 86 } 87 l.tl = tl 88 return nil 89 } 90 91 func (l *logmonImpl) Stop() error { 92 l.lock.Lock() 93 defer l.lock.Unlock() 94 if l.tl != nil { 95 l.tl.Close() 96 } 97 return nil 98 } 99 100 type TaskLogger struct { 101 config *LogConfig 102 103 // rotator for stdout 104 lro *logRotatorWrapper 105 106 // rotator for stderr 107 lre *logRotatorWrapper 108 } 109 110 // IsRunning will return true as long as one rotator wrapper is still running 111 func (tl *TaskLogger) IsRunning() bool { 112 if tl.lro != nil && tl.lro.isRunning() { 113 return true 114 } 115 if tl.lre != nil && tl.lre.isRunning() { 116 return true 117 } 118 119 return false 120 } 121 122 func (tl *TaskLogger) Close() { 123 var wg sync.WaitGroup 124 if tl.lro != nil { 125 wg.Add(1) 126 go func() { 127 tl.lro.Close() 128 wg.Done() 129 }() 130 } 131 if tl.lre != nil { 132 wg.Add(1) 133 go func() { 134 tl.lre.Close() 135 wg.Done() 136 }() 137 } 138 wg.Wait() 139 } 140 141 func NewTaskLogger(cfg *LogConfig, logger hclog.Logger) (*TaskLogger, error) { 142 tl := &TaskLogger{config: cfg} 143 144 logFileSize := int64(cfg.MaxFileSizeMB * 1024 * 1024) 145 lro, err := logging.NewFileRotator(cfg.LogDir, cfg.StdoutLogFile, 146 cfg.MaxFiles, logFileSize, logger) 147 if err != nil { 148 return nil, fmt.Errorf("failed to create stdout logfile for %q: %v", cfg.StdoutLogFile, err) 149 } 150 151 wrapperOut, err := newLogRotatorWrapper(cfg.StdoutFifo, logger, lro) 152 if err != nil { 153 return nil, err 154 } 155 156 tl.lro = wrapperOut 157 158 lre, err := logging.NewFileRotator(cfg.LogDir, cfg.StderrLogFile, 159 cfg.MaxFiles, logFileSize, logger) 160 if err != nil { 161 return nil, fmt.Errorf("failed to create stderr logfile for %q: %v", cfg.StderrLogFile, err) 162 } 163 164 wrapperErr, err := newLogRotatorWrapper(cfg.StderrFifo, logger, lre) 165 if err != nil { 166 return nil, err 167 } 168 169 tl.lre = wrapperErr 170 171 return tl, nil 172 173 } 174 175 // logRotatorWrapper wraps our log rotator and exposes a pipe that can feed the 176 // log rotator data. The processOutWriter should be attached to the process and 177 // data will be copied from the reader to the rotator. 178 type logRotatorWrapper struct { 179 fifoPath string 180 rotatorWriter *logging.FileRotator 181 hasFinishedCopied chan struct{} 182 logger hclog.Logger 183 184 processOutReader io.ReadCloser 185 openCompleted chan struct{} 186 } 187 188 // isRunning will return true until the reader is closed 189 func (l *logRotatorWrapper) isRunning() bool { 190 select { 191 case <-l.hasFinishedCopied: 192 return false 193 default: 194 return true 195 } 196 } 197 198 // newLogRotatorWrapper takes a rotator and returns a wrapper that has the 199 // processOutWriter to attach to the stdout or stderr of a process. 200 func newLogRotatorWrapper(path string, logger hclog.Logger, rotator *logging.FileRotator) (*logRotatorWrapper, error) { 201 logger.Info("opening fifo", "path", path) 202 fifoOpenFn, err := fifo.CreateAndRead(path) 203 if err != nil { 204 return nil, fmt.Errorf("failed to create fifo for extracting logs: %v", err) 205 } 206 207 wrap := &logRotatorWrapper{ 208 fifoPath: path, 209 rotatorWriter: rotator, 210 hasFinishedCopied: make(chan struct{}), 211 openCompleted: make(chan struct{}), 212 logger: logger, 213 } 214 wrap.start(fifoOpenFn) 215 return wrap, nil 216 } 217 218 // start starts a goroutine that copies from the pipe into the rotator. This is 219 // called by the constructor and not the user of the wrapper. 220 func (l *logRotatorWrapper) start(readerOpenFn func() (io.ReadCloser, error)) { 221 go func() { 222 defer close(l.hasFinishedCopied) 223 224 reader, err := readerOpenFn() 225 if err != nil { 226 close(l.openCompleted) 227 l.logger.Warn("failed to open log fifo", "error", err) 228 return 229 } 230 l.processOutReader = reader 231 close(l.openCompleted) 232 233 _, err = io.Copy(l.rotatorWriter, reader) 234 if err != nil { 235 l.logger.Warn("failed to read from log fifo", "error", err) 236 // Close reader to propagate io error across pipe. 237 // Note that this may block until the process exits on 238 // Windows due to 239 // https://github.com/PowerShell/PowerShell/issues/4254 240 // or similar issues. Since this is already running in 241 // a goroutine its safe to block until the process is 242 // force-killed. 243 reader.Close() 244 } 245 }() 246 return 247 } 248 249 // Close closes the rotator and the process writer to ensure that the Wait 250 // command exits. 251 func (l *logRotatorWrapper) Close() { 252 // Wait up to the close tolerance before we force close 253 select { 254 case <-l.hasFinishedCopied: 255 case <-time.After(processOutputCloseTolerance): 256 } 257 258 // Closing the read side of a pipe may block on Windows if the process 259 // is being debugged as in: 260 // https://github.com/PowerShell/PowerShell/issues/4254 261 // The pipe will be closed and cleaned up when the process exits. 262 closeDone := make(chan struct{}) 263 go func() { 264 defer close(closeDone) 265 266 // we must wait until reader is opened before we can close it, and cannot inteerrupt an in-flight open request 267 // The Close function uses processOutputCloseTolerance to protect against long running open called 268 // and then request will be interrupted and file will be closed on process shutdown 269 <-l.openCompleted 270 271 if l.processOutReader != nil { 272 err := l.processOutReader.Close() 273 if err != nil && !strings.Contains(err.Error(), "file already closed") { 274 l.logger.Warn("error closing read-side of process output pipe", "err", err) 275 } 276 } 277 278 }() 279 280 select { 281 case <-closeDone: 282 case <-time.After(processOutputCloseTolerance): 283 l.logger.Warn("timed out waiting for read-side of process output pipe to close") 284 } 285 286 l.rotatorWriter.Close() 287 return 288 }