Skip to content

Commit e82c43c

Browse files
authored
system-logs input ignores folders and add tests (#41296)
The system-logs input now does not count folders as an "existing file" when looking for files to decide between the journald and log inputs. Unit and integration tests are added for the system-logs input.
1 parent d2e6603 commit e82c43c

5 files changed

Lines changed: 340 additions & 4 deletions

File tree

filebeat/input/systemlogs/input.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package systemlogs
2020
import (
2121
"errors"
2222
"fmt"
23+
"os"
2324
"path/filepath"
2425

2526
"github.com/elastic/beats/v7/filebeat/channel"
@@ -145,17 +146,29 @@ func useJournald(c *conf.C) (bool, error) {
145146
if err != nil {
146147
return false, fmt.Errorf("cannot resolve glob: %w", err)
147148
}
148-
if len(paths) != 0 {
149-
// We found at least one system log file,
150-
// journald will not be used, return early
151-
logger.Info(
149+
150+
for _, p := range paths {
151+
stat, err := os.Stat(p)
152+
if err != nil {
153+
return false, fmt.Errorf("cannot stat '%s': %w", p, err)
154+
}
155+
156+
// Ignore directories
157+
if stat.IsDir() {
158+
continue
159+
}
160+
161+
// We found one file, return early
162+
logger.Infof(
152163
"using log input because file(s) was(were) found when testing glob '%s'",
153164
g)
154165
return false, nil
155166
}
156167
}
157168

158169
// if no system log files are found, then use jounrald
170+
logger.Info("no files were found, using journald input")
171+
159172
return true, nil
160173
}
161174

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//go:build linux
19+
20+
package systemlogs
21+
22+
import (
23+
"testing"
24+
25+
conf "github.com/elastic/elastic-agent-libs/config"
26+
)
27+
28+
func TestJournaldInputIsCreated(t *testing.T) {
29+
c := map[string]any{
30+
"files.paths": []string{"/file/does/not/exist"},
31+
// The 'journald' object needs to exist for the input to be instantiated
32+
"journald.enabled": true,
33+
}
34+
35+
cfg := conf.MustNewConfigFrom(c)
36+
37+
_, inp, err := configure(cfg)
38+
if err != nil {
39+
t.Fatalf("did not expect an error calling newV1Input: %s", err)
40+
}
41+
42+
type namer interface {
43+
Name() string
44+
}
45+
46+
i, isNamer := inp.(namer)
47+
if !isNamer {
48+
t.Fatalf("expecting an instance of *log.Input, got '%T' instead", inp)
49+
}
50+
51+
if got, expected := i.Name(), "journald"; got != expected {
52+
t.Fatalf("expecting '%s' input, got '%s'", expected, got)
53+
}
54+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package systemlogs
19+
20+
import (
21+
"os"
22+
"testing"
23+
24+
"github.com/elastic/beats/v7/filebeat/channel"
25+
"github.com/elastic/beats/v7/filebeat/input"
26+
"github.com/elastic/beats/v7/filebeat/input/log"
27+
"github.com/elastic/beats/v7/libbeat/beat"
28+
conf "github.com/elastic/elastic-agent-libs/config"
29+
)
30+
31+
func generateFile(t *testing.T) string {
32+
// Create a know file for testing, the content is not relevant
33+
// it just needs to exist
34+
knwonFile, err := os.CreateTemp(t.TempDir(), t.Name()+"knwonFile*")
35+
if err != nil {
36+
t.Fatalf("cannot create temporary file: %s", err)
37+
}
38+
39+
if _, err := knwonFile.WriteString("Bowties are cool"); err != nil {
40+
t.Fatalf("cannot write to temporary file '%s': %s", knwonFile.Name(), err)
41+
}
42+
knwonFile.Close()
43+
44+
return knwonFile.Name()
45+
}
46+
47+
func TestUseJournald(t *testing.T) {
48+
filename := generateFile(t)
49+
50+
testCases := map[string]struct {
51+
cfg map[string]any
52+
useJournald bool
53+
expectErr bool
54+
}{
55+
"No files found": {
56+
cfg: map[string]any{
57+
"files.paths": []string{"/file/does/not/exist"},
58+
},
59+
useJournald: true,
60+
},
61+
"File exists": {
62+
cfg: map[string]any{
63+
"files.paths": []string{filename},
64+
},
65+
useJournald: false,
66+
},
67+
"use_journald is true": {
68+
cfg: map[string]any{
69+
"use_journald": true,
70+
"journald": struct{}{},
71+
},
72+
useJournald: true,
73+
},
74+
"use_files is true": {
75+
cfg: map[string]any{
76+
"use_files": true,
77+
"journald": nil,
78+
"files": struct{}{},
79+
},
80+
useJournald: false,
81+
},
82+
"use_journald and use_files are true": {
83+
cfg: map[string]any{
84+
"use_files": true,
85+
"use_journald": true,
86+
"journald": struct{}{},
87+
},
88+
useJournald: false,
89+
expectErr: true,
90+
},
91+
}
92+
93+
for name, tc := range testCases {
94+
t.Run(name, func(t *testing.T) {
95+
cfg := conf.MustNewConfigFrom(tc.cfg)
96+
97+
useJournald, err := useJournald(cfg)
98+
if !tc.expectErr && err != nil {
99+
t.Fatalf("did not expect an error calling 'useJournald': %s", err)
100+
}
101+
if tc.expectErr && err == nil {
102+
t.Fatal("expecting an error when calling 'userJournald', got none")
103+
}
104+
105+
if useJournald != tc.useJournald {
106+
t.Fatalf("expecting 'useJournald' to be %t, got %t",
107+
tc.useJournald, useJournald)
108+
}
109+
})
110+
}
111+
}
112+
113+
func TestLogInputIsInstantiated(t *testing.T) {
114+
filename := generateFile(t)
115+
c := map[string]any{
116+
"files.paths": []string{filename},
117+
}
118+
119+
cfg := conf.MustNewConfigFrom(c)
120+
121+
inp, err := newV1Input(cfg, connectorMock{}, input.Context{})
122+
if err != nil {
123+
t.Fatalf("did not expect an error calling newV1Input: %s", err)
124+
}
125+
_, isLogInput := inp.(*log.Input)
126+
if !isLogInput {
127+
t.Fatalf("expecting an instance of *log.Input, got '%T' instead", inp)
128+
}
129+
}
130+
131+
type connectorMock struct{}
132+
133+
func (mock connectorMock) Connect(c *conf.C) (channel.Outleter, error) {
134+
return outleterMock{}, nil
135+
}
136+
137+
func (mock connectorMock) ConnectWith(c *conf.C, clientConfig beat.ClientConfig) (channel.Outleter, error) {
138+
return outleterMock{}, nil
139+
}
140+
141+
type outleterMock struct{}
142+
143+
func (o outleterMock) Close() error { return nil }
144+
func (o outleterMock) Done() <-chan struct{} { return make(chan struct{}) }
145+
func (o outleterMock) OnEvent(beat.Event) bool { return false }
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//go:build integration && linux
19+
20+
package integration
21+
22+
import (
23+
_ "embed"
24+
"fmt"
25+
"os"
26+
"path"
27+
"path/filepath"
28+
"testing"
29+
"time"
30+
31+
cp "github.com/otiai10/copy"
32+
33+
"github.com/elastic/beats/v7/libbeat/tests/integration"
34+
)
35+
36+
//go:embed testdata/filebeat_system_module.yml
37+
var systemModuleCfg string
38+
39+
// TestSystemLogsCanUseJournald aims to ensure the system-logs input can
40+
// correctly choose and start a journald input when the globs defined in
41+
// var.paths do not resolve to any file.
42+
func TestSystemLogsCanUseJournaldInput(t *testing.T) {
43+
filebeat := integration.NewBeat(
44+
t,
45+
"filebeat",
46+
"../../filebeat.test",
47+
)
48+
workDir := filebeat.TempDir()
49+
copyModulesDir(t, workDir)
50+
51+
// As the name says, we want this folder to exist bu t be empty
52+
globWithoutFiles := filepath.Join(filebeat.TempDir(), "this-folder-does-not-exist")
53+
yamlCfg := fmt.Sprintf(systemModuleCfg, globWithoutFiles, workDir)
54+
55+
filebeat.WriteConfigFile(yamlCfg)
56+
filebeat.Start()
57+
58+
filebeat.WaitForLogs(
59+
"no files were found, using journald input",
60+
10*time.Second,
61+
"system-logs did not select journald input")
62+
filebeat.WaitForLogs(
63+
"journalctl started with PID",
64+
10*time.Second,
65+
"system-logs did not start journald input")
66+
}
67+
68+
func TestSystemLogsCanUseLogInput(t *testing.T) {
69+
filebeat := integration.NewBeat(
70+
t,
71+
"filebeat",
72+
"../../filebeat.test",
73+
)
74+
workDir := filebeat.TempDir()
75+
copyModulesDir(t, workDir)
76+
77+
logFilePath := path.Join(workDir, "syslog")
78+
integration.GenerateLogFile(t, logFilePath, 5, false)
79+
yamlCfg := fmt.Sprintf(systemModuleCfg, logFilePath, workDir)
80+
81+
filebeat.WriteConfigFile(yamlCfg)
82+
filebeat.Start()
83+
84+
filebeat.WaitForLogs(
85+
"using log input because file(s) was(were) found",
86+
10*time.Second,
87+
"system-logs did not select the log input")
88+
filebeat.WaitForLogs(
89+
"Harvester started for paths:",
90+
10*time.Second,
91+
"system-logs did not start the log input")
92+
}
93+
94+
func copyModulesDir(t *testing.T, dst string) {
95+
pwd, err := os.Getwd()
96+
if err != nil {
97+
t.Fatalf("cannot get the current directory: %s", err)
98+
}
99+
localModules := filepath.Join(pwd, "../", "../", "module")
100+
localModulesD := filepath.Join(pwd, "../", "../", "modules.d")
101+
102+
if err := cp.Copy(localModules, filepath.Join(dst, "module")); err != nil {
103+
t.Fatalf("cannot copy 'module' folder to test folder: %s", err)
104+
}
105+
if err := cp.Copy(localModulesD, filepath.Join(dst, "modules.d")); err != nil {
106+
t.Fatalf("cannot copy 'modules.d' folder to test folder: %s", err)
107+
}
108+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
filebeat.modules:
2+
- module: system
3+
syslog:
4+
enabled: true
5+
var.paths:
6+
- "%s"
7+
8+
path.home: %s
9+
10+
queue.mem:
11+
flush.timeout: 0
12+
13+
output:
14+
file:
15+
path: ${path.home}
16+
filename: "output"

0 commit comments

Comments
 (0)