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 }