Skip to content

Add --file flag to 'minikube logs' to automatically put logs into a file. #11240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions cmd/minikube/cmd/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So are we expecting an absolute or relative path here? Enforcing one or the other will make things less confusing for the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be either. Isn't it standard to allow both relative and absolute paths for file parameters?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should allow user both, I image I would wanna do

minikube logs -f ~/Desktop/m.log

or

minikube logs -f /tmp/m.log

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())

Expand All @@ -65,19 +84,20 @@ 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)
}
return
}
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
Expand All @@ -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.")
}
3 changes: 2 additions & 1 deletion pkg/minikube/bootstrapper/bsutil/kverify/system_pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package kverify
import (
"context"
"fmt"
"os"
"strings"
"time"

Expand Down Expand Up @@ -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)
}
}
22 changes: 16 additions & 6 deletions pkg/minikube/logs/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"regexp"
Expand Down Expand Up @@ -93,16 +94,16 @@ 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+" &")
}
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")
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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"

Expand All @@ -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 {
Expand Down Expand Up @@ -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, "")
}

Expand Down
1 change: 1 addition & 0 deletions site/content/en/docs/commands/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 38 additions & 8 deletions test/integration/functional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func TestFunctional(t *testing.T) {
{"DryRun", validateDryRun},
{"StatusCmd", validateStatusCmd},
{"LogsCmd", validateLogsCmd},
{"LogsFileCmd", validateLogsFileCmd},
{"MountCmd", validateMountCmd},
{"ProfileCmd", validateProfileCmd},
{"ServiceCmd", validateServiceCmd},
Expand Down Expand Up @@ -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":
Expand All @@ -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) {
Expand Down