package view import ( "context" "fmt" "io" "os" "path/filepath" "strings" "time" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" ) const ( logTitle = "logs" logMessage = "[:orange:b]Waiting for logs...[::]" logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) " logFmt = " Logs([fg:bg:]%s) " // BOZO!! Canned! Need config tail line counts! tailLineCount = 50 defaultTimeout = 200 * time.Millisecond ) // Log represents a generic log viewer. type Log struct { *tview.Flex app *App logs *Details indicator *LogIndicator ansiWriter io.Writer cmdBuff *ui.CmdBuff model *model.Log } var _ model.Component = (*Log)(nil) // NewLog returns a new viewer. func NewLog(gvr client.GVR, path, co string, prev bool) *Log { l := Log{ Flex: tview.NewFlex(), cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), model: model.NewLog(gvr, buildLogOpts(path, co, prev, true, tailLineCount), defaultTimeout), } return &l } // Init initialiazes the viewer. func (l *Log) Init(ctx context.Context) (err error) { if l.app, err = extractApp(ctx); err != nil { return err } l.SetBorder(true) l.SetBorderPadding(0, 0, 1, 1) l.SetDirection(tview.FlexRow) l.indicator = NewLogIndicator(l.app.Config, l.app.Styles) l.AddItem(l.indicator, 1, 1, false) l.indicator.Refresh() l.logs = NewDetails(l.app, "", "", false) if err = l.logs.Init(ctx); err != nil { return err } l.logs.SetText(logMessage) l.logs.SetWrap(false) l.logs.SetMaxBuffer(l.app.Config.K9s.LogBufferSize) l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String()) l.AddItem(l.logs, 0, 1, true) l.bindKeys() l.logs.SetInputCapture(l.keyboard) l.StylesChanged(l.app.Styles) l.app.Styles.AddListener(l) l.goFullScreen() l.model.Init(l.app.factory) l.model.AddListener(l) l.updateTitle() l.cmdBuff.AddListener(l.app.Cmd()) l.cmdBuff.AddListener(l) return nil } // LogCleared clears the logs. func (l *Log) LogCleared() { l.app.QueueUpdateDraw(func() { l.logs.Clear() l.logs.ScrollTo(0, 0) }) } // LogFailed notifies an error occurred. func (l *Log) LogFailed(err error) { l.app.QueueUpdateDraw(func() { l.app.Flash().Err(err) }) } // LogChanged updates the logs. func (l *Log) LogChanged(lines []string) { l.app.QueueUpdateDraw(func() { l.Flush(lines) }) } // BufferChanged indicates the buffer was changed. func (l *Log) BufferChanged(s string) {} // BufferActive indicates the buff activity changed. func (l *Log) BufferActive(state bool, k ui.BufferKind) { l.app.BufferActive(state, k) } // StylesChanged reports skin changes. func (l *Log) StylesChanged(s *config.Styles) { l.SetBackgroundColor(s.Views().Log.BgColor.Color()) l.logs.SetTextColor(s.Views().Log.FgColor.Color()) l.logs.SetBackgroundColor(s.Views().Log.BgColor.Color()) } // GetModel returns the log model. func (l *Log) GetModel() *model.Log { return l.model } // Hints returns a collection of menu hints. func (l *Log) Hints() model.MenuHints { return l.logs.Actions().Hints() } // ExtraHints returns additional hints. func (l *Log) ExtraHints() map[string]string { return nil } // Start runs the component. func (l *Log) Start() { l.model.Start() l.app.SetFocus(l) } // Stop terminates the component. func (l *Log) Stop() { l.model.Stop() l.model.RemoveListener(l) l.app.Styles.RemoveListener(l) l.cmdBuff.RemoveListener(l) l.cmdBuff.RemoveListener(l.app.Cmd()) } // Name returns the component name. func (l *Log) Name() string { return logTitle } func (l *Log) bindKeys() { l.logs.Actions().Set(ui.KeyActions{ tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, true), ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true), // BOZO!! Log timestamps // ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.ToggleTimestampCmd, true), ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true), ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false), tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.resetCmd, false), tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), }) } func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey { key := evt.Key() if key == tcell.KeyUp || key == tcell.KeyDown { return evt } if key == tcell.KeyRune { if l.cmdBuff.IsActive() { l.cmdBuff.Add(evt.Rune()) if err := l.model.Filter(l.cmdBuff.String()); err != nil { l.app.Flash().Err(err) } l.updateTitle() return nil } key = extractKey(evt) } if a, ok := l.logs.Actions()[key]; ok { return a.Action(evt) } return evt } // Indicator returns the scroll mode viewer. func (l *Log) Indicator() *LogIndicator { return l.indicator } func (l *Log) updateTitle() { var fmat string path, co := l.model.GetPath(), l.model.GetContainer() if co == "" { fmat = ui.SkinTitle(fmt.Sprintf(logFmt, path), l.app.Styles.Frame()) } else { fmat = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co), l.app.Styles.Frame()) } buff := l.cmdBuff.String() if buff != "" { fmat += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame()) } l.SetTitle(fmat) } // Logs returns the log viewer. func (l *Log) Logs() *Details { return l.logs } func (l *Log) write(lines string) { fmt.Fprintln(l.ansiWriter, tview.Escape(lines)) } // Flush write logs to viewer. func (l *Log) Flush(lines []string) { l.write(strings.Join(lines, "\n")) l.indicator.Refresh() l.logs.ScrollToEnd() } // ---------------------------------------------------------------------------- // Actions()... func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { if !l.cmdBuff.IsActive() { return evt } l.cmdBuff.SetActive(false) if err := l.model.Filter(l.cmdBuff.String()); err != nil { l.app.Flash().Err(err) } l.updateTitle() return nil } func (l *Log) activateCmd(evt *tcell.EventKey) *tcell.EventKey { if l.app.InCmdMode() { return evt } l.app.Flash().Info("Filter mode activated.") l.cmdBuff.SetActive(true) return nil } func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { if !l.cmdBuff.IsActive() { return nil } l.cmdBuff.Delete() if err := l.model.Filter(l.cmdBuff.String()); err != nil { l.app.Flash().Err(err) } l.updateTitle() return nil } func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey { if !l.cmdBuff.InCmdMode() { l.cmdBuff.Reset() return l.app.PrevCmd(evt) } if l.cmdBuff.String() != "" { l.model.ClearFilter() } l.app.Flash().Info("Clearing filter...") l.cmdBuff.SetActive(false) l.cmdBuff.Reset() l.updateTitle() return nil } // SaveCmd dumps the logs to file. func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey { if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil { l.app.Flash().Err(err) } else { l.app.Flash().Infof("Log %s saved successfully!", path) } return nil } func ensureDir(dir string) error { return os.MkdirAll(dir, 0744) } func saveData(cluster, name, data string) (string, error) { dir := filepath.Join(config.K9sDumpDir, cluster) if err := ensureDir(dir); err != nil { return "", err } now := time.Now().UnixNano() fName := fmt.Sprintf("%s-%d.log", strings.Replace(name, "/", "-", -1), now) path := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_WRONLY file, err := os.OpenFile(path, mod, 0600) if err != nil { log.Error().Err(err).Msgf("LogFile create %s", path) return "", nil } defer func() { if err := file.Close(); err != nil { log.Error().Err(err).Msg("Closing Log file") } }() if _, err := file.Write([]byte(data)); err != nil { return "", err } return path, nil } func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey { l.app.Flash().Info("Clearing logs...") l.model.Clear() return nil } func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey { l.indicator.ToggleTextWrap() l.logs.SetWrap(l.indicator.textWrap) return nil } // ToggleTimeStampCmd toggles timestamp field. func (l *Log) ToggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey { l.model.Clear() l.indicator.ToggleTimestamp() l.model.ShowTimestamp(l.indicator.Timestamp()) l.model.Stop() l.model.Start() return nil } // ToggleAutoScrollCmd toggles autoscroll status. func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { l.indicator.ToggleAutoScroll() if l.indicator.AutoScroll() { l.model.Start() } else { l.model.Stop() } return nil } func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey { l.indicator.ToggleFullScreen() l.goFullScreen() return nil } func (l *Log) goFullScreen() { sidePadding := 1 if l.indicator.FullScreen() { sidePadding = 0 } l.SetFullScreen(l.indicator.FullScreen()) l.Box.SetBorder(!l.indicator.FullScreen()) l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding) } // ---------------------------------------------------------------------------- // Helpers... // AsKey converts rune to keyboard key., func extractKey(evt *tcell.EventKey) tcell.Key { key := tcell.Key(evt.Rune()) if evt.Modifiers() == tcell.ModAlt { key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers())) } return key } func buildLogOpts(path, co string, prevLogs, showTime bool, tailLineCount int) dao.LogOptions { return dao.LogOptions{ Path: path, Container: co, Lines: int64(tailLineCount), Previous: prevLogs, ShowTimestamp: showTime, } }