Skip to content

Commit a57171a

Browse files
authored
feat: [sc-108689] troubleshoot: journald collector (#1586)
* add schema for Journald Host Collector * implement journald host collector * update host collector * add --no-pager
1 parent 01d5804 commit a57171a

File tree

9 files changed

+378
-0
lines changed

9 files changed

+378
-0
lines changed

config/crds/troubleshoot.sh_hostcollectors.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,35 @@ spec:
14581458
exclude:
14591459
type: BoolString
14601460
type: object
1461+
journald:
1462+
properties:
1463+
collectorName:
1464+
type: string
1465+
dmesg:
1466+
type: boolean
1467+
exclude:
1468+
type: BoolString
1469+
lines:
1470+
type: integer
1471+
output:
1472+
type: string
1473+
reverse:
1474+
type: boolean
1475+
since:
1476+
type: string
1477+
system:
1478+
type: boolean
1479+
timeout:
1480+
type: string
1481+
units:
1482+
items:
1483+
type: string
1484+
type: array
1485+
until:
1486+
type: string
1487+
utc:
1488+
type: boolean
1489+
type: object
14611490
kernelConfigs:
14621491
properties:
14631492
collectorName:

config/crds/troubleshoot.sh_hostpreflights.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,35 @@ spec:
14581458
exclude:
14591459
type: BoolString
14601460
type: object
1461+
journald:
1462+
properties:
1463+
collectorName:
1464+
type: string
1465+
dmesg:
1466+
type: boolean
1467+
exclude:
1468+
type: BoolString
1469+
lines:
1470+
type: integer
1471+
output:
1472+
type: string
1473+
reverse:
1474+
type: boolean
1475+
since:
1476+
type: string
1477+
system:
1478+
type: boolean
1479+
timeout:
1480+
type: string
1481+
units:
1482+
items:
1483+
type: string
1484+
type: array
1485+
until:
1486+
type: string
1487+
utc:
1488+
type: boolean
1489+
type: object
14611490
kernelConfigs:
14621491
properties:
14631492
collectorName:

config/crds/troubleshoot.sh_supportbundles.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20057,6 +20057,35 @@ spec:
2005720057
exclude:
2005820058
type: BoolString
2005920059
type: object
20060+
journald:
20061+
properties:
20062+
collectorName:
20063+
type: string
20064+
dmesg:
20065+
type: boolean
20066+
exclude:
20067+
type: BoolString
20068+
lines:
20069+
type: integer
20070+
output:
20071+
type: string
20072+
reverse:
20073+
type: boolean
20074+
since:
20075+
type: string
20076+
system:
20077+
type: boolean
20078+
timeout:
20079+
type: string
20080+
units:
20081+
items:
20082+
type: string
20083+
type: array
20084+
until:
20085+
type: string
20086+
utc:
20087+
type: boolean
20088+
type: object
2006020089
kernelConfigs:
2006120090
properties:
2006220091
collectorName:

pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,20 @@ type HostKernelConfigs struct {
199199
HostCollectorMeta `json:",inline" yaml:",inline"`
200200
}
201201

202+
type HostJournald struct {
203+
HostCollectorMeta `json:",inline" yaml:",inline"`
204+
System bool `json:"system,omitempty" yaml:"system,omitempty"`
205+
Dmesg bool `json:"dmesg,omitempty" yaml:"dmesg,omitempty"`
206+
Units []string `json:"units,omitempty" yaml:"units,omitempty"`
207+
Since string `json:"since,omitempty" yaml:"since,omitempty"`
208+
Until string `json:"until,omitempty" yaml:"until,omitempty"`
209+
Output string `json:"output,omitempty" yaml:"output,omitempty"`
210+
Lines int `json:"lines,omitempty" yaml:"lines,omitempty"`
211+
Reverse bool `json:"reverse,omitempty" yaml:"reverse,omitempty"`
212+
Utc bool `json:"utc,omitempty" yaml:"utc,omitempty"`
213+
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
214+
}
215+
202216
type HostCollect struct {
203217
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
204218
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -224,6 +238,7 @@ type HostCollect struct {
224238
HostRun *HostRun `json:"run,omitempty" yaml:"run,omitempty"`
225239
HostCopy *HostCopy `json:"copy,omitempty" yaml:"copy,omitempty"`
226240
HostKernelConfigs *HostKernelConfigs `json:"kernelConfigs,omitempty" yaml:"kernelConfigs,omitempty"`
241+
HostJournald *HostJournald `json:"journald,omitempty" yaml:"journald,omitempty"`
227242
HostCGroups *HostCGroups `json:"cgroups,omitempty" yaml:"cgroups,omitempty"`
228243
}
229244

pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/collect/host_collector.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str
6363
return &CollectHostCopy{collector.HostCopy, bundlePath}, true
6464
case collector.HostKernelConfigs != nil:
6565
return &CollectHostKernelConfigs{collector.HostKernelConfigs, bundlePath}, true
66+
case collector.HostJournald != nil:
67+
return &CollectHostJournald{collector.HostJournald, bundlePath}, true
6668
case collector.HostCGroups != nil:
6769
return &CollectHostCGroups{collector.HostCGroups, bundlePath}, true
6870
default:

pkg/collect/host_journald.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package collect
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"os/exec"
9+
"path/filepath"
10+
"strconv"
11+
"time"
12+
13+
"github.com/pkg/errors"
14+
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
15+
"k8s.io/klog/v2"
16+
)
17+
18+
type CollectHostJournald struct {
19+
hostCollector *troubleshootv1beta2.HostJournald
20+
BundlePath string
21+
}
22+
23+
const HostJournaldPath = `host-collectors/journald/`
24+
25+
func (c *CollectHostJournald) Title() string {
26+
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "journald")
27+
}
28+
29+
func (c *CollectHostJournald) IsExcluded() (bool, error) {
30+
return isExcluded(c.hostCollector.Exclude)
31+
}
32+
33+
func (c *CollectHostJournald) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
34+
35+
// collector name check
36+
collectorName := c.hostCollector.CollectorName
37+
if collectorName == "" {
38+
return nil, errors.New("collector name is required")
39+
}
40+
41+
// timeout check
42+
timeout, err := getTimeout(c.hostCollector.Timeout)
43+
if err != nil {
44+
return nil, errors.Wrap(err, "failed to parse timeout")
45+
}
46+
47+
// set timeout context
48+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
49+
defer cancel()
50+
51+
// prepare command options
52+
cmdOptions, err := generateOptions(c.hostCollector)
53+
if err != nil {
54+
return nil, errors.Wrap(err, "failed to generate journalctl options")
55+
}
56+
57+
// run journalctl and capture output
58+
klog.V(2).Infof("Running journalctl with options: %v", cmdOptions)
59+
var stdout, stderr bytes.Buffer
60+
cmd := exec.CommandContext(ctx, "journalctl", cmdOptions...)
61+
cmd.Stdout = &stdout
62+
cmd.Stderr = &stderr
63+
64+
cmdInfo := HostRunInfo{
65+
Command: cmd.String(),
66+
ExitCode: "0",
67+
}
68+
69+
if err := cmd.Run(); err != nil {
70+
klog.V(2).Infof("journalctl command failed: %v", err)
71+
if err == context.DeadlineExceeded {
72+
cmdInfo.ExitCode = "124"
73+
cmdInfo.Error = fmt.Sprintf("command timed out after %s", timeout.String())
74+
} else if exitError, ok := err.(*exec.ExitError); ok {
75+
cmdInfo.ExitCode = strconv.Itoa(exitError.ExitCode())
76+
cmdInfo.Error = stderr.String()
77+
} else {
78+
return nil, errors.Wrap(err, "failed to run journalctl")
79+
}
80+
}
81+
82+
output := NewResult()
83+
84+
// write info file
85+
infoJsonBytes, err := json.Marshal(cmdInfo)
86+
if err != nil {
87+
return nil, errors.Wrap(err, "failed to marshal journalctl info result")
88+
}
89+
infoFileName := getOutputInfoFile(collectorName)
90+
output.SaveResult(c.BundlePath, infoFileName, bytes.NewBuffer(infoJsonBytes))
91+
92+
// write actual journalctl output
93+
outputFileName := getOutputFile(collectorName)
94+
klog.V(2).Infof("Saving journalctl output to %q in bundle", outputFileName)
95+
output.SaveResult(c.BundlePath, outputFileName, bytes.NewBuffer(stdout.Bytes()))
96+
97+
return output, nil
98+
}
99+
100+
func generateOptions(jd *troubleshootv1beta2.HostJournald) ([]string, error) {
101+
options := []string{}
102+
103+
if jd.System {
104+
options = append(options, "--system")
105+
}
106+
107+
if jd.Dmesg {
108+
options = append(options, "--dmesg")
109+
}
110+
111+
for _, unit := range jd.Units {
112+
options = append(options, "-u", unit)
113+
}
114+
115+
if jd.Since != "" {
116+
options = append(options, "--since", jd.Since)
117+
}
118+
119+
if jd.Until != "" {
120+
options = append(options, "--until", jd.Until)
121+
}
122+
123+
if jd.Output != "" {
124+
options = append(options, "--output", jd.Output)
125+
}
126+
127+
if jd.Lines > 0 {
128+
options = append(options, "-n", strconv.Itoa(jd.Lines))
129+
}
130+
131+
if jd.Reverse {
132+
options = append(options, "--reverse")
133+
}
134+
135+
if jd.Utc {
136+
options = append(options, "--utc")
137+
}
138+
139+
// opinionated on --no-pager
140+
options = append(options, "--no-pager")
141+
142+
return options, nil
143+
}
144+
145+
func getOutputFile(collectorName string) string {
146+
return filepath.Join(HostJournaldPath, collectorName+".txt")
147+
}
148+
149+
func getOutputInfoFile(collectorName string) string {
150+
return filepath.Join(HostJournaldPath, collectorName+"-info.json")
151+
}
152+
153+
func getTimeout(timeout string) (time.Duration, error) {
154+
if timeout == "" {
155+
return 30 * time.Second, nil
156+
}
157+
158+
return time.ParseDuration(timeout)
159+
}

pkg/collect/host_journald_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package collect
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
8+
)
9+
10+
func TestGenerateOptions(t *testing.T) {
11+
jd := &troubleshootv1beta2.HostJournald{
12+
System: true,
13+
Dmesg: true,
14+
Units: []string{"unit1", "unit2"},
15+
Since: "2022-01-01",
16+
Until: "2022-01-31",
17+
Output: "json",
18+
Lines: 100,
19+
Reverse: true,
20+
Utc: true,
21+
}
22+
23+
expectedOptions := []string{
24+
"--system",
25+
"--dmesg",
26+
"-u", "unit1",
27+
"-u", "unit2",
28+
"--since", "2022-01-01",
29+
"--until", "2022-01-31",
30+
"--output", "json",
31+
"-n", "100",
32+
"--reverse",
33+
"--utc",
34+
"--no-pager",
35+
}
36+
37+
options, err := generateOptions(jd)
38+
if err != nil {
39+
t.Fatalf("generateOptions failed with error: %v", err)
40+
}
41+
42+
if !reflect.DeepEqual(options, expectedOptions) {
43+
t.Errorf("generateOptions returned incorrect options.\nExpected: %v\nActual: %v", expectedOptions, options)
44+
}
45+
}

0 commit comments

Comments
 (0)