github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/logging/logger.go (about) 1 package logging 2 3 import ( 4 "fmt" 5 "io" 6 "regexp" 7 "strings" 8 "time" 9 10 "github.com/mutagen-io/mutagen/pkg/platform/terminal" 11 "github.com/mutagen-io/mutagen/pkg/stream" 12 ) 13 14 // Logger is the main logger type. A nil Logger is valid and all of its methods 15 // are no-ops. It is safe for concurrent usage and will serialize access to the 16 // underlying writer. 17 type Logger struct { 18 // level is the log level. 19 level Level 20 // scope is the logger's scope. 21 scope string 22 // writer is the underlying writer. 23 writer io.Writer 24 } 25 26 // NewLogger creates a new logger at the specified log level targeting the 27 // specified writer. The writer must be non-nil. The logger and any derived 28 // subloggers will coordinate access to the writer. Any terminal control 29 // characters will be neutralized before being written to the log. 30 func NewLogger(level Level, writer io.Writer) *Logger { 31 return &Logger{ 32 level: level, 33 writer: stream.NewConcurrentWriter(writer), 34 } 35 } 36 37 // Level returns the logger's log level. It can be used to restrict certain 38 // computations to cases where their results will actually be used, for example 39 // statistics that only need to be calculated when debugging. 40 func (l *Logger) Level() Level { 41 // If the logger is nil, then logging is disabled. 42 if l == nil { 43 return LevelDisabled 44 } 45 46 // Return the log level. 47 return l.level 48 } 49 50 // nameMatcher is used to validate names passed to Sublogger. 51 var nameMatcher = regexp.MustCompile("^[[:word:]]+$") 52 53 // Sublogger creates a new sublogger with the specified name. Names must be 54 // non-empty and may only contain the characters a-z, A-Z, 0-9, and underscores. 55 // Attempts to use an invalid name will result in a nil logger and a warning 56 // being issued on the current logger. 57 func (l *Logger) Sublogger(name string) *Logger { 58 // If the logger is nil, then the sublogger will be as well. 59 if l == nil { 60 return nil 61 } 62 63 // Validate the sublogger name. 64 if !nameMatcher.MatchString(name) { 65 l.Warn("attempt to create sublogger with invalid name") 66 return nil 67 } 68 69 // Compute the new logger's scope. 70 scope := name 71 if l.scope != "" { 72 scope = l.scope + "." + scope 73 } 74 75 // Create the new logger. 76 return &Logger{ 77 level: l.level, 78 scope: scope, 79 writer: l.writer, 80 } 81 } 82 83 // timestampFormat is the format in which timestamps should be rendered. 84 const timestampFormat = "2006-01-02 15:04:05.000000" 85 86 // write writes a log message to the underlying writer. 87 func (l *Logger) write(timestamp time.Time, level Level, message string) { 88 // If a carriage return is found, then truncate the message at that point. 89 if index := strings.IndexByte(message, '\r'); index >= 0 { 90 message = message[:index] + "...\n" 91 } 92 93 // Ensure that the only newline character in the message appears at the end 94 // of the string. If one appears earlier, then truncate the message at that 95 // point. If none appears, then something has gone wrong with formatting. 96 if index := strings.IndexByte(message, '\n'); index < 0 { 97 panic("no newline character found in formatted message") 98 } else if index != len(message)-1 { 99 message = message[:index] + "...\n" 100 } 101 102 // Compute the log line. 103 var line string 104 if l.scope != "" { 105 line = fmt.Sprintf("%s [%c] [%s] %s", 106 timestamp.Format(timestampFormat), level.abbreviation(), l.scope, message, 107 ) 108 } else { 109 line = fmt.Sprintf("%s [%c] %s", 110 timestamp.Format(timestampFormat), level.abbreviation(), message, 111 ) 112 } 113 114 // Neutralize any control characters in the line. 115 line = terminal.NeutralizeControlCharacters(line) 116 117 // Write the line. We can't do much with the error here, so we don't try. 118 // Practically speaking, most io.Writer implementations perform retries if a 119 // short write occurs, so retrying here (on top of that logic) probably 120 // wouldn't help much. Even if we wanted to, we'd be better off wrapping the 121 // writer in a hypothetical RetryingWriter in order to better encapsulate 122 // that logic and to avoid having to add a lock outside the writer. In any 123 // case, Go's standard log package also discards analogous errors, so we'll 124 // do the same for the time being. 125 l.writer.Write([]byte(line)) 126 } 127 128 // log provides logging with formatting semantics equivalent to fmt.Sprintln. 129 func (l *Logger) log(level Level, v ...any) { 130 if l != nil && l.level >= level { 131 l.write(time.Now(), level, fmt.Sprintln(v...)) 132 } 133 } 134 135 // logf provides logging with formatting semantics equivalent to fmt.Sprintf. It 136 // automatically appends a trailing newline to the format string. 137 func (l *Logger) logf(level Level, format string, v ...any) { 138 if l != nil && l.level >= level { 139 l.write(time.Now(), level, fmt.Sprintf(format+"\n", v...)) 140 } 141 } 142 143 // Error logs errors with formatting semantics equivalent to fmt.Sprintln. 144 func (l *Logger) Error(v ...any) { 145 l.log(LevelError, v...) 146 } 147 148 // Errorf logs errors with formatting semantics equivalent to fmt.Sprintf. A 149 // trailing newline is automatically appended and should not be included in the 150 // format string. 151 func (l *Logger) Errorf(format string, v ...any) { 152 l.logf(LevelError, format, v...) 153 } 154 155 // Warn logs warnings with formatting semantics equivalent to fmt.Sprintln. 156 func (l *Logger) Warn(v ...any) { 157 l.log(LevelWarn, v...) 158 } 159 160 // Warnf logs warnings with formatting semantics equivalent to fmt.Sprintf. A 161 // trailing newline is automatically appended and should not be included in the 162 // format string. 163 func (l *Logger) Warnf(format string, v ...any) { 164 l.logf(LevelWarn, format, v...) 165 } 166 167 // Info logs information with formatting semantics equivalent to fmt.Sprintln. 168 func (l *Logger) Info(v ...any) { 169 l.log(LevelInfo, v...) 170 } 171 172 // Infof logs information with formatting semantics equivalent to fmt.Sprintf. A 173 // trailing newline is automatically appended and should not be included in the 174 // format string. 175 func (l *Logger) Infof(format string, v ...any) { 176 l.logf(LevelInfo, format, v...) 177 } 178 179 // Debug logs debug information with formatting semantics equivalent to 180 // fmt.Sprintln. 181 func (l *Logger) Debug(v ...any) { 182 l.log(LevelDebug, v...) 183 } 184 185 // Debugf logs debug information with formatting semantics equivalent to 186 // fmt.Sprintf. A trailing newline is automatically appended and should not be 187 // included in the format string. 188 func (l *Logger) Debugf(format string, v ...any) { 189 l.logf(LevelDebug, format, v...) 190 } 191 192 // Trace logs tracing information with formatting semantics equivalent to 193 // fmt.Sprintln. 194 func (l *Logger) Trace(v ...any) { 195 l.log(LevelTrace, v...) 196 } 197 198 // Tracef logs tracing information with formatting semantics equivalent to 199 // fmt.Sprintf. A trailing newline is automatically appended and should not be 200 // included in the format string. 201 func (l *Logger) Tracef(format string, v ...any) { 202 l.logf(LevelTrace, format, v...) 203 } 204 205 // linePrefixMatcher matches the timestamp and level prefix of logging lines. 206 var linePrefixMatcher = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6} \[([` + abbreviations + `])\] `) 207 208 // Writer returns an io.Writer that logs incoming lines. If an incoming line is 209 // determined to be an output line from another logger, then it will be parsed 210 // and gated against this logger's level, its scope will be merged with that of 211 // this logger, and the combined line will be written. Otherwise, if an incoming 212 // line is not determined to be from another logger, than it will be written as 213 // a message with the specificed level. 214 // 215 // Note that unlike the Logger itself, the writer returned from this method is 216 // not safe for concurrent use by multiple Goroutines. An external locking 217 // mechanism should be added if concurrent use is necessary. 218 func (l *Logger) Writer(level Level) io.Writer { 219 // If the current logger is nil, then we can just discard all output. 220 if l == nil { 221 return io.Discard 222 } 223 224 // Create the writer. 225 return &stream.LineProcessor{ 226 Callback: func(line string) { 227 // Check if the line is output from a logger. If it's not, then we 228 // just log it as if it were any other message. 229 matches := linePrefixMatcher.FindStringSubmatch(line) 230 if len(matches) != 2 { 231 l.log(level, line) 232 return 233 } 234 235 // Decode the log level for the line. If the log level that it 236 // specifies is invalid, then just print an indicator that an 237 // invalid line was received. Otherwise, if the line level is beyond 238 // the threshold of this logger, then just ignore it. 239 if len(matches[1]) != 1 { 240 panic("line prefix matcher returned invalid match") 241 } else if level, ok := abbreviationToLevel(matches[1][0]); !ok { 242 l.Warn("<invalid incoming log line level>") 243 return 244 } else if l.level < level { 245 return 246 } 247 248 // If we have a non-empty scope, then inject it into the line. If 249 // not, then just add (back) the newline character. 250 if l.scope != "" { 251 line = fmt.Sprintf("%s[%s] %s\n", matches[0], l.scope, line[len(matches[0]):]) 252 } else { 253 line = line + "\n" 254 } 255 256 // Neutralize any control characters in the line. 257 line = terminal.NeutralizeControlCharacters(line) 258 259 // Write the line to the underlying writer. 260 l.writer.Write([]byte(line)) 261 }, 262 } 263 }