Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelogs/unreleased/9317-itrooz
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add color to velero logs
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/bombsimon/logrusr/v3 v3.0.0
github.com/evanphx/json-patch/v5 v5.9.11
github.com/fatih/color v1.18.0
github.com/go-logfmt/logfmt v0.4.0
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
Expand Down Expand Up @@ -135,6 +136,7 @@ require (
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.4 // indirect
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
Expand Down Expand Up @@ -499,6 +500,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/kopia/htmluibuild v0.0.1-0.20250607181534-77e0f3f9f557 h1:je1C/xnmKxnaJsIgj45me5qA51TgtK9uMwTxgDw+9H0=
github.com/kopia/htmluibuild v0.0.1-0.20250607181534-77e0f3f9f557/go.mod h1:h53A5JM3t2qiwxqxusBe+PFgGcgZdS+DWCQvG5PTlto=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand Down
6 changes: 5 additions & 1 deletion pkg/cmd/cli/backup/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/cmd"
"github.com/vmware-tanzu/velero/pkg/cmd/util/cacert"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
)

type LogsOptions struct {
Expand Down Expand Up @@ -86,7 +87,10 @@ func (l *LogsOptions) Run(c *cobra.Command, f client.Factory) error {
bslCACert = ""
}

err = downloadrequest.StreamWithBSLCACert(context.Background(), l.Client, f.Namespace(), l.BackupName, velerov1api.DownloadTargetKindBackupLog, os.Stdout, l.Timeout, l.InsecureSkipTLSVerify, l.CaCertFile, bslCACert)
w, wg := output.PrintLogsWithColor()
err = downloadrequest.StreamWithBSLCACert(context.Background(), l.Client, f.Namespace(), l.BackupName, velerov1api.DownloadTargetKindBackupLog, w, l.Timeout, l.InsecureSkipTLSVerify, l.CaCertFile, bslCACert)
w.Close() // signal we're done writing
wg.Wait() // wait for all logs to be processed and printed
return err
}

Expand Down
6 changes: 5 additions & 1 deletion pkg/cmd/cli/restore/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/cmd"
"github.com/vmware-tanzu/velero/pkg/cmd/util/cacert"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
)

func NewLogsCommand(f client.Factory) *cobra.Command {
Expand Down Expand Up @@ -77,7 +78,10 @@ func NewLogsCommand(f client.Factory) *cobra.Command {
bslCACert = ""
}

err = downloadrequest.StreamWithBSLCACert(context.Background(), kbClient, f.Namespace(), restoreName, velerov1api.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile, bslCACert)
w, wg := output.PrintLogsWithColor()
err = downloadrequest.StreamWithBSLCACert(context.Background(), kbClient, f.Namespace(), restoreName, velerov1api.DownloadTargetKindRestoreLog, w, timeout, insecureSkipTLSVerify, caCertFile, bslCACert)
w.Close() // signal we're done writing
wg.Wait() // wait for all logs to be processed and printed
cmd.CheckError(err)
},
}
Expand Down
127 changes: 127 additions & 0 deletions pkg/cmd/util/output/logs_color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright the Velero contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package output

import (
"fmt"
"io"
"os"
"strings"
"sync"
"unicode/utf8"

"github.com/fatih/color"
"github.com/go-logfmt/logfmt"
)

func getLevelColor(level string) *color.Color {
switch level {
case "info":
return color.New(color.FgGreen)
case "warning":
return color.New(color.FgYellow)
case "error":
return color.New(color.FgRed)
case "debug":
return color.New(color.FgBlue)
default:
return color.New()
}
}

// https://github.com/go-logfmt/logfmt/blob/e5396c6ee35145aead27da56e7921a7656f69624/encode.go#L235
func needsQuotedValueRune(r rune) bool {
return r <= ' ' || r == '=' || r == '"' || r == 0x7f || r == utf8.RuneError
}

// Process logs (by adding color) before printing them
func processAndPrintLogs(r io.Reader, w io.Writer) error {
d := logfmt.NewDecoder(r)
for d.ScanRecord() { // get record (line)
// Scan fields and get color
var fields [][2][]byte
var lineColor *color.Color
for d.ScanKeyval() {
fields = append(fields, [2][]byte{d.Key(), d.Value()})
if string(d.Key()) == "level" {
lineColor = getLevelColor(string(d.Value()))
}
}

// Re-encode with color. We do not use logfmt Encoder because it does not support color
for i, field := range fields {
key := string(field[0])
value := string(field[1])

// Quote if needed
if strings.IndexFunc(value, needsQuotedValueRune) != -1 {
value = fmt.Sprintf("%q", value)
}

// Add color
if lineColor != nil { // handle case where no color (log level) was found
if key == "level" {
colorCopy := *lineColor
value = colorCopy.Add(color.Bold).Sprintf("%s", value)
}
key = lineColor.Sprintf("%s", field[0])
}
if i != 0 {
fmt.Fprint(w, " ")
}
fmt.Fprintf(w, "%s=%s", key, value)
}
fmt.Fprintln(w)
}
if err := d.Err(); err != nil {
return fmt.Errorf("error processing logs: %v", err)
}
return nil
}

