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