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/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.")
}

View File

@ -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)
}
}

View File

@ -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, "")
}

View File

@ -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.

View File

@ -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) {