Skip to content
Open
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
78fbce1
[WIP] PoC to run Filestream as log input
belimawr Sep 12, 2025
5df3a38
mage update
belimawr Sep 12, 2025
d84f890
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Sep 22, 2025
937cc57
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Oct 6, 2025
1dbbe4a
Update configuration flag
belimawr Oct 6, 2025
d5fd62a
[WIP] Add happy path integration tests
belimawr Oct 6, 2025
44ba92f
improve tests
belimawr Oct 7, 2025
2d4d74e
Only run under EA and update tests
belimawr Oct 7, 2025
8aab9a7
[WIP] Config translate
belimawr Oct 7, 2025
04225e3
[WIP] Test for parsing config
belimawr Oct 7, 2025
fb963d4
Add feature flag to run log input as filestream
belimawr Oct 8, 2025
65f2dae
try fixing parsers
belimawr Oct 8, 2025
bb3008c
reformat code/fix code
belimawr Oct 8, 2025
1fbe05d
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Oct 8, 2025
c9a3264
New approach for config translation
belimawr Oct 8, 2025
e74987d
Add json parser and ensure Filestream config is valid
belimawr Oct 8, 2025
5c85111
Fix boolean types
belimawr Oct 8, 2025
1c0b381
Merge configs and remove log input only fields
belimawr Oct 8, 2025
fc22095
organise yaml
belimawr Oct 8, 2025
6ff022a
Set type and take_over
belimawr Oct 8, 2025
fd25b68
refactor code
belimawr Oct 8, 2025
22581aa
Rename test file
belimawr Oct 9, 2025
5030e8b
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Oct 9, 2025
0711490
cleanup tests
belimawr Oct 9, 2025
95d0095
handle file identity
belimawr Oct 9, 2025
68975b2
Fix Windows tests
belimawr Oct 9, 2025
56a0775
Correctly handle parsers
belimawr Oct 9, 2025
5e91c58
Convert Log input configuration to Filestream
belimawr Oct 9, 2025
eed3286
Fix error handling and linter warnings
belimawr Oct 9, 2025
68846f8
Merge branch 'log-to-filestream-config' of github.com:belimawr/beats …
belimawr Oct 9, 2025
2a803ed
Handle empty strings and empty objects
belimawr Oct 10, 2025
4386ba1
Refactor code
belimawr Oct 10, 2025
b0ff5ed
Ignore wrong types and greatly refactor code
belimawr Oct 10, 2025
4299e8b
Merge branch 'log-to-filestream-config' into POC-log-as-filestream
belimawr Oct 10, 2025
f27ac51
Refactor code and fix lint warnings
belimawr Oct 10, 2025
3db414f
Merge branch 'main' of github.com:elastic/beats into log-to-filestrea…
belimawr Oct 10, 2025
f51096d
update function call
belimawr Oct 10, 2025
6fbc623
Fix spelling and clean up comments
belimawr Oct 10, 2025
2042946
Remove Unknown configType
belimawr Oct 10, 2025
6f35111
Merge branch 'log-to-filestream-config' into POC-log-as-filestream
belimawr Oct 10, 2025
0f0956c
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Oct 20, 2025
3303f85
Revert making config struct exported for Log and Filestream inputs
belimawr Oct 20, 2025
3c6ff48
Improve comments/documentation
belimawr Oct 20, 2025
a3afeea
Validate Log input config on runAsFilestream
belimawr Oct 21, 2025
5117dd6
[WIP] Run Log input system tests with Filestream
belimawr Oct 21, 2025
a9818cc
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Oct 27, 2025
60cfbbf
test_modules.py working and test_close_renamed from test_harvester.py
belimawr Oct 27, 2025
3d2b833
support multiline and autodiscover python tests
belimawr Oct 29, 2025
1e615f6
Fix python test
belimawr Oct 29, 2025
75444b5
Fix fields description
belimawr Oct 29, 2025
072633d
Reload inputs tests, make the modules test actually use filestream
belimawr Oct 29, 2025
ac2c6eb
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Oct 29, 2025
2dd4cb0
mage check
belimawr Oct 30, 2025
9842567
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Oct 31, 2025
2e9f6cc
Update elastic-agent-libs
belimawr Oct 31, 2025
15e3422
Run Log input Python tests with Filestream
belimawr Oct 31, 2025
d76c4ad
Add more integration tests
belimawr Oct 31, 2025
d2e4059
refactor mage target name
belimawr Nov 1, 2025
c1f363c
PR improvements
belimawr Nov 1, 2025
09d0683
remove import
belimawr Nov 1, 2025
48529c7
fix failing test
belimawr Nov 3, 2025
efc6654
add unit tests for runAsFilestream
belimawr Nov 3, 2025
a9e539d
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Nov 4, 2025
b19a2aa
Add new test and use common prefix
belimawr Nov 4, 2025
76bf082
Refactor tests
belimawr Nov 4, 2025
3ddd356
Small refactoring
belimawr Nov 4, 2025
d979d76
Merge branch 'main' of github.com:elastic/beats into POC-log-as-files…
belimawr Nov 6, 2025
bbf93c7
make update/check
belimawr Nov 6, 2025
64385c0
Merge branch 'main' into POC-log-as-filestream
belimawr Nov 7, 2025
b819a97
Move mage PythonIntegTestLogAsFilestream to its own pipeline step
belimawr Nov 7, 2025
41824e4
Merge branch 'POC-log-as-filestream' of github.com:belimawr/beats int…
belimawr Nov 7, 2025
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
1 change: 1 addition & 0 deletions .buildkite/filebeat/filebeat-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ steps:
command: |
cd filebeat
mage pythonIntegTest
mage PythonIntegTestLogAsFilestream
retry:
automatic:
- limit: 1
Expand Down
16 changes: 16 additions & 0 deletions docs/reference/filebeat/exported-fields-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ Contains log file lines.
required: False


