github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/daemon/logger/loggerutils/logfile.go (about) 1 package loggerutils // import "github.com/docker/docker/daemon/logger/loggerutils" 2 3 import ( 4 "compress/gzip" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/fs" 10 "math" 11 "os" 12 "strconv" 13 "sync" 14 "time" 15 16 "github.com/docker/docker/daemon/logger" 17 "github.com/docker/docker/pkg/pools" 18 "github.com/pkg/errors" 19 "github.com/sirupsen/logrus" 20 ) 21 22 // rotateFileMetadata is a metadata of the gzip header of the compressed log file 23 type rotateFileMetadata struct { 24 LastTime time.Time `json:"lastTime,omitempty"` 25 } 26 27 // LogFile is Logger implementation for default Docker logging. 28 type LogFile struct { 29 mu sync.Mutex // protects the logfile access 30 closed chan struct{} 31 rotateMu sync.Mutex // blocks the next rotation until the current rotation is completed 32 // Lock out readers while performing a non-atomic sequence of filesystem 33 // operations (RLock: open, Lock: rename, delete). 34 // 35 // fsopMu should be locked for writing only while holding rotateMu. 36 fsopMu sync.RWMutex 37 38 // Logger configuration 39 40 capacity int64 // maximum size of each file 41 maxFiles int // maximum number of files 42 compress bool // whether old versions of log files are compressed 43 perms os.FileMode 44 45 // Log file codec 46 47 createDecoder MakeDecoderFn 48 getTailReader GetTailReaderFunc 49 50 // Log reader state in a 1-buffered channel. 51 // 52 // Share memory by communicating: receive to acquire, send to release. 53 // The state struct is passed around by value so that use-after-send 54 // bugs cannot escalate to data races. 55 // 56 // A method which receives the state value takes ownership of it. The 57 // owner is responsible for either passing ownership along or sending 58 // the state back to the channel. By convention, the semantics of 59 // passing along ownership is expressed with function argument types. 60 // Methods which take a pointer *logReadState argument borrow the state, 61 // analogous to functions which require a lock to be held when calling. 62 // The caller retains ownership. Calling a method which which takes a 63 // value logFileState argument gives ownership to the callee. 64 read chan logReadState 65 66 decompress *sharedTempFileConverter 67 68 pos logPos // Current log file write position. 69 f *os.File // Current log file for writing. 70 lastTimestamp time.Time // timestamp of the last log 71 } 72 73 type logPos struct { 74 // Size of the current file. 75 size int64 76 // File rotation sequence number (modulo 2**16). 77 rotation uint16 78 } 79 80 type logReadState struct { 81 // Current log file position. 82 pos logPos 83 // Wait list to be notified of the value of pos next time it changes. 84 wait []chan<- logPos 85 } 86 87 // MakeDecoderFn creates a decoder 88 type MakeDecoderFn func(rdr io.Reader) Decoder 89 90 // Decoder is for reading logs 91 // It is created by the log reader by calling the `MakeDecoderFunc` 92 type Decoder interface { 93 // Reset resets the decoder 94 // Reset is called for certain events, such as log rotations 95 Reset(io.Reader) 96 // Decode decodes the next log messeage from the stream 97 Decode() (*logger.Message, error) 98 // Close signals to the decoder that it can release whatever resources it was using. 99 Close() 100 } 101 102 // SizeReaderAt defines a ReaderAt that also reports its size. 103 // This is used for tailing log files. 104 type SizeReaderAt interface { 105 io.Reader 106 io.ReaderAt 107 Size() int64 108 } 109 110 type readAtCloser interface { 111 io.ReaderAt 112 io.Closer 113 } 114 115 // GetTailReaderFunc is used to truncate a reader to only read as much as is required 116 // in order to get the passed in number of log lines. 117 // It returns the sectioned reader, the number of lines that the section reader 118 // contains, and any error that occurs. 119 type GetTailReaderFunc func(ctx context.Context, f SizeReaderAt, nLogLines int) (rdr io.Reader, nLines int, err error) 120 121 // NewLogFile creates new LogFile 122 func NewLogFile(logPath string, capacity int64, maxFiles int, compress bool, decodeFunc MakeDecoderFn, perms os.FileMode, getTailReader GetTailReaderFunc) (*LogFile, error) { 123 log, err := openFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, perms) 124 if err != nil { 125 return nil, err 126 } 127 128 size, err := log.Seek(0, io.SeekEnd) 129 if err != nil { 130 return nil, err 131 } 132 133 pos := logPos{ 134 size: size, 135 // Force a wraparound on first rotation to shake out any 136 // modular-arithmetic bugs. 137 rotation: math.MaxUint16, 138 } 139 st := make(chan logReadState, 1) 140 st <- logReadState{pos: pos} 141 142 return &LogFile{ 143 f: log, 144 read: st, 145 pos: pos, 146 closed: make(chan struct{}), 147 capacity: capacity, 148 maxFiles: maxFiles, 149 compress: compress, 150 decompress: newSharedTempFileConverter(decompress), 151 createDecoder: decodeFunc, 152 perms: perms, 153 getTailReader: getTailReader, 154 }, nil 155 } 156 157 // WriteLogEntry writes the provided log message to the current log file. 158 // This may trigger a rotation event if the max file/capacity limits are hit. 159 func (w *LogFile) WriteLogEntry(timestamp time.Time, marshalled []byte) error { 160 select { 161 case <-w.closed: 162 return errors.New("cannot write because the output file was closed") 163 default: 164 } 165 w.mu.Lock() 166 defer w.mu.Unlock() 167 168 // Are we due for a rotation? 169 if w.capacity != -1 && w.pos.size >= w.capacity { 170 if err := w.rotate(); err != nil { 171 return errors.Wrap(err, "error rotating log file") 172 } 173 } 174 175 n, err := w.f.Write(marshalled) 176 if err != nil { 177 return errors.Wrap(err, "error writing log entry") 178 } 179 w.pos.size += int64(n) 180 w.lastTimestamp = timestamp 181 182 // Notify any waiting readers that there is a new log entry to read. 183 st := <-w.read 184 defer func() { w.read <- st }() 185 st.pos = w.pos 186 187 for _, c := range st.wait { 188 c <- st.pos 189 } 190 // Optimization: retain the backing array to save a heap allocation next 191 // time a reader appends to the list. 192 if st.wait != nil { 193 st.wait = st.wait[:0] 194 } 195 return nil 196 } 197 198 func (w *LogFile) rotate() (retErr error) { 199 w.rotateMu.Lock() 200 noCompress := w.maxFiles <= 1 || !w.compress 201 defer func() { 202 // If we aren't going to run the goroutine to compress the log file, then we need to unlock in this function. 203 // Otherwise the lock will be released in the goroutine that handles compression. 204 if retErr != nil || noCompress { 205 w.rotateMu.Unlock() 206 } 207 }() 208 209 fname := w.f.Name() 210 if err := w.f.Close(); err != nil { 211 // if there was an error during a prior rotate, the file could already be closed 212 if !errors.Is(err, fs.ErrClosed) { 213 return errors.Wrap(err, "error closing file") 214 } 215 } 216 217 file, err := func() (*os.File, error) { 218 w.fsopMu.Lock() 219 defer w.fsopMu.Unlock() 220 221 if err := rotate(fname, w.maxFiles, w.compress); err != nil { 222 logrus.WithError(err).Warn("Error rotating log file, log data may have been lost") 223 } else { 224 // We may have readers working their way through the 225 // current log file so we can't truncate it. We need to 226 // start writing new logs to an empty file with the same 227 // name as the current one so we need to rotate the 228 // current file out of the way. 229 if w.maxFiles < 2 { 230 if err := unlink(fname); err != nil && !errors.Is(err, fs.ErrNotExist) { 231 logrus.WithError(err).Error("Error unlinking current log file") 232 } 233 } else { 234 if err := os.Rename(fname, fname+".1"); err != nil && !errors.Is(err, fs.ErrNotExist) { 235 logrus.WithError(err).Error("Error renaming current log file") 236 } 237 } 238 } 239 240 // Notwithstanding the above, open with the truncate flag anyway 241 // in case rotation didn't work out as planned. 242 return openFile(fname, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, w.perms) 243 }() 244 if err != nil { 245 return err 246 } 247 w.f = file 248 w.pos = logPos{rotation: w.pos.rotation + 1} 249 250 if noCompress { 251 return nil 252 } 253 254 ts := w.lastTimestamp 255 go func() { 256 defer w.rotateMu.Unlock() 257 // No need to hold fsopMu as at no point will the filesystem be 258 // in a state which would cause problems for readers. Opening 259 // the uncompressed file is tried first, falling back to the 260 // compressed one. compressFile only deletes the uncompressed 261 // file once the compressed one is fully written out, so at no 262 // point during the compression process will a reader fail to 263 // open a complete copy of the file. 264 if err := compressFile(fname+".1", ts); err != nil { 265 logrus.WithError(err).Error("Error compressing log file after rotation") 266 } 267 }() 268 269 return nil 270 } 271 272 func rotate(name string, maxFiles int, compress bool) error { 273 if maxFiles < 2 { 274 return nil 275 } 276 277 var extension string 278 if compress { 279 extension = ".gz" 280 } 281 282 lastFile := fmt.Sprintf("%s.%d%s", name, maxFiles-1, extension) 283 err := unlink(lastFile) 284 if err != nil && !errors.Is(err, fs.ErrNotExist) { 285 return errors.Wrap(err, "error removing oldest log file") 286 } 287 288 for i := maxFiles - 1; i > 1; i-- { 289 toPath := name + "." + strconv.Itoa(i) + extension 290 fromPath := name + "." + strconv.Itoa(i-1) + extension 291 err := os.Rename(fromPath, toPath) 292 logrus.WithError(err).WithField("source", fromPath).WithField("target", toPath).Trace("Rotating log file") 293 if err != nil && !errors.Is(err, fs.ErrNotExist) { 294 return err 295 } 296 } 297 298 return nil 299 } 300 301 func compressFile(fileName string, lastTimestamp time.Time) (retErr error) { 302 file, err := open(fileName) 303 if err != nil { 304 if errors.Is(err, fs.ErrNotExist) { 305 logrus.WithField("file", fileName).WithError(err).Debug("Could not open log file to compress") 306 return nil 307 } 308 return errors.Wrap(err, "failed to open log file") 309 } 310 defer func() { 311 file.Close() 312 if retErr == nil { 313 err := unlink(fileName) 314 if err != nil && !errors.Is(err, fs.ErrNotExist) { 315 retErr = errors.Wrap(err, "failed to remove source log file") 316 } 317 } 318 }() 319 320 outFile, err := openFile(fileName+".gz", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0640) 321 if err != nil { 322 return errors.Wrap(err, "failed to open or create gzip log file") 323 } 324 defer func() { 325 outFile.Close() 326 if retErr != nil { 327 if err := unlink(fileName + ".gz"); err != nil && !errors.Is(err, fs.ErrNotExist) { 328 logrus.WithError(err).Error("Error cleaning up after failed log compression") 329 } 330 } 331 }() 332 333 compressWriter := gzip.NewWriter(outFile) 334 defer compressWriter.Close() 335 336 // Add the last log entry timestamp to the gzip header 337 extra := rotateFileMetadata{} 338 extra.LastTime = lastTimestamp 339 compressWriter.Header.Extra, err = json.Marshal(&extra) 340 if err != nil { 341 // Here log the error only and don't return since this is just an optimization. 342 logrus.Warningf("Failed to marshal gzip header as JSON: %v", err) 343 } 344 345 _, err = pools.Copy(compressWriter, file) 346 if err != nil { 347 return errors.Wrapf(err, "error compressing log file %s", fileName) 348 } 349 350 return nil 351 } 352 353 // MaxFiles return maximum number of files 354 func (w *LogFile) MaxFiles() int { 355 return w.maxFiles 356 } 357 358 // Close closes underlying file and signals all readers to stop. 359 func (w *LogFile) Close() error { 360 w.mu.Lock() 361 defer w.mu.Unlock() 362 select { 363 case <-w.closed: 364 return nil 365 default: 366 } 367 if err := w.f.Close(); err != nil && !errors.Is(err, fs.ErrClosed) { 368 return err 369 } 370 close(w.closed) 371 // Wait until any in-progress rotation is complete. 372 w.rotateMu.Lock() 373 w.rotateMu.Unlock() //nolint:staticcheck 374 return nil 375 } 376 377 // ReadLogs decodes entries from log files. 378 // 379 // It is the caller's responsibility to call ConsumerGone on the LogWatcher. 380 func (w *LogFile) ReadLogs(config logger.ReadConfig) *logger.LogWatcher { 381 watcher := logger.NewLogWatcher() 382 // Lock out filesystem operations so that we can capture the read 383 // position and atomically open the corresponding log file, without the 384 // file getting rotated out from under us. 385 w.fsopMu.RLock() 386 // Capture the read position synchronously to ensure that we start 387 // following from the last entry logged before ReadLogs was called, 388 // which is required for flake-free unit testing. 389 st := <-w.read 390 pos := st.pos 391 w.read <- st 392 go w.readLogsLocked(pos, config, watcher) 393 return watcher 394 } 395 396 // readLogsLocked is the bulk of the implementation of ReadLogs. 397 // 398 // w.fsopMu must be locked for reading when calling this method. 399 // w.fsopMu.RUnlock() is called before returning. 400 func (w *LogFile) readLogsLocked(currentPos logPos, config logger.ReadConfig, watcher *logger.LogWatcher) { 401 defer close(watcher.Msg) 402 403 currentFile, err := open(w.f.Name()) 404 if err != nil { 405 watcher.Err <- err 406 return 407 } 408 defer currentFile.Close() 409 410 dec := w.createDecoder(nil) 411 defer dec.Close() 412 413 currentChunk := io.NewSectionReader(currentFile, 0, currentPos.size) 414 fwd := newForwarder(config) 415 416 if config.Tail != 0 { 417 // TODO(@cpuguy83): Instead of opening every file, only get the files which 418 // are needed to tail. 419 // This is especially costly when compression is enabled. 420 files, err := w.openRotatedFiles(config) 421 if err != nil { 422 watcher.Err <- err 423 return 424 } 425 426 closeFiles := func() { 427 for _, f := range files { 428 f.Close() 429 } 430 } 431 432 readers := make([]SizeReaderAt, 0, len(files)+1) 433 for _, f := range files { 434 switch ff := f.(type) { 435 case SizeReaderAt: 436 readers = append(readers, ff) 437 case interface{ Stat() (fs.FileInfo, error) }: 438 stat, err := ff.Stat() 439 if err != nil { 440 watcher.Err <- errors.Wrap(err, "error reading size of rotated file") 441 closeFiles() 442 return 443 } 444 readers = append(readers, io.NewSectionReader(f, 0, stat.Size())) 445 default: 446 panic(fmt.Errorf("rotated file value %#v (%[1]T) has neither Size() nor Stat() methods", f)) 447 } 448 } 449 if currentChunk.Size() > 0 { 450 readers = append(readers, currentChunk) 451 } 452 453 ok := tailFiles(readers, watcher, dec, w.getTailReader, config.Tail, fwd) 454 closeFiles() 455 if !ok { 456 return 457 } 458 } else { 459 w.fsopMu.RUnlock() 460 } 461 462 if !config.Follow { 463 return 464 } 465 466 (&follow{ 467 LogFile: w, 468 Watcher: watcher, 469 Decoder: dec, 470 Forwarder: fwd, 471 }).Do(currentFile, currentPos) 472 } 473 474 // openRotatedFiles returns a slice of files open for reading, in order from 475 // oldest to newest, and calls w.fsopMu.RUnlock() before returning. 476 // 477 // This method must only be called with w.fsopMu locked for reading. 478 func (w *LogFile) openRotatedFiles(config logger.ReadConfig) (files []readAtCloser, err error) { 479 type rotatedFile struct { 480 f *os.File 481 compressed bool 482 } 483 484 var q []rotatedFile 485 defer func() { 486 if err != nil { 487 for _, qq := range q { 488 qq.f.Close() 489 } 490 for _, f := range files { 491 f.Close() 492 } 493 } 494 }() 495 496 q, err = func() (q []rotatedFile, err error) { 497 defer w.fsopMu.RUnlock() 498 499 q = make([]rotatedFile, 0, w.maxFiles) 500 for i := w.maxFiles; i > 1; i-- { 501 var f rotatedFile 502 f.f, err = open(fmt.Sprintf("%s.%d", w.f.Name(), i-1)) 503 if err != nil { 504 if !errors.Is(err, fs.ErrNotExist) { 505 return nil, errors.Wrap(err, "error opening rotated log file") 506 } 507 f.compressed = true 508 f.f, err = open(fmt.Sprintf("%s.%d.gz", w.f.Name(), i-1)) 509 if err != nil { 510 if !errors.Is(err, fs.ErrNotExist) { 511 return nil, errors.Wrap(err, "error opening file for decompression") 512 } 513 continue 514 } 515 } 516 q = append(q, f) 517 } 518 return q, nil 519 }() 520 if err != nil { 521 return nil, err 522 } 523 524 for len(q) > 0 { 525 qq := q[0] 526 q = q[1:] 527 if qq.compressed { 528 defer qq.f.Close() 529 f, err := w.maybeDecompressFile(qq.f, config) 530 if err != nil { 531 return nil, err 532 } 533 if f != nil { 534 // The log before `config.Since` does not need to read 535 files = append(files, f) 536 } 537 } else { 538 files = append(files, qq.f) 539 } 540 } 541 return files, nil 542 } 543 544 func (w *LogFile) maybeDecompressFile(cf *os.File, config logger.ReadConfig) (readAtCloser, error) { 545 rc, err := gzip.NewReader(cf) 546 if err != nil { 547 return nil, errors.Wrap(err, "error making gzip reader for compressed log file") 548 } 549 defer rc.Close() 550 551 // Extract the last log entry timestramp from the gzip header 552 extra := &rotateFileMetadata{} 553 err = json.Unmarshal(rc.Header.Extra, extra) 554 if err == nil && !extra.LastTime.IsZero() && extra.LastTime.Before(config.Since) { 555 return nil, nil 556 } 557 tmpf, err := w.decompress.Do(cf) 558 return tmpf, errors.Wrap(err, "error decompressing log file") 559 } 560 561 func decompress(dst io.WriteSeeker, src io.ReadSeeker) error { 562 if _, err := src.Seek(0, io.SeekStart); err != nil { 563 return err 564 } 565 rc, err := gzip.NewReader(src) 566 if err != nil { 567 return err 568 } 569 _, err = pools.Copy(dst, rc) 570 if err != nil { 571 return err 572 } 573 return rc.Close() 574 } 575 576 func tailFiles(files []SizeReaderAt, watcher *logger.LogWatcher, dec Decoder, getTailReader GetTailReaderFunc, nLines int, fwd *forwarder) (cont bool) { 577 ctx, cancel := context.WithCancel(context.Background()) 578 defer cancel() 579 580 cont = true 581 // TODO(@cpuguy83): we should plumb a context through instead of dealing with `WatchClose()` here. 582 go func() { 583 select { 584 case <-ctx.Done(): 585 case <-watcher.WatchConsumerGone(): 586 cancel() 587 } 588 }() 589 590 readers := make([]io.Reader, 0, len(files)) 591 592 if nLines > 0 { 593 for i := len(files) - 1; i >= 0 && nLines > 0; i-- { 594 tail, n, err := getTailReader(ctx, files[i], nLines) 595 if err != nil { 596 watcher.Err <- errors.Wrap(err, "error finding file position to start log tailing") 597 return false 598 } 599 nLines -= n 600 readers = append([]io.Reader{tail}, readers...) 601 } 602 } else { 603 for _, r := range files { 604 readers = append(readers, r) 605 } 606 } 607 608 rdr := io.MultiReader(readers...) 609 dec.Reset(rdr) 610 return fwd.Do(watcher, dec) 611 } 612 613 type forwarder struct { 614 since, until time.Time 615 } 616 617 func newForwarder(config logger.ReadConfig) *forwarder { 618 return &forwarder{since: config.Since, until: config.Until} 619 } 620 621 // Do reads log messages from dec and sends the messages matching the filter 622 // conditions to watcher. Do returns cont=true iff it has read all messages from 623 // dec without encountering a message with a timestamp which is after the 624 // configured until time. 625 func (fwd *forwarder) Do(watcher *logger.LogWatcher, dec Decoder) (cont bool) { 626 for { 627 msg, err := dec.Decode() 628 if err != nil { 629 if errors.Is(err, io.EOF) { 630 return true 631 } 632 watcher.Err <- err 633 return false 634 } 635 if !fwd.since.IsZero() { 636 if msg.Timestamp.Before(fwd.since) { 637 continue 638 } 639 // We've found our first message with a timestamp >= since. As message 640 // timestamps might not be monotonic, we need to skip the since check for all 641 // subsequent messages so we do not filter out later messages which happen to 642 // have timestamps before since. 643 fwd.since = time.Time{} 644 } 645 if !fwd.until.IsZero() && msg.Timestamp.After(fwd.until) { 646 return false 647 } 648 select { 649 case <-watcher.WatchConsumerGone(): 650 return false 651 case watcher.Msg <- msg: 652 } 653 } 654 }