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 }