mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-09-03 00:47:17 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user