Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
525
plugins/inputs/tail/tail.go
Normal file
525
plugins/inputs/tail/tail.go
Normal file
|
@ -0,0 +1,525 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
//go:build !solaris
|
||||
|
||||
package tail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dimchansky/utfbom"
|
||||
"github.com/influxdata/tail"
|
||||
"github.com/pborman/ansi"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/internal/globpath"
|
||||
"github.com/influxdata/telegraf/plugins/common/encoding"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
"github.com/influxdata/telegraf/plugins/parsers"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
|
||||
offsets = make(map[string]int64)
|
||||
offsetsMutex = new(sync.Mutex)
|
||||
)
|
||||
|
||||
type Tail struct {
|
||||
Files []string `toml:"files"`
|
||||
FromBeginning bool `toml:"from_beginning" deprecated:"1.34.0;1.40.0;use 'initial_read_offset' with value 'beginning' instead"`
|
||||
InitialReadOffset string `toml:"initial_read_offset"`
|
||||
Pipe bool `toml:"pipe"`
|
||||
WatchMethod string `toml:"watch_method"`
|
||||
MaxUndeliveredLines int `toml:"max_undelivered_lines"`
|
||||
CharacterEncoding string `toml:"character_encoding"`
|
||||
PathTag string `toml:"path_tag"`
|
||||
|
||||
Filters []string `toml:"filters"`
|
||||
filterColors bool
|
||||
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
tailers map[string]*tail.Tail
|
||||
tailersMutex sync.RWMutex
|
||||
offsets map[string]int64
|
||||
parserFunc telegraf.ParserFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
acc telegraf.TrackingAccumulator
|
||||
|
||||
MultilineConfig multilineConfig `toml:"multiline"`
|
||||
multiline *multiline
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
sem semaphore
|
||||
decoder *encoding.Decoder
|
||||
}
|
||||
|
||||
type empty struct{}
|
||||
type semaphore chan empty
|
||||
|
||||
func (*Tail) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (t *Tail) SetParserFunc(fn telegraf.ParserFunc) {
|
||||
t.parserFunc = fn
|
||||
}
|
||||
|
||||
func (t *Tail) Init() error {
|
||||
// Backward compatibility setting
|
||||
if t.InitialReadOffset == "" {
|
||||
if t.FromBeginning {
|
||||
t.InitialReadOffset = "beginning"
|
||||
} else {
|
||||
t.InitialReadOffset = "saved-or-end"
|
||||
}
|
||||
}
|
||||
|
||||
// Check settings
|
||||
switch t.InitialReadOffset {
|
||||
case "":
|
||||
t.InitialReadOffset = "saved-or-end"
|
||||
case "beginning", "end", "saved-or-end", "saved-or-beginning":
|
||||
default:
|
||||
return fmt.Errorf("invalid 'initial_read_offset' setting %q", t.InitialReadOffset)
|
||||
}
|
||||
|
||||
if t.MaxUndeliveredLines == 0 {
|
||||
return errors.New("max_undelivered_lines must be positive")
|
||||
}
|
||||
t.sem = make(semaphore, t.MaxUndeliveredLines)
|
||||
|
||||
for _, filter := range t.Filters {
|
||||
if filter == "ansi_color" {
|
||||
t.filterColors = true
|
||||
}
|
||||
}
|
||||
|
||||
// init offsets
|
||||
t.offsets = make(map[string]int64)
|
||||
|
||||
dec, err := encoding.NewDecoder(t.CharacterEncoding)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating decoder failed: %w", err)
|
||||
}
|
||||
t.decoder = dec
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tail) Start(acc telegraf.Accumulator) error {
|
||||
t.acc = acc.WithTracking(t.MaxUndeliveredLines)
|
||||
|
||||
t.ctx, t.cancel = context.WithCancel(context.Background())
|
||||
|
||||
t.wg.Add(1)
|
||||
go func() {
|
||||
defer t.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
return
|
||||
case <-t.acc.Delivered():
|
||||
<-t.sem
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var err error
|
||||
t.multiline, err = t.MultilineConfig.newMultiline()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.tailers = make(map[string]*tail.Tail)
|
||||
|
||||
err = t.tailNewFiles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// assumption that once Start is called, all parallel plugins have already been initialized
|
||||
offsetsMutex.Lock()
|
||||
offsets = make(map[string]int64)
|
||||
offsetsMutex.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tail) getSeekInfo(file string) (*tail.SeekInfo, error) {
|
||||
// Pipes do not support seeking
|
||||
if t.Pipe {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Determine the actual position for continuing
|
||||
switch t.InitialReadOffset {
|
||||
case "beginning":
|
||||
return &tail.SeekInfo{Whence: 0, Offset: 0}, nil
|
||||
case "end":
|
||||
return &tail.SeekInfo{Whence: 2, Offset: 0}, nil
|
||||
case "", "saved-or-end":
|
||||
if offset, ok := t.offsets[file]; ok {
|
||||
t.Log.Debugf("Using offset %d for %q", offset, file)
|
||||
return &tail.SeekInfo{Whence: 0, Offset: offset}, nil
|
||||
}
|
||||
return &tail.SeekInfo{Whence: 2, Offset: 0}, nil
|
||||
case "saved-or-beginning":
|
||||
if offset, ok := t.offsets[file]; ok {
|
||||
t.Log.Debugf("Using offset %d for %q", offset, file)
|
||||
return &tail.SeekInfo{Whence: 0, Offset: offset}, nil
|
||||
}
|
||||
return &tail.SeekInfo{Whence: 0, Offset: 0}, nil
|
||||
default:
|
||||
return nil, errors.New("invalid 'initial_read_offset' setting")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tail) GetState() interface{} {
|
||||
return t.offsets
|
||||
}
|
||||
|
||||
func (t *Tail) SetState(state interface{}) error {
|
||||
offsetsState, ok := state.(map[string]int64)
|
||||
if !ok {
|
||||
return errors.New("state has to be of type 'map[string]int64'")
|
||||
}
|
||||
for k, v := range offsetsState {
|
||||
t.offsets[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tail) Gather(_ telegraf.Accumulator) error {
|
||||
return t.tailNewFiles()
|
||||
}
|
||||
|
||||
func (t *Tail) Stop() {
|
||||
t.tailersMutex.Lock()
|
||||
defer t.tailersMutex.Unlock()
|
||||
|
||||
for filename, tailer := range t.tailers {
|
||||
if !t.Pipe {
|
||||
// store offset for resume
|
||||
offset, err := tailer.Tell()
|
||||
if err == nil {
|
||||
t.Log.Debugf("Recording offset %d for %q", offset, tailer.Filename)
|
||||
t.offsets[tailer.Filename] = offset
|
||||
} else {
|
||||
t.Log.Errorf("Recording offset for %q: %s", tailer.Filename, err.Error())
|
||||
}
|
||||
}
|
||||
err := tailer.Stop()
|
||||
if err != nil {
|
||||
t.Log.Errorf("Stopping tail on %q: %s", tailer.Filename, err.Error())
|
||||
}
|
||||
|
||||
// Explicitly delete the tailer from the map to avoid memory leaks
|
||||
delete(t.tailers, filename)
|
||||
}
|
||||
|
||||
t.cancel()
|
||||
t.wg.Wait()
|
||||
|
||||
// persist offsets
|
||||
offsetsMutex.Lock()
|
||||
for k, v := range t.offsets {
|
||||
offsets[k] = v
|
||||
}
|
||||
offsetsMutex.Unlock()
|
||||
}
|
||||
|
||||
func (t *Tail) tailNewFiles() error {
|
||||
var poll bool
|
||||
if t.WatchMethod == "poll" {
|
||||
poll = true
|
||||
}
|
||||
|
||||
// Track files that we're currently processing
|
||||
currentFiles := make(map[string]bool)
|
||||
|
||||
// Create a "tailer" for each file
|
||||
for _, filepath := range t.Files {
|
||||
g, err := globpath.Compile(filepath)
|
||||
if err != nil {
|
||||
t.Log.Errorf("Glob %q failed to compile: %s", filepath, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range g.Match() {
|
||||
// Mark this file as currently being processed
|
||||
currentFiles[file] = true
|
||||
|
||||
// Check if we're already tailing this file
|
||||
t.tailersMutex.RLock()
|
||||
_, alreadyTailing := t.tailers[file]
|
||||
t.tailersMutex.RUnlock()
|
||||
|
||||
if alreadyTailing {
|
||||
// already tailing this file
|
||||
continue
|
||||
}
|
||||
|
||||
seek, err := t.getSeekInfo(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailer, err := tail.TailFile(file,
|
||||
tail.Config{
|
||||
ReOpen: true,
|
||||
Follow: true,
|
||||
Location: seek,
|
||||
MustExist: true,
|
||||
Poll: poll,
|
||||
Pipe: t.Pipe,
|
||||
Logger: tail.DiscardingLogger,
|
||||
OpenReaderFunc: func(rd io.Reader) io.Reader {
|
||||
r, _ := utfbom.Skip(t.decoder.Reader(rd))
|
||||
return r
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Log.Debugf("Failed to open file (%s): %v", file, err)
|
||||
continue
|
||||
}
|
||||
|
||||
t.Log.Debugf("Tail added for %q", file)
|
||||
|
||||
parser, err := t.parserFunc()
|
||||
if err != nil {
|
||||
t.Log.Errorf("Creating parser: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// create a goroutine for each "tailer"
|
||||
t.wg.Add(1)
|
||||
|
||||
// Store the tailer in the map before starting the goroutine
|
||||
t.tailersMutex.Lock()
|
||||
t.tailers[tailer.Filename] = tailer
|
||||
t.tailersMutex.Unlock()
|
||||
|
||||
go func(tl *tail.Tail) {
|
||||
defer t.wg.Done()
|
||||
t.receiver(parser, tl)
|
||||
|
||||
t.Log.Debugf("Tail removed for %q", tl.Filename)
|
||||
|
||||
if err := tl.Err(); err != nil {
|
||||
if strings.HasSuffix(err.Error(), "permission denied") {
|
||||
t.Log.Errorf("Deleting tailer for %q due to: %v", tl.Filename, err)
|
||||
t.tailersMutex.Lock()
|
||||
delete(t.tailers, tl.Filename)
|
||||
t.tailersMutex.Unlock()
|
||||
} else {
|
||||
t.Log.Errorf("Tailing %q: %s", tl.Filename, err.Error())
|
||||
}
|
||||
}
|
||||
}(tailer)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up tailers for files that are no longer being monitored
|
||||
return t.cleanupUnusedTailers(currentFiles)
|
||||
}
|
||||
|
||||
// cleanupUnusedTailers stops and removes tailers for files that are no longer being monitored.
|
||||
// It uses defer to ensure the mutex is always unlocked, even if errors occur.
|
||||
func (t *Tail) cleanupUnusedTailers(currentFiles map[string]bool) error {
|
||||
t.tailersMutex.Lock()
|
||||
defer t.tailersMutex.Unlock()
|
||||
|
||||
for file, tailer := range t.tailers {
|
||||
if !currentFiles[file] {
|
||||
// This file is no longer in our glob pattern matches
|
||||
// We need to stop tailing it and remove it from our list
|
||||
t.Log.Debugf("Removing tailer for %q as it's no longer in the glob pattern", file)
|
||||
|
||||
// Save the current offset for potential future use
|
||||
if !t.Pipe {
|
||||
offset, err := tailer.Tell()
|
||||
if err == nil {
|
||||
t.Log.Debugf("Recording offset %d for %q", offset, tailer.Filename)
|
||||
t.offsets[tailer.Filename] = offset
|
||||
} else {
|
||||
t.Log.Errorf("Recording offset for %q: %s", tailer.Filename, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the tailer
|
||||
err := tailer.Stop()
|
||||
if err != nil {
|
||||
t.Log.Errorf("Stopping tail on %q: %s", tailer.Filename, err.Error())
|
||||
}
|
||||
|
||||
// Remove from our map
|
||||
delete(t.tailers, file)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseLine(parser telegraf.Parser, line string) ([]telegraf.Metric, error) {
|
||||
m, err := parser.Parse([]byte(line))
|
||||
if err != nil {
|
||||
if errors.Is(err, parsers.ErrEOF) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
|
||||
// receiver is launched as a goroutine to continuously watch a tailed logfile
|
||||
// for changes, parse any incoming messages, and add to the accumulator.
|
||||
func (t *Tail) receiver(parser telegraf.Parser, tailer *tail.Tail) {
|
||||
// holds the individual lines of multi-line log entries.
|
||||
var buffer bytes.Buffer
|
||||
|
||||
var timer *time.Timer
|
||||
var timeout <-chan time.Time
|
||||
|
||||
// The multiline mode requires a timer in order to flush the multiline buffer
|
||||
// if no new lines are incoming.
|
||||
if t.multiline.isEnabled() {
|
||||
timer = time.NewTimer(time.Duration(*t.MultilineConfig.Timeout))
|
||||
timeout = timer.C
|
||||
}
|
||||
|
||||
channelOpen := true
|
||||
tailerOpen := true
|
||||
var line *tail.Line
|
||||
|
||||
for {
|
||||
line = nil
|
||||
|
||||
if timer != nil {
|
||||
timer.Reset(time.Duration(*t.MultilineConfig.Timeout))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
channelOpen = false
|
||||
case line, tailerOpen = <-tailer.Lines:
|
||||
if !tailerOpen {
|
||||
channelOpen = false
|
||||
}
|
||||
case <-timeout:
|
||||
}
|
||||
|
||||
var text string
|
||||
|
||||
if line != nil {
|
||||
// Fix up files with Windows line endings.
|
||||
text = strings.TrimRight(line.Text, "\r")
|
||||
|
||||
if t.multiline.isEnabled() {
|
||||
if text = t.multiline.processLine(text, &buffer); text == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if line == nil || !channelOpen || !tailerOpen {
|
||||
if text += flush(&buffer); text == "" {
|
||||
if !channelOpen {
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if line != nil && line.Err != nil {
|
||||
t.Log.Errorf("Tailing %q: %s", tailer.Filename, line.Err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if t.filterColors {
|
||||
out, err := ansi.Strip([]byte(text))
|
||||
if err != nil {
|
||||
t.Log.Errorf("Cannot strip ansi colors from %s: %s", text, err)
|
||||
}
|
||||
text = string(out)
|
||||
}
|
||||
|
||||
metrics, err := parseLine(parser, text)
|
||||
if err != nil {
|
||||
t.Log.Errorf("Malformed log line in %q: [%q]: %s",
|
||||
tailer.Filename, text, err.Error())
|
||||
continue
|
||||
}
|
||||
if len(metrics) == 0 {
|
||||
once.Do(func() {
|
||||
t.Log.Debug(internal.NoMetricsCreatedMsg)
|
||||
})
|
||||
}
|
||||
if t.PathTag != "" {
|
||||
for _, metric := range metrics {
|
||||
metric.AddTag(t.PathTag, tailer.Filename)
|
||||
}
|
||||
}
|
||||
|
||||
// try writing out metric first without blocking
|
||||
select {
|
||||
case t.sem <- empty{}:
|
||||
t.acc.AddTrackingMetricGroup(metrics)
|
||||
if t.ctx.Err() != nil {
|
||||
return // exit!
|
||||
}
|
||||
continue // next loop
|
||||
default:
|
||||
// no room. switch to blocking write.
|
||||
}
|
||||
|
||||
// Block until plugin is stopping or room is available to add metrics.
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
return
|
||||
// Tail is trying to close so drain the sem to allow the receiver
|
||||
// to exit. This condition is hit when the tailer may have hit the
|
||||
// maximum undelivered lines and is trying to close.
|
||||
case <-tailer.Dying():
|
||||
<-t.sem
|
||||
case t.sem <- empty{}:
|
||||
t.acc.AddTrackingMetricGroup(metrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTail() *Tail {
|
||||
offsetsMutex.Lock()
|
||||
offsetsCopy := make(map[string]int64, len(offsets))
|
||||
for k, v := range offsets {
|
||||
offsetsCopy[k] = v
|
||||
}
|
||||
offsetsMutex.Unlock()
|
||||
|
||||
return &Tail{
|
||||
MaxUndeliveredLines: 1000,
|
||||
offsets: offsetsCopy,
|
||||
PathTag: "path",
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("tail", func() telegraf.Input {
|
||||
return newTail()
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue