From 459f53907c254241bfa9c9b823fd34e3cd19b597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20=C4=8Cekrli=C4=87?= Date: Sun, 2 Dec 2018 23:06:00 +0100 Subject: [PATCH] Added additional severity levels and ability to customize levels. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit: - Aligns the severity levels in loggo with syslog severity levels. This makes it easier to export logging through loggo directly to syslog. - Enables the user to customize / define custom severity levels. Some users like to define levels such as TRACE1, TRACE2 and TRACE3 for additional fine logging. This commit makes it possible to customize these levels on a per-project basis. **Backward compatibility notice:** While there should be no issues with backward compatibility and this should work completely out of the box, please notice that, to align the code with syslog levels, the **values for the severity levels have been changed** and now **lower values** are considered more level. Signed-off-by: Bojan Čekrlić --- README.md | 46 ++++++++++++++----- config_test.go | 2 +- context.go | 5 ++- context_test.go | 3 +- level.go | 105 ++++++++++++++++++++----------------------- level_test.go | 39 +++++++++++++--- loggocolor/writer.go | 47 ++++++++++++++++--- module.go | 8 +++- 8 files changed, 169 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 5adae48..ba350d6 100644 --- a/README.md +++ b/README.md @@ -349,19 +349,38 @@ Level holds a severity level. ``` go const ( - UNSPECIFIED Level = iota - TRACE - DEBUG - INFO - WARNING - ERROR - CRITICAL + UNSPECIFIED Level = 0000 + EMERGENCY Level = 0001 + ALERT Level = 1000 + CRITICAL Level = 2000 + ERROR Level = 3000 + WARNING Level = 4000 + NOTICE Level = 5000 + INFO Level = 6000 + DEBUG Level = 7000 + TRACE Level = 8000 ) ``` -The severity levels. Higher values are more considered more -important. - - +The severity levels. **Lower values** are more considered more +important, as with syslog. The levels have been synchronized with +[syslog severity levels](https://en.wikipedia.org/wiki/Syslog#Severity_level) with +addition of the `TRACE` level. + +Custom levels may be defined by overriding the `loggo.Levels` map: +``` go +var Levels = map[Level]LevelType{ + UNSPECIFIED: {"", UNSPECIFIED, "UNSPECIFIED"}, + TRACE: {"TRACE", TRACE, "TRACE"}, + DEBUG: {"DEBUG", DEBUG, "DEBUG"}, + INFO: {"INFO", INFO, "INFO"}, + NOTICE: {"NOTE", NOTICE, "NOTICE"}, + WARNING: {"WARN", WARNING, "WARNING"}, + ERROR: {"ERROR", ERROR, "ERROR"}, + CRITICAL: {"CRIT", CRITICAL, "CRITICAL"}, + ALERT: {"ALERT", ALERT, "ALERT"}, + EMERGENCY: {"EMERG", EMERGENCY, "EMERGENCY"}, +} +``` @@ -567,6 +586,11 @@ the effective log level of the logger. Note that the writers may also filter out messages that are less than their registered minimum severity level. +**Please note:** As levels *NOTICE*, *ALERT* and *EMERGENCY* are seldom used, +there are no corresponding `Noticef`, `Alertf` and `Emergencyf` methods. Use +the method `Logf(level, ...)` instead. + +This method is also appropriate if you define your custom log levels. ### func (Logger) Name diff --git a/config_test.go b/config_test.go index fa64d50..40048a6 100644 --- a/config_test.go +++ b/config_test.go @@ -52,7 +52,7 @@ func (*ConfigSuite) TestParseConfigValue(c *gc.C) { } } -func (*ConfigSuite) TestPaarseConfigurationString(c *gc.C) { +func (*ConfigSuite) TestParseConfigurationString(c *gc.C) { for i, test := range []struct { configuration string expected Config diff --git a/context.go b/context.go index 155454f..9696b7d 100644 --- a/context.go +++ b/context.go @@ -28,9 +28,12 @@ type Context struct { // NewLoggers returns a new Context with no writers set. // If the root level is UNSPECIFIED, WARNING is used. func NewContext(rootLevel Level) *Context { - if rootLevel < TRACE || rootLevel > CRITICAL { + if rootLevel == UNSPECIFIED { + rootLevel = WARNING + } else if _, ok := Levels[rootLevel]; !ok { rootLevel = WARNING } + context := &Context{ modules: make(map[string]*module), writers: make(map[string]Writer), diff --git a/context_test.go b/context_test.go index e012fa2..ab0a5a6 100644 --- a/context_test.go +++ b/context_test.go @@ -38,11 +38,12 @@ func (*ContextSuite) TestNewContextRootLevel(c *gc.C) { level: loggo.Level(42), expected: loggo.WARNING, }} { - c.Log("%d: %s", i, test.level) + c.Logf("Iteration %d: level=%v, expected=%v", i, test.level, test.expected) context := loggo.NewContext(test.level) cfg := context.Config() c.Check(cfg, gc.HasLen, 1) value, found := cfg[""] + c.Logf(" value=%v found=%v", value, found) c.Check(found, gc.Equals, true) c.Check(value, gc.Equals, test.expected) } diff --git a/level.go b/level.go index f6a5c4f..9ead09c 100644 --- a/level.go +++ b/level.go @@ -4,6 +4,7 @@ package loggo import ( + "fmt" "strings" "sync/atomic" ) @@ -11,83 +12,75 @@ import ( // The severity levels. Higher values are more considered more // important. const ( - UNSPECIFIED Level = iota - TRACE - DEBUG - INFO - WARNING - ERROR - CRITICAL + UNSPECIFIED Level = 0000 + EMERGENCY Level = 0001 // Syslog level 0 + ALERT Level = 1000 // Syslog level 1 + CRITICAL Level = 2000 // Syslog level 2 + ERROR Level = 3000 // Syslog level 3 + WARNING Level = 4000 // Syslog level 4 + NOTICE Level = 5000 // Syslog level 5 + INFO Level = 6000 // Syslog level 6 + DEBUG Level = 7000 // Syslog level 7 + TRACE Level = 8000 ) +var Levels = map[Level]LevelType{ + UNSPECIFIED: {"", UNSPECIFIED, "UNSPECIFIED"}, + TRACE: {"TRACE", TRACE, "TRACE"}, + DEBUG: {"DEBUG", DEBUG, "DEBUG"}, // Syslog 7 + INFO: {"INFO", INFO, "INFO"}, // Syslog 6 + NOTICE: {"NOTE", NOTICE, "NOTICE"}, // Syslog 5 + WARNING: {"WARN", WARNING, "WARNING"}, // Syslog 4 + ERROR: {"ERROR", ERROR, "ERROR"}, // Syslog 3 + CRITICAL: {"CRIT", CRITICAL, "CRITICAL"}, // Syslog 2 + ALERT: {"ALERT", ALERT, "ALERT"}, // Syslog 1 + EMERGENCY: {"EMERG", EMERGENCY, "EMERGENCY"}, // Syslog 0 +} + // Level holds a severity level. type Level uint32 +type LevelType struct { + ShortName string + Value Level + LongName string +} + // ParseLevel converts a string representation of a logging level to a // Level. It returns the level and whether it was valid or not. func ParseLevel(level string) (Level, bool) { - level = strings.ToUpper(level) - switch level { - case "UNSPECIFIED": - return UNSPECIFIED, true - case "TRACE": - return TRACE, true - case "DEBUG": - return DEBUG, true - case "INFO": - return INFO, true - case "WARN", "WARNING": - return WARNING, true - case "ERROR": - return ERROR, true - case "CRITICAL": - return CRITICAL, true - default: + if len(level) == 0 { return UNSPECIFIED, false } + + level = strings.ToUpper(level) + for _, l := range Levels { + if level == l.LongName || level == l.ShortName { + return l.Value, true + } + } + return UNSPECIFIED, false } // String implements Stringer. func (level Level) String() string { - switch level { - case UNSPECIFIED: - return "UNSPECIFIED" - case TRACE: - return "TRACE" - case DEBUG: - return "DEBUG" - case INFO: - return "INFO" - case WARNING: - return "WARNING" - case ERROR: - return "ERROR" - case CRITICAL: - return "CRITICAL" - default: - return "" + if l, ok := Levels[level]; ok { + return l.LongName + } else { + return "UNKNOWN" } } // Short returns a five character string to use in // aligned logging output. func (level Level) Short() string { - switch level { - case TRACE: - return "TRACE" - case DEBUG: - return "DEBUG" - case INFO: - return "INFO " - case WARNING: - return "WARN " - case ERROR: - return "ERROR" - case CRITICAL: - return "CRITC" - default: - return " " + if l, ok := Levels[level]; ok { + return fmt.Sprintf("%5s", l.ShortName) + } else { + return "UNKNOWN" } + return "UNKN " + } // get atomically gets the value of the given level. diff --git a/level_test.go b/level_test.go index 084cf0c..2358c91 100644 --- a/level_test.go +++ b/level_test.go @@ -38,6 +38,12 @@ var parseLevelTests = []struct { }, { str: "INFO", level: loggo.INFO, +}, { + str: "notE", + level: loggo.NOTICE, +}, { + str: "NoTICE", + level: loggo.NOTICE, }, { str: "warn", level: loggo.WARNING, @@ -60,19 +66,35 @@ var parseLevelTests = []struct { str: "critical", level: loggo.CRITICAL, }, { - str: "not_specified", - fail: true, + str: "Alert", + level: loggo.ALERT, +}, { + str: "EMERG", + level: loggo.EMERGENCY, +}, { + str: "EMERGENCY", + level: loggo.EMERGENCY, +}, { + str: "Emerg", + level: loggo.EMERGENCY, +}, { + str: "not_specified", + level: loggo.UNSPECIFIED, + fail: true, }, { - str: "other", - fail: true, + str: "other", + level: loggo.UNSPECIFIED, + fail: true, }, { - str: "", - fail: true, + str: "", + level: loggo.UNSPECIFIED, + fail: true, }} func (s *LevelSuite) TestParseLevel(c *gc.C) { for _, test := range parseLevelTests { level, ok := loggo.ParseLevel(test.str) + c.Logf("str=%s, level=%v, ok=%v", test.str, level, ok) c.Assert(level, gc.Equals, test.level) c.Assert(ok, gc.Equals, !test.fail) } @@ -83,10 +105,13 @@ var levelStringValueTests = map[loggo.Level]string{ loggo.DEBUG: "DEBUG", loggo.TRACE: "TRACE", loggo.INFO: "INFO", + loggo.NOTICE: "NOTICE", loggo.WARNING: "WARNING", loggo.ERROR: "ERROR", loggo.CRITICAL: "CRITICAL", - loggo.Level(42): "", // other values are unknown + loggo.ALERT: "ALERT", + loggo.EMERGENCY: "EMERGENCY", + loggo.Level(42): "UNKNOWN", // other values are unknown } func (s *LevelSuite) TestLevelStringValue(c *gc.C) { diff --git a/loggocolor/writer.go b/loggocolor/writer.go index 305c42f..c23c8e7 100644 --- a/loggocolor/writer.go +++ b/loggocolor/writer.go @@ -5,25 +5,45 @@ import ( "io" "path/filepath" - "github.com/juju/loggo" "github.com/juju/ansiterm" + "github.com/juju/loggo" + "strconv" ) var ( // SeverityColor defines the colors for the levels output by the ColorWriter. SeverityColor = map[loggo.Level]*ansiterm.Context{ - loggo.TRACE: ansiterm.Foreground(ansiterm.Default), + loggo.TRACE: ansiterm.Foreground(ansiterm.Gray), loggo.DEBUG: ansiterm.Foreground(ansiterm.Green), loggo.INFO: ansiterm.Foreground(ansiterm.BrightBlue), + loggo.NOTICE: ansiterm.Foreground(ansiterm.BrightGreen), loggo.WARNING: ansiterm.Foreground(ansiterm.Yellow), loggo.ERROR: ansiterm.Foreground(ansiterm.BrightRed), - loggo.CRITICAL: &ansiterm.Context{ + loggo.CRITICAL: { + Foreground: ansiterm.White, + Background: ansiterm.Red, + }, + loggo.ALERT: { + Foreground: ansiterm.White, + Background: ansiterm.Red, + }, + loggo.EMERGENCY: { Foreground: ansiterm.White, Background: ansiterm.Red, }, } // LocationColor defines the colors for the location output by the ColorWriter. LocationColor = ansiterm.Foreground(ansiterm.BrightBlue) + // TimeStampColor defines the colors of timestamps + TimeStampColor = ansiterm.Foreground(ansiterm.Yellow) + // ModuleColor defines the color of module name + ModuleColor = ansiterm.Foreground(ansiterm.Blue) + + // How long (padded) should be the module name + ModuleLength = 35 + + // How long (padded) should be the location name + LocationLength = 25 ) type colorWriter struct { @@ -42,9 +62,22 @@ func (w *colorWriter) Write(entry loggo.Entry) { // Just get the basename from the filename filename := filepath.Base(entry.Filename) - fmt.Fprintf(w.writer, "%s ", ts) - SeverityColor[entry.Level].Fprintf(w.writer, entry.Level.Short()) - fmt.Fprintf(w.writer, " %s ", entry.Module) - LocationColor.Fprintf(w.writer, "%s:%d ", filename, entry.Line) + TimeStampColor.Fprintf(w.writer, "%s", ts) + fmt.Fprintf(w.writer, " ") + + SeverityColor[entry.Level].Fprintf(w.writer, "%5s", entry.Level.Short()) + fmt.Fprintf(w.writer, " ") + + module := entry.Module + if len(module) > ModuleLength { + module = "..." + module[len(module)-(ModuleLength-3):] + } + + ModuleColor.Fprintf(w.writer, "%-"+strconv.Itoa(ModuleLength)+"s", module) + fmt.Fprintf(w.writer, " ") + + line := fmt.Sprintf("%s:%d", filename, entry.Line) + LocationColor.Fprintf(w.writer, "%-"+strconv.Itoa(LocationLength)+"s ", line) + fmt.Fprintln(w.writer, entry.Message) } diff --git a/module.go b/module.go index 8153be5..01e996a 100644 --- a/module.go +++ b/module.go @@ -26,10 +26,14 @@ func (module *module) Name() string { } func (m *module) willWrite(level Level) bool { - if level < TRACE || level > CRITICAL { + if level == UNSPECIFIED { return false } - return level >= m.getEffectiveLogLevel() + if _, ok := Levels[level]; !ok { + return false + } + + return level <= m.getEffectiveLogLevel() } func (module *module) getEffectiveLogLevel() Level {