From c61f150809763c7fb3a3d558081423f0d2a94adb Mon Sep 17 00:00:00 2001 From: Roland Oldenburg Date: Tue, 29 Oct 2024 07:46:52 +0100 Subject: [PATCH 1/7] First working version with jq cmdline --- internal/config/json/schemas/k9s.json | 24 +++- internal/config/logger.go | 35 +++++- internal/dao/log_item.go | 11 +- internal/dao/log_options.go | 4 + internal/dao/log_options_json.go | 163 ++++++++++++++++++++++++++ internal/dao/pod.go | 18 +++ internal/model/log.go | 6 + internal/view/container.go | 6 + internal/view/log.go | 38 +++++- internal/view/log_indicator.go | 20 +++- internal/view/log_json.go | 98 ++++++++++++++++ internal/view/logger.go | 1 - internal/view/logs_extender.go | 12 ++ 13 files changed, 424 insertions(+), 12 deletions(-) create mode 100644 internal/dao/log_options_json.go create mode 100644 internal/view/log_json.go diff --git a/internal/config/json/schemas/k9s.json b/internal/config/json/schemas/k9s.json index e217bbec29..6a38928628 100644 --- a/internal/config/json/schemas/k9s.json +++ b/internal/config/json/schemas/k9s.json @@ -103,7 +103,29 @@ "buffer": {"type": "integer"}, "sinceSeconds": {"type": "integer"}, "textWrap": {"type": "boolean"}, - "showTime": {"type": "boolean"} + "showTime": {"type": "boolean"}, + "decodeJson": {"type": "boolean"}, + "json": { + "type": "object", + "additionalProperties": false, + "properties": { + "globalExpressions": {"type": "string"}, + "defaultTemplate": {"type": "string"}, + "templates": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": {"type": "string"}, + "loglevel": {"type": "string"}, + "datetime": {"type": "string"}, + "message": {"type": "string"} + } + } + } + } + } } }, "thresholds": { diff --git a/internal/config/logger.go b/internal/config/logger.go index 4c6a6a1832..2578e08563 100644 --- a/internal/config/logger.go +++ b/internal/config/logger.go @@ -14,13 +14,37 @@ const ( DefaultSinceSeconds = -1 // tail logs by default ) +type JsonTemplate struct { + Name string `json:"name" yaml:"name"` + LogLevelExpression string `json:"loglevel" yaml:"loglevel"` + DateTimeExpression string `json:"datetime" yaml:"datetime"` + MessageExpression string `json:"message" yaml:"message"` +} + +type JsonConfig struct { + GlobalExpressions string `json:"globalExpressions" yaml:"globalExpressions"` + DefaultTemplate string `json:"defaultTemplate" yaml:"defaultTemplate"` + Templates []JsonTemplate `json:"templates" yaml:"templates"` +} + +// NewJsonConfig returns a new instance. +func NewJsonConfig() JsonConfig { + return JsonConfig{ + GlobalExpressions: "", + DefaultTemplate: "", + Templates: []JsonTemplate{}, + } +} + // Logger tracks logger options. type Logger struct { - TailCount int64 `json:"tail" yaml:"tail"` - BufferSize int `json:"buffer" yaml:"buffer"` - SinceSeconds int64 `json:"sinceSeconds" yaml:"sinceSeconds"` - TextWrap bool `json:"textWrap" yaml:"textWrap"` - ShowTime bool `json:"showTime" yaml:"showTime"` + TailCount int64 `json:"tail" yaml:"tail"` + BufferSize int `json:"buffer" yaml:"buffer"` + SinceSeconds int64 `json:"sinceSeconds" yaml:"sinceSeconds"` + TextWrap bool `json:"textWrap" yaml:"textWrap"` + ShowTime bool `json:"showTime" yaml:"showTime"` + DecodeJson bool `json:"decodeJson" yaml:"decodeJson"` + JsonConfig JsonConfig `json:"json" yaml:"json"` } // NewLogger returns a new instance. @@ -29,6 +53,7 @@ func NewLogger() Logger { TailCount: DefaultLoggerTailCount, BufferSize: MaxLogThreshold, SinceSeconds: DefaultSinceSeconds, + JsonConfig: NewJsonConfig(), } } diff --git a/internal/dao/log_item.go b/internal/dao/log_item.go index ff90bbff3b..85db9cd8ff 100644 --- a/internal/dao/log_item.go +++ b/internal/dao/log_item.go @@ -5,6 +5,7 @@ package dao import ( "bytes" + "regexp" ) // LogChan represents a channel for logs. @@ -12,6 +13,8 @@ type LogChan chan *LogItem var ItemEOF = new(LogItem) +var dateTimePrefixRegEx = regexp.MustCompile("^\\d{4}-\\d{2}-\\d{2}T?.*") + // LogItem represents a container log line. type LogItem struct { Pod, Container string @@ -68,13 +71,17 @@ func (l *LogItem) Size() int { // Render returns a log line as string. func (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) { - index := bytes.Index(l.Bytes, []byte{' '}) + var index = -1 + if dateTimePrefixRegEx.MatchString(string(l.Bytes)) { + index = bytes.Index(l.Bytes, []byte{' '}) + } if showTime && index > 0 { bb.WriteString("[gray::b]") bb.Write(l.Bytes[:index]) bb.WriteString(" ") if l := 30 - len(l.Bytes[:index]); l > 0 { - bb.Write(bytes.Repeat([]byte{' '}, l)) + // turned off to avoid empty column spaces + // bb.Write(bytes.Repeat([]byte{' '}, l)) } bb.WriteString("[-::-]") } diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index edd20afb6b..2a8fdd24bf 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -27,6 +27,8 @@ type LogOptions struct { MultiPods bool ShowTimestamp bool AllContainers bool + DecodeJson bool + Json JsonOptions } // Info returns the option pod and container info. @@ -52,6 +54,8 @@ func (o *LogOptions) Clone() *LogOptions { SinceTime: o.SinceTime, SinceSeconds: o.SinceSeconds, AllContainers: o.AllContainers, + DecodeJson: o.DecodeJson, + Json: o.Json, } } diff --git a/internal/dao/log_options_json.go b/internal/dao/log_options_json.go new file mode 100644 index 0000000000..9493f7869d --- /dev/null +++ b/internal/dao/log_options_json.go @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "fmt" + "github.com/derailed/k9s/internal/config" + "slices" + "strings" +) + +// JsonTemplateListener represents a json template selection listener. +type JsonTemplateListener interface { + // JsonTemplateChanged indicates template was changed. + JsonTemplateChanged() +} + +type JsonTemplate struct { + Name string + LogLevelExpression string + DateTimeExpression string + MessageExpression string +} + +// Clone clones options. +func (o *JsonTemplate) Clone() *JsonTemplate { + return &JsonTemplate{ + Name: o.Name, + LogLevelExpression: o.LogLevelExpression, + DateTimeExpression: o.DateTimeExpression, + MessageExpression: o.MessageExpression, + } +} + +type JsonOptions struct { + GlobalExpressions string + CurrentTemplateIndex int + Templates []JsonTemplate + listeners []JsonTemplateListener +} + +func TemplatesFromConfig(config config.JsonConfig) []JsonTemplate { + var templates []JsonTemplate + for _, obj := range config.Templates { + templates = append(templates, JsonTemplate{ + Name: obj.Name, + LogLevelExpression: obj.LogLevelExpression, + DateTimeExpression: obj.DateTimeExpression, + MessageExpression: obj.MessageExpression, + }) + } + if templates == nil { + templates = []JsonTemplate{ + { + Name: "default", + LogLevelExpression: ".level", + DateTimeExpression: ".timestamp", + MessageExpression: ".message", + }, + } + } + return templates +} + +// SetCurrentTemplateByName Set the currently selected template. +func (o *JsonOptions) SetCurrentTemplateByName(templateName string) { + o.CurrentTemplateIndex = 0 + o.CurrentTemplateIndex = slices.IndexFunc(o.Templates, func(j JsonTemplate) bool { + return j.Name == templateName + }) + if (o.CurrentTemplateIndex < 0) || (o.CurrentTemplateIndex >= len(o.Templates)) { + o.CurrentTemplateIndex = 0 + } + o.NotifyListeners() +} + +// IterateToNextTemplate Iterates the currently selected template to next (back to first on end). +func (o *JsonOptions) IterateToNextTemplate() { + o.CurrentTemplateIndex = (o.CurrentTemplateIndex + 1) % len(o.Templates) + o.NotifyListeners() +} + +// SetCurrentTemplate Set the currently selected template. +func (o *JsonOptions) SetCurrentTemplate(index int) *JsonTemplate { + o.CurrentTemplateIndex = index + o.NotifyListeners() + return o.GetCurrentTemplate() +} + +// UpdateCurrentTemplate Updates the currently selected template and notifies listeners. +func (o *JsonOptions) UpdateCurrentTemplate(LogLevelExpression string, DateTimeExpression string, MessageExpression string) *JsonTemplate { + var template = o.GetCurrentTemplate() + template.LogLevelExpression = LogLevelExpression + template.DateTimeExpression = DateTimeExpression + template.MessageExpression = MessageExpression + o.NotifyListeners() + return template +} + +// GetCurrentTemplate Return the currently selected template. +func (o *JsonOptions) GetCurrentTemplate() *JsonTemplate { + return &o.Templates[o.CurrentTemplateIndex] +} + +// GetAllTemplateNames Return all template names. +func (o *JsonOptions) GetAllTemplateNames() []string { + var names []string + for _, obj := range o.Templates { + names = append(names, obj.Name) + } + return names +} + +func (o *JsonOptions) GetCurrentJsonQuery() string { + var template = o.GetCurrentTemplate() + var query = `%s . as $line | try ( capture("^(?[0-9-:]{8,10}[^0-9][0-9\\.:]{0,10}[^ ]+) (?.*)") | .ts as $k8sts | .js | fromjson | { datetime:(%s), lvl:(%s), msg:(%s) } | "\(.datetime) \(.lvl) \(.msg)" ) catch $line` + var globalExpression = strings.Trim(strings.Trim(strings.ReplaceAll(o.GlobalExpressions, "\n", " "), " "), ";") + if len(globalExpression) > 0 { + globalExpression += ";" + } + return fmt.Sprintf(query, globalExpression, template.DateTimeExpression, template.LogLevelExpression, template.MessageExpression) +} + +// Clone clones options. +func (o *JsonOptions) Clone() *JsonOptions { + cTemplates := make([]JsonTemplate, len(o.Templates)) + for i, t := range o.Templates { + cTemplates[i] = *t.Clone() + } + return &JsonOptions{ + GlobalExpressions: o.GlobalExpressions, + CurrentTemplateIndex: o.CurrentTemplateIndex, + Templates: cTemplates, + } +} + +// NotifyListeners notifies all template listeners about a change. +func (o *JsonOptions) NotifyListeners() { + for _, lis := range o.listeners { + lis.JsonTemplateChanged() + } +} + +// AddListener adds a new model listener. +func (o *JsonOptions) AddListener(listener JsonTemplateListener) { + o.listeners = append(o.listeners, listener) +} + +// RemoveListener delete a listener from the list. +func (o *JsonOptions) RemoveListener(listener JsonTemplateListener) { + victim := -1 + for i, lis := range o.listeners { + if lis == listener { + victim = i + break + } + } + + if victim >= 0 { + o.listeners = append(o.listeners[:victim], o.listeners[victim+1:]...) + } +} diff --git a/internal/dao/pod.go b/internal/dao/pod.go index f4ee453dbe..2233652f2f 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "os/exec" "sync" "time" @@ -376,6 +377,23 @@ func readLogs(ctx context.Context, wg *sync.WaitGroup, stream io.ReadCloser, out } wg.Done() }() + if opts.DecodeJson { + cmd := exec.Command("jq", "--unbuffered", "-R", "-r", opts.Json.GetCurrentJsonQuery()) + cmd.Stdin = bufio.NewReader(stream) + newStream, err := cmd.StdoutPipe() + if err != nil { + log.Warn().Err(err).Msg("log-reader error on STDOUT pipe for jq") + } + newCombinedStream, err := cmd.StderrPipe() + if err != nil { + log.Warn().Err(err).Msg("log-reader error on STDERR pipe for jq") + } + stream = io.NopCloser(io.MultiReader(newStream, newCombinedStream)) + + if err := cmd.Start(); err != nil { + log.Warn().Err(err).Msgf("Could not start jq") + } + } log.Debug().Msgf(">>> LOG-READER PROCESSING %#v", opts) r := bufio.NewReader(stream) diff --git a/internal/model/log.go b/internal/model/log.go index d194fcf017..cad53e1293 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -92,6 +92,12 @@ func (l *Log) ToggleShowTimestamp(b bool) { l.Refresh() } +// ToggleDecodeJson toggles to decode json in logs. +func (l *Log) ToggleDecodeJson(b bool, ctx context.Context) { + l.logOptions.DecodeJson = b + l.Restart(ctx) +} + func (l *Log) Head(ctx context.Context) { l.mx.Lock() { diff --git a/internal/view/container.go b/internal/view/container.go index babcb2b8db..660d87f2ed 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -120,7 +120,13 @@ func (c *Container) logOptions(prev bool) (*dao.LogOptions, error) { SingleContainer: true, ShowTimestamp: cfg.ShowTime, Previous: prev, + DecodeJson: cfg.DecodeJson, + Json: dao.JsonOptions{ + GlobalExpressions: cfg.JsonConfig.GlobalExpressions, + Templates: dao.TemplatesFromConfig(cfg.JsonConfig), + }, } + opts.Json.SetCurrentTemplateByName(cfg.JsonConfig.DefaultTemplate) return &opts, nil } diff --git a/internal/view/log.go b/internal/view/log.go index be01ed5a5d..e490b99e63 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -47,6 +47,7 @@ type Log struct { mx sync.Mutex follow bool requestOneRefresh bool + jsonForm LogTemplateForm } var _ model.Component = (*Log)(nil) @@ -58,7 +59,6 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log { model: model.NewLog(gvr, opts, defaultFlushTimeout), follow: true, } - return &l } @@ -72,10 +72,12 @@ func (l *Log) Init(ctx context.Context) (err error) { } l.model.Configure(l.app.Config.K9s.Logger) + l.jsonForm = *NewLogTemplateForm(l.app, &l.model.LogOptions().Json) + l.SetBorder(true) l.SetDirection(tview.FlexRow) - l.indicator = NewLogIndicator(l.app.Config, l.app.Styles, l.isContainerLogView()) + l.indicator = NewLogIndicator(l.app.Config, l.app.Styles, l.isContainerLogView(), l.model.LogOptions().DecodeJson) l.AddItem(l.indicator, 1, 1, false) if !l.model.HasDefaultContainer() { l.indicator.ToggleAllContainers() @@ -102,6 +104,7 @@ func (l *Log) Init(ctx context.Context) (err error) { l.updateTitle() l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime) + l.model.ToggleDecodeJson(l.app.Config.K9s.Logger.DecodeJson, ctx) return nil } @@ -223,6 +226,7 @@ func (l *Log) Start() { l.model.Start(l.getContext()) l.model.AddListener(l) l.app.Styles.AddListener(l) + l.jsonForm.model.AddListener(l) l.logs.cmdBuff.AddListener(l) l.logs.cmdBuff.AddListener(l.app.Prompt()) l.updateTitle() @@ -234,6 +238,7 @@ func (l *Log) Stop() { l.model.Stop() l.cancel() l.app.Styles.RemoveListener(l) + l.jsonForm.model.RemoveListener(l) l.logs.cmdBuff.RemoveListener(l) l.logs.cmdBuff.RemoveListener(l.app.Prompt()) } @@ -257,6 +262,9 @@ func (l *Log) bindKeys() { ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true), ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true), ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true), + ui.KeyJ: ui.NewKeyAction("Toggle JSON Decode", l.toggleDecodeJsonCmd, true), + ui.KeyShiftJ: ui.NewKeyAction("JSON Templates…", l.jsonForm.showJsonTemplatesCmd, true), + tcell.KeyCtrlJ: ui.NewKeyAction("Iterate JSON Tmpl.", l.iterateJsonTemplateCmd, true), ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), ui.KeyC: ui.NewKeyAction("Copy", cpCmd(l.app.Flash(), l.logs.TextView), true), @@ -266,6 +274,17 @@ func (l *Log) bindKeys() { } } +// JsonTemplateChanged indicates template was changed. +func (l *Log) JsonTemplateChanged() { + l.updateTitle() + l.model.Restart(l.getContext()) +} + +func (l *Log) iterateJsonTemplateCmd(evt *tcell.EventKey) *tcell.EventKey { + l.model.LogOptions().Json.IterateToNextTemplate() + return nil +} + func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey { if !l.logs.cmdBuff.IsActive() { if l.logs.cmdBuff.GetText() == "" { @@ -321,6 +340,11 @@ func (l *Log) updateTitle() { title += ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), l.app.Styles.Frame()) } + if l.model.LogOptions().DecodeJson { + jsonTemplateName := l.model.LogOptions().Json.GetCurrentTemplate().Name + title += ui.SkinTitle(fmt.Sprintf("[[::b]%s[-::]] ", jsonTemplateName), l.app.Styles.Frame()) + } + buff := l.logs.cmdBuff.GetText() if buff != "" { title += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame()) @@ -469,6 +493,16 @@ func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } +func (l *Log) toggleDecodeJsonCmd(evt *tcell.EventKey) *tcell.EventKey { + l.indicator.ToggleDecodeJson() + ctx := l.getContext() + l.model.ToggleDecodeJson(l.indicator.decodeJson, ctx) + l.indicator.Refresh() + l.updateTitle() + + return nil +} + func (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey { if l.app.InCmdMode() { return evt diff --git a/internal/view/log_indicator.go b/internal/view/log_indicator.go index ce80654b09..3dbef3943a 100644 --- a/internal/view/log_indicator.go +++ b/internal/view/log_indicator.go @@ -25,10 +25,11 @@ type LogIndicator struct { showTime bool allContainers bool shouldDisplayAllContainers bool + decodeJson bool } // NewLogIndicator returns a new indicator. -func NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bool) *LogIndicator { +func NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bool, decodeJson bool) *LogIndicator { l := LogIndicator{ styles: styles, TextView: tview.NewTextView(), @@ -38,6 +39,7 @@ func NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bo textWrap: cfg.K9s.Logger.TextWrap, showTime: cfg.K9s.Logger.ShowTime, shouldDisplayAllContainers: allContainers, + decodeJson: decodeJson, } l.StylesChanged(styles) styles.AddListener(&l) @@ -69,6 +71,11 @@ func (l *LogIndicator) TextWrap() bool { return l.textWrap } +// DecodeJson reports the current wrap mode. +func (l *LogIndicator) DecodeJson() bool { + return l.decodeJson +} + // FullScreen reports the current screen mode. func (l *LogIndicator) FullScreen() bool { return l.fullScreen @@ -79,6 +86,11 @@ func (l *LogIndicator) ToggleTimestamp() { l.showTime = !l.showTime } +// ToggleDecodeJson toggles the json decode mode. +func (l *LogIndicator) ToggleDecodeJson() { + l.decodeJson = !l.decodeJson +} + // ToggleFullScreen toggles the screen mode. func (l *LogIndicator) ToggleFullScreen() { l.fullScreen = !l.fullScreen @@ -148,6 +160,12 @@ func (l *LogIndicator) Refresh() { l.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, "Timestamps", spacer)...) } + if l.DecodeJson() { + l.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, "Json", spacer)...) + } else { + l.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, "Json", spacer)...) + } + if l.TextWrap() { l.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, "Wrap", "")...) } else { diff --git a/internal/view/log_json.go b/internal/view/log_json.go new file mode 100644 index 0000000000..67444a0c18 --- /dev/null +++ b/internal/view/log_json.go @@ -0,0 +1,98 @@ +package view + +import ( + "fmt" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/tcell/v2" + "github.com/derailed/tview" +) + +// LogTemplateForm represents a json log template viewer. +type LogTemplateForm struct { + model *dao.JsonOptions + app *App + form *tview.Form +} + +// NewLogTemplateForm returns a new json template form. +func NewLogTemplateForm(app *App, opts *dao.JsonOptions) *LogTemplateForm { + l := LogTemplateForm{ + model: opts, + app: app, + } + + return &l +} + +const jsonTemplateDialogKey = "jsonTemplateDialog" +const labelLogLevel = "Log Level :" +const labelDateTime = "Date Time :" +const labelMessage = "Message :" + +func (l *LogTemplateForm) showJsonTemplatesCmd(_ *tcell.EventKey) *tcell.EventKey { + currentTemplate := l.model.GetCurrentTemplate() + + form := tview.NewForm(). + AddDropDown("Template :", + l.model.GetAllTemplateNames(), + l.model.CurrentTemplateIndex, + l.jsonTemplateSelected). + AddInputField(labelLogLevel, currentTemplate.LogLevelExpression, 0, nil, nil). + AddInputField(labelDateTime, currentTemplate.DateTimeExpression, 0, nil, nil). + AddInputField(labelMessage, currentTemplate.MessageExpression, 0, nil, nil). + AddButton("Apply", l.applyNewJsonExpressions). + AddButton("Quit", l.dismissDialog) + + form.SetItemPadding(0) + + styles := l.app.Styles.Dialog() + form.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(styles.ButtonBgColor.Color()). + SetButtonTextColor(styles.ButtonFgColor.Color()). + SetLabelColor(styles.LabelFgColor.Color()). + SetFieldTextColor(styles.FieldFgColor.Color()). + SetFieldBackgroundColor(tcell.GetColor("darkslategray").TrueColor()) + + confirm := tview.NewModalForm(" JSON Expressions ", form) + confirm.SetText(fmt.Sprintf("Set field expressions")) + confirm.SetDoneFunc(func(int, string) { + l.dismissDialog() + }) + l.form = form + l.model.AddListener(l) + + l.app.Content.AddPage(jsonTemplateDialogKey, confirm, true, false) + l.app.Content.ShowPage(jsonTemplateDialogKey) + + return nil +} + +func (l *LogTemplateForm) JsonTemplateChanged() { + var template = l.model.GetCurrentTemplate() + if l.form != nil { + l.form.GetFormItemByLabel(labelLogLevel).(*tview.InputField).SetText(template.LogLevelExpression) + l.form.GetFormItemByLabel(labelDateTime).(*tview.InputField).SetText(template.DateTimeExpression) + l.form.GetFormItemByLabel(labelMessage).(*tview.InputField).SetText(template.MessageExpression) + } +} + +func (l *LogTemplateForm) jsonTemplateSelected(_ string, optionIndex int) { + if optionIndex == l.model.CurrentTemplateIndex { + return + } + l.model.SetCurrentTemplate(optionIndex) +} + +func (l *LogTemplateForm) applyNewJsonExpressions() { + logLevelExpression := l.form.GetFormItemByLabel(labelLogLevel).(*tview.InputField).GetText() + dateTimeExpression := l.form.GetFormItemByLabel(labelDateTime).(*tview.InputField).GetText() + messageExpression := l.form.GetFormItemByLabel(labelMessage).(*tview.InputField).GetText() + l.model.UpdateCurrentTemplate(logLevelExpression, dateTimeExpression, messageExpression) + l.dismissDialog() +} + +func (l *LogTemplateForm) dismissDialog() { + l.form = nil + l.model.RemoveListener(l) + l.app.Content.RemovePage(jsonTemplateDialogKey) +} diff --git a/internal/view/logger.go b/internal/view/logger.go index 7fc649f0ec..ff46e0989c 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -5,7 +5,6 @@ package view import ( "context" - "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go index d2a8ad4f39..442eeae1eb 100644 --- a/internal/view/logs_extender.go +++ b/internal/view/logs_extender.go @@ -86,7 +86,13 @@ func (l *LogsExtender) buildLogOpts(path, co string, prevLogs bool) *dao.LogOpti Lines: int64(cfg.TailCount), Previous: prevLogs, ShowTimestamp: cfg.ShowTime, + DecodeJson: cfg.DecodeJson, + Json: dao.JsonOptions{ + GlobalExpressions: cfg.JsonConfig.GlobalExpressions, + Templates: dao.TemplatesFromConfig(cfg.JsonConfig), + }, } + opts.Json.SetCurrentTemplateByName(cfg.JsonConfig.DefaultTemplate) if opts.Container == "" { opts.AllContainers = true } @@ -104,9 +110,15 @@ func podLogOptions(app *App, fqn string, prev bool, m metav1.ObjectMeta, spec v1 SinceSeconds: cfg.SinceSeconds, SingleContainer: len(cc) == 1, ShowTimestamp: cfg.ShowTime, + DecodeJson: cfg.DecodeJson, Previous: prev, + Json: dao.JsonOptions{ + GlobalExpressions: cfg.JsonConfig.GlobalExpressions, + Templates: dao.TemplatesFromConfig(cfg.JsonConfig), + }, } ) + opts.Json.SetCurrentTemplateByName(cfg.JsonConfig.DefaultTemplate) if c, ok := dao.GetDefaultContainer(m, spec); ok { opts.Container, opts.DefaultContainer = c, c } else if len(cc) == 1 { From 7f0c12012509014f6263175b6f151128df5e08ec Mon Sep 17 00:00:00 2001 From: Roland Oldenburg Date: Mon, 18 Nov 2024 10:14:56 +0100 Subject: [PATCH 2/7] Using GoJQ, debug mode, UI template check --- go.mod | 2 + go.sum | 4 ++ internal/config/json/schemas/k9s.json | 1 + internal/config/logger.go | 2 + internal/dao/log_options.go | 36 ++++++++++++++++ internal/dao/log_options_json.go | 55 ++++++++++++++++++++++--- internal/dao/pod.go | 20 +-------- internal/view/container.go | 1 + internal/view/log.go | 4 +- internal/view/log_json.go | 59 +++++++++++++++++++++------ internal/view/logger.go | 1 + internal/view/logs_extender.go | 2 + 12 files changed, 148 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index 16b2c774ae..3e33ce0d8d 100644 --- a/go.mod +++ b/go.mod @@ -175,6 +175,8 @@ require ( github.com/iancoleman/strcase v0.3.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/itchyny/gojq v0.12.16 + github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect diff --git a/go.sum b/go.sum index 13c08fad3e..3eea7f925e 100644 --- a/go.sum +++ b/go.sum @@ -758,6 +758,10 @@ github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g= +github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= diff --git a/internal/config/json/schemas/k9s.json b/internal/config/json/schemas/k9s.json index 6a38928628..b1de9c8f5b 100644 --- a/internal/config/json/schemas/k9s.json +++ b/internal/config/json/schemas/k9s.json @@ -109,6 +109,7 @@ "type": "object", "additionalProperties": false, "properties": { + "debug": {"type": "boolean"}, "globalExpressions": {"type": "string"}, "defaultTemplate": {"type": "string"}, "templates": { diff --git a/internal/config/logger.go b/internal/config/logger.go index 2578e08563..b8c92ce0d6 100644 --- a/internal/config/logger.go +++ b/internal/config/logger.go @@ -22,6 +22,7 @@ type JsonTemplate struct { } type JsonConfig struct { + Debug bool `json:"debug" yaml:"debug"` GlobalExpressions string `json:"globalExpressions" yaml:"globalExpressions"` DefaultTemplate string `json:"defaultTemplate" yaml:"defaultTemplate"` Templates []JsonTemplate `json:"templates" yaml:"templates"` @@ -30,6 +31,7 @@ type JsonConfig struct { // NewJsonConfig returns a new instance. func NewJsonConfig() JsonConfig { return JsonConfig{ + Debug: false, GlobalExpressions: "", DefaultTemplate: "", Templates: []JsonTemplate{}, diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index 2a8fdd24bf..a1f7e468cb 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -4,7 +4,11 @@ package dao import ( + bytes "bytes" + "errors" "fmt" + "github.com/rs/zerolog/log" + "io" "time" "github.com/derailed/k9s/internal/client" @@ -115,6 +119,38 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions { return &opts } +// HandleJson if JSON decoding is turned on, processes JSON templates with JQ +func (o *LogOptions) HandleJson(bb []byte) []byte { + if !o.DecodeJson { + return bb + } + + var b bytes.Buffer + orgLine := string(bb) + result, _ := o.Json.GetCompiledJsonQuery().Run(orgLine).Next() + err := PrintJsonResult(result, &b) + if err != nil { + log.Trace().AnErr("Error", err).Msg("Error printing JQ result") + fmt.Fprintf(&b, "%s: %s\n", "JQ", err) + fmt.Fprintf(&b, "Original line: %s", orgLine) + } + if b.Bytes()[b.Len()-1] != '\n' { + b.WriteByte('\n') + } + return b.Bytes() +} + +func PrintJsonResult(value any, outStream io.Writer) error { + if err, ok := value.(error); ok { + return err + } + if s, ok := value.(string); ok { + _, err := outStream.Write([]byte(s)) + return err + } + return errors.New("JQ result not a string") +} + // ToLogItem add a log header to display po/co information along with the log message. func (o *LogOptions) ToLogItem(bytes []byte) *LogItem { item := NewLogItem(bytes) diff --git a/internal/dao/log_options_json.go b/internal/dao/log_options_json.go index 9493f7869d..44e147d2d6 100644 --- a/internal/dao/log_options_json.go +++ b/internal/dao/log_options_json.go @@ -6,6 +6,8 @@ package dao import ( "fmt" "github.com/derailed/k9s/internal/config" + "github.com/itchyny/gojq" + "github.com/rs/zerolog/log" "slices" "strings" ) @@ -34,8 +36,10 @@ func (o *JsonTemplate) Clone() *JsonTemplate { } type JsonOptions struct { + Debug bool GlobalExpressions string CurrentTemplateIndex int + CompiledQuery *gojq.Code Templates []JsonTemplate listeners []JsonTemplateListener } @@ -72,28 +76,32 @@ func (o *JsonOptions) SetCurrentTemplateByName(templateName string) { if (o.CurrentTemplateIndex < 0) || (o.CurrentTemplateIndex >= len(o.Templates)) { o.CurrentTemplateIndex = 0 } + o.CompiledQuery = nil o.NotifyListeners() } // IterateToNextTemplate Iterates the currently selected template to next (back to first on end). func (o *JsonOptions) IterateToNextTemplate() { o.CurrentTemplateIndex = (o.CurrentTemplateIndex + 1) % len(o.Templates) + o.CompiledQuery = nil o.NotifyListeners() } // SetCurrentTemplate Set the currently selected template. func (o *JsonOptions) SetCurrentTemplate(index int) *JsonTemplate { o.CurrentTemplateIndex = index + o.CompiledQuery = nil o.NotifyListeners() return o.GetCurrentTemplate() } // UpdateCurrentTemplate Updates the currently selected template and notifies listeners. -func (o *JsonOptions) UpdateCurrentTemplate(LogLevelExpression string, DateTimeExpression string, MessageExpression string) *JsonTemplate { +func (o *JsonOptions) UpdateCurrentTemplate(logLevelExpression string, dateTimeExpression string, messageExpression string) *JsonTemplate { var template = o.GetCurrentTemplate() - template.LogLevelExpression = LogLevelExpression - template.DateTimeExpression = DateTimeExpression - template.MessageExpression = MessageExpression + template.LogLevelExpression = logLevelExpression + template.DateTimeExpression = dateTimeExpression + template.MessageExpression = messageExpression + o.CompiledQuery = nil o.NotifyListeners() return template } @@ -103,6 +111,25 @@ func (o *JsonOptions) GetCurrentTemplate() *JsonTemplate { return &o.Templates[o.CurrentTemplateIndex] } +func (o *JsonOptions) TestJsonQueryCode(logLevelExpression string, dateTimeExpression string, messageExpression string) error { + template := JsonTemplate{ + LogLevelExpression: logLevelExpression, + DateTimeExpression: dateTimeExpression, + MessageExpression: messageExpression, + } + query, err := gojq.Parse(o.GetJsonQuery(&template)) + if err != nil { + return err + } + + _, err = gojq.Compile(query) + if err != nil { + return err + } + + return nil +} + // GetAllTemplateNames Return all template names. func (o *JsonOptions) GetAllTemplateNames() []string { var names []string @@ -113,8 +140,15 @@ func (o *JsonOptions) GetAllTemplateNames() []string { } func (o *JsonOptions) GetCurrentJsonQuery() string { - var template = o.GetCurrentTemplate() + return o.GetJsonQuery(o.GetCurrentTemplate()) +} + +func (o *JsonOptions) GetJsonQuery(template *JsonTemplate) string { var query = `%s . as $line | try ( capture("^(?[0-9-:]{8,10}[^0-9][0-9\\.:]{0,10}[^ ]+) (?.*)") | .ts as $k8sts | .js | fromjson | { datetime:(%s), lvl:(%s), msg:(%s) } | "\(.datetime) \(.lvl) \(.msg)" ) catch $line` + if o.Debug { + // remove try catch in order to see the parsing errors in output + query = `%s . as $line | ( capture("^(?[0-9-:]{8,10}[^0-9][0-9\\.:]{0,10}[^ ]+) (?.*)") | .ts as $k8sts | .js | fromjson | { datetime:(%s), lvl:(%s), msg:(%s) } | "\(.datetime) \(.lvl) \(.msg)" )` + } var globalExpression = strings.Trim(strings.Trim(strings.ReplaceAll(o.GlobalExpressions, "\n", " "), " "), ";") if len(globalExpression) > 0 { globalExpression += ";" @@ -122,6 +156,16 @@ func (o *JsonOptions) GetCurrentJsonQuery() string { return fmt.Sprintf(query, globalExpression, template.DateTimeExpression, template.LogLevelExpression, template.MessageExpression) } +func (o *JsonOptions) GetCompiledJsonQuery() *gojq.Code { + query, err := gojq.Parse(o.GetCurrentJsonQuery()) + if err != nil { + log.Warn().Err(err).Msg("Failed to parse jq query") + return nil + } + o.CompiledQuery, err = gojq.Compile(query) + return o.CompiledQuery +} + // Clone clones options. func (o *JsonOptions) Clone() *JsonOptions { cTemplates := make([]JsonTemplate, len(o.Templates)) @@ -129,6 +173,7 @@ func (o *JsonOptions) Clone() *JsonOptions { cTemplates[i] = *t.Clone() } return &JsonOptions{ + Debug: o.Debug, GlobalExpressions: o.GlobalExpressions, CurrentTemplateIndex: o.CurrentTemplateIndex, Templates: cTemplates, diff --git a/internal/dao/pod.go b/internal/dao/pod.go index 2233652f2f..bbfabcc1c6 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "os/exec" "sync" "time" @@ -377,30 +376,13 @@ func readLogs(ctx context.Context, wg *sync.WaitGroup, stream io.ReadCloser, out } wg.Done() }() - if opts.DecodeJson { - cmd := exec.Command("jq", "--unbuffered", "-R", "-r", opts.Json.GetCurrentJsonQuery()) - cmd.Stdin = bufio.NewReader(stream) - newStream, err := cmd.StdoutPipe() - if err != nil { - log.Warn().Err(err).Msg("log-reader error on STDOUT pipe for jq") - } - newCombinedStream, err := cmd.StderrPipe() - if err != nil { - log.Warn().Err(err).Msg("log-reader error on STDERR pipe for jq") - } - stream = io.NopCloser(io.MultiReader(newStream, newCombinedStream)) - - if err := cmd.Start(); err != nil { - log.Warn().Err(err).Msgf("Could not start jq") - } - } log.Debug().Msgf(">>> LOG-READER PROCESSING %#v", opts) r := bufio.NewReader(stream) for { var item *LogItem if bytes, err := r.ReadBytes('\n'); err == nil { - item = opts.ToLogItem(tview.EscapeBytes(bytes)) + item = opts.ToLogItem(tview.EscapeBytes(opts.HandleJson(bytes))) } else { if errors.Is(err, io.EOF) { e := fmt.Errorf("stream closed %w for %s", err, opts.Info()) diff --git a/internal/view/container.go b/internal/view/container.go index 660d87f2ed..d2f170ee42 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -122,6 +122,7 @@ func (c *Container) logOptions(prev bool) (*dao.LogOptions, error) { Previous: prev, DecodeJson: cfg.DecodeJson, Json: dao.JsonOptions{ + Debug: cfg.JsonConfig.Debug, GlobalExpressions: cfg.JsonConfig.GlobalExpressions, Templates: dao.TemplatesFromConfig(cfg.JsonConfig), }, diff --git a/internal/view/log.go b/internal/view/log.go index e490b99e63..834616f34f 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -59,6 +59,7 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log { model: model.NewLog(gvr, opts, defaultFlushTimeout), follow: true, } + return &l } @@ -495,8 +496,7 @@ func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) toggleDecodeJsonCmd(evt *tcell.EventKey) *tcell.EventKey { l.indicator.ToggleDecodeJson() - ctx := l.getContext() - l.model.ToggleDecodeJson(l.indicator.decodeJson, ctx) + l.model.ToggleDecodeJson(l.indicator.decodeJson, l.getContext()) l.indicator.Refresh() l.updateTitle() diff --git a/internal/view/log_json.go b/internal/view/log_json.go index 67444a0c18..db44b73dd0 100644 --- a/internal/view/log_json.go +++ b/internal/view/log_json.go @@ -12,6 +12,7 @@ type LogTemplateForm struct { model *dao.JsonOptions app *App form *tview.Form + modal *tview.ModalForm } // NewLogTemplateForm returns a new json template form. @@ -33,13 +34,10 @@ func (l *LogTemplateForm) showJsonTemplatesCmd(_ *tcell.EventKey) *tcell.EventKe currentTemplate := l.model.GetCurrentTemplate() form := tview.NewForm(). - AddDropDown("Template :", - l.model.GetAllTemplateNames(), - l.model.CurrentTemplateIndex, - l.jsonTemplateSelected). - AddInputField(labelLogLevel, currentTemplate.LogLevelExpression, 0, nil, nil). - AddInputField(labelDateTime, currentTemplate.DateTimeExpression, 0, nil, nil). - AddInputField(labelMessage, currentTemplate.MessageExpression, 0, nil, nil). + AddDropDown("Template", l.model.GetAllTemplateNames(), l.model.CurrentTemplateIndex, l.jsonTemplateSelected). + AddInputField(labelLogLevel, currentTemplate.LogLevelExpression, 0, nil, l.validateLogLevelExpression). + AddInputField(labelDateTime, currentTemplate.DateTimeExpression, 0, nil, l.validateLogLevelExpression). + AddInputField(labelMessage, currentTemplate.MessageExpression, 0, nil, l.validateLogLevelExpression). AddButton("Apply", l.applyNewJsonExpressions). AddButton("Quit", l.dismissDialog) @@ -53,15 +51,16 @@ func (l *LogTemplateForm) showJsonTemplatesCmd(_ *tcell.EventKey) *tcell.EventKe SetFieldTextColor(styles.FieldFgColor.Color()). SetFieldBackgroundColor(tcell.GetColor("darkslategray").TrueColor()) - confirm := tview.NewModalForm(" JSON Expressions ", form) - confirm.SetText(fmt.Sprintf("Set field expressions")) - confirm.SetDoneFunc(func(int, string) { + modal := tview.NewModalForm(" JSON Expressions ", form) + modal.SetText(fmt.Sprintf("Set field expressions")) + modal.SetDoneFunc(func(int, string) { l.dismissDialog() }) l.form = form + l.modal = modal l.model.AddListener(l) - l.app.Content.AddPage(jsonTemplateDialogKey, confirm, true, false) + l.app.Content.AddPage(jsonTemplateDialogKey, modal, true, false) l.app.Content.ShowPage(jsonTemplateDialogKey) return nil @@ -87,8 +86,42 @@ func (l *LogTemplateForm) applyNewJsonExpressions() { logLevelExpression := l.form.GetFormItemByLabel(labelLogLevel).(*tview.InputField).GetText() dateTimeExpression := l.form.GetFormItemByLabel(labelDateTime).(*tview.InputField).GetText() messageExpression := l.form.GetFormItemByLabel(labelMessage).(*tview.InputField).GetText() - l.model.UpdateCurrentTemplate(logLevelExpression, dateTimeExpression, messageExpression) - l.dismissDialog() + + err := l.model.TestJsonQueryCode(logLevelExpression, dateTimeExpression, messageExpression) + if err == nil { + l.model.UpdateCurrentTemplate(logLevelExpression, dateTimeExpression, messageExpression) + l.dismissDialog() + } +} + +func (l *LogTemplateForm) validateLogLevelExpression(logLevelExpression string) { + dateTimeExpression := l.form.GetFormItemByLabel(labelDateTime).(*tview.InputField).GetText() + messageExpression := l.form.GetFormItemByLabel(labelMessage).(*tview.InputField).GetText() + l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) +} + +func (l *LogTemplateForm) validateDateTimeExpression(dateTimeExpression string) { + logLevelExpression := l.form.GetFormItemByLabel(labelLogLevel).(*tview.InputField).GetText() + messageExpression := l.form.GetFormItemByLabel(labelMessage).(*tview.InputField).GetText() + l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) +} + +func (l *LogTemplateForm) validateMessageExpression(messageExpression string) { + logLevelExpression := l.form.GetFormItemByLabel(labelLogLevel).(*tview.InputField).GetText() + dateTimeExpression := l.form.GetFormItemByLabel(labelDateTime).(*tview.InputField).GetText() + l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) +} + +func (l *LogTemplateForm) validateExpressions(logLevelExpression string, dateTimeExpression string, messageExpression string) error { + err := l.model.TestJsonQueryCode(logLevelExpression, dateTimeExpression, messageExpression) + if err != nil { + l.modal.SetTextColor(l.app.Styles.Frame().Status.ErrorColor.Color()) + l.modal.SetText(fmt.Sprintf("Set field expressions\nProblem: %s", err.Error())) + } else { + l.modal.SetTextColor(l.app.Styles.Dialog().FgColor.Color()) + l.modal.SetText("Set field expressions") + } + return err } func (l *LogTemplateForm) dismissDialog() { diff --git a/internal/view/logger.go b/internal/view/logger.go index ff46e0989c..7fc649f0ec 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -5,6 +5,7 @@ package view import ( "context" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go index 442eeae1eb..c62158b429 100644 --- a/internal/view/logs_extender.go +++ b/internal/view/logs_extender.go @@ -88,6 +88,7 @@ func (l *LogsExtender) buildLogOpts(path, co string, prevLogs bool) *dao.LogOpti ShowTimestamp: cfg.ShowTime, DecodeJson: cfg.DecodeJson, Json: dao.JsonOptions{ + Debug: cfg.JsonConfig.Debug, GlobalExpressions: cfg.JsonConfig.GlobalExpressions, Templates: dao.TemplatesFromConfig(cfg.JsonConfig), }, @@ -113,6 +114,7 @@ func podLogOptions(app *App, fqn string, prev bool, m metav1.ObjectMeta, spec v1 DecodeJson: cfg.DecodeJson, Previous: prev, Json: dao.JsonOptions{ + Debug: cfg.JsonConfig.Debug, GlobalExpressions: cfg.JsonConfig.GlobalExpressions, Templates: dao.TemplatesFromConfig(cfg.JsonConfig), }, From cd8f7288a3759389bf358de00370945f341fd863 Mon Sep 17 00:00:00 2001 From: Roland Oldenburg Date: Mon, 18 Nov 2024 10:30:07 +0100 Subject: [PATCH 3/7] Add version suffix for "j" for "json" --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 63f85e86aa..406d04b013 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H: else DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") endif -VERSION ?= v0.32.7 +VERSION ?= v0.32.7j IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} From 0dd823b082afe13439b35832148011a38d9e9034 Mon Sep 17 00:00:00 2001 From: Roland Oldenburg Date: Mon, 18 Nov 2024 11:24:59 +0100 Subject: [PATCH 4/7] Changed version appearance, previous approach was breaking --- Makefile | 2 +- internal/model/cluster_info.go | 2 +- internal/view/app.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 406d04b013..63f85e86aa 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H: else DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") endif -VERSION ?= v0.32.7j +VERSION ?= v0.32.7 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 62f6e7f1dd..5de4bdd03b 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -154,7 +154,7 @@ func (c *ClusterInfo) Refresh() { } v2 := NewSemVer(latestRev) - data.K9sVer, data.K9sLatest = v1.String(), v2.String() + data.K9sVer, data.K9sLatest = v1.String()+"j", v2.String() if v1.IsCurrent(v2) { data.K9sLatest = "" } diff --git a/internal/view/app.go b/internal/view/app.go index 4ac7e7c2b1..91136774a2 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -166,7 +166,7 @@ func (a *App) layout(ctx context.Context) { main.AddItem(flash, 1, 1, false) a.Main.AddPage("main", main, true, false) - a.Main.AddPage("splash", ui.NewSplash(a.Styles, a.version), true, true) + a.Main.AddPage("splash", ui.NewSplash(a.Styles, a.version+"j"), true, true) a.toggleHeader(!a.Config.K9s.IsHeadless(), !a.Config.K9s.IsLogoless()) } From cbe4db315202c73c41f400a8bf31a02118d9c63e Mon Sep 17 00:00:00 2001 From: Roland Oldenburg Date: Mon, 2 Dec 2024 17:59:31 +0100 Subject: [PATCH 5/7] Fix unit tests --- cmd/testdata/k9s.yaml | 6 ++++++ internal/config/testdata/configs/default.yaml | 6 ++++++ internal/config/testdata/configs/expected.yaml | 6 ++++++ internal/config/testdata/configs/k9s.yaml | 6 ++++++ internal/model/log.go | 6 ++++-- internal/view/log_indicator_test.go | 6 +++--- internal/view/log_int_test.go | 8 ++++---- 7 files changed, 35 insertions(+), 9 deletions(-) diff --git a/cmd/testdata/k9s.yaml b/cmd/testdata/k9s.yaml index 0587cd29bd..8326e401eb 100644 --- a/cmd/testdata/k9s.yaml +++ b/cmd/testdata/k9s.yaml @@ -4,6 +4,12 @@ k9s: logger: tail: 200 buffer: 2000 + decodeJson: false + json: + debug: false + globalExpressions: "" + defaultTemplate: "" + templates: [] currentContext: minikube currentCluster: minikube clusters: diff --git a/internal/config/testdata/configs/default.yaml b/internal/config/testdata/configs/default.yaml index abf8432ba4..0ef470a104 100644 --- a/internal/config/testdata/configs/default.yaml +++ b/internal/config/testdata/configs/default.yaml @@ -32,6 +32,12 @@ k9s: sinceSeconds: -1 textWrap: false showTime: false + decodeJson: false + json: + debug: false + globalExpressions: "" + defaultTemplate: "" + templates: [] thresholds: cpu: critical: 90 diff --git a/internal/config/testdata/configs/expected.yaml b/internal/config/testdata/configs/expected.yaml index e85a32f160..0dc0655e0e 100644 --- a/internal/config/testdata/configs/expected.yaml +++ b/internal/config/testdata/configs/expected.yaml @@ -32,6 +32,12 @@ k9s: sinceSeconds: -1 textWrap: false showTime: false + decodeJson: false + json: + debug: false + globalExpressions: "" + defaultTemplate: "" + templates: [] thresholds: cpu: critical: 90 diff --git a/internal/config/testdata/configs/k9s.yaml b/internal/config/testdata/configs/k9s.yaml index 8f3546d357..fd27970959 100644 --- a/internal/config/testdata/configs/k9s.yaml +++ b/internal/config/testdata/configs/k9s.yaml @@ -32,6 +32,12 @@ k9s: sinceSeconds: -1 textWrap: false showTime: false + decodeJson: false + json: + debug: false + globalExpressions: "" + defaultTemplate: "" + templates: [] thresholds: cpu: critical: 90 diff --git a/internal/model/log.go b/internal/model/log.go index cad53e1293..34b1e60351 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -94,8 +94,10 @@ func (l *Log) ToggleShowTimestamp(b bool) { // ToggleDecodeJson toggles to decode json in logs. func (l *Log) ToggleDecodeJson(b bool, ctx context.Context) { - l.logOptions.DecodeJson = b - l.Restart(ctx) + if l.logOptions.DecodeJson != b { + l.logOptions.DecodeJson = b + l.Restart(ctx) + } } func (l *Log) Head(ctx context.Context) { diff --git a/internal/view/log_indicator_test.go b/internal/view/log_indicator_test.go index 0c793f594d..378fea4b34 100644 --- a/internal/view/log_indicator_test.go +++ b/internal/view/log_indicator_test.go @@ -18,10 +18,10 @@ func TestLogIndicatorRefresh(t *testing.T) { e string }{ "all-containers": { - view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[::b]AllContainers:[gray::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n", + view.NewLogIndicator(config.NewConfig(nil), defaults, true, false), "[::b]AllContainers:[gray::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Json:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n", }, "plain": { - view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n", + view.NewLogIndicator(config.NewConfig(nil), defaults, false, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Json:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n", }, } @@ -36,7 +36,7 @@ func TestLogIndicatorRefresh(t *testing.T) { func BenchmarkLogIndicatorRefresh(b *testing.B) { defaults := config.NewStyles() - v := view.NewLogIndicator(config.NewConfig(nil), defaults, true) + v := view.NewLogIndicator(config.NewConfig(nil), defaults, true, false) b.ReportAllocs() b.ResetTimer() diff --git a/internal/view/log_int_test.go b/internal/view/log_int_test.go index f6b5e4aeab..68e24a8b86 100644 --- a/internal/view/log_int_test.go +++ b/internal/view/log_int_test.go @@ -26,10 +26,10 @@ func TestLogAutoScroll(t *testing.T) { v.GetModel().Set(ii) v.GetModel().Notify() - assert.Equal(t, 16, len(v.Hints())) + assert.Equal(t, 19, len(v.Hints())) v.toggleAutoScrollCmd(nil) - assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true)) + assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Json:Off Wrap:Off", v.Indicator().GetText(true)) } func TestLogViewNav(t *testing.T) { @@ -78,7 +78,7 @@ func TestLogTimestamp(t *testing.T) { &dao.LogItem{ Pod: "fred/blee", Container: "c1", - Bytes: []byte("ttt Testing 1, 2, 3\n"), + Bytes: []byte("1999-12-31T19ttt Testing 1, 2, 3\n"), }, ) var list logList @@ -90,7 +90,7 @@ func TestLogTimestamp(t *testing.T) { ii.Lines(0, true, ll) l.Flush(ll) - assert.Equal(t, fmt.Sprintf("%-30s %s", "ttt", "fred/blee c1 Testing 1, 2, 3\n"), l.Logs().GetText(true)) + assert.Equal(t, fmt.Sprintf("%s %s", "1999-12-31T19ttt", "fred/blee c1 Testing 1, 2, 3\n"), l.Logs().GetText(true)) assert.Equal(t, 2, list.change) assert.Equal(t, 2, list.clear) assert.Equal(t, 0, list.fail) From bd31bbe167570ef3ebfe34c2dcb71c41fd4e6508 Mon Sep 17 00:00:00 2001 From: Roland Oldenburg Date: Mon, 2 Dec 2024 18:21:12 +0100 Subject: [PATCH 6/7] Fix linter issues --- internal/dao/log_item.go | 2 +- internal/dao/log_options.go | 4 ++-- internal/dao/log_options_json.go | 11 ++++++----- internal/view/log_json.go | 16 ++++++++++------ 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/internal/dao/log_item.go b/internal/dao/log_item.go index 85db9cd8ff..549b77de4c 100644 --- a/internal/dao/log_item.go +++ b/internal/dao/log_item.go @@ -13,7 +13,7 @@ type LogChan chan *LogItem var ItemEOF = new(LogItem) -var dateTimePrefixRegEx = regexp.MustCompile("^\\d{4}-\\d{2}-\\d{2}T?.*") +var dateTimePrefixRegEx = regexp.MustCompile(`^\\d{4}-\\d{2}-\\d{2}T?.*`) // LogItem represents a container log line. type LogItem struct { diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index a1f7e468cb..4aa5554d71 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -4,14 +4,14 @@ package dao import ( - bytes "bytes" + "bytes" "errors" "fmt" - "github.com/rs/zerolog/log" "io" "time" "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/internal/dao/log_options_json.go b/internal/dao/log_options_json.go index 44e147d2d6..958fbee5a0 100644 --- a/internal/dao/log_options_json.go +++ b/internal/dao/log_options_json.go @@ -5,11 +5,12 @@ package dao import ( "fmt" + "slices" + "strings" + "github.com/derailed/k9s/internal/config" "github.com/itchyny/gojq" "github.com/rs/zerolog/log" - "slices" - "strings" ) // JsonTemplateListener represents a json template selection listener. @@ -45,7 +46,7 @@ type JsonOptions struct { } func TemplatesFromConfig(config config.JsonConfig) []JsonTemplate { - var templates []JsonTemplate + templates := make([]JsonTemplate, 0, len(config.Templates)) for _, obj := range config.Templates { templates = append(templates, JsonTemplate{ Name: obj.Name, @@ -132,7 +133,7 @@ func (o *JsonOptions) TestJsonQueryCode(logLevelExpression string, dateTimeExpre // GetAllTemplateNames Return all template names. func (o *JsonOptions) GetAllTemplateNames() []string { - var names []string + names := make([]string, 0, len(o.Templates)) for _, obj := range o.Templates { names = append(names, obj.Name) } @@ -162,7 +163,7 @@ func (o *JsonOptions) GetCompiledJsonQuery() *gojq.Code { log.Warn().Err(err).Msg("Failed to parse jq query") return nil } - o.CompiledQuery, err = gojq.Compile(query) + o.CompiledQuery, _ = gojq.Compile(query) return o.CompiledQuery } diff --git a/internal/view/log_json.go b/internal/view/log_json.go index db44b73dd0..55b2790b2c 100644 --- a/internal/view/log_json.go +++ b/internal/view/log_json.go @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "fmt" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/tcell/v2" "github.com/derailed/tview" @@ -36,8 +40,8 @@ func (l *LogTemplateForm) showJsonTemplatesCmd(_ *tcell.EventKey) *tcell.EventKe form := tview.NewForm(). AddDropDown("Template", l.model.GetAllTemplateNames(), l.model.CurrentTemplateIndex, l.jsonTemplateSelected). AddInputField(labelLogLevel, currentTemplate.LogLevelExpression, 0, nil, l.validateLogLevelExpression). - AddInputField(labelDateTime, currentTemplate.DateTimeExpression, 0, nil, l.validateLogLevelExpression). - AddInputField(labelMessage, currentTemplate.MessageExpression, 0, nil, l.validateLogLevelExpression). + AddInputField(labelDateTime, currentTemplate.DateTimeExpression, 0, nil, l.validateDateTimeExpression). + AddInputField(labelMessage, currentTemplate.MessageExpression, 0, nil, l.validateMessageExpression). AddButton("Apply", l.applyNewJsonExpressions). AddButton("Quit", l.dismissDialog) @@ -52,7 +56,7 @@ func (l *LogTemplateForm) showJsonTemplatesCmd(_ *tcell.EventKey) *tcell.EventKe SetFieldBackgroundColor(tcell.GetColor("darkslategray").TrueColor()) modal := tview.NewModalForm(" JSON Expressions ", form) - modal.SetText(fmt.Sprintf("Set field expressions")) + modal.SetText("Set field expressions") modal.SetDoneFunc(func(int, string) { l.dismissDialog() }) @@ -97,19 +101,19 @@ func (l *LogTemplateForm) applyNewJsonExpressions() { func (l *LogTemplateForm) validateLogLevelExpression(logLevelExpression string) { dateTimeExpression := l.form.GetFormItemByLabel(labelDateTime).(*tview.InputField).GetText() messageExpression := l.form.GetFormItemByLabel(labelMessage).(*tview.InputField).GetText() - l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) + _ = l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) } func (l *LogTemplateForm) validateDateTimeExpression(dateTimeExpression string) { logLevelExpression := l.form.GetFormItemByLabel(labelLogLevel).(*tview.InputField).GetText() messageExpression := l.form.GetFormItemByLabel(labelMessage).(*tview.InputField).GetText() - l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) + _ = l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) } func (l *LogTemplateForm) validateMessageExpression(messageExpression string) { logLevelExpression := l.form.GetFormItemByLabel(labelLogLevel).(*tview.InputField).GetText() dateTimeExpression := l.form.GetFormItemByLabel(labelDateTime).(*tview.InputField).GetText() - l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) + _ = l.validateExpressions(logLevelExpression, dateTimeExpression, messageExpression) } func (l *LogTemplateForm) validateExpressions(logLevelExpression string, dateTimeExpression string, messageExpression string) error { From bf4029a5208921c217318b4915a1207438af4ec7 Mon Sep 17 00:00:00 2001 From: Roland Oldenburg Date: Mon, 9 Dec 2024 11:55:44 +0100 Subject: [PATCH 7/7] Fix issue from linter fix --- internal/dao/log_item.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/dao/log_item.go b/internal/dao/log_item.go index 549b77de4c..3f6ebe0a69 100644 --- a/internal/dao/log_item.go +++ b/internal/dao/log_item.go @@ -13,7 +13,7 @@ type LogChan chan *LogItem var ItemEOF = new(LogItem) -var dateTimePrefixRegEx = regexp.MustCompile(`^\\d{4}-\\d{2}-\\d{2}T?.*`) +var dateTimePrefixRegEx = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T?.*`) // LogItem represents a container log line. type LogItem struct {