mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 15:11:02 +00:00
Merge 0f0783ae3c into 7e7aadc207
This commit is contained in:
commit
a98cc2c6b1
11 changed files with 449 additions and 3 deletions
|
|
@ -0,0 +1,2 @@
|
|||
logs:
|
||||
pager: "lnav"
|
||||
|
|
@ -64,6 +64,7 @@ logs:
|
|||
timestamps: false
|
||||
since: '60m' # set to '' to show all logs
|
||||
tail: '' # set to 200 to show last 200 lines of logs
|
||||
pager: 'less -R' # command for external logs pager (e.g. 'less -R', 'lnav')
|
||||
commandTemplates:
|
||||
dockerCompose: docker compose # Determines the Docker Compose command to run, referred to as .DockerCompose in commandTemplates
|
||||
restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}'
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ type LogsConfig struct {
|
|||
Timestamps bool `yaml:"timestamps,omitempty"`
|
||||
Since string `yaml:"since,omitempty"`
|
||||
Tail string `yaml:"tail,omitempty"`
|
||||
Pager string `yaml:"pager,omitempty"` // Configuration for external logs viewer (e.g. "lnav", "less -R")
|
||||
}
|
||||
|
||||
// GetDefaultConfig returns the application default configuration NOTE (to
|
||||
|
|
@ -383,6 +384,7 @@ func GetDefaultConfig() UserConfig {
|
|||
Timestamps: false,
|
||||
Since: "60m",
|
||||
Tail: "",
|
||||
Pager: "less -R",
|
||||
},
|
||||
CommandTemplates: CommandTemplatesConfig{
|
||||
DockerCompose: "docker compose",
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
|
|
@ -136,13 +140,18 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
stdoutWriter := NewLogWriter(writer, false)
|
||||
stderrWriter := NewLogWriter(writer, true)
|
||||
defer stdoutWriter.Close()
|
||||
defer stderrWriter.Close()
|
||||
|
||||
if ctr.Details.Config.Tty {
|
||||
_, err = io.Copy(writer, readCloser)
|
||||
_, err = io.Copy(stdoutWriter, readCloser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(writer, writer, readCloser)
|
||||
_, err = stdcopy.StdCopy(stdoutWriter, stderrWriter, readCloser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -150,3 +159,137 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
type LogWriter struct {
|
||||
writer io.Writer
|
||||
lineBuffer []byte
|
||||
isStderr bool
|
||||
}
|
||||
|
||||
func NewLogWriter(writer io.Writer, isStderr bool) *LogWriter {
|
||||
return &LogWriter{
|
||||
writer: writer,
|
||||
lineBuffer: make([]byte, 0),
|
||||
isStderr: isStderr,
|
||||
}
|
||||
}
|
||||
|
||||
func (lw *LogWriter) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
lw.lineBuffer = append(lw.lineBuffer, p...)
|
||||
|
||||
for {
|
||||
idx := bytes.IndexByte(lw.lineBuffer, '\n')
|
||||
if idx == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
line := string(lw.lineBuffer[:idx])
|
||||
lw.lineBuffer = lw.lineBuffer[idx+1:]
|
||||
|
||||
colorised := utils.ColoriseLog(line)
|
||||
if lw.isStderr {
|
||||
colorised = "\x1b[31;1m" + colorised + "\x1b[0m"
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(lw.writer, colorised)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (lw *LogWriter) Close() error {
|
||||
if len(lw.lineBuffer) > 0 {
|
||||
line := string(lw.lineBuffer)
|
||||
colorised := utils.ColoriseLog(line)
|
||||
if lw.isStderr {
|
||||
colorised = "\x1b[31;1m" + colorised + "\x1b[0m"
|
||||
}
|
||||
_, err := fmt.Fprint(lw.writer, colorised)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainerViewLogsExternal(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return gui.handleViewLogsExternal(ctr)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMainViewLogsExternal(g *gocui.Gui, v *gocui.View) error {
|
||||
currentSideViewName := gui.currentSideViewName()
|
||||
if currentSideViewName == "containers" {
|
||||
return gui.handleContainerViewLogsExternal(g, v)
|
||||
} else if currentSideViewName == "services" {
|
||||
return gui.handleServiceViewLogsExternal(g, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleViewLogsExternal(container *commands.Container) error {
|
||||
pager := gui.Config.UserConfig.Logs.Pager
|
||||
if pager == "" {
|
||||
return gui.createErrorPanel("No external pager configured. Please set 'logs.pager' in config.yml")
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "lazydocker-*.log")
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer func() {
|
||||
tmpFile.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
}()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
defer signal.Stop(stop)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
<-stop
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Start writing container logs to the temporary file in background
|
||||
go func() {
|
||||
_ = gui.writeContainerLogs(container, ctx, tmpFile)
|
||||
}()
|
||||
|
||||
// Wait briefly for some logs to be written so the pager has content on start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if err := gui.g.Suspend(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := gui.g.Resume(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
var cmdStr string
|
||||
if strings.Contains(pager, "{{filename}}") {
|
||||
cmdStr = strings.ReplaceAll(pager, "{{filename}}", tmpPath)
|
||||
} else {
|
||||
cmdStr = pager + " " + tmpPath
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", cmdStr)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
_ = cmd.Run()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,23 @@ func (gui *Gui) switchFocusAux(newView *gocui.View) error {
|
|||
|
||||
gui.g.Cursor = newView.Editable
|
||||
|
||||
// Automatically control mouse capture mode depending on focus
|
||||
if newView.Name() == "main" {
|
||||
if gui.g.Mouse {
|
||||
gui.g.Mouse = false
|
||||
gocui.Screen.DisableMouse()
|
||||
}
|
||||
} else {
|
||||
if !gui.Config.UserConfig.Gui.IgnoreMouseEvents {
|
||||
if !gui.g.Mouse {
|
||||
gui.g.Mouse = true
|
||||
gocui.Screen.EnableMouse()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Force refresh the bottom information panel with updated status text
|
||||
_ = gui.renderString(gui.g, "information", gui.getInformationContent())
|
||||
|
||||
if err := gui.renderPanelOptions(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package gui
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
|
|
@ -74,6 +75,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Modifier: gocui.ModNone,
|
||||
Handler: gui.quit,
|
||||
},
|
||||
|
||||
{
|
||||
ViewName: "",
|
||||
Key: gocui.KeyPgup,
|
||||
|
|
@ -234,6 +236,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Handler: gui.handleContainerViewLogs,
|
||||
Description: gui.Tr.ViewLogs,
|
||||
},
|
||||
{
|
||||
ViewName: "containers",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleContainerViewLogsExternal,
|
||||
Description: "View logs with external pager",
|
||||
},
|
||||
{
|
||||
ViewName: "containers",
|
||||
Key: 'E',
|
||||
|
|
@ -318,6 +327,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Handler: gui.handleServiceRenderLogsToMain,
|
||||
Description: gui.Tr.ViewLogs,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceViewLogsExternal,
|
||||
Description: "View logs with external pager",
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'U',
|
||||
|
|
@ -437,6 +453,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Handler: gui.handleExitMain,
|
||||
Description: gui.Tr.Return,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMainViewLogsExternal,
|
||||
Description: "View logs with external pager",
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowLeft,
|
||||
|
|
@ -587,7 +610,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
}
|
||||
}
|
||||
|
||||
return bindings
|
||||
return translateKeybindingsToKorean(bindings)
|
||||
}
|
||||
|
||||
func (gui *Gui) keybindings(g *gocui.Gui) error {
|
||||
|
|
@ -611,3 +634,86 @@ func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error {
|
|||
return f()
|
||||
}
|
||||
}
|
||||
|
||||
type koreanKey struct {
|
||||
key rune
|
||||
modifier gocui.Modifier
|
||||
}
|
||||
|
||||
var englishToKorean = map[rune]koreanKey{
|
||||
// Lowercase
|
||||
'q': {key: 'ㅂ', modifier: gocui.ModNone},
|
||||
'w': {key: 'ㅈ', modifier: gocui.ModNone},
|
||||
'e': {key: 'ㄷ', modifier: gocui.ModNone},
|
||||
'r': {key: 'ㄱ', modifier: gocui.ModNone},
|
||||
't': {key: 'ㅅ', modifier: gocui.ModNone},
|
||||
'y': {key: 'ㅛ', modifier: gocui.ModNone},
|
||||
'u': {key: 'ㅕ', modifier: gocui.ModNone},
|
||||
'i': {key: 'ㅑ', modifier: gocui.ModNone},
|
||||
'o': {key: 'ㅐ', modifier: gocui.ModNone},
|
||||
'p': {key: 'ㅔ', modifier: gocui.ModNone},
|
||||
'a': {key: 'ㅁ', modifier: gocui.ModNone},
|
||||
's': {key: 'ㄴ', modifier: gocui.ModNone},
|
||||
'd': {key: 'ㅇ', modifier: gocui.ModNone},
|
||||
'f': {key: 'ㄹ', modifier: gocui.ModNone},
|
||||
'g': {key: 'ㅎ', modifier: gocui.ModNone},
|
||||
'h': {key: 'ㅗ', modifier: gocui.ModNone},
|
||||
'j': {key: 'ㅓ', modifier: gocui.ModNone},
|
||||
'k': {key: 'ㅏ', modifier: gocui.ModNone},
|
||||
'l': {key: 'ㅣ', modifier: gocui.ModNone},
|
||||
'z': {key: 'ㅋ', modifier: gocui.ModNone},
|
||||
'x': {key: 'ㅌ', modifier: gocui.ModNone},
|
||||
'c': {key: 'ㅊ', modifier: gocui.ModNone},
|
||||
'v': {key: 'ㅍ', modifier: gocui.ModNone},
|
||||
'b': {key: 'ㅠ', modifier: gocui.ModNone},
|
||||
'n': {key: 'ㅜ', modifier: gocui.ModNone},
|
||||
'm': {key: 'ㅡ', modifier: gocui.ModNone},
|
||||
|
||||
// Uppercase (Double Consonants - ModNone)
|
||||
'Q': {key: 'ㅃ', modifier: gocui.ModNone},
|
||||
'W': {key: 'ㅉ', modifier: gocui.ModNone},
|
||||
'E': {key: 'ㄸ', modifier: gocui.ModNone},
|
||||
'R': {key: 'ㄲ', modifier: gocui.ModNone},
|
||||
'T': {key: 'ㅆ', modifier: gocui.ModNone},
|
||||
'O': {key: 'ㅒ', modifier: gocui.ModNone},
|
||||
'P': {key: 'ㅖ', modifier: gocui.ModNone},
|
||||
|
||||
// Uppercase (No Double Consonant - ModShift)
|
||||
'A': {key: 'ㅁ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'S': {key: 'ㄴ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'D': {key: 'ㅇ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'F': {key: 'ㄹ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'G': {key: 'ㅎ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'H': {key: 'ㅗ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'J': {key: 'ㅓ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'K': {key: 'ㅏ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'L': {key: 'ㅣ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'Z': {key: 'ㅋ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'X': {key: 'ㅌ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'C': {key: 'ㅊ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'V': {key: 'ㅍ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'B': {key: 'ㅠ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'N': {key: 'ㅜ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'M': {key: 'ㅡ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'Y': {key: 'ㅛ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'U': {key: 'ㅕ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
'I': {key: 'ㅑ', modifier: gocui.Modifier(tcell.ModShift)},
|
||||
}
|
||||
|
||||
func translateKeybindingsToKorean(bindings []*Binding) []*Binding {
|
||||
var translated []*Binding
|
||||
for _, binding := range bindings {
|
||||
if r, ok := binding.Key.(rune); ok {
|
||||
if kor, exists := englishToKorean[r]; exists {
|
||||
translated = append(translated, &Binding{
|
||||
ViewName: binding.ViewName,
|
||||
Handler: binding.Handler,
|
||||
Key: kor.key,
|
||||
Modifier: binding.Modifier | kor.modifier,
|
||||
Description: binding.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return append(bindings, translated...)
|
||||
}
|
||||
|
|
|
|||
75
pkg/gui/keybindings_test.go
Normal file
75
pkg/gui/keybindings_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTranslateKeybindingsToKorean(t *testing.T) {
|
||||
inputBindings := []*Binding{
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 's',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: nil,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'S',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: nil,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: nil,
|
||||
},
|
||||
{
|
||||
ViewName: "global",
|
||||
Key: gocui.KeyCtrlC,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: nil,
|
||||
},
|
||||
}
|
||||
|
||||
result := translateKeybindingsToKorean(inputBindings)
|
||||
|
||||
// The original bindings must remain intact, and new ones should be appended
|
||||
assert.Len(t, result, len(inputBindings)+3)
|
||||
|
||||
// Verify original bindings are still present and unaltered
|
||||
for i, b := range inputBindings {
|
||||
assert.Equal(t, b.Key, result[i].Key)
|
||||
assert.Equal(t, b.Modifier, result[i].Modifier)
|
||||
}
|
||||
|
||||
// Verify translated Korean bindings are appended correctly
|
||||
// 's' -> 'ㄴ' (gocui.ModNone)
|
||||
// 'S' -> 'ㄴ' (gocui.Modifier(tcell.ModShift))
|
||||
// 'l' -> 'ㅣ' (gocui.ModNone)
|
||||
|
||||
// We expect translated bindings to be appended:
|
||||
foundS := false
|
||||
foundShiftS := false
|
||||
foundL := false
|
||||
|
||||
for _, b := range result[len(inputBindings):] {
|
||||
if b.Key == 'ㄴ' && b.Modifier == gocui.ModNone {
|
||||
foundS = true
|
||||
}
|
||||
if b.Key == 'ㄴ' && b.Modifier == gocui.Modifier(tcell.ModShift) {
|
||||
foundShiftS = true
|
||||
}
|
||||
if b.Key == 'ㅣ' && b.Modifier == gocui.ModNone {
|
||||
foundL = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, foundS, "Expected to find translated Korean binding for 's'")
|
||||
assert.True(t, foundShiftS, "Expected to find translated Korean binding for 'S'")
|
||||
assert.True(t, foundL, "Expected to find translated Korean binding for 'l'")
|
||||
}
|
||||
|
|
@ -333,6 +333,17 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error
|
|||
return gui.runSubprocess(c)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceViewLogsExternal(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if service.Container == nil {
|
||||
return gui.createErrorPanel("No container running for this service")
|
||||
}
|
||||
return gui.handleViewLogsExternal(service.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||
|
|
|
|||
|
|
@ -195,6 +195,13 @@ func (gui *Gui) setInitialViewContent() error {
|
|||
|
||||
func (gui *Gui) getInformationContent() string {
|
||||
informationStr := gui.Config.Version
|
||||
|
||||
mouseStatus := "Mouse: ON"
|
||||
if !gui.g.Mouse {
|
||||
mouseStatus = "Mouse: OFF (Drag to copy)"
|
||||
}
|
||||
informationStr = mouseStatus + " | " + informationStr
|
||||
|
||||
if !gui.g.Mouse {
|
||||
return informationStr
|
||||
}
|
||||
|
|
|
|||
41
pkg/utils/logs.go
Normal file
41
pkg/utils/logs.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errRegex = regexp.MustCompile(`(?i)\b(error|err|critical|fatal)\b`)
|
||||
warnRegex = regexp.MustCompile(`(?i)\b(warning|warn)\b`)
|
||||
infoRegex = regexp.MustCompile(`(?i)\b(info)\b`)
|
||||
debugRegex = regexp.MustCompile(`(?i)\b(debug)\b`)
|
||||
|
||||
timestampRegex = regexp.MustCompile(`\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b`)
|
||||
)
|
||||
|
||||
// ColoriseLog applies basic ANSI syntax highlighting to typical log statements.
|
||||
func ColoriseLog(line string) string {
|
||||
if strings.Contains(line, "\x1b[") {
|
||||
return line
|
||||
}
|
||||
|
||||
line = timestampRegex.ReplaceAllStringFunc(line, func(ts string) string {
|
||||
return "\x1b[90m" + ts + "\x1b[0m"
|
||||
})
|
||||
|
||||
line = errRegex.ReplaceAllStringFunc(line, func(match string) string {
|
||||
return "\x1b[31;1m" + match + "\x1b[0m"
|
||||
})
|
||||
line = warnRegex.ReplaceAllStringFunc(line, func(match string) string {
|
||||
return "\x1b[33;1m" + match + "\x1b[0m"
|
||||
})
|
||||
line = infoRegex.ReplaceAllStringFunc(line, func(match string) string {
|
||||
return "\x1b[32m" + match + "\x1b[0m"
|
||||
})
|
||||
line = debugRegex.ReplaceAllStringFunc(line, func(match string) string {
|
||||
return "\x1b[34m" + match + "\x1b[0m"
|
||||
})
|
||||
|
||||
return line
|
||||
}
|
||||
41
pkg/utils/logs_test.go
Normal file
41
pkg/utils/logs_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestColoriseLog(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "Highlight error log level",
|
||||
input: "ERROR: database failed to connect",
|
||||
contains: []string{"\x1b[31;1mERROR\x1b[0m"},
|
||||
},
|
||||
{
|
||||
name: "Highlight warning log level",
|
||||
input: "[WARN] deprecation warning",
|
||||
contains: []string{"\x1b[33;1mWARN\x1b[0m"},
|
||||
},
|
||||
{
|
||||
name: "Highlight timestamp",
|
||||
input: "2026-06-04T12:00:00Z something happened",
|
||||
contains: []string{"\x1b[90m2026-06-04T12:00:00Z\x1b[0m"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ColoriseLog(tt.input)
|
||||
for _, c := range tt.contains {
|
||||
if !strings.Contains(got, c) {
|
||||
t.Errorf("expected %q to contain %q, but it was %q", got, c, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue