feat: [sc-108689] troubleshoot: journald collector (#1586)

* add schema for Journald Host Collector

* implement journald host collector

* update host collector

* add --no-pager
This commit is contained in:
Gerard Nguyen
2024-07-29 09:44:43 +10:00
committed by GitHub
parent 01d5804977
commit d3c3fa5c01
9 changed files with 378 additions and 0 deletions
@@ -1458,6 +1458,35 @@ spec:
exclude:
type: BoolString
type: object
journald:
properties:
collectorName:
type: string
dmesg:
type: boolean
exclude:
type: BoolString
lines:
type: integer
output:
type: string
reverse:
type: boolean
since:
type: string
system:
type: boolean
timeout:
type: string
units:
items:
type: string
type: array
until:
type: string
utc:
type: boolean
type: object
kernelConfigs:
properties:
collectorName:
@@ -1458,6 +1458,35 @@ spec:
exclude:
type: BoolString
type: object
journald:
properties:
collectorName:
type: string
dmesg:
type: boolean
exclude:
type: BoolString
lines:
type: integer
output:
type: string
reverse:
type: boolean
since:
type: string
system:
type: boolean
timeout:
type: string
units:
items:
type: string
type: array
until:
type: string
utc:
type: boolean
type: object
kernelConfigs:
properties:
collectorName:
@@ -20057,6 +20057,35 @@ spec:
exclude:
type: BoolString
type: object
journald:
properties:
collectorName:
type: string
dmesg:
type: boolean
exclude:
type: BoolString
lines:
type: integer
output:
type: string
reverse:
type: boolean
since:
type: string
system:
type: boolean
timeout:
type: string
units:
items:
type: string
type: array
until:
type: string
utc:
type: boolean
type: object
kernelConfigs:
properties:
collectorName:
@@ -199,6 +199,20 @@ type HostKernelConfigs struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
type HostJournald struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
System bool `json:"system,omitempty" yaml:"system,omitempty"`
Dmesg bool `json:"dmesg,omitempty" yaml:"dmesg,omitempty"`
Units []string `json:"units,omitempty" yaml:"units,omitempty"`
Since string `json:"since,omitempty" yaml:"since,omitempty"`
Until string `json:"until,omitempty" yaml:"until,omitempty"`
Output string `json:"output,omitempty" yaml:"output,omitempty"`
Lines int `json:"lines,omitempty" yaml:"lines,omitempty"`
Reverse bool `json:"reverse,omitempty" yaml:"reverse,omitempty"`
Utc bool `json:"utc,omitempty" yaml:"utc,omitempty"`
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
}
type HostCollect struct {
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -224,6 +238,7 @@ type HostCollect struct {
HostRun *HostRun `json:"run,omitempty" yaml:"run,omitempty"`
HostCopy *HostCopy `json:"copy,omitempty" yaml:"copy,omitempty"`
HostKernelConfigs *HostKernelConfigs `json:"kernelConfigs,omitempty" yaml:"kernelConfigs,omitempty"`
HostJournald *HostJournald `json:"journald,omitempty" yaml:"journald,omitempty"`
HostCGroups *HostCGroups `json:"cgroups,omitempty" yaml:"cgroups,omitempty"`
}
@@ -2093,6 +2093,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = new(HostKernelConfigs)
(*in).DeepCopyInto(*out)
}
if in.HostJournald != nil {
in, out := &in.HostJournald, &out.HostJournald
*out = new(HostJournald)
(*in).DeepCopyInto(*out)
}
if in.HostCGroups != nil {
in, out := &in.HostCGroups, &out.HostCGroups
*out = new(HostCGroups)
@@ -2288,6 +2293,27 @@ func (in *HostHTTP) DeepCopy() *HostHTTP {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostJournald) DeepCopyInto(out *HostJournald) {
*out = *in
in.HostCollectorMeta.DeepCopyInto(&out.HostCollectorMeta)
if in.Units != nil {
in, out := &in.Units, &out.Units
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostJournald.
func (in *HostJournald) DeepCopy() *HostJournald {
if in == nil {
return nil
}
out := new(HostJournald)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostKernelConfigs) DeepCopyInto(out *HostKernelConfigs) {
*out = *in
+2
View File
@@ -63,6 +63,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str
return &CollectHostCopy{collector.HostCopy, bundlePath}, true
case collector.HostKernelConfigs != nil:
return &CollectHostKernelConfigs{collector.HostKernelConfigs, bundlePath}, true
case collector.HostJournald != nil:
return &CollectHostJournald{collector.HostJournald, bundlePath}, true
case collector.HostCGroups != nil:
return &CollectHostCGroups{collector.HostCGroups, bundlePath}, true
default:
+159
View File
@@ -0,0 +1,159 @@
package collect
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"strconv"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"k8s.io/klog/v2"
)
type CollectHostJournald struct {
hostCollector *troubleshootv1beta2.HostJournald
BundlePath string
}
const HostJournaldPath = `host-collectors/journald/`
func (c *CollectHostJournald) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "journald")
}
func (c *CollectHostJournald) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostJournald) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
// collector name check
collectorName := c.hostCollector.CollectorName
if collectorName == "" {
return nil, errors.New("collector name is required")
}
// timeout check
timeout, err := getTimeout(c.hostCollector.Timeout)
if err != nil {
return nil, errors.Wrap(err, "failed to parse timeout")
}
// set timeout context
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// prepare command options
cmdOptions, err := generateOptions(c.hostCollector)
if err != nil {
return nil, errors.Wrap(err, "failed to generate journalctl options")
}
// run journalctl and capture output
klog.V(2).Infof("Running journalctl with options: %v", cmdOptions)
var stdout, stderr bytes.Buffer
cmd := exec.CommandContext(ctx, "journalctl", cmdOptions...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmdInfo := HostRunInfo{
Command: cmd.String(),
ExitCode: "0",
}
if err := cmd.Run(); err != nil {
klog.V(2).Infof("journalctl command failed: %v", err)
if err == context.DeadlineExceeded {
cmdInfo.ExitCode = "124"
cmdInfo.Error = fmt.Sprintf("command timed out after %s", timeout.String())
} else if exitError, ok := err.(*exec.ExitError); ok {
cmdInfo.ExitCode = strconv.Itoa(exitError.ExitCode())
cmdInfo.Error = stderr.String()
} else {
return nil, errors.Wrap(err, "failed to run journalctl")
}
}
output := NewResult()
// write info file
infoJsonBytes, err := json.Marshal(cmdInfo)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal journalctl info result")
}
infoFileName := getOutputInfoFile(collectorName)
output.SaveResult(c.BundlePath, infoFileName, bytes.NewBuffer(infoJsonBytes))
// write actual journalctl output
outputFileName := getOutputFile(collectorName)
klog.V(2).Infof("Saving journalctl output to %q in bundle", outputFileName)
output.SaveResult(c.BundlePath, outputFileName, bytes.NewBuffer(stdout.Bytes()))
return output, nil
}
func generateOptions(jd *troubleshootv1beta2.HostJournald) ([]string, error) {
options := []string{}
if jd.System {
options = append(options, "--system")
}
if jd.Dmesg {
options = append(options, "--dmesg")
}
for _, unit := range jd.Units {
options = append(options, "-u", unit)
}
if jd.Since != "" {
options = append(options, "--since", jd.Since)
}
if jd.Until != "" {
options = append(options, "--until", jd.Until)
}
if jd.Output != "" {
options = append(options, "--output", jd.Output)
}
if jd.Lines > 0 {
options = append(options, "-n", strconv.Itoa(jd.Lines))
}
if jd.Reverse {
options = append(options, "--reverse")
}
if jd.Utc {
options = append(options, "--utc")
}
// opinionated on --no-pager
options = append(options, "--no-pager")
return options, nil
}
func getOutputFile(collectorName string) string {
return filepath.Join(HostJournaldPath, collectorName+".txt")
}
func getOutputInfoFile(collectorName string) string {
return filepath.Join(HostJournaldPath, collectorName+"-info.json")
}
func getTimeout(timeout string) (time.Duration, error) {
if timeout == "" {
return 30 * time.Second, nil
}
return time.ParseDuration(timeout)
}
+45
View File
@@ -0,0 +1,45 @@
package collect
import (
"reflect"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
func TestGenerateOptions(t *testing.T) {
jd := &troubleshootv1beta2.HostJournald{
System: true,
Dmesg: true,
Units: []string{"unit1", "unit2"},
Since: "2022-01-01",
Until: "2022-01-31",
Output: "json",
Lines: 100,
Reverse: true,
Utc: true,
}
expectedOptions := []string{
"--system",
"--dmesg",
"-u", "unit1",
"-u", "unit2",
"--since", "2022-01-01",
"--until", "2022-01-31",
"--output", "json",
"-n", "100",
"--reverse",
"--utc",
"--no-pager",
}
options, err := generateOptions(jd)
if err != nil {
t.Fatalf("generateOptions failed with error: %v", err)
}
if !reflect.DeepEqual(options, expectedOptions) {
t.Errorf("generateOptions returned incorrect options.\nExpected: %v\nActual: %v", expectedOptions, options)
}
}
@@ -19094,6 +19094,50 @@
}
}
},
"journald": {
"type": "object",
"properties": {
"collectorName": {
"type": "string"
},
"dmesg": {
"type": "boolean"
},
"exclude": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
},
"lines": {
"type": "integer"
},
"output": {
"type": "string"
},
"reverse": {
"type": "boolean"
},
"since": {
"type": "string"
},
"system": {
"type": "boolean"
},
"timeout": {
"type": "string"
},
"units": {
"type": "array",
"items": {
"type": "string"
}
},
"until": {
"type": "string"
},
"utc": {
"type": "boolean"
}
}
},
"kernelConfigs": {
"type": "object",
"properties": {