From 98f4d4706bb9da1fa4956f341f9ab736ed2bb3a2 Mon Sep 17 00:00:00 2001 From: Andriy Dzikh Date: Thu, 29 Apr 2021 16:12:58 -0700 Subject: [PATCH 1/4] Add --file flag to 'minikube logs' to automatically put logs into a file. --- cmd/minikube/cmd/logs.go | 23 +++++++++++++++---- .../bsutil/kverify/system_pods.go | 3 ++- pkg/minikube/logs/logs.go | 22 +++++++++++++----- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/cmd/minikube/cmd/logs.go b/cmd/minikube/cmd/logs.go index 9d102ad88ea2..972f445ccb71 100644 --- a/cmd/minikube/cmd/logs.go +++ b/cmd/minikube/cmd/logs.go @@ -44,6 +44,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 +54,18 @@ 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) + if err != nil { + exit.Error(reason.Usage, "Failed to create file", err) + } + defer logOutput.Close() + } + + logs.OutputOffline(numberOfLines, logOutput) co := mustload.Running(ClusterFlagValue()) @@ -65,8 +78,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 +88,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 +106,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.") } diff --git a/pkg/minikube/bootstrapper/bsutil/kverify/system_pods.go b/pkg/minikube/bootstrapper/bsutil/kverify/system_pods.go index 2c9653393f3d..a16694fc2031 100644 --- a/pkg/minikube/bootstrapper/bsutil/kverify/system_pods.go +++ b/pkg/minikube/bootstrapper/bsutil/kverify/system_pods.go @@ -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) } } diff --git a/pkg/minikube/logs/logs.go b/pkg/minikube/logs/logs.go index a6d966c19cd0..4db98ea075f4 100644 --- a/pkg/minikube/logs/logs.go +++ b/pkg/minikube/logs/logs.go @@ -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, "") } From 9f9dc2c3262195777d042b8fda1b2b73ca827778 Mon Sep 17 00:00:00 2001 From: Andriy Dzikh Date: Mon, 3 May 2021 11:23:22 -0700 Subject: [PATCH 2/4] Update docs using `make generate-docs`. --- site/content/en/docs/commands/logs.md | 1 + 1 file changed, 1 insertion(+) diff --git a/site/content/en/docs/commands/logs.md b/site/content/en/docs/commands/logs.md index 997e9a68b34c..d97d912d8885 100644 --- a/site/content/en/docs/commands/logs.md +++ b/site/content/en/docs/commands/logs.md @@ -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. From 5776597eb37e6934a9125ad09641f617b7303a20 Mon Sep 17 00:00:00 2001 From: Andriy Dzikh Date: Tue, 4 May 2021 15:58:21 -0700 Subject: [PATCH 3/4] Move deferred close to before error exit. --- cmd/minikube/cmd/logs.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/minikube/cmd/logs.go b/cmd/minikube/cmd/logs.go index 972f445ccb71..9bf434847982 100644 --- a/cmd/minikube/cmd/logs.go +++ b/cmd/minikube/cmd/logs.go @@ -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" @@ -59,10 +60,15 @@ var logsCmd = &cobra.Command{ 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) } - defer logOutput.Close() } logs.OutputOffline(numberOfLines, logOutput) From 4051ba55b4dc74977d68409e8a2712b2dd7954a1 Mon Sep 17 00:00:00 2001 From: Andriy Dzikh Date: Tue, 4 May 2021 15:59:00 -0700 Subject: [PATCH 4/4] Create functional integration test for logs --file argument. --- test/integration/functional_test.go | 46 ++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/test/integration/functional_test.go b/test/integration/functional_test.go index ab3a7088f0cb..185515f979a0 100644 --- a/test/integration/functional_test.go +++ b/test/integration/functional_test.go @@ -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) {