Merge pull request #11240 from andriyDev/LogsFile

Add --file flag to 'minikube logs' to automatically put logs into a file.
pull/11305/head
Medya Ghazizadeh 2021-05-05 20:11:05 -07:00 committed by GitHub
commit 681d4badeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 19 deletions

View File

@ -21,6 +21,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"k8s.io/klog/v2"
cmdcfg "k8s.io/minikube/cmd/minikube/cmd/config" cmdcfg "k8s.io/minikube/cmd/minikube/cmd/config"
"k8s.io/minikube/pkg/minikube/cluster" "k8s.io/minikube/pkg/minikube/cluster"
"k8s.io/minikube/pkg/minikube/cruntime" "k8s.io/minikube/pkg/minikube/cruntime"
@ -44,6 +45,8 @@ var (
numberOfLines int numberOfLines int
// showProblems only shows lines that match known issues // showProblems only shows lines that match known issues
showProblems bool showProblems bool
// fileOutput is where to write logs to. If omitted, writes to stdout.
fileOutput string
) )
// logsCmd represents the logs command // logsCmd represents the logs command
@ -52,7 +55,23 @@ var logsCmd = &cobra.Command{
Short: "Returns logs to debug a local Kubernetes cluster", Short: "Returns logs to debug a local Kubernetes cluster",
Long: `Gets the logs of the running instance, used for debugging minikube, not user code.`, Long: `Gets the logs of the running instance, used for debugging minikube, not user code.`,
Run: func(cmd *cobra.Command, args []string) { 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()) co := mustload.Running(ClusterFlagValue())
@ -65,8 +84,9 @@ var logsCmd = &cobra.Command{
if err != nil { if err != nil {
exit.Error(reason.InternalNewRuntime, "Unable to get runtime", err) exit.Error(reason.InternalNewRuntime, "Unable to get runtime", err)
} }
if followLogs { 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 { if err != nil {
exit.Error(reason.InternalLogFollow, "Follow", err) exit.Error(reason.InternalLogFollow, "Follow", err)
} }
@ -74,10 +94,10 @@ var logsCmd = &cobra.Command{
} }
if showProblems { if showProblems {
problems := logs.FindProblems(cr, bs, *co.Config, co.CP.Runner) problems := logs.FindProblems(cr, bs, *co.Config, co.CP.Runner)
logs.OutputProblems(problems, numberOfProblems) logs.OutputProblems(problems, numberOfProblems, logOutput)
return 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 { if err != nil {
out.Ln("") out.Ln("")
// Avoid exit.Error, since it outputs the issue URL // 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().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().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(&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.")
} }

View File

@ -20,6 +20,7 @@ package kverify
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"strings" "strings"
"time" "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) { func announceProblems(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.ClusterConfig, cr command.Runner) {
problems := logs.FindProblems(r, bs, cfg, cr) problems := logs.FindProblems(r, bs, cfg, cr)
if len(problems) > 0 { if len(problems) > 0 {
logs.OutputProblems(problems, 5) logs.OutputProblems(problems, 5, os.Stderr)
time.Sleep(kconst.APICallRetryInterval * 15) time.Sleep(kconst.APICallRetryInterval * 15)
} }
} }

View File

@ -21,6 +21,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
@ -93,7 +94,7 @@ type logRunner interface {
const lookBackwardsCount = 400 const lookBackwardsCount = 400
// Follow follows logs from multiple files in tail(1) format // 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{} cs := []string{}
for _, v := range logCommands(r, bs, cfg, 0, true) { for _, v := range logCommands(r, bs, cfg, 0, true) {
cs = append(cs, v+" &") cs = append(cs, v+" &")
@ -101,8 +102,8 @@ func Follow(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.Cluster
cs = append(cs, "wait") cs = append(cs, "wait")
cmd := exec.Command("/bin/bash", "-c", strings.Join(cs, " ")) cmd := exec.Command("/bin/bash", "-c", strings.Join(cs, " "))
cmd.Stdout = os.Stdout cmd.Stdout = logOutput
cmd.Stderr = os.Stdout cmd.Stderr = logOutput
if _, err := cr.RunCmd(cmd); err != nil { if _, err := cr.RunCmd(cmd); err != nil {
return errors.Wrapf(err, "log follow") 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. // 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 { for name, lines := range problems {
out.FailureT("Problems detected in {{.name}}:", out.V{"name": name}) out.FailureT("Problems detected in {{.name}}:", out.V{"name": name})
if len(lines) > maxLines { 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 // 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 := logCommands(r, bs, cfg, lines, false)
cmds["kernel"] = "uptime && uname -a && grep PRETTY /etc/os-release" 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) names = append(names, k)
} }
out.SetOutFile(logOutput)
defer out.SetOutFile(os.Stdout)
sort.Strings(names) sort.Strings(names)
failed := []string{} failed := []string{}
for i, name := range names { for i, name := range names {
@ -238,13 +245,16 @@ func outputLastStart() error {
} }
// OutputOffline outputs logs that don't need a running cluster. // 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 { if err := outputAudit(lines); err != nil {
klog.Errorf("failed to output audit logs: %v", err) klog.Errorf("failed to output audit logs: %v", err)
} }
if err := outputLastStart(); err != nil { if err := outputLastStart(); err != nil {
klog.Errorf("failed to output last start logs: %v", err) klog.Errorf("failed to output last start logs: %v", err)
} }
out.Styled(style.Empty, "") out.Styled(style.Empty, "")
} }

View File

@ -20,6 +20,7 @@ minikube logs [flags]
### Options ### 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. -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) -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. --node string The node to get logs from. Defaults to the primary control plane.

View File

@ -118,6 +118,7 @@ func TestFunctional(t *testing.T) {
{"DryRun", validateDryRun}, {"DryRun", validateDryRun},
{"StatusCmd", validateStatusCmd}, {"StatusCmd", validateStatusCmd},
{"LogsCmd", validateLogsCmd}, {"LogsCmd", validateLogsCmd},
{"LogsFileCmd", validateLogsFileCmd},
{"MountCmd", validateMountCmd}, {"MountCmd", validateMountCmd},
{"ProfileCmd", validateProfileCmd}, {"ProfileCmd", validateProfileCmd},
{"ServiceCmd", validateServiceCmd}, {"ServiceCmd", validateServiceCmd},
@ -1057,12 +1058,7 @@ func validateConfigCmd(ctx context.Context, t *testing.T, profile string) {
} }
} }
// validateLogsCmd asserts basic "logs" command functionality func checkSaneLogs(t *testing.T, logs string) {
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)
}
expectedWords := []string{"apiserver", "Linux", "kubelet", "Audit", "Last Start"} expectedWords := []string{"apiserver", "Linux", "kubelet", "Audit", "Last Start"}
switch ContainerRuntime() { switch ContainerRuntime() {
case "docker": case "docker":
@ -1074,12 +1070,46 @@ func validateLogsCmd(ctx context.Context, t *testing.T, profile string) {
} }
for _, word := range expectedWords { for _, word := range expectedWords {
if !strings.Contains(rr.Stdout.String(), word) { if !strings.Contains(logs, word) {
t.Errorf("expected minikube logs to include word: -%q- but got \n***%s***\n", word, rr.Output()) 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 // validateProfileCmd asserts "profile" command functionality
func validateProfileCmd(ctx context.Context, t *testing.T, profile string) { func validateProfileCmd(ctx context.Context, t *testing.T, profile string) {
t.Run("profile_not_create", func(t *testing.T) { t.Run("profile_not_create", func(t *testing.T) {