diff --git a/internal/views/log.go b/internal/views/log.go index 79079614..313cbd0d 100644 --- a/internal/views/log.go +++ b/internal/views/log.go @@ -3,7 +3,10 @@ package views import ( "fmt" "io" + "os" + "path/filepath" "strings" + "time" "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" @@ -21,6 +24,7 @@ type logView struct { ansiWriter io.Writer autoScroll bool actions keyActions + path string } func newLogView(title string, parent masterView) *logView { @@ -47,13 +51,14 @@ func newLogView(title string, parent masterView) *logView { v.AddItem(v.logs, 0, 1, true) v.actions = keyActions{ - tcell.KeyEscape: {description: "Back", action: v.backCmd, visible: true}, - KeyC: {description: "Clear", action: v.clearCmd, visible: true}, - KeyS: {description: "Toggle AutoScroll", action: v.toggleScrollCmd, visible: true}, - KeyG: {description: "Top", action: v.topCmd, visible: false}, - KeyShiftG: {description: "Bottom", action: v.bottomCmd, visible: false}, - KeyF: {description: "Up", action: v.pageUpCmd, visible: false}, - KeyB: {description: "Down", action: v.pageDownCmd, visible: false}, + tcell.KeyEscape: newKeyAction("Back", v.backCmd, true), + KeyC: newKeyAction("Clear", v.clearCmd, true), + KeyS: newKeyAction("Toggle AutoScroll", v.toggleScrollCmd, true), + KeyG: newKeyAction("Top", v.topCmd, false), + KeyShiftG: newKeyAction("Bottom", v.bottomCmd, false), + KeyF: newKeyAction("Up", v.pageUpCmd, false), + KeyB: newKeyAction("Down", v.pageDownCmd, false), + tcell.KeyCtrlS: newKeyAction("Save", v.saveCmd, true), } v.logs.SetInputCapture(v.keyboard) @@ -111,6 +116,37 @@ func (v *logView) update() { // ---------------------------------------------------------------------------- // Actions... +func (v *logView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { + if err := os.MkdirAll(K9sDump, 0744); err != nil { + log.Error().Err(err).Msgf("Mkdir K9s dump") + return nil + } + + now := time.Now().UnixNano() + fName := fmt.Sprintf("%s-%d.log", strings.Replace(v.path, "/", "-", -1), now) + + path := filepath.Join(K9sDump, fName) + mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY + file, err := os.OpenFile(path, mod, 0644) + defer func() { + if file != nil { + file.Close() + } + }() + if err != nil { + log.Error().Err(err).Msgf("LogFile create %s", path) + return nil + } + + if _, err := fmt.Fprintf(file, v.logs.GetText(true)); err != nil { + log.Error().Err(err).Msgf("Log dump %s", v.path) + } + v.app.flash().infof("Log %s saved successfully!", path) + log.Debug().Msgf("Log %s saved successfully!", path) + + return nil +} + func (v *logView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey { v.autoScroll = !v.autoScroll if v.autoScroll { diff --git a/internal/views/logs.go b/internal/views/logs.go index ac41fdda..142002e2 100644 --- a/internal/views/logs.go +++ b/internal/views/logs.go @@ -121,6 +121,7 @@ func (v *logsView) doLoad(path, co string) error { l.logs.Clear() fmat := skinTitle(fmt.Sprintf(logFmt, path, co), v.parent.appView().styles.Style) l.SetTitle(fmat) + l.path = path c := make(chan string, 10) go func(l *logView) { diff --git a/internal/views/table.go b/internal/views/table.go index 06f54eb3..7660ddbe 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -1,11 +1,15 @@ package views import ( + "encoding/csv" "errors" "fmt" + "os" + "path/filepath" "regexp" "sort" "strings" + "time" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/resource" @@ -86,8 +90,7 @@ func newTableView(app *appView, title string) *tableView { func (v *tableView) bindKeys() { v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, false) - v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true) - v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true) + v.actions[tcell.KeyCtrlS] = newKeyAction("Save", v.saveCmd, true) v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false) v.actions[tcell.KeyEscape] = newKeyAction("Filter Reset", v.resetCmd, false) @@ -96,6 +99,9 @@ func (v *tableView) bindKeys() { v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) + + v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true) + v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true) } func (v *tableView) clearSelection() { @@ -133,6 +139,57 @@ func (v *tableView) setSelection() { } } +// K9sDump represents a directory where K9s artifacts will be persisted. +var K9sDump = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-screens-%s", config.MustK9sUser())) + +const ( + fullFmat = "%s-%s-%d.csv" + noNSFmat = "%s-%d.csv" +) + +func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { + if err := os.MkdirAll(K9sDump, 0744); err != nil { + log.Error().Err(err).Msgf("Mkdir K9s dump") + return nil + } + + ns, now := v.data.Namespace, time.Now().UnixNano() + if ns == resource.AllNamespaces { + ns = resource.AllNamespace + } + fName := fmt.Sprintf(fullFmat, v.baseTitle, ns, now) + if ns == resource.NotNamespaced { + fName = fmt.Sprintf(noNSFmat, v.baseTitle, now) + } + + path := filepath.Join(K9sDump, fName) + mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY + file, err := os.OpenFile(path, mod, 0644) + defer func() { + if file != nil { + file.Close() + } + }() + if err != nil { + log.Error().Err(err).Msgf("CSV create %s", path) + return nil + } + + w := csv.NewWriter(file) + w.Write(v.data.Header) + for _, r := range v.data.Rows { + w.Write(r.Fields) + } + w.Flush() + if err := w.Error(); err != nil { + log.Error().Err(err).Msgf("Screen dump %s", v.baseTitle) + } + v.app.flash().infof("File %s saved successfully!", path) + log.Debug().Msgf("File %s saved successfully!", path) + + return nil +} + func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { if v.cmdBuff.isActive() { v.cmdBuff.setActive(false) diff --git a/main.go b/main.go index 47240c58..d5722b96 100644 --- a/main.go +++ b/main.go @@ -12,15 +12,21 @@ import ( func init() { config.EnsurePath(config.K9sLogs, config.DefaultDirMod) - - mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY - if file, err := os.OpenFile(config.K9sLogs, mod, config.DefaultFileMod); err == nil { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) - } else { - panic(err) - } } func main() { + mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY + file, err := os.OpenFile(config.K9sLogs, mod, config.DefaultFileMod) + defer func() { + if file != nil { + file.Close() + } + }() + if err != nil { + panic(err) + } + + log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) + cmd.Execute() }