**`log.file.device_id`**
: The device ID used for the log file, this is used by the 'native' file identity.

type: keyword

required: False


**`log.file.inode`**
: The inode of the log file, this is used by the 'native' file identity.

type: long

required: False


**`stream`**
: Log stream when reading container logs, can be 'stdout' or 'stderr'

Expand Down
14 changes: 14 additions & 0 deletions filebeat/_meta/fields.common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@
description: >
The file offset the reported line starts at.
- name: log.file.device_id
type: keyword
required: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this targeting 9.3.0? Will it be in GA? Feel free to adjust as needed.

Suggested change
required: false
required: false
version:
ga: 9.3.0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those fields have existed since a long time, some of the tests introduced by this PR check the fields generated by the input against fields.yml, so it made sense to me to add them.

I could also modify the test to ignore those fields. Let me think a bit more about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those fields have existed since a long time

If they've existed since before 9.0.0, I'm ok with just leaving out versioning information.

description: >-
The device ID used for the log file, this is used by the
'native' file identity.
- name: log.file.inode
type: long
required: false
Copy link
Contributor

@colleenmcginnis colleenmcginnis Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this targeting 9.3.0? Will it be in GA? Feel free to adjust as needed.

Suggested change
required: false
required: false
version:
ga: 9.3.0

description: >-
The inode of the log file, this is used by the 'native' file
identity.
- name: stream
type: keyword
required: false
Expand Down
2 changes: 1 addition & 1 deletion filebeat/include/fields.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion filebeat/include/list.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions filebeat/input/default-inputs/inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package inputs
import (
"github.com/elastic/beats/v7/filebeat/input/filestream"
"github.com/elastic/beats/v7/filebeat/input/kafka"
"github.com/elastic/beats/v7/filebeat/input/logv2"
"github.com/elastic/beats/v7/filebeat/input/net/tcp"
"github.com/elastic/beats/v7/filebeat/input/net/udp"
"github.com/elastic/beats/v7/filebeat/input/unix"
Expand All @@ -44,5 +45,6 @@ func genericInputs(log *logp.Logger, components statestore.States) []v2.Plugin {
tcp.Plugin(),
udp.Plugin(),
unix.Plugin(),
logv2.PluginV2(log, components),
}
}
12 changes: 12 additions & 0 deletions filebeat/input/log/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@

// Input
if c.Type == harvester.LogType && len(c.Paths) == 0 {
return fmt.Errorf("No paths were defined for input")

Check failure on line 161 in filebeat/input/log/config.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

ST1005: error strings should not be capitalized (staticcheck)
}

if c.CleanInactive != 0 && c.IgnoreOlder == 0 {
Expand All @@ -172,12 +172,12 @@
// Harvester
if c.JSON != nil && len(c.JSON.MessageKey) == 0 &&
c.Multiline != nil {
return fmt.Errorf("When using the JSON decoder and multiline together, you need to specify a message_key value")

Check failure on line 175 in filebeat/input/log/config.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

ST1005: error strings should not be capitalized (staticcheck)
}

if c.JSON != nil && len(c.JSON.MessageKey) == 0 &&
(len(c.IncludeLines) > 0 || len(c.ExcludeLines) > 0) {
return fmt.Errorf("When using the JSON decoder and line filtering together, you need to specify a message_key value")

Check failure on line 180 in filebeat/input/log/config.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

ST1005: error strings should not be capitalized (staticcheck)
}

if c.ScanSort != "" {
Expand All @@ -196,7 +196,7 @@
return nil
}

// checkUnsupportedParams checks if unsupported/deprecated/discouraged paramaters are set and logs a warning

Check failure on line 199 in filebeat/input/log/config.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

`paramaters` is a misspelling of `parameters` (misspell)
func (c config) checkUnsupportedParams(logger *logp.Logger) {
if c.ScanSort != "" {
logger.Warn(cfgwarn.Experimental("scan_sort is used."))
Expand Down Expand Up @@ -232,10 +232,22 @@
for _, path := range c.Paths {
pathAbs, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("Failed to get the absolute path for %s: %v", path, err)

Check failure on line 235 in filebeat/input/log/config.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}
paths = append(paths, pathAbs)
}
c.Paths = paths
return nil
}

// IsConfigValid is meant to be used by logv2 to validate whether cfg is
// a valid Log input configuration.
// It avoids exporting [config] and [defaultConfig]
func IsConfigValid(cfg *conf.C) error {
c := defaultConfig()
if err := cfg.Unpack(&c); err != nil {
return fmt.Errorf("cannot unpack config: %w", err)
}

return c.Validate()
}
12 changes: 5 additions & 7 deletions filebeat/input/log/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ import (
"github.com/elastic/elastic-agent-libs/monitoring"
)

// The Log input does not define an init function, nor registers itself
// because the 'logv2' input already does it using 'log' as the input Name.
// 'logv2' is a "proxy input" that can run either the Log input of Filestream
// input depending on configuration and feature flag.

const (
recursiveGlobDepth = 8
harvesterErrMsg = "Harvester could not be started on new file: %s, Err: %s"
Expand All @@ -60,13 +65,6 @@ var (
errDeprecated = errors.New("Log input is deprecated. Use Filestream input instead. Follow our migration guide https://www.elastic.co/guide/en/beats/filebeat/current/migrate-to-filestream.html")
)

func init() {
err := input.Register("log", NewInput)
if err != nil {
panic(err)
}
}

// Input contains the input and its config
type Input struct {
cfg *conf.C
Expand Down
182 changes: 182 additions & 0 deletions filebeat/input/logv2/input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package logv2

import (
"errors"
"fmt"

"github.com/elastic/beats/v7/filebeat/channel"
v1 "github.com/elastic/beats/v7/filebeat/input"
"github.com/elastic/beats/v7/filebeat/input/filestream"
loginput "github.com/elastic/beats/v7/filebeat/input/log"
v2 "github.com/elastic/beats/v7/filebeat/input/v2"
"github.com/elastic/beats/v7/libbeat/feature"
"github.com/elastic/beats/v7/libbeat/features"
"github.com/elastic/beats/v7/libbeat/management"
"github.com/elastic/beats/v7/libbeat/statestore"
"github.com/elastic/elastic-agent-libs/config"
"github.com/elastic/elastic-agent-libs/logp"
"github.com/elastic/go-concert/unison"
)

const pluginName = "log"

func init() {
// Register an input V1, to replace the Log input one.
if err := v1.Register(pluginName, newV1Input); err != nil {
panic(err)
}
}

// runAsFilestream validates cfg as a Log input configuration, if it is
// valid, then checks whether the configuration should be run as
// Filestream input. On any error the boolean value must be ignore and
// no input started. runAsFilestream also sets the input type accordingly.
func runAsFilestream(cfg *config.C) (bool, error) {
// First of all, ensure the Log input configuration is valid.
// This ensures we return configuration errors compatible
// with the log input.
if err := loginput.IsConfigValid(cfg); err != nil {
return false, err
}

// Global feature flag that forces all Log input instances
// to run as Filestream.
if features.LogInputRunFilestream() {
return true, nil
}

// Only allow to run the Log input as Filestream if Filebeat
// is running under Elastic Agent.
if !management.UnderAgent() {
return false, nil
}

if ok := cfg.HasField("run_as_filestream"); ok {
runAsFilestream, err := cfg.Bool("run_as_filestream", -1)
if err != nil {
return false, fmt.Errorf("cannot parse 'run_as_filestream': %w", err)
}

if runAsFilestream {
// ID is required to run as Filestream input
if !cfg.HasField("id") {
return false, errors.New("'id' is required to run 'log' input as 'filestream'")
}

// This should never fail because the Log input configuration
// already reads 'type' and validates it is a string. Overriding
// the field should never fail.
if err := cfg.SetString("type", -1, "filestream"); err != nil {
return false, fmt.Errorf("cannot set 'type': %w", err)
}

return true, nil
}
}

return false, nil
}

// newV1Input instantiates the Log input. If 'run_as_filestream' is
// true, then v2.ErrUnknownInput is returned so the Filestream input
// can be instantiated.
func newV1Input(
cfg *config.C,
outlet channel.Connector,
context v1.Context,
logger *logp.Logger,
) (v1.Input, error) {
// Inputs V1 should be tried last, so if this function is run we are
// supposed to be running as the Log input. However do not rely on the
// factory implementation, also check whether to run as Log or Filestream
// inputs.
asFilestream, err := runAsFilestream(cfg)
if err != nil {
return nil, err
}

if asFilestream {
return nil, v2.ErrUnknownInput
}

inp, err := loginput.NewInput(cfg, outlet, context, logger)
if err != nil {
return nil, fmt.Errorf("cannot create log input: %w", err)
}

logger.Debug("Log input running as Log input")
return inp, err
}

// PluginV2 returns a v2.Plugin with a manager that checks whether
// the config is from a Log input that should run as Filestream.
// If that is the case the Log input configuration is converted to
// Filestream and the Filestream input returned.
// Otherwise v2.ErrUnknownInput is returned.
func PluginV2(logger *logp.Logger, store statestore.States) v2.Plugin {
// The InputManager for Filestream input is from an internal package, so we
// cannot instantiate it directly here. To circumvent that, we instantiate
// the whole Filestream Plugin
filestreamPlugin := filestream.Plugin(logger, store)

m := manager{
next: filestreamPlugin.Manager,
logger: logger,
}

p := v2.Plugin{
Name: pluginName,
Stability: feature.Stable,
Info: "log input running filestream",
Doc: "Log input running Filestream input",
Manager: m,
}
return p
}

type manager struct {
next v2.InputManager
logger *logp.Logger
}

func (m manager) Init(grp unison.Group) error {
return m.next.Init(grp)
}

func (m manager) Create(cfg *config.C) (v2.Input, error) {
// When inputs are created, inputs V2 are tried first, so if we
// are supposed to run as the Log input, return v2.ErrUnknownInput
asFilestream, err := runAsFilestream(cfg)
if err != nil {
return nil, err
}

if !asFilestream {
return nil, v2.ErrUnknownInput
}

newCfg, err := convertConfig(m.logger, cfg)
if err != nil {
return nil, fmt.Errorf("cannot translate log config to filestream: %w", err)
}

m.logger.Debug("Log input running as Filestream input")
return m.next.Create(newCfg)
}
90 changes: 90 additions & 0 deletions filebeat/input/logv2/input_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package logv2

import (
"testing"

"github.com/elastic/beats/v7/libbeat/management"
"github.com/elastic/elastic-agent-libs/config"
)

func TestRunAsFilestream(t *testing.T) {
testCases := map[string]struct {
cfg *config.C
expectErr bool
expected bool
underAgent bool
}{
"simplest log input config": {
underAgent: true,
expected: false,
cfg: config.MustNewConfigFrom(map[string]any{
"paths": []string{"/var/log.log"},
}),
},
"log input invalid config": {
// empty config is always invalid
cfg: config.NewConfig(),
expectErr: true,
},
"invalid 'run_as_filestream'": {
underAgent: true,
cfg: config.MustNewConfigFrom(map[string]any{
"paths": []string{"/var/log.log"},
"run_as_filestream": 42,
}),
expectErr: true,
},
"no filestream id": {
underAgent: true,
cfg: config.MustNewConfigFrom(map[string]any{
"paths": []string{"/var/log.log"},
"run_as_filestream": true,
}),
expectErr: true,
},
"not under Elastic Agent": {
underAgent: false,
cfg: config.MustNewConfigFrom(map[string]any{
"paths": []string{"/var/log.log"},
}),
expectErr: false,
expected: false,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
underAgent := management.UnderAgent()
t.Cleanup(func() {
management.SetUnderAgent(underAgent)
})
management.SetUnderAgent(tc.underAgent)

got, err := runAsFilestream(tc.cfg)
if err != nil && !tc.expectErr {
t.Errorf("did not expect an error: %s", err)
}

if got != tc.expected {
t.Errorf("expecting 'runAsFilestream' to return %t, got %t", tc.expected, got)
}
})
}
}
Loading
Loading