diff --git a/emitter/gkelog/emitter.go b/emitter/gkelog/emitter.go index 45a06c6..00e3afb 100644 --- a/emitter/gkelog/emitter.go +++ b/emitter/gkelog/emitter.go @@ -22,12 +22,13 @@ var DefaultLogger = alog.New(alog.WithEmitter(Emitter())) type contextKey string var ( - severityKey = contextKey("severity") - requestKey = contextKey("request") - statusKey = contextKey("status") - latencyKey = contextKey("latency") - traceKey = contextKey("trace") - spanKey = contextKey("span") + severityKey = contextKey("severity") + minSeverityKey = contextKey("minSeverity") + requestKey = contextKey("request") + statusKey = contextKey("status") + latencyKey = contextKey("latency") + traceKey = contextKey("trace") + spanKey = contextKey("span") ) // WithSeverity returns a copy of parent with the specified severity value. @@ -35,6 +36,12 @@ func WithSeverity(parent context.Context, severity string) context.Context { return context.WithValue(parent, severityKey, severity) } +// WithMinSeverity sets the minimum severity to log. Use one of the Severity* +// constants. +func WithMinSeverity(parent context.Context, severity string) context.Context { + return context.WithValue(parent, minSeverityKey, severity) +} + // WithRequest returns a copy of parent with the specified http.Request value. // It also calls WithRequestTrace to add trace information to the context. func WithRequest(parent context.Context, req *http.Request) context.Context { diff --git a/emitter/gkelog/emitter_test.go b/emitter/gkelog/emitter_test.go index 1e64562..8bc7f2c 100644 --- a/emitter/gkelog/emitter_test.go +++ b/emitter/gkelog/emitter_test.go @@ -108,6 +108,21 @@ func TestLogSeverity(t *testing.T) { } } +func TestMinLogSeverity(t *testing.T) { + b := &bytes.Buffer{} + ctx := WithMinSeverity(context.Background(), SeverityWarning) + l := alog.New(alog.WithEmitter(Emitter(WithWriter(b))), zeroTimeOpt) + + LogInfo(ctx, l, "NOT LOGGED") // because Info is lower than Warning + LogError(ctx, l, "LOGGED") + + want := `{"time":"0001-01-01T00:00:00Z", "severity":"ERROR", "message":"LOGGED"}` + "\n" + got := b.String() + if got != want { + t.Errorf("got:\n%s\nwant:\n%s", got, want) + } +} + func TestRequest(t *testing.T) { b := &bytes.Buffer{} ctx := context.Background() diff --git a/emitter/gkelog/severity.go b/emitter/gkelog/severity.go index 98cfd0a..e497db5 100644 --- a/emitter/gkelog/severity.go +++ b/emitter/gkelog/severity.go @@ -21,13 +21,32 @@ const ( SeverityEmergency = "EMERGENCY" // One or more systems are unusable. ) +// the priority of each severity level +var severityPriority map[string]uint8 = map[string]uint8{ + SeverityDebug: 0, + SeverityInfo: 1, + SeverityNotice: 2, + SeverityWarning: 3, + SeverityError: 4, + SeverityCritical: 5, + SeverityAlert: 6, + SeverityEmergency: 7, + SeverityDefault: 8, // default will almost always log and should probably not be used +} + // Separate private function so that LogSeverity and the other logs functions // will have the same stack frame depth and thus use the same calldepth value. // See https://golang.org/pkg/runtime/#Caller and // https://godoc.org/github.com/vimeo/alog#Logger.Output func logSeverity(ctx context.Context, logger *alog.Logger, s string, f string, v ...interface{}) { - ctx = WithSeverity(ctx, s) - logger.Output(ctx, 3, fmt.Sprintf(f, v...)) + minSeverity := uint8(0) + if minSeverityVal := ctx.Value(minSeverityKey); minSeverityVal != nil { + minSeverity = severityPriority[minSeverityVal.(string)] + } + if severityPriority[s] >= minSeverity { + ctx = WithSeverity(ctx, s) + logger.Output(ctx, 3, fmt.Sprintf(f, v...)) + } } // LogSeverity writes a log entry using the specified severity diff --git a/leveled/level_string.go b/leveled/level_string.go new file mode 100644 index 0000000..6b4461d --- /dev/null +++ b/leveled/level_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type Level -linecomment"; DO NOT EDIT. + +package leveled + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Debug-0] + _ = x[Info-1] + _ = x[Warning-2] + _ = x[Error-3] + _ = x[Critical-4] +} + +const _Level_name = "debuginfowarningerrorcritical" + +var _Level_index = [...]uint8{0, 5, 9, 16, 21, 29} + +func (i Level) String() string { + if i >= Level(len(_Level_index)-1) { + return "Level(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Level_name[_Level_index[i]:_Level_index[i+1]] +} diff --git a/leveled/logger.go b/leveled/logger.go index 474f66b..357b69f 100644 --- a/leveled/logger.go +++ b/leveled/logger.go @@ -7,6 +7,20 @@ import ( "github.com/vimeo/alog/v3" ) +// Level represents the severity of a log message. +type Level uint8 + +const ( + Debug Level = iota // debug + Info // info + Warning // warning + Error // error + Critical // critical +) + +// LevelKey is the tag key associated with a level. +const LevelKey = "level" + // Logger is an interface that implements logging functions for different levels of severity. type Logger interface { // Debug logs debugging or trace information. @@ -25,8 +39,20 @@ type Logger interface { Critical(ctx context.Context, f string, v ...interface{}) } +// FilteredLogger amends the Logger interface with a Log method that accepts the +// level to log at. +type FilteredLogger interface { + Logger + Log(ctx context.Context, level Level, f string, v ...interface{}) + SetMinLevel(level Level) +} + type defaultLogger struct { *alog.Logger + + // Indicates the minimum level to log at. If MinLevel is greater than the + // level of a given log message, the log message will be suppressed. + MinLevel Level } // Default returns a Logger that wraps the provided `alog.Logger`. @@ -39,32 +65,50 @@ func Default(logger *alog.Logger) Logger { } } +// Filtered returns an advanced logger that allows for setting the minimum +// level. +func Filtered(logger *alog.Logger) FilteredLogger { + return &defaultLogger{ + Logger: logger, + } +} + // Debug implements Logger.Debug func (d *defaultLogger) Debug(ctx context.Context, f string, v ...interface{}) { - ctx = alog.AddTags(ctx, "level", "debug") - d.Logger.Output(ctx, 3, fmt.Sprintf(f, v...)) + d.Log(ctx, Debug, f, v...) } // Info implements Logger.Info func (d *defaultLogger) Info(ctx context.Context, f string, v ...interface{}) { - ctx = alog.AddTags(ctx, "level", "info") - d.Logger.Output(ctx, 3, fmt.Sprintf(f, v...)) + d.Log(ctx, Info, f, v...) } // Warning implements Logger.Warning func (d *defaultLogger) Warning(ctx context.Context, f string, v ...interface{}) { - ctx = alog.AddTags(ctx, "level", "warning") - d.Logger.Output(ctx, 3, fmt.Sprintf(f, v...)) + d.Log(ctx, Warning, f, v...) } // Error implements Logger.Error func (d *defaultLogger) Error(ctx context.Context, f string, v ...interface{}) { - ctx = alog.AddTags(ctx, "level", "error") - d.Logger.Output(ctx, 3, fmt.Sprintf(f, v...)) + d.Log(ctx, Error, f, v...) } // Critical implements Logger.Critical func (d *defaultLogger) Critical(ctx context.Context, f string, v ...interface{}) { - ctx = alog.AddTags(ctx, "level", "critical") - d.Logger.Output(ctx, 3, fmt.Sprintf(f, v...)) + d.Log(ctx, Critical, f, v...) } + +// Log implements FilteredLogger.Log +func (d *defaultLogger) Log(ctx context.Context, level Level, f string, v ...interface{}) { + if level >= d.MinLevel { + ctx = alog.AddTags(ctx, LevelKey, level.String()) + d.Logger.Output(ctx, 3, fmt.Sprintf(f, v...)) + } +} + +// SetMinLevel sets the minimum level that will be logged and implements FilteredLogger. +func (d *defaultLogger) SetMinLevel(level Level) { + d.MinLevel = level +} + +//go:generate go run golang.org/x/tools/cmd/stringer@latest -type Level -linecomment diff --git a/leveled/logger_test.go b/leveled/logger_test.go index 0b697a7..12ee467 100644 --- a/leveled/logger_test.go +++ b/leveled/logger_test.go @@ -21,3 +21,18 @@ func TestLogger(t *testing.T) { t.Errorf("got: %#q, want: %#q", got, want) } } + +func TestLogLevels(t *testing.T) { + b := &bytes.Buffer{} + l := Filtered(alog.New(alog.WithEmitter(textlog.Emitter(b)))) + l.SetMinLevel(Warning) + + ctx := context.Background() + ctx = alog.AddTags(ctx, "key", "value") + l.Error(ctx, "I get logged") + l.Debug(ctx, "I don't get logged") + const want = `[key=value level=error] I get logged` + "\n" + if got := b.String(); got != want { + t.Errorf("got: %#q, want: %#q", got, want) + } +}