type nopCloser struct {
io.Writer
}

func (nopCloser) Close() error { return nil }

// Print logfmt-formatted logs to stdout with color based on log level
// if color.NoColor is set, logs will be directly piped to stdout without processing
// Returns the writer to write logs to, and a waitgroup to wait for processing to finish
// Writer must be closed once all logs have been written
// Note: this function is a wrapper around processAndPrintLogs to avoid always creating a goroutine
func PrintLogsWithColor() (io.WriteCloser, *sync.WaitGroup) {
// If NoColor, do not parse logs and directly fall back to stdout
var wg sync.WaitGroup
if color.NoColor {
return nopCloser{os.Stdout}, &wg
} else {
// Else, create a goroutine to process logs.
wg.Add(1)
pr, pw := io.Pipe()

// Create coroutine to process logs
go func(pr *io.PipeReader) {
defer wg.Done()
err := processAndPrintLogs(pr, os.Stdout)
if err != nil {
fmt.Fprintf(os.Stderr, "error processing logs: %v\n", err)
}
}(pr)
return pw, &wg
}
}
76 changes: 76 additions & 0 deletions pkg/cmd/util/output/logs_color_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
Copyright the Velero contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package output

import (
"bytes"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

var LOG_LINE = "level=info msg=\"This is a test log\" key1=value1 key2=\"value with spaces\""
var LOG_LINE_2 = "level=INVALID msg=\"This is a second test log\" key1=value1 key2=\"value 2 with spaces\""
var LOG_LINE_3 = "level=error msg=\"This is a thirdtest log\" key1=value1 key2=\"value 3 with spaces\""

// Note that all comparisons in this file work because color.NoColor is set to false by default, and thus no colors are added, even
// through the color adding code is run.

func TestColoredLogHasLog(t *testing.T) {
inputBuf := &bytes.Buffer{}
inputBuf.WriteString(LOG_LINE)

outputBuf := &bytes.Buffer{}
err := processAndPrintLogs(inputBuf, outputBuf)
if err != nil {
t.Fatalf("processAndPrintLogs returned error: %v", err)
}

assert.Contains(t, outputBuf.String(), "This is a test log")
}

// Test log line is unchanged since log is decomposed and re-composed
func TestColoredLogIsSameAsUncoloredLog(t *testing.T) {
inputBuf := &bytes.Buffer{}
inputBuf.WriteString(LOG_LINE)

outputBuf := &bytes.Buffer{}
err := processAndPrintLogs(inputBuf, outputBuf)
if err != nil {
t.Fatalf("processAndPrintLogs returned error: %v", err)
}

assert.Equal(t, LOG_LINE+"\n", outputBuf.String())
}

// Test all log lines are sent correctly (and unchanged)
func TestMultipleColoredLogs(t *testing.T) {
inputBuf := &bytes.Buffer{}
inputBuf.WriteString(LOG_LINE)
inputBuf.WriteString("\n")
inputBuf.WriteString(LOG_LINE_2)
inputBuf.WriteString("\n")
inputBuf.WriteString(LOG_LINE_3)

outputBuf := &bytes.Buffer{}
err := processAndPrintLogs(inputBuf, outputBuf)
if err != nil {
t.Fatalf("processAndPrintLogs returned error: %v", err)
}

assert.Equal(t, fmt.Sprintf("%v\n%v\n%v\n", LOG_LINE, LOG_LINE_2, LOG_LINE_3), outputBuf.String())
}
8 changes: 4 additions & 4 deletions pkg/cmd/velero/velero.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func NewCommand(name string) *cobra.Command {
// Declare cmdFeatures and cmdColorzied here so we can access them in the PreRun hooks
// without doing a chain of calls into the command's FlagSet
var cmdFeatures veleroflag.StringArray
var cmdColorzied veleroflag.OptionalBool
var cmdColorized veleroflag.OptionalBool

c := &cobra.Command{
Use: name,
Expand All @@ -86,8 +86,8 @@ operations can also be performed as 'velero backup get' and 'velero schedule cre
features.Enable(cmdFeatures...)

switch {
case cmdColorzied.Value != nil:
color.NoColor = !*cmdColorzied.Value
case cmdColorized.Value != nil:
color.NoColor = !*cmdColorized.Value
default:
color.NoColor = !config.Colorized()
}
Expand All @@ -101,7 +101,7 @@ operations can also be performed as 'velero backup get' and 'velero schedule cre
c.PersistentFlags().Var(&cmdFeatures, "features", "Comma-separated list of features to enable for this Velero process. Combines with values from $HOME/.config/velero/config.json if present")

// Color will be enabled or disabled for all subcommands
c.PersistentFlags().Var(&cmdColorzied, "colorized", "Show colored output in TTY. Overrides 'colorized' value from $HOME/.config/velero/config.json if present. Enabled by default")
c.PersistentFlags().Var(&cmdColorized, "colorized", "Show colored output in TTY. Overrides 'colorized' value from $HOME/.config/velero/config.json if present. Enabled by default")

c.AddCommand(
backup.NewCommand(f),
Expand Down