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