github.com/tiagovtristao/plz@v13.4.0+incompatible/src/cli/logging.go (about)

     1  // Contains various utility functions related to logging.
     2  
     3  package cli
     4  
     5  import (
     6  	"bytes"
     7  	"container/list"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path"
    12  	"regexp"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/peterebden/go-cli-init"
    17  	"golang.org/x/crypto/ssh/terminal"
    18  	"gopkg.in/op/go-logging.v1"
    19  )
    20  
    21  var log = logging.MustGetLogger("cli")
    22  
    23  // StdErrIsATerminal is true if the process' stderr is an interactive TTY.
    24  var StdErrIsATerminal = terminal.IsTerminal(int(os.Stderr.Fd()))
    25  
    26  // StdOutIsATerminal is true if the process' stdout is an interactive TTY.
    27  var StdOutIsATerminal = terminal.IsTerminal(int(os.Stdout.Fd()))
    28  
    29  // StripAnsi is a regex to find & replace ANSI console escape sequences.
    30  var StripAnsi = regexp.MustCompile("\x1b[^m]+m")
    31  
    32  // logLevel is the current verbosity level that is set.
    33  var logLevel = logging.WARNING
    34  
    35  var fileLogLevel = logging.WARNING
    36  var fileBackend *logging.LogBackend
    37  
    38  // A Verbosity is used as a flag to define logging verbosity.
    39  type Verbosity = cli.Verbosity
    40  
    41  type logFileWriter struct {
    42  	file io.Writer
    43  }
    44  
    45  func (writer logFileWriter) Write(p []byte) (n int, err error) {
    46  	return writer.file.Write(StripAnsi.ReplaceAllLiteral(p, []byte{}))
    47  }
    48  
    49  // InitLogging initialises logging backends.
    50  func InitLogging(verbosity Verbosity) {
    51  	logLevel = logging.Level(verbosity)
    52  	logging.SetFormatter(logFormatter())
    53  	setLogBackend(logging.NewLogBackend(os.Stderr, "", 0))
    54  }
    55  
    56  // InitFileLogging initialises an optional logging backend to a file.
    57  func InitFileLogging(logFile string, logFileLevel Verbosity) {
    58  	fileLogLevel = logging.Level(logFileLevel)
    59  	if err := os.MkdirAll(path.Dir(logFile), os.ModeDir|0775); err != nil {
    60  		log.Fatalf("Error creating log file directory: %s", err)
    61  	}
    62  	if file, err := os.Create(logFile); err != nil {
    63  		log.Fatalf("Error opening log file: %s", err)
    64  	} else {
    65  		fileBackend = logging.NewLogBackend(logFileWriter{file: file}, "", 0)
    66  		setLogBackend(logging.NewLogBackend(os.Stderr, "", 0))
    67  	}
    68  }
    69  
    70  func logFormatter() logging.Formatter {
    71  	formatStr := "%{time:15:04:05.000} %{level:7s}: %{message}"
    72  	if StdErrIsATerminal {
    73  		formatStr = "%{color}" + formatStr + "%{color:reset}"
    74  	}
    75  	return logging.MustStringFormatter(formatStr)
    76  }
    77  
    78  func setLogBackend(backend logging.Backend) {
    79  	backendLeveled := logging.AddModuleLevel(backend)
    80  	backendLeveled.SetLevel(logLevel, "")
    81  	if fileBackend == nil {
    82  		logging.SetBackend(backendLeveled)
    83  	} else {
    84  		fileBackendLeveled := logging.AddModuleLevel(fileBackend)
    85  		fileBackendLeveled.SetLevel(fileLogLevel, "")
    86  		logging.SetBackend(backendLeveled, fileBackendLeveled)
    87  	}
    88  }
    89  
    90  type logBackendFacade struct {
    91  	realBackend *LogBackend // To work around the logging interface requiring us to pass by value.
    92  }
    93  
    94  func (backend logBackendFacade) Log(level logging.Level, calldepth int, rec *logging.Record) error {
    95  	var b bytes.Buffer
    96  	backend.realBackend.Formatter.Format(calldepth, rec, &b)
    97  	if rec.Level <= logging.CRITICAL {
    98  		fmt.Print(b.String()) // Don't capture critical messages, just die immediately.
    99  		os.Exit(1)
   100  	}
   101  	backend.realBackend.Lock()
   102  	defer backend.realBackend.Unlock()
   103  	backend.realBackend.LogMessages.PushBack(strings.TrimSpace(b.String()))
   104  	backend.realBackend.RecalcLines()
   105  	return nil
   106  }
   107  
   108  // LogBackend is the backend we use for logging during the interactive console display.
   109  type LogBackend struct {
   110  	sync.Mutex                                                            // Protects access to LogMessages
   111  	Rows, Cols, MaxRecords, InteractiveRows, MaxInteractiveRows, maxLines int
   112  	Output                                                                []string
   113  	LogMessages                                                           *list.List
   114  	Formatter                                                             logging.Formatter // TODO(pebers): seems a bit weird that we have to have this here, but it doesn't
   115  } //               seem to be possible to retrieve the formatter from outside the package?
   116  
   117  // RecalcLines recalculates how many lines we have available, typically in response to the window size changing
   118  func (backend *LogBackend) RecalcLines() {
   119  	for backend.LogMessages.Len() >= backend.MaxRecords {
   120  		backend.LogMessages.Remove(backend.LogMessages.Front())
   121  	}
   122  	backend.maxLines = backend.Rows - backend.InteractiveRows - 1
   123  	if backend.maxLines > 15 {
   124  		backend.maxLines = 15 // Cap it here so we don't log too much
   125  	} else if backend.maxLines <= 0 {
   126  		backend.maxLines = 1 // Set a minimum so we don't have negative indices later.
   127  	}
   128  	backend.Output = backend.calcOutput()
   129  	backend.MaxInteractiveRows = backend.Rows - len(backend.Output) - 1
   130  }
   131  
   132  // NewLogBackend constructs a new logging backend.
   133  func NewLogBackend(interactiveRows int) *LogBackend {
   134  	return &LogBackend{
   135  		InteractiveRows: interactiveRows,
   136  		MaxRecords:      10,
   137  		LogMessages:     list.New(),
   138  		Formatter:       logFormatter(),
   139  	}
   140  }
   141  
   142  func (backend *LogBackend) calcOutput() []string {
   143  	ret := []string{}
   144  	for e := backend.LogMessages.Back(); e != nil; e = e.Prev() {
   145  		new := backend.lineWrap(e.Value.(string))
   146  		if len(ret)+len(new) <= backend.maxLines {
   147  			ret = append(ret, new...)
   148  		}
   149  	}
   150  	if len(ret) > 0 {
   151  		ret = append(ret, "Messages:")
   152  	}
   153  	return reverse(ret)
   154  }
   155  
   156  // SetActive sets this backend as the currently active log backend.
   157  func (backend *LogBackend) SetActive() {
   158  	setLogBackend(logBackendFacade{backend})
   159  }
   160  
   161  // Deactivate removes this backend as the currently active log backend.
   162  func (backend *LogBackend) Deactivate() {
   163  	setLogBackend(logging.NewLogBackend(os.Stderr, "", 0))
   164  }
   165  
   166  // Wraps a string across multiple lines. Returned slice is reversed.
   167  func (backend *LogBackend) lineWrap(msg string) []string {
   168  	lines := strings.Split(msg, "\n")
   169  	wrappedLines := make([]string, 0, len(lines))
   170  	for _, line := range lines {
   171  		for i := 0; i < len(line); {
   172  			split := i + findSplit(line[i:], backend.Cols)
   173  			wrappedLines = append(wrappedLines, line[i:split])
   174  			i = split
   175  		}
   176  	}
   177  	if len(wrappedLines) > backend.maxLines {
   178  		return reverse(wrappedLines[:backend.maxLines])
   179  	}
   180  	return reverse(wrappedLines)
   181  }
   182  
   183  func reverse(s []string) []string {
   184  	if len(s) > 1 {
   185  		r := []string{}
   186  		for i := len(s) - 1; i >= 0; i-- {
   187  			r = append(r, s[i])
   188  		}
   189  		return r
   190  	}
   191  	return s
   192  }
   193  
   194  // Tries to find an appropriate point to word wrap line, taking shell escape codes into account.
   195  // (Note that because the escape codes are not visible, we can run past the max length for one of them)
   196  func findSplit(line string, guess int) int {
   197  	if guess >= len(line) {
   198  		return len(line)
   199  	}
   200  	r := regexp.MustCompilePOSIX(fmt.Sprintf(".{%d,%d}(\\x1b[^m]+m)?", guess/2, guess))
   201  	m := r.FindStringIndex(line)
   202  	if m != nil {
   203  		return m[1] // second element in slice is the end index
   204  	}
   205  	return guess // Dunno what to do at this point. It's probably unlikely to happen often though.
   206  }