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  }