github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/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 "os" 10 "runtime" 11 "strconv" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/docker/docker/daemon/logger" 17 "github.com/docker/docker/pkg/filenotify" 18 "github.com/docker/docker/pkg/pools" 19 "github.com/docker/docker/pkg/pubsub" 20 "github.com/pkg/errors" 21 "github.com/sirupsen/logrus" 22 ) 23 24 const tmpLogfileSuffix = ".tmp" 25 26 // rotateFileMetadata is a metadata of the gzip header of the compressed log file 27 type rotateFileMetadata struct { 28 LastTime time.Time `json:"lastTime,omitempty"` 29 } 30 31 // refCounter is a counter of logfile being referenced 32 type refCounter struct { 33 mu sync.Mutex 34 counter map[string]int 35 } 36 37 // Reference increase the reference counter for specified logfile 38 func (rc *refCounter) GetReference(fileName string, openRefFile func(fileName string, exists bool) (*os.File, error)) (*os.File, error) { 39 rc.mu.Lock() 40 defer rc.mu.Unlock() 41 42 var ( 43 file *os.File 44 err error 45 ) 46 _, ok := rc.counter[fileName] 47 file, err = openRefFile(fileName, ok) 48 if err != nil { 49 return nil, err 50 } 51 52 if ok { 53 rc.counter[fileName]++ 54 } else if file != nil { 55 rc.counter[file.Name()] = 1 56 } 57 58 return file, nil 59 } 60 61 // Dereference reduce the reference counter for specified logfile 62 func (rc *refCounter) Dereference(fileName string) error { 63 rc.mu.Lock() 64 defer rc.mu.Unlock() 65 66 rc.counter[fileName]-- 67 if rc.counter[fileName] <= 0 { 68 delete(rc.counter, fileName) 69 err := os.Remove(fileName) 70 if err != nil && !os.IsNotExist(err) { 71 return err 72 } 73 } 74 return nil 75 } 76 77 // LogFile is Logger implementation for default Docker logging. 78 type LogFile struct { 79 mu sync.RWMutex // protects the logfile access 80 f *os.File // store for closing 81 closed bool 82 rotateMu sync.Mutex // blocks the next rotation until the current rotation is completed 83 capacity int64 // maximum size of each file 84 currentSize int64 // current size of the latest file 85 maxFiles int // maximum number of files 86 compress bool // whether old versions of log files are compressed 87 lastTimestamp time.Time // timestamp of the last log 88 filesRefCounter refCounter // keep reference-counted of decompressed files 89 notifyReaders *pubsub.Publisher 90 marshal logger.MarshalFunc 91 createDecoder MakeDecoderFn 92 getTailReader GetTailReaderFunc 93 perms os.FileMode 94 } 95 96 // MakeDecoderFn creates a decoder 97 type MakeDecoderFn func(rdr io.Reader) Decoder 98 99 // Decoder is for reading logs 100 // It is created by the log reader by calling the `MakeDecoderFunc` 101 type Decoder interface { 102 // Reset resets the decoder 103 // Reset is called for certain events, such as log rotations 104 Reset(io.Reader) 105 // Decode decodes the next log messeage from the stream 106 Decode() (*logger.Message, error) 107 // Close signals to the decoder that it can release whatever resources it was using. 108 Close() 109 } 110 111 // SizeReaderAt defines a ReaderAt that also reports its size. 112 // This is used for tailing log files. 113 type SizeReaderAt interface { 114 io.ReaderAt 115 Size() int64 116 } 117 118 // GetTailReaderFunc is used to truncate a reader to only read as much as is required 119 // in order to get the passed in number of log lines. 120 // It returns the sectioned reader, the number of lines that the section reader 121 // contains, and any error that occurs. 122 type GetTailReaderFunc func(ctx context.Context, f SizeReaderAt, nLogLines int) (rdr io.Reader, nLines int, err error) 123 124 // NewLogFile creates new LogFile 125 func NewLogFile(logPath string, capacity int64, maxFiles int, compress bool, marshaller logger.MarshalFunc, decodeFunc MakeDecoderFn, perms os.FileMode, getTailReader GetTailReaderFunc) (*LogFile, error) { 126 log, err := openFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, perms) 127 if err != nil { 128 return nil, err 129 } 130 131 size, err := log.Seek(0, io.SeekEnd) 132 if err != nil { 133 return nil, err 134 } 135 136 return &LogFile{ 137 f: log, 138 capacity: capacity, 139 currentSize: size, 140 maxFiles: maxFiles, 141 compress: compress, 142 filesRefCounter: refCounter{counter: make(map[string]int)}, 143 notifyReaders: pubsub.NewPublisher(0, 1), 144 marshal: marshaller, 145 createDecoder: decodeFunc, 146 perms: perms, 147 getTailReader: getTailReader, 148 }, nil 149 } 150 151 // WriteLogEntry writes the provided log message to the current log file. 152 // This may trigger a rotation event if the max file/capacity limits are hit. 153 func (w *LogFile) WriteLogEntry(msg *logger.Message) error { 154 b, err := w.marshal(msg) 155 if err != nil { 156 return errors.Wrap(err, "error marshalling log message") 157 } 158 159 logger.PutMessage(msg) 160 161 w.mu.Lock() 162 if w.closed { 163 w.mu.Unlock() 164 return errors.New("cannot write because the output file was closed") 165 } 166 167 if err := w.checkCapacityAndRotate(); err != nil { 168 w.mu.Unlock() 169 return errors.Wrap(err, "error rotating log file") 170 } 171 172 n, err := w.f.Write(b) 173 if err == nil { 174 w.currentSize += int64(n) 175 w.lastTimestamp = msg.Timestamp 176 } 177 178 w.mu.Unlock() 179 return errors.Wrap(err, "error writing log entry") 180 } 181 182 func (w *LogFile) checkCapacityAndRotate() (retErr error) { 183 if w.capacity == -1 { 184 return nil 185 } 186 if w.currentSize < w.capacity { 187 return nil 188 } 189 190 w.rotateMu.Lock() 191 noCompress := w.maxFiles <= 1 || !w.compress 192 defer func() { 193 // If we aren't going to run the goroutine to compress the log file, then we need to unlock in this function. 194 // Otherwise the lock will be released in the goroutine that handles compression. 195 if retErr != nil || noCompress { 196 w.rotateMu.Unlock() 197 } 198 }() 199 200 fname := w.f.Name() 201 if err := w.f.Close(); err != nil { 202 // if there was an error during a prior rotate, the file could already be closed 203 if !errors.Is(err, os.ErrClosed) { 204 return errors.Wrap(err, "error closing file") 205 } 206 } 207 208 if err := rotate(fname, w.maxFiles, w.compress); err != nil { 209 logrus.WithError(err).Warn("Error rotating log file, log data may have been lost") 210 } else { 211 var renameErr error 212 for i := 0; i < 10; i++ { 213 if renameErr = os.Rename(fname, fname+".1"); renameErr != nil && !os.IsNotExist(renameErr) { 214 logrus.WithError(renameErr).WithField("file", fname).Debug("Error rotating current container log file, evicting readers and retrying") 215 w.notifyReaders.Publish(renameErr) 216 time.Sleep(100 * time.Millisecond) 217 continue 218 } 219 break 220 } 221 if renameErr != nil { 222 logrus.WithError(renameErr).Error("Error renaming current log file") 223 } 224 } 225 226 file, err := openFile(fname, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, w.perms) 227 if err != nil { 228 return err 229 } 230 w.f = file 231 w.currentSize = 0 232 233 w.notifyReaders.Publish(struct{}{}) 234 235 if noCompress { 236 return nil 237 } 238 239 ts := w.lastTimestamp 240 241 go func() { 242 if err := compressFile(fname+".1", ts); err != nil { 243 logrus.WithError(err).Error("Error compressing log file after rotation") 244 } 245 w.rotateMu.Unlock() 246 }() 247 248 return nil 249 } 250 251 func rotate(name string, maxFiles int, compress bool) error { 252 if maxFiles < 2 { 253 return nil 254 } 255 256 var extension string 257 if compress { 258 extension = ".gz" 259 } 260 261 lastFile := fmt.Sprintf("%s.%d%s", name, maxFiles-1, extension) 262 err := os.Remove(lastFile) 263 if err != nil && !os.IsNotExist(err) { 264 return errors.Wrap(err, "error removing oldest log file") 265 } 266 267 for i := maxFiles - 1; i > 1; i-- { 268 toPath := name + "." + strconv.Itoa(i) + extension 269 fromPath := name + "." + strconv.Itoa(i-1) + extension 270 logrus.WithField("source", fromPath).WithField("target", toPath).Trace("Rotating log file") 271 if err := os.Rename(fromPath, toPath); err != nil && !os.IsNotExist(err) { 272 return err 273 } 274 } 275 276 return nil 277 } 278 279 func compressFile(fileName string, lastTimestamp time.Time) (retErr error) { 280 file, err := open(fileName) 281 if err != nil { 282 if os.IsNotExist(err) { 283 logrus.WithField("file", fileName).WithError(err).Debug("Could not open log file to compress") 284 return nil 285 } 286 return errors.Wrap(err, "failed to open log file") 287 } 288 defer func() { 289 file.Close() 290 if retErr == nil { 291 err := os.Remove(fileName) 292 if err != nil && !os.IsNotExist(err) { 293 retErr = errors.Wrap(err, "failed to remove source log file") 294 } 295 } 296 }() 297 298 outFile, err := openFile(fileName+".gz", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0640) 299 if err != nil { 300 return errors.Wrap(err, "failed to open or create gzip log file") 301 } 302 defer func() { 303 outFile.Close() 304 if retErr != nil { 305 if err := os.Remove(fileName + ".gz"); err != nil && !os.IsExist(err) { 306 logrus.WithError(err).Error("Error cleaning up after failed log compression") 307 } 308 } 309 }() 310 311 compressWriter := gzip.NewWriter(outFile) 312 defer compressWriter.Close() 313 314 // Add the last log entry timestamp to the gzip header 315 extra := rotateFileMetadata{} 316 extra.LastTime = lastTimestamp 317 compressWriter.Header.Extra, err = json.Marshal(&extra) 318 if err != nil { 319 // Here log the error only and don't return since this is just an optimization. 320 logrus.Warningf("Failed to marshal gzip header as JSON: %v", err) 321 } 322 323 _, err = pools.Copy(compressWriter, file) 324 if err != nil { 325 return errors.Wrapf(err, "error compressing log file %s", fileName) 326 } 327 328 return nil 329 } 330 331 // MaxFiles return maximum number of files 332 func (w *LogFile) MaxFiles() int { 333 return w.maxFiles 334 } 335 336 // Close closes underlying file and signals all readers to stop. 337 func (w *LogFile) Close() error { 338 w.mu.Lock() 339 defer w.mu.Unlock() 340 if w.closed { 341 return nil 342 } 343 if err := w.f.Close(); err != nil && !errors.Is(err, os.ErrClosed) { 344 return err 345 } 346 w.closed = true 347 return nil 348 } 349 350 // ReadLogs decodes entries from log files and sends them the passed in watcher 351 // 352 // Note: Using the follow option can become inconsistent in cases with very frequent rotations and max log files is 1. 353 // TODO: Consider a different implementation which can effectively follow logs under frequent rotations. 354 func (w *LogFile) ReadLogs(config logger.ReadConfig, watcher *logger.LogWatcher) { 355 w.mu.RLock() 356 currentFile, err := open(w.f.Name()) 357 if err != nil { 358 w.mu.RUnlock() 359 watcher.Err <- err 360 return 361 } 362 defer currentFile.Close() 363 364 dec := w.createDecoder(nil) 365 defer dec.Close() 366 367 currentChunk, err := newSectionReader(currentFile) 368 if err != nil { 369 w.mu.RUnlock() 370 watcher.Err <- err 371 return 372 } 373 374 notifyEvict := w.notifyReaders.SubscribeTopicWithBuffer(func(i interface{}) bool { 375 _, ok := i.(error) 376 return ok 377 }, 1) 378 defer w.notifyReaders.Evict(notifyEvict) 379 380 if config.Tail != 0 { 381 // TODO(@cpuguy83): Instead of opening every file, only get the files which 382 // are needed to tail. 383 // This is especially costly when compression is enabled. 384 files, err := w.openRotatedFiles(config) 385 w.mu.RUnlock() 386 if err != nil { 387 watcher.Err <- err 388 return 389 } 390 391 closeFiles := func() { 392 for _, f := range files { 393 f.Close() 394 fileName := f.Name() 395 if strings.HasSuffix(fileName, tmpLogfileSuffix) { 396 err := w.filesRefCounter.Dereference(fileName) 397 if err != nil { 398 logrus.WithError(err).WithField("file", fileName).Error("Failed to dereference the log file") 399 } 400 } 401 } 402 } 403 404 readers := make([]SizeReaderAt, 0, len(files)+1) 405 for _, f := range files { 406 stat, err := f.Stat() 407 if err != nil { 408 watcher.Err <- errors.Wrap(err, "error reading size of rotated file") 409 closeFiles() 410 return 411 } 412 readers = append(readers, io.NewSectionReader(f, 0, stat.Size())) 413 } 414 if currentChunk.Size() > 0 { 415 readers = append(readers, currentChunk) 416 } 417 418 ok := tailFiles(readers, watcher, dec, w.getTailReader, config, notifyEvict) 419 closeFiles() 420 if !ok { 421 return 422 } 423 w.mu.RLock() 424 } 425 426 if !config.Follow || w.closed { 427 w.mu.RUnlock() 428 return 429 } 430 w.mu.RUnlock() 431 432 notifyRotate := w.notifyReaders.SubscribeTopic(func(i interface{}) bool { 433 _, ok := i.(struct{}) 434 return ok 435 }) 436 defer w.notifyReaders.Evict(notifyRotate) 437 438 followLogs(currentFile, watcher, notifyRotate, notifyEvict, dec, config.Since, config.Until) 439 } 440 441 func (w *LogFile) openRotatedFiles(config logger.ReadConfig) (files []*os.File, err error) { 442 w.rotateMu.Lock() 443 defer w.rotateMu.Unlock() 444 445 defer func() { 446 if err == nil { 447 return 448 } 449 for _, f := range files { 450 f.Close() 451 if strings.HasSuffix(f.Name(), tmpLogfileSuffix) { 452 err := os.Remove(f.Name()) 453 if err != nil && !os.IsNotExist(err) { 454 logrus.Warnf("Failed to remove logfile: %v", err) 455 } 456 } 457 } 458 }() 459 460 for i := w.maxFiles; i > 1; i-- { 461 f, err := open(fmt.Sprintf("%s.%d", w.f.Name(), i-1)) 462 if err != nil { 463 if !os.IsNotExist(err) { 464 return nil, errors.Wrap(err, "error opening rotated log file") 465 } 466 467 fileName := fmt.Sprintf("%s.%d.gz", w.f.Name(), i-1) 468 decompressedFileName := fileName + tmpLogfileSuffix 469 tmpFile, err := w.filesRefCounter.GetReference(decompressedFileName, func(refFileName string, exists bool) (*os.File, error) { 470 if exists { 471 return open(refFileName) 472 } 473 return decompressfile(fileName, refFileName, config.Since) 474 }) 475 476 if err != nil { 477 if !errors.Is(err, os.ErrNotExist) { 478 return nil, errors.Wrap(err, "error getting reference to decompressed log file") 479 } 480 continue 481 } 482 if tmpFile == nil { 483 // The log before `config.Since` does not need to read 484 break 485 } 486 487 files = append(files, tmpFile) 488 continue 489 } 490 files = append(files, f) 491 } 492 493 return files, nil 494 } 495 496 func decompressfile(fileName, destFileName string, since time.Time) (*os.File, error) { 497 cf, err := open(fileName) 498 if err != nil { 499 return nil, errors.Wrap(err, "error opening file for decompression") 500 } 501 defer cf.Close() 502 503 rc, err := gzip.NewReader(cf) 504 if err != nil { 505 return nil, errors.Wrap(err, "error making gzip reader for compressed log file") 506 } 507 defer rc.Close() 508 509 // Extract the last log entry timestramp from the gzip header 510 extra := &rotateFileMetadata{} 511 err = json.Unmarshal(rc.Header.Extra, extra) 512 if err == nil && extra.LastTime.Before(since) { 513 return nil, nil 514 } 515 516 rs, err := openFile(destFileName, os.O_CREATE|os.O_RDWR, 0640) 517 if err != nil { 518 return nil, errors.Wrap(err, "error creating file for copying decompressed log stream") 519 } 520 521 _, err = pools.Copy(rs, rc) 522 if err != nil { 523 rs.Close() 524 rErr := os.Remove(rs.Name()) 525 if rErr != nil && !os.IsNotExist(rErr) { 526 logrus.Errorf("Failed to remove logfile: %v", rErr) 527 } 528 return nil, errors.Wrap(err, "error while copying decompressed log stream to file") 529 } 530 531 return rs, nil 532 } 533 534 func newSectionReader(f *os.File) (*io.SectionReader, error) { 535 // seek to the end to get the size 536 // we'll leave this at the end of the file since section reader does not advance the reader 537 size, err := f.Seek(0, io.SeekEnd) 538 if err != nil { 539 return nil, errors.Wrap(err, "error getting current file size") 540 } 541 return io.NewSectionReader(f, 0, size), nil 542 } 543 544 func tailFiles(files []SizeReaderAt, watcher *logger.LogWatcher, dec Decoder, getTailReader GetTailReaderFunc, config logger.ReadConfig, notifyEvict <-chan interface{}) (cont bool) { 545 nLines := config.Tail 546 547 ctx, cancel := context.WithCancel(context.Background()) 548 defer cancel() 549 550 cont = true 551 // TODO(@cpuguy83): we should plumb a context through instead of dealing with `WatchClose()` here. 552 go func() { 553 select { 554 case err := <-notifyEvict: 555 if err != nil { 556 watcher.Err <- err.(error) 557 cont = false 558 cancel() 559 } 560 case <-ctx.Done(): 561 case <-watcher.WatchConsumerGone(): 562 cont = false 563 cancel() 564 } 565 }() 566 567 readers := make([]io.Reader, 0, len(files)) 568 569 if config.Tail > 0 { 570 for i := len(files) - 1; i >= 0 && nLines > 0; i-- { 571 tail, n, err := getTailReader(ctx, files[i], nLines) 572 if err != nil { 573 watcher.Err <- errors.Wrap(err, "error finding file position to start log tailing") 574 return 575 } 576 nLines -= n 577 readers = append([]io.Reader{tail}, readers...) 578 } 579 } else { 580 for _, r := range files { 581 readers = append(readers, &wrappedReaderAt{ReaderAt: r}) 582 } 583 } 584 585 rdr := io.MultiReader(readers...) 586 dec.Reset(rdr) 587 588 for { 589 msg, err := dec.Decode() 590 if err != nil { 591 if !errors.Is(err, io.EOF) { 592 watcher.Err <- err 593 } 594 return 595 } 596 if !config.Since.IsZero() && msg.Timestamp.Before(config.Since) { 597 continue 598 } 599 if !config.Until.IsZero() && msg.Timestamp.After(config.Until) { 600 return 601 } 602 select { 603 case <-ctx.Done(): 604 return 605 case watcher.Msg <- msg: 606 } 607 } 608 } 609 610 func watchFile(name string) (filenotify.FileWatcher, error) { 611 var fileWatcher filenotify.FileWatcher 612 613 if runtime.GOOS == "windows" { 614 // FileWatcher on Windows files is based on the syscall notifications which has an issue because of file caching. 615 // It is based on ReadDirectoryChangesW() which doesn't detect writes to the cache. It detects writes to disk only. 616 // Because of the OS lazy writing, we don't get notifications for file writes and thereby the watcher 617 // doesn't work. Hence for Windows we will use poll based notifier. 618 fileWatcher = filenotify.NewPollingWatcher() 619 } else { 620 var err error 621 fileWatcher, err = filenotify.New() 622 if err != nil { 623 return nil, err 624 } 625 } 626 627 logger := logrus.WithFields(logrus.Fields{ 628 "module": "logger", 629 "file": name, 630 }) 631 632 if err := fileWatcher.Add(name); err != nil { 633 // we will retry using file poller. 634 logger.WithError(err).Warnf("falling back to file poller") 635 fileWatcher.Close() 636 fileWatcher = filenotify.NewPollingWatcher() 637 638 if err := fileWatcher.Add(name); err != nil { 639 fileWatcher.Close() 640 logger.WithError(err).Debugf("error watching log file for modifications") 641 return nil, err 642 } 643 } 644 645 return fileWatcher, nil 646 } 647 648 type wrappedReaderAt struct { 649 io.ReaderAt 650 pos int64 651 } 652 653 func (r *wrappedReaderAt) Read(p []byte) (int, error) { 654 n, err := r.ReaderAt.ReadAt(p, r.pos) 655 r.pos += int64(n) 656 return n, err 657 }