...

Source file src/cdr.dev/slog/slog.go

Documentation: cdr.dev/slog

     1  // Package slog implements minimal structured logging.
     2  //
     3  // See https://cdr.dev/slog for overview docs and a comparison with existing libraries.
     4  //
     5  // The examples are the best way to understand how to use this library effectively.
     6  //
     7  // The Logger type implements a high level API around the Sink interface.
     8  // Logger implements Sink as well to allow composition.
     9  //
    10  // Implementations of the Sink interface are available in the sloggers subdirectory.
    11  package slog // import "cdr.dev/slog"
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"os"
    17  	"runtime"
    18  	"sync"
    19  	"time"
    20  
    21  	"go.opencensus.io/trace"
    22  )
    23  
    24  var defaultExitFn = os.Exit
    25  
    26  // Sink is the destination of a Logger.
    27  //
    28  // All sinks must be safe for concurrent use.
    29  type Sink interface {
    30  	LogEntry(ctx context.Context, e SinkEntry)
    31  	Sync()
    32  }
    33  
    34  // Log logs the given entry with the context to the
    35  // underlying sinks.
    36  //
    37  // It extends the entry with the set fields and names.
    38  func (l Logger) Log(ctx context.Context, e SinkEntry) {
    39  	if e.Level < l.level {
    40  		return
    41  	}
    42  
    43  	e.Fields = l.fields.append(e.Fields)
    44  	e.LoggerNames = appendNames(l.names, e.LoggerNames...)
    45  
    46  	for _, s := range l.sinks {
    47  		s.LogEntry(ctx, e)
    48  	}
    49  }
    50  
    51  // Sync calls Sync on all the underlying sinks.
    52  func (l Logger) Sync() {
    53  	for _, s := range l.sinks {
    54  		s.Sync()
    55  	}
    56  }
    57  
    58  // Logger wraps Sink with a nice API to log entries.
    59  //
    60  // Logger is safe for concurrent use.
    61  type Logger struct {
    62  	sinks []Sink
    63  	level Level
    64  
    65  	names  []string
    66  	fields Map
    67  
    68  	skip int
    69  	exit func(int)
    70  }
    71  
    72  // Make creates a logger that writes logs to the passed sinks at LevelInfo.
    73  func Make(sinks ...Sink) Logger {
    74  	return Logger{
    75  		sinks: sinks,
    76  		level: LevelInfo,
    77  
    78  		exit: os.Exit,
    79  	}
    80  }
    81  
    82  // Debug logs the msg and fields at LevelDebug.
    83  func (l Logger) Debug(ctx context.Context, msg string, fields ...Field) {
    84  	l.log(ctx, LevelDebug, msg, fields)
    85  }
    86  
    87  // Info logs the msg and fields at LevelInfo.
    88  func (l Logger) Info(ctx context.Context, msg string, fields ...Field) {
    89  	l.log(ctx, LevelInfo, msg, fields)
    90  }
    91  
    92  // Warn logs the msg and fields at LevelWarn.
    93  func (l Logger) Warn(ctx context.Context, msg string, fields ...Field) {
    94  	l.log(ctx, LevelWarn, msg, fields)
    95  }
    96  
    97  // Error logs the msg and fields at LevelError.
    98  //
    99  // It will then Sync().
   100  func (l Logger) Error(ctx context.Context, msg string, fields ...Field) {
   101  	l.log(ctx, LevelError, msg, fields)
   102  	l.Sync()
   103  }
   104  
   105  // Critical logs the msg and fields at LevelCritical.
   106  //
   107  // It will then Sync().
   108  func (l Logger) Critical(ctx context.Context, msg string, fields ...Field) {
   109  	l.log(ctx, LevelCritical, msg, fields)
   110  	l.Sync()
   111  }
   112  
   113  // Fatal logs the msg and fields at LevelFatal.
   114  //
   115  // It will then Sync() and os.Exit(1).
   116  func (l Logger) Fatal(ctx context.Context, msg string, fields ...Field) {
   117  	l.log(ctx, LevelFatal, msg, fields)
   118  	l.Sync()
   119  
   120  	if l.exit == nil {
   121  		l.exit = defaultExitFn
   122  	}
   123  
   124  	l.exit(1)
   125  }
   126  
   127  // With returns a Logger that prepends the given fields on every
   128  // logged entry.
   129  //
   130  // It will append to any fields already in the Logger.
   131  func (l Logger) With(fields ...Field) Logger {
   132  	l.fields = l.fields.append(fields)
   133  	return l
   134  }
   135  
   136  // Named appends the name to the set names
   137  // on the logger.
   138  func (l Logger) Named(name string) Logger {
   139  	l.names = appendNames(l.names, name)
   140  	return l
   141  }
   142  
   143  // Leveled returns a Logger that only logs entries
   144  // equal to or above the given level.
   145  func (l Logger) Leveled(level Level) Logger {
   146  	l.level = level
   147  	l.sinks = append([]Sink(nil), l.sinks...)
   148  	return l
   149  }
   150  
   151  // AppendSinks appends the sinks to the set sink
   152  // targets on the logger.
   153  func (l Logger) AppendSinks(s ...Sink) Logger {
   154  	l.sinks = append(l.sinks, s...)
   155  	return l
   156  }
   157  
   158  func (l Logger) log(ctx context.Context, level Level, msg string, fields Map) {
   159  	ent := l.entry(ctx, level, msg, fields)
   160  	l.Log(ctx, ent)
   161  }
   162  
   163  func (l Logger) entry(ctx context.Context, level Level, msg string, fields Map) SinkEntry {
   164  	ent := SinkEntry{
   165  		Time:        time.Now().UTC(),
   166  		Level:       level,
   167  		Message:     msg,
   168  		Fields:      fieldsFromContext(ctx).append(fields),
   169  		SpanContext: trace.FromContext(ctx).SpanContext(),
   170  	}
   171  	ent = ent.fillLoc(l.skip + 3)
   172  	return ent
   173  }
   174  
   175  var helpers sync.Map
   176  
   177  // Helper marks the calling function as a helper
   178  // and skips it for source location information.
   179  // It's the slog equivalent of testing.TB.Helper().
   180  func Helper() {
   181  	_, _, fn := location(1)
   182  	helpers.LoadOrStore(fn, struct{}{})
   183  }
   184  
   185  func (ent SinkEntry) fillFromFrame(f runtime.Frame) SinkEntry {
   186  	ent.Func = f.Function
   187  	ent.File = f.File
   188  	ent.Line = f.Line
   189  	return ent
   190  }
   191  
   192  func (ent SinkEntry) fillLoc(skip int) SinkEntry {
   193  	// Copied from testing.T
   194  	const maxStackLen = 50
   195  	var pc [maxStackLen]uintptr
   196  
   197  	// Skip two extra frames to account for this function
   198  	// and runtime.Callers itself.
   199  	n := runtime.Callers(skip+2, pc[:])
   200  	frames := runtime.CallersFrames(pc[:n])
   201  	for {
   202  		frame, more := frames.Next()
   203  		_, helper := helpers.Load(frame.Function)
   204  		if !helper || !more {
   205  			// Found a frame that wasn't a helper function.
   206  			// Or we ran out of frames to check.
   207  			return ent.fillFromFrame(frame)
   208  		}
   209  	}
   210  }
   211  
   212  func location(skip int) (file string, line int, fn string) {
   213  	pc, file, line, _ := runtime.Caller(skip + 1)
   214  	f := runtime.FuncForPC(pc)
   215  	return file, line, f.Name()
   216  }
   217  
   218  func appendNames(names []string, names2 ...string) []string {
   219  	if len(names2) == 0 {
   220  		return names
   221  	}
   222  	names3 := make([]string, 0, len(names)+len(names2))
   223  	names3 = append(names3, names...)
   224  	names3 = append(names3, names2...)
   225  	return names3
   226  }
   227  
   228  // Field represents a log field.
   229  type Field struct {
   230  	Name  string
   231  	Value interface{}
   232  }
   233  
   234  // F is a convenience constructor for Field.
   235  func F(name string, value interface{}) Field {
   236  	return Field{Name: name, Value: value}
   237  }
   238  
   239  // M is a convenience constructor for Map
   240  func M(fs ...Field) Map {
   241  	return fs
   242  }
   243  
   244  // Error is the standard key used for logging a Go error value.
   245  func Error(err error) Field {
   246  	return F("error", err)
   247  }
   248  
   249  type fieldsKey struct{}
   250  
   251  func fieldsWithContext(ctx context.Context, fields Map) context.Context {
   252  	return context.WithValue(ctx, fieldsKey{}, fields)
   253  }
   254  
   255  func fieldsFromContext(ctx context.Context) Map {
   256  	l, _ := ctx.Value(fieldsKey{}).(Map)
   257  	return l
   258  }
   259  
   260  // With returns a context that contains the given fields.
   261  //
   262  // Any logs written with the provided context will have the given logs prepended.
   263  //
   264  // It will append to any fields already in ctx.
   265  func With(ctx context.Context, fields ...Field) context.Context {
   266  	f1 := fieldsFromContext(ctx)
   267  	f2 := f1.append(fields)
   268  	return fieldsWithContext(ctx, f2)
   269  }
   270  
   271  // SinkEntry represents the structure of a log entry.
   272  // It is the argument to the sink when logging.
   273  type SinkEntry struct {
   274  	Time time.Time
   275  
   276  	Level   Level
   277  	Message string
   278  
   279  	LoggerNames []string
   280  
   281  	Func string
   282  	File string
   283  	Line int
   284  
   285  	SpanContext trace.SpanContext
   286  
   287  	Fields Map
   288  }
   289  
   290  // Level represents a log level.
   291  type Level int
   292  
   293  // The supported log levels.
   294  //
   295  // The default level is Info.
   296  const (
   297  	// LevelDebug is used for development and debugging messages.
   298  	LevelDebug Level = iota
   299  
   300  	// LevelInfo is used for normal informational messages.
   301  	LevelInfo
   302  
   303  	// LevelWarn is used when something has possibly gone wrong.
   304  	LevelWarn
   305  
   306  	// LevelError is used when something has certainly gone wrong.
   307  	LevelError
   308  
   309  	// LevelCritical is used when when something has gone wrong and should
   310  	// be immediately investigated.
   311  	LevelCritical
   312  
   313  	// LevelFatal is used when the process is about to exit due to an error.
   314  	LevelFatal
   315  )
   316  
   317  var levelStrings = map[Level]string{
   318  	LevelDebug:    "DEBUG",
   319  	LevelInfo:     "INFO",
   320  	LevelWarn:     "WARN",
   321  	LevelError:    "ERROR",
   322  	LevelCritical: "CRITICAL",
   323  	LevelFatal:    "FATAL",
   324  }
   325  
   326  // String implements fmt.Stringer.
   327  func (l Level) String() string {
   328  	s, ok := levelStrings[l]
   329  	if !ok {
   330  		return fmt.Sprintf("slog.Level(%v)", int(l))
   331  	}
   332  	return s
   333  }
   334  

View as plain text