Merge pull request #11240 from andriyDev/LogsFile
Add --file flag to 'minikube logs' to automatically put logs into a file.pull/11305/head
commit
681d4badeb
|
@ -21,6 +21,7 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
cmdcfg "k8s.io/minikube/cmd/minikube/cmd/config"
|
||||
"k8s.io/minikube/pkg/minikube/cluster"
|
||||
"k8s.io/minikube/pkg/minikube/cruntime"
|
||||
|
@ -44,6 +45,8 @@ var (
|
|||
numberOfLines int
|
||||
// showProblems only shows lines that match known issues
|
||||
showProblems bool
|
||||
// fileOutput is where to write logs to. If omitted, writes to stdout.
|
||||
fileOutput string
|
||||
)
|
||||
|
||||
// logsCmd represents the logs command
|
||||
|
@ -52,7 +55,23 @@ var logsCmd = &cobra.Command{
|
|||
Short: "Returns logs to debug a local Kubernetes cluster",
|
||||
Long: `Gets the logs of the running instance, used for debugging minikube, not user code.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logs.OutputOffline(numberOfLines)
|
||||
var logOutput *os.File = os.Stdout
|
||||
var err error
|
||||
|
||||
if fileOutput != "" {
|
||||
logOutput, err = os.Create(fileOutput)
|
||||
defer func() {
|
||||
err := logOutput.Close()
|
||||
if err != nil {
|
||||
klog.Warning("Failed to close file: %v", err)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
exit.Error(reason.Usage, "Failed to create file", err)
|
||||
}
|
||||
}
|
||||
|
||||
logs.OutputOffline(numberOfLines, logOutput)
|
||||
|
||||
co := mustload.Running(ClusterFlagValue())
|
||||
|
||||
|
@ -65,8 +84,9 @@ var logsCmd = &cobra.Command{
|
|||
if err != nil {
|
||||
exit.Error(reason.InternalNewRuntime, "Unable to get runtime", err)
|
||||
}
|
||||
|
||||
if followLogs {
|
||||
err := logs.Follow(cr, bs, *co.Config, co.CP.Runner)
|
||||
err := logs.Follow(cr, bs, *co.Config, co.CP.Runner, logOutput)
|
||||
if err != nil {
|
||||
exit.Error(reason.InternalLogFollow, "Follow", err)
|
||||
}
|
||||
|
@ -74,10 +94,10 @@ var logsCmd = &cobra.Command{
|
|||
}
|
||||
if showProblems {
|
||||
problems := logs.FindProblems(cr, bs, *co.Config, co.CP.Runner)
|
||||
logs.OutputProblems(problems, numberOfProblems)
|
||||
logs.OutputProblems(problems, numberOfProblems, logOutput)
|
||||
return
|
||||
}
|
||||
err = logs.Output(cr, bs, *co.Config, co.CP.Runner, numberOfLines)
|
||||
err = logs.Output(cr, bs, *co.Config, co.CP.Runner, numberOfLines, logOutput)
|
||||
if err != nil {
|
||||
out.Ln("")
|
||||
// Avoid exit.Error, since it outputs the issue URL
|
||||
|
@ -92,4 +112,5 @@ func init() {
|
|||
logsCmd.Flags().BoolVar(&showProblems, "problems", false, "Show only log entries which point to known problems")
|
||||
logsCmd.Flags().IntVarP(&numberOfLines, "length", "n", 60, "Number of lines back to go within the log")
|
||||
logsCmd.Flags().StringVar(&nodeName, "node", "", "The node to get logs from. Defaults to the primary control plane.")
|
||||
logsCmd.Flags().StringVar(&fileOutput, "file", "", "If present, writes to the provided file instead of stdout.")
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package kverify
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -150,7 +151,7 @@ func podStatusMsg(pod core.Pod) string {
|
|||
func announceProblems(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.ClusterConfig, cr command.Runner) {
|
||||
problems := logs.FindProblems(r, bs, cfg, cr)
|
||||
if len(problems) > 0 {
|
||||
logs.OutputProblems(problems, 5)
|
||||
logs.OutputProblems(problems, 5, os.Stderr)
|
||||
time.Sleep(kconst.APICallRetryInterval * 15)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
|
@ -93,7 +94,7 @@ type logRunner interface {
|
|||
const lookBackwardsCount = 400
|
||||
|
||||
// Follow follows logs from multiple files in tail(1) format
|
||||
func Follow(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.ClusterConfig, cr logRunner) error {
|
||||
func Follow(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.ClusterConfig, cr logRunner, logOutput io.Writer) error {
|
||||
cs := []string{}
|
||||
for _, v := range logCommands(r, bs, cfg, 0, true) {
|
||||
cs = append(cs, v+" &")
|
||||
|
@ -101,8 +102,8 @@ func Follow(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.Cluster
|
|||
cs = append(cs, "wait")
|
||||
|
||||
cmd := exec.Command("/bin/bash", "-c", strings.Join(cs, " "))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stdout
|
||||
cmd.Stdout = logOutput
|
||||
cmd.Stderr = logOutput
|
||||
if _, err := cr.RunCmd(cmd); err != nil {
|
||||
return errors.Wrapf(err, "log follow")
|
||||
}
|
||||
|
@ -146,7 +147,10 @@ func FindProblems(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.C
|
|||
}
|
||||
|
||||
// OutputProblems outputs discovered problems.
|
||||
func OutputProblems(problems map[string][]string, maxLines int) {
|
||||
func OutputProblems(problems map[string][]string, maxLines int, logOutput *os.File) {
|
||||
out.SetErrFile(logOutput)
|
||||
defer out.SetErrFile(os.Stderr)
|
||||
|
||||
for name, lines := range problems {
|
||||
out.FailureT("Problems detected in {{.name}}:", out.V{"name": name})
|
||||
if len(lines) > maxLines {
|
||||
|
@ -159,7 +163,7 @@ func OutputProblems(problems map[string][]string, maxLines int) {
|
|||
}
|
||||
|
||||
// Output displays logs from multiple sources in tail(1) format
|
||||
func Output(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.ClusterConfig, runner command.Runner, lines int) error {
|
||||
func Output(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.ClusterConfig, runner command.Runner, lines int, logOutput *os.File) error {
|
||||
cmds := logCommands(r, bs, cfg, lines, false)
|
||||
cmds["kernel"] = "uptime && uname -a && grep PRETTY /etc/os-release"
|
||||
|
||||
|
@ -168,6 +172,9 @@ func Output(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.Cluster
|
|||
names = append(names, k)
|
||||
}
|
||||
|
||||
out.SetOutFile(logOutput)
|
||||
defer out.SetOutFile(os.Stdout)
|
||||
|
||||
sort.Strings(names)
|
||||
failed := []string{}
|
||||
for i, name := range names {
|
||||
|
@ -238,13 +245,16 @@ func outputLastStart() error {
|
|||
}
|
||||
|
||||
// OutputOffline outputs logs that don't need a running cluster.
|
||||
func OutputOffline(lines int) {
|
||||
func OutputOffline(lines int, logOutput *os.File) {
|
||||
out.SetOutFile(logOutput)
|
||||
defer out.SetOutFile(os.Stdout)
|
||||
if err := outputAudit(lines); err != nil {
|
||||
klog.Errorf("failed to output audit logs: %v", err)
|
||||
}
|
||||
if err := outputLastStart(); err != nil {
|
||||
klog.Errorf("failed to output last start logs: %v", err)
|
||||
}
|
||||
|
||||
out.Styled(style.Empty, "")
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ minikube logs [flags]
|
|||
### Options
|
||||
|
||||
```
|
||||
--file string If present, writes to the provided file instead of stdout.
|
||||
-f, --follow Show only the most recent journal entries, and continuously print new entries as they are appended to the journal.
|
||||
-n, --length int Number of lines back to go within the log (default 60)
|
||||
--node string The node to get logs from. Defaults to the primary control plane.
|
||||
|
|
|
@ -118,6 +118,7 @@ func TestFunctional(t *testing.T) {
|
|||
{"DryRun", validateDryRun},
|
||||
{"StatusCmd", validateStatusCmd},
|
||||
{"LogsCmd", validateLogsCmd},
|
||||
{"LogsFileCmd", validateLogsFileCmd},
|
||||
{"MountCmd", validateMountCmd},
|
||||
{"ProfileCmd", validateProfileCmd},
|
||||
{"ServiceCmd", validateServiceCmd},
|
||||
|
@ -1057,12 +1058,7 @@ func validateConfigCmd(ctx context.Context, t *testing.T, profile string) {
|
|||
}
|
||||
}
|
||||
|
||||
// validateLogsCmd asserts basic "logs" command functionality
|
||||
func validateLogsCmd(ctx context.Context, t *testing.T, profile string) {
|
||||
rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "logs"))
|
||||
if err != nil {
|
||||
t.Errorf("%s failed: %v", rr.Command(), err)
|
||||
}
|
||||
func checkSaneLogs(t *testing.T, logs string) {
|
||||
expectedWords := []string{"apiserver", "Linux", "kubelet", "Audit", "Last Start"}
|
||||
switch ContainerRuntime() {
|
||||
case "docker":
|
||||
|
@ -1074,12 +1070,46 @@ func validateLogsCmd(ctx context.Context, t *testing.T, profile string) {
|
|||
}
|
||||
|
||||
for _, word := range expectedWords {
|
||||
if !strings.Contains(rr.Stdout.String(), word) {
|
||||
t.Errorf("expected minikube logs to include word: -%q- but got \n***%s***\n", word, rr.Output())
|
||||
if !strings.Contains(logs, word) {
|
||||
t.Errorf("expected minikube logs to include word: -%q- but got \n***%s***\n", word, logs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateLogsCmd asserts basic "logs" command functionality
|
||||
func validateLogsCmd(ctx context.Context, t *testing.T, profile string) {
|
||||
rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "logs"))
|
||||
if err != nil {
|
||||
t.Errorf("%s failed: %v", rr.Command(), err)
|
||||
}
|
||||
|
||||
checkSaneLogs(t, rr.Stdout.String())
|
||||
}
|
||||
|
||||
// validateLogsFileCmd asserts "logs --file" command functionality
|
||||
func validateLogsFileCmd(ctx context.Context, t *testing.T, profile string) {
|
||||
dname, err := ioutil.TempDir("", profile)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create temp dir: %v", err)
|
||||
}
|
||||
logFileName := filepath.Join(dname, "logs.txt")
|
||||
|
||||
rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "logs", "--file", logFileName))
|
||||
if err != nil {
|
||||
t.Errorf("%s failed: %v", rr.Command(), err)
|
||||
}
|
||||
if rr.Stdout.String() != "" {
|
||||
t.Errorf("expected empty minikube logs output, but got: \n***%s***\n", rr.Output())
|
||||
}
|
||||
|
||||
logs, err := ioutil.ReadFile(logFileName)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to read logs output '%s': %v", logFileName, err)
|
||||
}
|
||||
|
||||
checkSaneLogs(t, string(logs))
|
||||
}
|
||||
|
||||
// validateProfileCmd asserts "profile" command functionality
|
||||
func validateProfileCmd(ctx context.Context, t *testing.T, profile string) {
|
||||
t.Run("profile_not_create", func(t *testing.T) {
|
||||
|
|
Loading…
Reference in New Issue