Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[receiver/filelog] Limit files consumed per polling cycle #18477

Merged
merged 7 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .chloggen/filelog-max-batches.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: 'enhancement'

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: filelogreceiver

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Added `max_batches` configuration parameter to limit the number of batches processed in a polling interval.

# One or more tracking issues related to the change
issues: [18476]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
1 change: 1 addition & 0 deletions pkg/stanza/docs/operators/file_input.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The `file_input` operator reads logs from files. It will place the lines read in
| `fingerprint_size` | `1kb` | The number of bytes with which to identify a file. The first bytes in the file are used as the fingerprint. Decreasing this value at any point will cause existing fingerprints to forgotten, meaning that all files will be read from the beginning (one time). |
| `max_log_size` | `1MiB` | The maximum size of a log entry to read before failing. Protects against reading large amounts of data into memory |.
| `max_concurrent_files` | 1024 | The maximum number of log files from which logs will be read concurrently (minimum = 2). If the number of files matched in the `include` pattern exceeds half of this number, then files will be processed in batches. |
| `max_batches` | 0 | Only applicable when files must be batched in order to respect max_concurrent_files. This value limits the number of batches that will be processed during a single poll interval. A value of 0 indicates no limit. |
cpheps marked this conversation as resolved.
Show resolved Hide resolved
| `delete_after_read` | `false` | If `true`, each log file will be read and then immediately deleted. Requires that the `filelog.allowFileDeletion` feature gate is enabled. |
| `attributes` | {} | A map of `key: value` pairs to add to the entry's attributes. |
| `resource` | {} | A map of `key: value` pairs to add to the entry's resource. |
Expand Down
8 changes: 8 additions & 0 deletions pkg/stanza/fileconsumer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package fileconsumer // import "github.com/open-telemetry/opentelemetry-collecto

import (
"bufio"
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -51,6 +52,7 @@ func NewConfig() *Config {
FingerprintSize: DefaultFingerprintSize,
MaxLogSize: defaultMaxLogSize,
MaxConcurrentFiles: defaultMaxConcurrentFiles,
MaxBatches: 0,
}
}

Expand All @@ -66,6 +68,7 @@ type Config struct {
FingerprintSize helper.ByteSize `mapstructure:"fingerprint_size,omitempty"`
MaxLogSize helper.ByteSize `mapstructure:"max_log_size,omitempty"`
MaxConcurrentFiles int `mapstructure:"max_concurrent_files,omitempty"`
MaxBatches int `mapstructure:"max_batches,omitempty"`
DeleteAfterRead bool `mapstructure:"delete_after_read,omitempty"`
Splitter helper.SplitterConfig `mapstructure:",squash,omitempty"`
}
Expand Down Expand Up @@ -139,6 +142,7 @@ func (c Config) buildManager(logger *zap.SugaredLogger, emit EmitFunc, factory s
roller: newRoller(),
pollInterval: c.PollInterval,
maxBatchFiles: c.MaxConcurrentFiles / 2,
maxBatches: c.MaxBatches,
deleteAfterRead: c.DeleteAfterRead,
knownFiles: make([]*Reader, 0, 10),
seenPaths: make(map[string]struct{}, 100),
Expand Down Expand Up @@ -182,6 +186,10 @@ func (c Config) validate() error {
return fmt.Errorf("`delete_after_read` cannot be used with `start_at: end`")
}

if c.MaxBatches < 0 {
return errors.New("`max_batches` must not be negative")
}

_, err := c.Splitter.EncodingConfig.Build()
if err != nil {
return err
Expand Down
26 changes: 26 additions & 0 deletions pkg/stanza/fileconsumer/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,14 @@ func TestUnmarshal(t *testing.T) {
return newMockOperatorConfig(cfg)
}(),
},
{
Name: "max_batches_1",
Expect: func() *mockOperatorConfig {
cfg := NewConfig()
cfg.MaxBatches = 1
return newMockOperatorConfig(cfg)
}(),
},
},
}.Run(t)
}
Expand Down Expand Up @@ -492,6 +500,24 @@ func TestBuild(t *testing.T) {
require.Error,
nil,
},
{
"InvalidMaxBatches",
func(f *Config) {
f.MaxBatches = -1
},
require.Error,
nil,
},
{
"ValidMaxBatches",
func(f *Config) {
f.MaxBatches = 6
},
require.NoError,
func(t *testing.T, m *Manager) {
require.Equal(t, 6, m.maxBatches)
},
},
}

for _, tc := range cases {
Expand Down
13 changes: 13 additions & 0 deletions pkg/stanza/fileconsumer/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type Manager struct {
persister operator.Persister

pollInterval time.Duration
maxBatches int
maxBatchFiles int
deleteAfterRead bool

Expand Down Expand Up @@ -112,10 +113,22 @@ func (m *Manager) poll(ctx context.Context) {
m.knownFiles[i].generation++
}

// Used to keep track of the number of batches processed in this poll cycle
batchesProcessed := 0

// Get the list of paths on disk
matches := m.finder.FindFiles()
for len(matches) > m.maxBatchFiles {
m.consume(ctx, matches[:m.maxBatchFiles])

// If a maxBatches is set, check if we have hit the limit
if m.maxBatches != 0 {
batchesProcessed++
if batchesProcessed >= m.maxBatches {
return
}
}

matches = matches[m.maxBatchFiles:]
}
m.consume(ctx, matches)
Expand Down
90 changes: 90 additions & 0 deletions pkg/stanza/fileconsumer/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -856,13 +856,16 @@ func TestFileBatching(t *testing.T) {
linesPerFile := 10
maxConcurrentFiles := 20
maxBatchFiles := maxConcurrentFiles / 2
// Explicitly setting maxBatches to ensure a value of 0 does not enforce a limit
maxBatches := 0

expectedBatches := files / maxBatchFiles // assumes no remainder

tempDir := t.TempDir()
cfg := NewConfig().includeDir(tempDir)
cfg.StartAt = "beginning"
cfg.MaxConcurrentFiles = maxConcurrentFiles
cfg.MaxBatches = maxBatches
emitCalls := make(chan *emitParams, files*linesPerFile)
operator := buildTestManagerWithEmit(t, cfg, emitCalls)
operator.persister = testutil.NewMockPersister("test")
Expand Down Expand Up @@ -1232,3 +1235,90 @@ func TestDeleteAfterRead(t *testing.T) {
require.True(t, os.IsNotExist(err))
}
}

func TestMaxBatching(t *testing.T) {
t.Parallel()

files := 50
linesPerFile := 10
maxConcurrentFiles := 20
maxBatchFiles := maxConcurrentFiles / 2
maxBatches := 2

expectedBatches := maxBatches
expectedMaxFilesPerPoll := maxBatches * maxBatchFiles

tempDir := t.TempDir()
cfg := NewConfig().includeDir(tempDir)
cfg.StartAt = "beginning"
cfg.MaxConcurrentFiles = maxConcurrentFiles
cfg.MaxBatches = maxBatches
emitCalls := make(chan *emitParams, files*linesPerFile)
operator := buildTestManagerWithEmit(t, cfg, emitCalls)
operator.persister = testutil.NewMockPersister("test")

core, observedLogs := observer.New(zap.DebugLevel)
operator.SugaredLogger = zap.New(core).Sugar()

temps := make([]*os.File, 0, files)
for i := 0; i < files; i++ {
temps = append(temps, openTemp(t, tempDir))
}

// Write logs to each file
numExpectedTokens := expectedMaxFilesPerPoll * linesPerFile
for i, temp := range temps {
for j := 0; j < linesPerFile; j++ {
message := fmt.Sprintf("%s %d %d", tokenWithLength(100), i, j)
_, err := temp.WriteString(message + "\n")
require.NoError(t, err)
}
}

// Poll and wait for all lines
operator.poll(context.Background())
actualTokens := make([][]byte, 0, numExpectedTokens)
actualTokens = append(actualTokens, waitForNTokens(t, emitCalls, numExpectedTokens)...)
require.Len(t, actualTokens, numExpectedTokens)

// During the first poll, we expect one log per batch and one log per file
require.Equal(t, expectedMaxFilesPerPoll+expectedBatches, observedLogs.Len())
logNum := 0
for b := 0; b < expectedBatches; b++ {
log := observedLogs.All()[logNum]
require.Equal(t, "Consuming files", log.Message)
require.Equal(t, zapcore.DebugLevel, log.Level)
logNum++

for f := 0; f < maxBatchFiles; f++ {
log = observedLogs.All()[logNum]
require.Equal(t, "Started watching file", log.Message)
require.Equal(t, zapcore.InfoLevel, log.Level)
logNum++
}
}

// Write more logs to each file so we can validate that all files are still known
for i, temp := range temps {
for j := 0; j < linesPerFile; j++ {
message := fmt.Sprintf("%s %d %d", tokenWithLength(20), i, j)
_, err := temp.WriteString(message + "\n")
require.NoError(t, err)
}
}

// Poll again and wait for all new lines
operator.poll(context.Background())
actualTokens = make([][]byte, 0, numExpectedTokens)
actualTokens = append(actualTokens, waitForNTokens(t, emitCalls, numExpectedTokens)...)
require.Len(t, actualTokens, numExpectedTokens)

// During the second poll, we only expect one log per batch
require.Equal(t, expectedMaxFilesPerPoll+expectedBatches*2, observedLogs.Len())
for b := logNum; b < observedLogs.Len(); b++ {
log := observedLogs.All()[logNum]
require.Equal(t, "Consuming files", log.Message)
require.Equal(t, zapcore.DebugLevel, log.Level)
logNum++
}
}
3 changes: 3 additions & 0 deletions pkg/stanza/fileconsumer/testdata/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,6 @@ poll_interval_no_units:
start_at_string:
type: mock
start_at: "beginning"
max_batches_1:
type: mock
max_batches: 1
5 changes: 3 additions & 2 deletions receiver/filelogreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
| Stability | [beta] |
| Supported pipeline types | logs |
| Distributions | [contrib] |

kkjkh
cpheps marked this conversation as resolved.
Show resolved Hide resolved
Tails and parses logs from files.

## Configuration
Expand All @@ -25,7 +25,8 @@ Tails and parses logs from files.
| `poll_interval` | 200ms | The duration between filesystem polls |
| `fingerprint_size` | `1kb` | The number of bytes with which to identify a file. The first bytes in the file are used as the fingerprint. Decreasing this value at any point will cause existing fingerprints to forgotten, meaning that all files will be read from the beginning (one time) |
| `max_log_size` | `1MiB` | The maximum size of a log entry to read. A log entry will be truncated if it is larger than `max_log_size`. Protects against reading large amounts of data into memory |
| `max_concurrent_files` | 1024 | The maximum number of log files from which logs will be read concurrently. If the number of files matched in the `include` pattern exceeds this number, then files will be processed in batches. One batch will be processed per `poll_interval` |
| `max_concurrent_files` | 1024 | The maximum number of log files from which logs will be read concurrently. If the number of files matched in the `include` pattern exceeds this number, then files will be processed in batches. |
| `max_batches` | 0 | Only applicable when files must be batched in order to respect max_concurrent_files. This value limits the number of batches that will be processed during a single poll interval. A value of 0 indicates no limit. |
| `delete_after_read` | `false` | If `true`, each log file will be read and then immediately deleted. Requires that the `filelog.allowFileDeletion` feature gate is enabled. |
| `attributes` | {} | A map of `key: value` pairs to add to the entry's attributes |
| `resource` | {} | A map of `key: value` pairs to add to the entry's resource |
Expand Down