github.com/banzaicloud/operator-tools@v0.28.10/pkg/logger/logsink.go (about)

     1  // Copyright © 2020 Banzai Cloud
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package logger
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"emperror.dev/errors"
    26  	"github.com/briandowns/spinner"
    27  	"github.com/fatih/color"
    28  	"github.com/go-logr/logr"
    29  	"github.com/spf13/cast"
    30  	terminal "github.com/wayneashleyberry/terminal-dimensions"
    31  )
    32  
    33  const (
    34  	defaultDisableColor = false
    35  )
    36  
    37  type SpinnerLogSink struct {
    38  	names  []string
    39  	values []interface{}
    40  	out    io.Writer
    41  	err    io.Writer
    42  
    43  	grouppable         bool
    44  	truncate           bool
    45  	showTime           bool
    46  	grouped            bool
    47  	disableColor       bool
    48  	checkMark          rune
    49  	errorMark          rune
    50  	separatorCharacter rune
    51  	timeFormat         string
    52  
    53  	colors Colors
    54  
    55  	spinner *spinner.Spinner
    56  
    57  	mux sync.Mutex
    58  }
    59  
    60  func NewSpinnerLogSink(options ...Option) *SpinnerLogSink {
    61  	l := &SpinnerLogSink{
    62  		names: []string{},
    63  		out:   os.Stderr,
    64  		err:   os.Stderr,
    65  
    66  		checkMark:          '✓',
    67  		errorMark:          '✗',
    68  		separatorCharacter: '❯',
    69  		colors: Colors{
    70  			Info:  color.FgGreen,
    71  			Error: color.FgRed,
    72  			Key:   color.FgHiGreen,
    73  		},
    74  		disableColor: defaultDisableColor,
    75  
    76  		mux: sync.Mutex{},
    77  	}
    78  
    79  	for _, opt := range options {
    80  		opt(l)
    81  	}
    82  
    83  	return l
    84  }
    85  
    86  func (log *SpinnerLogSink) Init(_ logr.RuntimeInfo) {}
    87  
    88  // Info implements logr.LogSink interface
    89  func (log *SpinnerLogSink) Info(level int, msg string, keysAndValues ...interface{}) {
    90  
    91  	colorPrinter := log.getColorPrinter(log.colors.Info)
    92  
    93  	if !log.Enabled(level) {
    94  		return
    95  	}
    96  	allVal := append(keysAndValues, log.values...)
    97  	if len(allVal) > 0 {
    98  		msg = fmt.Sprintf("%s %s", msg, log.joinAndSeparatePairs(allVal))
    99  	}
   100  
   101  	names := log.printNames()
   102  	if names != "" {
   103  		msg = fmt.Sprintf("%s %c %s", names, log.separatorCharacter, msg)
   104  	}
   105  
   106  	if log.timeFormat != "" && log.showTime {
   107  		msg = fmt.Sprintf("[%s] %s", time.Now().Format(log.timeFormat), msg)
   108  	}
   109  
   110  	if log.truncate {
   111  		w, _ := terminal.Width()
   112  		// vscode debug window returns 0 width (without an error)
   113  		if w > 3 {
   114  			w -= 3 // reduced by 3 (spinner char, space, leeway)
   115  			msg = log.truncateString(msg, int(w))
   116  		}
   117  	}
   118  
   119  	if log.spinner == nil {
   120  		log.initSpinner()
   121  	}
   122  
   123  	log.mux.Lock()
   124  	if log.spinner != nil {
   125  		log.spinner.Writer = log.out
   126  		log.spinner.Suffix = " " + msg // Append text after the spinner
   127  		log.spinner.FinalMSG = colorPrinter.Sprintf("%c", log.checkMark) + log.spinner.Suffix + "\n"
   128  	}
   129  	log.mux.Unlock()
   130  
   131  	if log.spinner != nil && !log.grouped {
   132  		log.stopSpinner()
   133  	}
   134  }
   135  
   136  // Enabled implements logr.LogSink interface
   137  func (log *SpinnerLogSink) Enabled(level int) bool {
   138  	return GlobalLogLevel >= level
   139  }
   140  
   141  // Error implements logr.LogSink interface
   142  func (log *SpinnerLogSink) Error(e error, msg string, keysAndValues ...interface{}) {
   143  	allVal := append(keysAndValues, log.values...)
   144  
   145  	colorPrinter := log.getColorPrinter(log.colors.Error)
   146  
   147  	if msg != "" {
   148  		msg = colorPrinter.Sprintf("%s: %s", msg, log.getDetailedErr(e))
   149  	} else {
   150  		msg = colorPrinter.Sprintf("%s", log.getDetailedErr(e))
   151  	}
   152  	if len(allVal) > 0 {
   153  		msg = fmt.Sprintf("%s %s", msg, log.joinAndSeparatePairs(allVal))
   154  	}
   155  
   156  	names := log.printNames()
   157  	if names != "" {
   158  		msg = fmt.Sprintf("%s %c %s", names, log.separatorCharacter, msg)
   159  	}
   160  
   161  	if log.timeFormat != "" && log.showTime {
   162  		msg = fmt.Sprintf("[%s] %s", time.Now().Format(log.timeFormat), msg)
   163  	}
   164  
   165  	if log.spinner == nil {
   166  		log.initSpinner()
   167  	} else {
   168  		log.spinner.Restart()
   169  	}
   170  
   171  	log.spinner.Writer = log.err
   172  	log.spinner.Suffix = " " + msg // Append text after the spinner
   173  	log.spinner.FinalMSG = colorPrinter.Sprintf("%c", log.errorMark) + log.spinner.Suffix + "\n"
   174  
   175  	log.stopSpinner()
   176  }
   177  
   178  // WithName implements logr.LogSink interface
   179  func (log *SpinnerLogSink) WithName(name string) logr.LogSink {
   180  	l := log.copyLogger()
   181  	l.names = append(l.names, name)
   182  
   183  	return l
   184  }
   185  
   186  // WithValues implements logr.LogSink interface
   187  func (log *SpinnerLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink {
   188  	l := log.copyLogger()
   189  	l.values = append(l.values, keysAndValues...)
   190  
   191  	return l
   192  }
   193  
   194  func (log *SpinnerLogSink) SetOptions(options ...Option) {
   195  	for _, opt := range options {
   196  		opt(log)
   197  	}
   198  }
   199  
   200  func (log *SpinnerLogSink) ShowTime(f bool) *SpinnerLogSink {
   201  	l := log.copyLogger()
   202  	l.showTime = f
   203  
   204  	return l
   205  }
   206  
   207  func (log *SpinnerLogSink) Grouped(state bool) {
   208  	if !log.grouppable {
   209  		return
   210  	}
   211  
   212  	if state && log.spinner == nil {
   213  		log.initSpinner()
   214  	} else if !state && log.spinner != nil {
   215  		log.stopSpinner()
   216  	}
   217  
   218  	log.grouped = state
   219  }
   220  
   221  func (log *SpinnerLogSink) GetValues() []interface{} {
   222  	return log.values
   223  }
   224  
   225  func (log *SpinnerLogSink) AddValues(keyAndValues []interface{}) {
   226  	log.values = append(log.values, keyAndValues...)
   227  }
   228  
   229  func (log *SpinnerLogSink) AddName(name string) {
   230  	log.names = append(log.names, name)
   231  }
   232  
   233  func (log *SpinnerLogSink) GetGrouppable() bool {
   234  	return log.grouppable
   235  }
   236  
   237  func (log *SpinnerLogSink) SetGrouppable(state bool) {
   238  	log.grouppable = state
   239  }
   240  
   241  func (log *SpinnerLogSink) GetShowTime() bool {
   242  	return log.showTime
   243  }
   244  
   245  func (log *SpinnerLogSink) SetShowTime(time bool) {
   246  	log.showTime = time
   247  }
   248  
   249  func (log *SpinnerLogSink) SetDisableColor(disableColor bool) {
   250  	log.disableColor = disableColor
   251  }
   252  
   253  func (log *SpinnerLogSink) GetSpinner() *spinner.Spinner {
   254  	return log.spinner
   255  }
   256  
   257  func (log *SpinnerLogSink) StopSpinner() {
   258  	log.stopSpinner()
   259  }
   260  
   261  func (log *SpinnerLogSink) InitSpinner() {
   262  	log.initSpinner()
   263  }
   264  
   265  func (log *SpinnerLogSink) Copy() *SpinnerLogSink {
   266  	return log.copyLogger()
   267  }
   268  
   269  func (log *SpinnerLogSink) printNames() string {
   270  	return strings.Join(log.names, "/")
   271  }
   272  
   273  func (log *SpinnerLogSink) initSpinner() {
   274  	log.mux.Lock()
   275  	defer log.mux.Unlock()
   276  	log.spinner = spinner.New(
   277  		spinner.CharSets[21],
   278  		100*time.Millisecond,
   279  		spinner.WithHiddenCursor(false),
   280  		spinner.WithWriter(log.out),
   281  	)
   282  	_ = log.spinner.Color("green")
   283  	log.spinner.Start()
   284  }
   285  
   286  func (log *SpinnerLogSink) stopSpinner() {
   287  	log.mux.Lock()
   288  	defer log.mux.Unlock()
   289  	if log.spinner != nil {
   290  		log.spinner.Stop()
   291  		log.spinner = nil
   292  	}
   293  }
   294  
   295  func (log *SpinnerLogSink) copyLogger() *SpinnerLogSink {
   296  	names := make([]string, len(log.names))
   297  	copy(names, log.names)
   298  
   299  	values := make([]interface{}, len(log.values))
   300  	copy(values, log.values)
   301  
   302  	return &SpinnerLogSink{
   303  		names:              log.names,
   304  		values:             log.values,
   305  		out:                log.out,
   306  		err:                log.err,
   307  		grouppable:         log.grouppable,
   308  		truncate:           log.truncate,
   309  		timeFormat:         log.timeFormat,
   310  		showTime:           log.showTime,
   311  		checkMark:          log.checkMark,
   312  		errorMark:          log.errorMark,
   313  		separatorCharacter: log.separatorCharacter,
   314  		colors:             log.colors,
   315  		disableColor:       log.disableColor,
   316  
   317  		grouped: log.grouped,
   318  		spinner: log.spinner,
   319  
   320  		mux: sync.Mutex{},
   321  	}
   322  }
   323  
   324  func (*SpinnerLogSink) truncateString(str string, num int) string {
   325  	bnoden := str
   326  	if len(str) > num {
   327  		if num > 3 { //nolint:gomnd
   328  			num -= 3
   329  		}
   330  		bnoden = str[0:num] + "..."
   331  	}
   332  	return bnoden
   333  }
   334  
   335  func (log *SpinnerLogSink) joinAndSeparatePairs(values []interface{}) string {
   336  	joined := ""
   337  	c := log.colors.Key
   338  	for i, v := range values {
   339  		s, err := cast.ToStringE(v)
   340  		if err != nil {
   341  			s = fmt.Sprintf("%v", v)
   342  		}
   343  
   344  		colorPrinter := log.getColorPrinter(c)
   345  		joined += colorPrinter.Sprint(s)
   346  
   347  		if i%2 == 0 {
   348  			c = 0
   349  			joined += "="
   350  		} else {
   351  			c = log.colors.Key
   352  			if i < len(values)-1 {
   353  				joined += ", "
   354  			}
   355  		}
   356  	}
   357  	return joined
   358  }
   359  
   360  func (log *SpinnerLogSink) getDetailedErr(err error) string {
   361  	if err == nil {
   362  		return ""
   363  	}
   364  
   365  	details := errors.GetDetails(err)
   366  	if len(details) == 0 {
   367  		return err.Error()
   368  	}
   369  	return fmt.Sprintf("%s (%s)", err.Error(), log.joinAndSeparatePairs(details))
   370  }
   371  
   372  func (log *SpinnerLogSink) getColorPrinter(attribute color.Attribute) *color.Color {
   373  	colorPrinter := color.New(attribute)
   374  	if log.disableColor {
   375  		colorPrinter.DisableColor()
   376  	}
   377  
   378  	return colorPrinter
   379  }