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 }