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/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.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue