github.com/alibaba/ilogtail/pkg@v0.0.0-20250526110833-c53b480d046c/helper/log_file_reader.go (about)

     1  // Copyright 2021 iLogtail Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package helper
    16  
    17  import (
    18  	"context"
    19  	"io"
    20  	"os"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/alibaba/ilogtail/pkg/logger"
    25  	"github.com/alibaba/ilogtail/pkg/selfmonitor"
    26  	"github.com/alibaba/ilogtail/pkg/util"
    27  )
    28  
    29  type ReaderMetricTracker struct {
    30  	OpenCounter        selfmonitor.CounterMetric
    31  	CloseCounter       selfmonitor.CounterMetric
    32  	FileSizeCounter    selfmonitor.CounterMetric
    33  	FileRotatorCounter selfmonitor.CounterMetric
    34  	ReadCounter        selfmonitor.CounterMetric
    35  	ReadSizeCounter    selfmonitor.CounterMetric
    36  	ProcessLatency     selfmonitor.LatencyMetric
    37  }
    38  
    39  func NewReaderMetricTracker(mr *selfmonitor.MetricsRecord) *ReaderMetricTracker {
    40  	return &ReaderMetricTracker{
    41  		OpenCounter:        selfmonitor.NewCounterMetricAndRegister(mr, "open_count"),
    42  		CloseCounter:       selfmonitor.NewCounterMetricAndRegister(mr, "close_count"),
    43  		FileSizeCounter:    selfmonitor.NewCounterMetricAndRegister(mr, "file_size"),
    44  		FileRotatorCounter: selfmonitor.NewCounterMetricAndRegister(mr, "file_rotate"),
    45  		ReadCounter:        selfmonitor.NewCounterMetricAndRegister(mr, "read_count"),
    46  		ReadSizeCounter:    selfmonitor.NewCounterMetricAndRegister(mr, "read_size"),
    47  		ProcessLatency:     selfmonitor.NewLatencyMetricAndRegister(mr, "log_process_latency"),
    48  	}
    49  }
    50  
    51  type LogFileReaderConfig struct {
    52  	ReadIntervalMs   int
    53  	MaxReadBlockSize int
    54  	CloseFileSec     int
    55  	Tracker          *ReaderMetricTracker
    56  }
    57  
    58  var DefaultLogFileReaderConfig = LogFileReaderConfig{
    59  	ReadIntervalMs:   1000,
    60  	MaxReadBlockSize: 512 * 1024,
    61  	CloseFileSec:     60,
    62  	Tracker:          nil,
    63  }
    64  
    65  type LogFileReaderCheckPoint struct {
    66  	Path   string
    67  	Offset int64
    68  	State  StateOS
    69  }
    70  
    71  // IsSame check if the checkpoints is same
    72  func (checkpoint *LogFileReaderCheckPoint) IsSame(other *LogFileReaderCheckPoint) bool {
    73  	if checkpoint.Path != other.Path || checkpoint.Offset != other.Offset {
    74  		return false
    75  	}
    76  	return !checkpoint.State.IsChange(other.State)
    77  }
    78  
    79  type LogFileReader struct {
    80  	Config LogFileReaderConfig
    81  
    82  	file           *os.File
    83  	lastCheckpoint LogFileReaderCheckPoint
    84  	checkpoint     LogFileReaderCheckPoint
    85  	processor      LogFileProcessor
    86  	nowBlock       []byte
    87  	lastBufferSize int
    88  	lastBufferTime time.Time
    89  	readWhenStart  bool
    90  	checkpointLock sync.Mutex
    91  	shutdown       chan struct{}
    92  	logContext     context.Context
    93  	waitgroup      sync.WaitGroup
    94  	foundFile      bool
    95  }
    96  
    97  func NewLogFileReader(context context.Context, checkpoint LogFileReaderCheckPoint, config LogFileReaderConfig, processor LogFileProcessor) (*LogFileReader, error) {
    98  	readWhenStart := false
    99  	foundFile := true
   100  	if checkpoint.State.IsEmpty() {
   101  		if newStat, err := os.Stat(checkpoint.Path); err == nil {
   102  			checkpoint.State = GetOSState(newStat)
   103  		} else {
   104  			if os.IsNotExist(err) {
   105  				foundFile = false
   106  			}
   107  			logger.Warning(context, "STAT_FILE_ALARM", "stat file error when create reader, file", checkpoint.Path, "error", err.Error())
   108  		}
   109  	}
   110  	if !checkpoint.State.IsEmpty() {
   111  		if deltaNano := time.Now().UnixNano() - int64(checkpoint.State.ModifyTime); deltaNano >= 0 && deltaNano < 180*1e9 {
   112  			readWhenStart = true
   113  			logger.Info(context, "read file", checkpoint.Path, "first read", readWhenStart)
   114  		} else {
   115  			logger.Info(context, "read file", checkpoint.Path, "since offset", checkpoint.Offset)
   116  		}
   117  	}
   118  	return &LogFileReader{
   119  		checkpoint:     checkpoint,
   120  		Config:         config,
   121  		processor:      processor,
   122  		logContext:     context,
   123  		lastBufferSize: 0,
   124  		nowBlock:       nil,
   125  		readWhenStart:  readWhenStart,
   126  		foundFile:      foundFile,
   127  	}, nil
   128  }
   129  
   130  // GetLastEndOfLine return new read bytes end with '\n'
   131  // @note will return n + r.lastBufferSize when n + r.lastBufferSize == len(r.nowBlock)
   132  func (r *LogFileReader) GetLastEndOfLine(n int) int {
   133  	blockSize := n + r.lastBufferSize
   134  	// if block size >= r.Config.MaxReadBlockSize, return n
   135  	if blockSize == len(r.nowBlock) {
   136  		return n
   137  	}
   138  	for i := blockSize - 1; i >= r.lastBufferSize; i-- {
   139  		if r.nowBlock[i] == '\n' {
   140  			return i - r.lastBufferSize + 1
   141  		}
   142  	}
   143  	return 0
   144  }
   145  
   146  func (r *LogFileReader) CheckFileChange() bool {
   147  	switch newStat, err := os.Stat(r.checkpoint.Path); {
   148  	case err == nil: // stat by filename to check if size changed or file changed
   149  		newOsStat := GetOSState(newStat)
   150  		// logger.Debug("check file change", newOsStat.String())
   151  		if r.checkpoint.State.IsChange(newOsStat) {
   152  			needResetOffset := false
   153  			if r.checkpoint.State.IsFileChange(newOsStat) {
   154  				needResetOffset = true
   155  				logger.Info(r.logContext, "file dev inode changed, read to end and force read from beginning, file", r.checkpoint.Path, "old", r.checkpoint.State.String(), "new", newOsStat.String(), "offset", r.checkpoint.Offset)
   156  				// read to end or shutdown
   157  				if r.file != nil {
   158  					r.ReadAndProcess(false)
   159  					r.CloseFile("open file and dev inode changed")
   160  				}
   161  				if r.Config.Tracker != nil {
   162  					r.Config.Tracker.FileRotatorCounter.Add(1)
   163  				}
   164  				// if file change, force flush last buffer
   165  				if r.lastBufferSize > 0 {
   166  					processSize := r.processor.Process(r.nowBlock[0:r.lastBufferSize], time.Hour)
   167  					r.UpdateProcessResult(0, processSize)
   168  				}
   169  			}
   170  			r.checkpointLock.Lock()
   171  			r.checkpoint.State = newOsStat
   172  			if needResetOffset {
   173  				r.checkpoint.Offset = 0
   174  			}
   175  			r.checkpointLock.Unlock()
   176  			return true
   177  		}
   178  		r.foundFile = true
   179  	case r.file != nil: // Fallback to stat by file handle to check if size changed. This is necessary because the file path may become inaccessible in certain scenarios, such as when a container is stopped, but the file handle remains valid.
   180  		if newStat, statErr := r.file.Stat(); statErr == nil {
   181  			newOsStat := GetOSState(newStat)
   182  			// logger.Debug("check file change", newOsStat.String())
   183  			if r.checkpoint.State.IsChange(newOsStat) {
   184  				r.checkpointLock.Lock()
   185  				r.checkpoint.State = newOsStat
   186  				r.checkpointLock.Unlock()
   187  				return true
   188  			}
   189  		}
   190  	default:
   191  		if os.IsNotExist(err) {
   192  			if r.foundFile {
   193  				logger.Warning(r.logContext, "STAT_FILE_ALARM", "stat file error, file", r.checkpoint.Path, "error", err.Error())
   194  				r.foundFile = false
   195  			}
   196  		} else {
   197  			logger.Warning(r.logContext, "STAT_FILE_ALARM", "stat file error, file", r.checkpoint.Path, "error", err.Error())
   198  		}
   199  	}
   200  	return false
   201  }
   202  
   203  func (r *LogFileReader) GetProcessor() LogFileProcessor {
   204  	return r.processor
   205  }
   206  
   207  func (r *LogFileReader) GetCheckpoint() (checkpoint LogFileReaderCheckPoint, updateFlag bool) {
   208  	r.checkpointLock.Lock()
   209  	defer func() {
   210  		r.lastCheckpoint = r.checkpoint
   211  		r.checkpointLock.Unlock()
   212  	}()
   213  	r.lastCheckpoint = r.checkpoint
   214  	return r.checkpoint, r.lastCheckpoint.IsSame(&r.checkpoint)
   215  }
   216  
   217  func (r *LogFileReader) UpdateProcessResult(readN, processedN int) {
   218  	if readN+r.lastBufferSize == processedN {
   219  		r.lastBufferSize = 0
   220  		r.lastBufferTime = time.Now()
   221  	} else {
   222  		if processedN != 0 {
   223  			// need move buffer
   224  			copy(r.nowBlock, r.nowBlock[processedN:readN+r.lastBufferSize])
   225  			r.lastBufferTime = time.Now()
   226  		}
   227  		r.lastBufferSize = readN + r.lastBufferSize - processedN
   228  	}
   229  	r.checkpointLock.Lock()
   230  	defer r.checkpointLock.Unlock()
   231  	r.checkpoint.Offset += int64(processedN)
   232  }
   233  
   234  func (r *LogFileReader) ProcessAfterRead(n int) {
   235  	// if no more file, check and process last buffer
   236  	if n == 0 {
   237  		if r.lastBufferSize > 0 {
   238  			// logger.Debug("no more file, check and process last buffer", string(r.nowBlock[0:r.lastBufferSize]))
   239  			processSize := r.processor.Process(r.nowBlock[0:r.lastBufferSize], time.Since(r.lastBufferTime))
   240  			if processSize > n+r.lastBufferSize {
   241  				processSize = n + r.lastBufferSize
   242  			}
   243  			r.UpdateProcessResult(n, processSize)
   244  		} else {
   245  			// if no data, just call process and give a empty []byte array
   246  			r.processor.Process(r.nowBlock[0:0], time.Since(r.lastBufferTime))
   247  		}
   248  	} else {
   249  		processSize := r.processor.Process(r.nowBlock[0:r.lastBufferSize+n], time.Duration(0))
   250  		if processSize > n+r.lastBufferSize {
   251  			processSize = n + r.lastBufferSize
   252  		}
   253  		// logger.Debug("process file, len", r.lastBufferSize+n, "processed size", processSize)
   254  		r.UpdateProcessResult(n, processSize)
   255  	}
   256  }
   257  
   258  func (r *LogFileReader) ReadOpen() error {
   259  	if r.file == nil {
   260  		var err error
   261  		r.file, err = ReadOpen(r.checkpoint.Path)
   262  		if r.Config.Tracker != nil {
   263  			r.Config.Tracker.OpenCounter.Add(1)
   264  		}
   265  		logger.Debug(r.logContext, "open file for read, file", r.checkpoint.Path, "offset", r.checkpoint.Offset, "status", r.checkpoint.State)
   266  		return err
   267  	}
   268  	return nil
   269  }
   270  
   271  func (r *LogFileReader) ReadAndProcess(once bool) {
   272  	if r.nowBlock == nil {
   273  		// once only be true when shutdown, and we don't need to init r.nowBlock when shutdown because this file is never readed
   274  		if once {
   275  			return
   276  		}
   277  		// lazy init
   278  		r.nowBlock = make([]byte, r.Config.MaxReadBlockSize)
   279  	}
   280  	if err := r.ReadOpen(); err == nil {
   281  		file := r.file
   282  		// double check
   283  		if newStat, statErr := file.Stat(); statErr == nil {
   284  			newOsStat := GetOSState(newStat)
   285  
   286  			// logger.Debug("check file dev inode, file", r.checkpoint.Path, "old", r.checkpoint.State.String(), "new", newOsStat.String(), "offset", r.checkpoint.Offset)
   287  
   288  			// check file dev+inode changed
   289  			if r.checkpoint.State.IsFileChange(newOsStat) {
   290  				logger.Info(r.logContext, "file dev inode changed, force read from beginning, file", r.checkpoint.Path, "old", r.checkpoint.State.String(), "new", newOsStat.String(), "offset", r.checkpoint.Offset)
   291  				// if file dev+inode changed, force flush last buffer
   292  				if r.lastBufferSize > 0 {
   293  					processSize := r.processor.Process(r.nowBlock[0:r.lastBufferSize], time.Hour)
   294  					r.UpdateProcessResult(0, processSize)
   295  				}
   296  				if r.Config.Tracker != nil {
   297  					r.Config.Tracker.FileRotatorCounter.Add(1)
   298  				}
   299  				r.checkpointLock.Lock()
   300  				r.checkpoint.Offset = 0
   301  				r.checkpoint.State = newOsStat
   302  				r.checkpointLock.Unlock()
   303  				r.CloseFile("file changed(rotate)")
   304  				return
   305  			}
   306  
   307  			// check file truncated
   308  			if newOsStat.Size < r.checkpoint.Offset {
   309  				logger.Info(r.logContext, "file truncated, force read from beginning, file", r.checkpoint.Path, "old", r.checkpoint.State.String(), "new", newOsStat.String(), "offset", r.checkpoint.Offset)
   310  				// if file dev+inode changed, force flush last buffer
   311  				if r.lastBufferSize > 0 {
   312  					processSize := r.processor.Process(r.nowBlock[0:r.lastBufferSize], time.Hour)
   313  					r.UpdateProcessResult(0, processSize)
   314  				}
   315  				if r.Config.Tracker != nil {
   316  					r.Config.Tracker.FileRotatorCounter.Add(1)
   317  				}
   318  				r.checkpointLock.Lock()
   319  				if newOsStat.Size < 10*1024*1024 {
   320  					r.checkpoint.Offset = 0
   321  				} else {
   322  					r.checkpoint.Offset = newOsStat.Size - 1024*1024
   323  				}
   324  				r.checkpoint.State = newOsStat
   325  				r.checkpointLock.Unlock()
   326  				r.CloseFile("file changed(truncate)")
   327  				return
   328  			}
   329  		} else {
   330  			logger.Warning(r.logContext, "STAT_FILE_ALARM", "stat file error, file", r.checkpoint.Path, "error", statErr.Error())
   331  		}
   332  		for {
   333  			n, readErr := file.ReadAt(r.nowBlock[r.lastBufferSize:], int64(r.lastBufferSize)+r.checkpoint.Offset)
   334  			needBreak := false
   335  			if r.Config.Tracker != nil {
   336  				r.Config.Tracker.ReadCounter.Add(1)
   337  				r.Config.Tracker.ReadSizeCounter.Add(int64(n))
   338  			}
   339  			if once || n < r.Config.MaxReadBlockSize-r.lastBufferSize {
   340  				needBreak = true
   341  			}
   342  			if readErr != nil {
   343  				if readErr != io.EOF {
   344  					logger.Warning(r.logContext, "READ_FILE_ALARM", "read file error, file", r.checkpoint.Path, "error", readErr.Error())
   345  					break
   346  				}
   347  				logger.Debug(r.logContext, "read end of file", r.checkpoint.Path, "offset", r.checkpoint.Offset, "last buffer size", r.lastBufferSize, "read n", n, "stat", r.checkpoint.State.String())
   348  				needBreak = true
   349  			}
   350  			// only accept buffer end of '\n'
   351  			n = r.GetLastEndOfLine(n)
   352  			// logger.Debug("after check last end of line, n", n)
   353  			r.ProcessAfterRead(n)
   354  			if !needBreak {
   355  				// check shutdown
   356  				select {
   357  				case <-r.shutdown:
   358  					logger.Info(r.logContext, "receive stop signal when read data, path", r.checkpoint.Path)
   359  					needBreak = true
   360  				default:
   361  				}
   362  			}
   363  			if needBreak {
   364  				break
   365  			}
   366  		}
   367  	} else {
   368  		logger.Warning(r.logContext, "READ_FILE_ALARM", "open file for read error, file", r.checkpoint.Path, "error", err.Error())
   369  	}
   370  }
   371  
   372  func (r *LogFileReader) CloseFile(reason string) {
   373  	if r.file != nil {
   374  		_ = r.file.Close()
   375  		r.file = nil
   376  		if r.Config.Tracker != nil {
   377  			r.Config.Tracker.CloseCounter.Add(1)
   378  		}
   379  		logger.Debug(r.logContext, "close file, reason", reason, "file", r.checkpoint.Path, "offset", r.checkpoint.Offset, "status", r.checkpoint.State)
   380  	}
   381  }
   382  
   383  func (r *LogFileReader) Run() {
   384  	defer func() {
   385  		r.CloseFile("run done")
   386  		r.waitgroup.Done()
   387  		panicRecover(r.logContext, r.checkpoint.Path)
   388  	}()
   389  	lastReadTime := time.Now()
   390  	tracker := r.Config.Tracker
   391  	for {
   392  		startProcessTime := time.Now()
   393  		if r.readWhenStart || r.CheckFileChange() {
   394  			r.readWhenStart = false
   395  			r.ReadAndProcess(false)
   396  			lastReadTime = startProcessTime
   397  		} else {
   398  			r.ProcessAfterRead(0)
   399  			if startProcessTime.Sub(lastReadTime) > time.Second*time.Duration(r.Config.CloseFileSec) {
   400  				r.CloseFile("no read timeout")
   401  				lastReadTime = startProcessTime
   402  			}
   403  		}
   404  		if tracker != nil {
   405  			tracker.ProcessLatency.Observe(float64(time.Since(startProcessTime)))
   406  		}
   407  		endProcessTime := time.Now()
   408  		sleepDuration := time.Millisecond*time.Duration(r.Config.ReadIntervalMs) - endProcessTime.Sub(startProcessTime)
   409  		// logger.Debug(r.logContext, "sleep duration", sleepDuration, "normal", r.Config.ReadIntervalMs, "path", r.checkpoint.Path)
   410  		if util.RandomSleep(sleepDuration, 0.1, r.shutdown) {
   411  			r.ReadAndProcess(true)
   412  			if r.lastBufferSize > 0 {
   413  				processSize := r.processor.Process(r.nowBlock[0:r.lastBufferSize], time.Hour)
   414  				r.UpdateProcessResult(0, processSize)
   415  			}
   416  			// [to #37527076] force flush last log buffer in processor
   417  			r.processor.Process(r.nowBlock[0:0], time.Hour)
   418  			break
   419  		}
   420  	}
   421  }
   422  
   423  // SetForceRead force read file when reader start
   424  func (r *LogFileReader) SetForceRead() {
   425  	r.readWhenStart = true
   426  }
   427  
   428  func (r *LogFileReader) Start() {
   429  	r.shutdown = make(chan struct{})
   430  	r.waitgroup.Add(1)
   431  	go r.Run()
   432  }
   433  
   434  func (r *LogFileReader) Stop() {
   435  	close(r.shutdown)
   436  	r.waitgroup.Wait()
   437  }