2026-08-15 01:36:51 +08:00

134 lines
3.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package collect
import (
"bytes"
"context"
"io"
"os"
"strings"
"time"
"git.opencomputing.cn/yumoqing/host-metrics-collector/internal/checkpoint"
"git.opencomputing.cn/yumoqing/host-metrics-collector/internal/model"
)
// LogTail 持续读取单个日志文件的新增内容,并通过 checkpoint.Store 记录
// 已消费 offset,实现进程重启后的断点续传。采用轮询式 tail,避免对
// inotify 的额外依赖,跨平台且足够轻量。
type LogTail struct {
path string
store checkpoint.Store
poll time.Duration
onEntry func(model.LogRecord)
}
// NewLogTail 创建日志 tail 采集器;onEntry 每读到一行完整日志时回调。
func NewLogTail(path string, store checkpoint.Store, poll time.Duration, onEntry func(model.LogRecord)) *LogTail {
if poll <= 0 {
poll = time.Second
}
return &LogTail{path: path, store: store, poll: poll, onEntry: onEntry}
}
// Run 阻塞执行采集,直到 ctx 被取消。
func (t *LogTail) Run(ctx context.Context) {
offset, err := t.store.GetLogOffset(t.path)
if err != nil {
offset = 0
}
var partial []byte
for {
select {
case <-ctx.Done():
return
default:
}
offset, partial = t.readOnce(offset, partial)
select {
case <-ctx.Done():
return
case <-time.After(t.poll):
}
}
}
// readOnce 从 offset 处读取文件新增内容,返回新的 offset 与未成行的残留字节。
func (t *LogTail) readOnce(offset int64, partial []byte) (int64, []byte) {
f, err := os.Open(t.path)
if err != nil {
return offset, partial
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return offset, partial
}
// 文件被截断或轮转:从头重新消费。
if info.Size() < offset {
offset = 0
partial = partial[:0]
}
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return offset, partial
}
data, err := io.ReadAll(f)
if err != nil {
return offset, partial
}
if len(data) == 0 {
return offset, partial
}
partial = append(partial, data...)
for {
idx := bytes.IndexByte(partial, '\n')
if idx < 0 {
break
}
line := partial[:idx]
partial = partial[idx+1:]
offset += int64(idx) + 1
t.emit(line, offset)
}
return offset, partial
}
func (t *LogTail) emit(line []byte, offset int64) {
msg := strings.TrimRight(string(line), "\r")
rec := model.LogRecord{
Timestamp: time.Now().Unix(),
Level: detectLevel(msg),
Source: t.path,
Message: msg,
File: t.path,
Offset: offset,
}
// 先投递给上层(进入内存缓冲),再推进 offset;若进程在两步之间退出,
// 重启后会从旧 offset 重读,得到「至少一次」语义,避免丢日志。
if t.onEntry != nil {
t.onEntry(rec)
}
// 检查点写入失败不阻断采集;下轮会按旧 offset 重读(至少一次语义)。
if err := t.store.SetLogOffset(t.path, offset); err != nil {
// 可选:由上层记录日志,这里保持轻量。
}
}
// detectLevel 从日志内容中识别常见级别,未命中默认 INFO。
func detectLevel(msg string) string {
upper := strings.ToUpper(msg)
for _, lvl := range []string{"FATAL", "ERROR", "WARN", "INFO", "DEBUG"} {
if strings.Contains(upper, lvl) {
return lvl
}
}
return "INFO"
}