365 lines
8.6 KiB
Go
365 lines
8.6 KiB
Go
package view
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/derailed/k9s/internal"
|
|
"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"
|
|
logBuffSize = 100
|
|
|
|
// FlushTimeout represents a duration between log flushes.
|
|
FlushTimeout = 200 * time.Millisecond
|
|
|
|
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
|
|
logFmt = " Logs([fg:bg:]%s) "
|
|
)
|
|
|
|
// Log represents a generic log viewer.
|
|
type Log struct {
|
|
*tview.Flex
|
|
|
|
app *App
|
|
logs *Details
|
|
indicator *LogIndicator
|
|
ansiWriter io.Writer
|
|
path, container string
|
|
cancelFn context.CancelFunc
|
|
previous bool
|
|
gvr client.GVR
|
|
}
|
|
|
|
var _ model.Component = &Log{}
|
|
|
|
// NewLog returns a new viewer.
|
|
func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
|
|
return &Log{
|
|
gvr: gvr,
|
|
Flex: tview.NewFlex(),
|
|
path: path,
|
|
container: co,
|
|
previous: prev,
|
|
}
|
|
}
|
|
|
|
// Init initialiazes the viewer.
|
|
func (l *Log) Init(ctx context.Context) (err error) {
|
|
log.Debug().Msgf(">>> Logs INIT")
|
|
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.Styles)
|
|
l.AddItem(l.indicator, 1, 1, false)
|
|
|
|
l.logs = NewDetails(l.app, "", "")
|
|
if err = l.logs.Init(ctx); err != nil {
|
|
return err
|
|
}
|
|
l.logs.SetWrap(false)
|
|
l.logs.SetMaxBuffer(l.app.Config.K9s.LogBufferSize)
|
|
|
|
l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor, l.app.Styles.Views().Log.BgColor)
|
|
l.AddItem(l.logs, 0, 1, true)
|
|
l.bindKeys()
|
|
l.logs.SetInputCapture(l.keyboard)
|
|
|
|
l.StylesChanged(l.app.Styles)
|
|
l.app.Styles.AddListener(l)
|
|
|
|
return nil
|
|
}
|
|
|
|
// StylesChanged reports skin changes.
|
|
func (l *Log) StylesChanged(s *config.Styles) {
|
|
l.SetBackgroundColor(config.AsColor(s.Views().Log.BgColor))
|
|
l.logs.SetTextColor(config.AsColor(s.Views().Log.FgColor))
|
|
l.logs.SetBackgroundColor(config.AsColor(s.Views().Log.BgColor))
|
|
}
|
|
|
|
// Hints returns a collection of menu hints.
|
|
func (l *Log) Hints() model.MenuHints {
|
|
return l.logs.Actions().Hints()
|
|
}
|
|
|
|
// Start runs the component.
|
|
func (l *Log) Start() {
|
|
l.Stop()
|
|
if err := l.doLoad(); err != nil {
|
|
l.app.Flash().Err(err)
|
|
l.log("😂 Doh! No logs are available at this time. Check again later on...")
|
|
return
|
|
}
|
|
l.app.SetFocus(l)
|
|
}
|
|
|
|
// Stop terminates the component.
|
|
func (l *Log) Stop() {
|
|
if l.cancelFn != nil {
|
|
log.Debug().Msgf("<<<< Logger STOP!")
|
|
l.cancelFn()
|
|
l.cancelFn = nil
|
|
}
|
|
l.app.Styles.RemoveListener(l)
|
|
}
|
|
|
|
// Name returns the component name.
|
|
func (l *Log) Name() string { return logTitle }
|
|
|
|
func (l *Log) bindKeys() {
|
|
l.logs.Actions().Set(ui.KeyActions{
|
|
tcell.KeyEscape: ui.NewKeyAction("Back", l.app.PrevCmd, true),
|
|
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
|
|
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
|
|
ui.KeyG: ui.NewKeyAction("Top", l.topCmd, false),
|
|
ui.KeyShiftF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true),
|
|
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true),
|
|
ui.KeyShiftG: ui.NewKeyAction("Bottom", l.bottomCmd, false),
|
|
ui.KeyF: ui.NewKeyAction("Up", l.pageUpCmd, false),
|
|
ui.KeyB: ui.NewKeyAction("Down", l.pageDownCmd, false),
|
|
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
|
|
})
|
|
}
|
|
|
|
func (l *Log) doLoad() error {
|
|
l.logs.Clear()
|
|
l.setTitle(l.path, l.container)
|
|
|
|
var ctx context.Context
|
|
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.app.factory)
|
|
ctx, l.cancelFn = context.WithCancel(ctx)
|
|
|
|
c := make(chan string, 10)
|
|
go l.updateLogs(ctx, c, logBuffSize)
|
|
|
|
accessor, err := dao.AccessorFor(l.app.factory, l.gvr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger, ok := accessor.(dao.Loggable)
|
|
if !ok {
|
|
return fmt.Errorf("Resource %s is not tailable", l.gvr)
|
|
}
|
|
|
|
if err := logger.TailLogs(ctx, c, l.logOpts(l.path, l.container, l.previous)); err != nil {
|
|
l.cancelFn()
|
|
close(c)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *Log) logOpts(path, co string, prevLogs bool) dao.LogOptions {
|
|
return dao.LogOptions{
|
|
Path: path,
|
|
Container: co,
|
|
Lines: int64(l.app.Config.K9s.LogRequestSize),
|
|
Previous: prevLogs,
|
|
}
|
|
}
|
|
|
|
func (l *Log) updateLogs(ctx context.Context, c <-chan string, buffSize int) {
|
|
defer func() {
|
|
log.Debug().Msgf("updateLogs view bailing out!")
|
|
}()
|
|
buff, index := make([]string, buffSize), 0
|
|
for {
|
|
select {
|
|
case line, ok := <-c:
|
|
if !ok {
|
|
log.Debug().Msgf("Closed channel detected. Bailing out...")
|
|
l.Flush(index, buff)
|
|
return
|
|
}
|
|
if index < buffSize {
|
|
buff[index] = line
|
|
index++
|
|
continue
|
|
}
|
|
l.Flush(index, buff)
|
|
index = 0
|
|
buff[index] = line
|
|
index++
|
|
case <-time.After(FlushTimeout):
|
|
l.Flush(index, buff)
|
|
index = 0
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Indicator returns the scroll mode viewer.
|
|
func (l *Log) Indicator() *LogIndicator {
|
|
return l.indicator
|
|
}
|
|
|
|
func (l *Log) setTitle(path, co string) {
|
|
var fmat string
|
|
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())
|
|
}
|
|
l.path = path
|
|
l.SetTitle(fmat)
|
|
}
|
|
|
|
func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|
key := evt.Key()
|
|
if key == tcell.KeyRune {
|
|
key = tcell.Key(evt.Rune())
|
|
}
|
|
if m, ok := l.logs.Actions()[key]; ok {
|
|
log.Debug().Msgf(">> LogView handled %s", tcell.KeyNames[key])
|
|
return m.Action(evt)
|
|
}
|
|
|
|
return evt
|
|
}
|
|
|
|
// Logs returns the log viewer.
|
|
func (l *Log) Logs() *Details {
|
|
return l.logs
|
|
}
|
|
|
|
func (l *Log) log(lines string) {
|
|
fmt.Fprintln(l.ansiWriter, tview.Escape(lines))
|
|
log.Debug().Msgf("LOG LINES %d", l.logs.GetLineCount())
|
|
}
|
|
|
|
// Flush write logs to viewer.
|
|
func (l *Log) Flush(index int, buff []string) {
|
|
if index == 0 || !l.indicator.AutoScroll() {
|
|
return
|
|
}
|
|
l.log(strings.Join(buff[:index], "\n"))
|
|
l.app.QueueUpdateDraw(func() {
|
|
l.indicator.Refresh()
|
|
l.logs.ScrollToEnd()
|
|
})
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Actions()...
|
|
|
|
// 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.path, 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) topCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
l.app.Flash().Info("Top of logs...")
|
|
l.logs.ScrollToBeginning()
|
|
return nil
|
|
}
|
|
|
|
func (l *Log) bottomCmd(*tcell.EventKey) *tcell.EventKey {
|
|
l.app.Flash().Info("Bottom of logs...")
|
|
l.logs.ScrollToEnd()
|
|
return nil
|
|
}
|
|
|
|
func (l *Log) pageUpCmd(*tcell.EventKey) *tcell.EventKey {
|
|
if l.logs.PageUp() {
|
|
l.app.Flash().Info("Reached Top ...")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *Log) pageDownCmd(*tcell.EventKey) *tcell.EventKey {
|
|
if l.logs.PageDown() {
|
|
l.app.Flash().Info("Reached Bottom ...")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
|
l.app.Flash().Info("Clearing logs...")
|
|
l.logs.Clear()
|
|
l.logs.ScrollTo(0, 0)
|
|
return nil
|
|
}
|
|
|
|
func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey {
|
|
l.indicator.ToggleTextWrap()
|
|
l.logs.SetWrap(l.indicator.textWrap)
|
|
return nil
|
|
}
|
|
|
|
func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
l.indicator.ToggleAutoScroll()
|
|
return nil
|
|
}
|
|
|
|
func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey {
|
|
l.indicator.ToggleFullScreen()
|
|
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)
|
|
|
|
return nil
|
|
}
|