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