diff --git a/README.md b/README.md index 49355e9..01b83bb 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,18 @@ Formatters: - `DefaultFormatter` - `ColouredFormatter` -Example usage. Create a new package `log` in your app such that: +## Configuration + +The logger can be configured using optional configuration functions: + +- `WithLogLevel(level Level)` - Sets the minimum log level to output (default: `INFO`) +- `WithFormatter(formatter Formatter)` - Sets the log formatter (default: `DefaultFormatter`) + +## Example Usage + +### Basic Usage + +Create a new package `log` in your app: ```go package log @@ -29,7 +40,8 @@ import ( ) var ( - logger = logging.New(nil, nil, new(logging.ColouredFormatter)) + // Create logger with default configuration (INFO level, DefaultFormatter) + logger = logging.New(nil, nil) // INFO ... INFO = logger[logging.INFO] @@ -42,6 +54,41 @@ var ( ) ``` +### Custom Configuration + +You can customize the logger using configuration options: + +```go +package log + +import ( + "github.com/RichardKnop/logging" +) + +var ( + // Create logger with coloured formatter and DEBUG level + logger = logging.New( + nil, + nil, + logging.WithLogLevel(logging.DEBUG), + logging.WithFormatter(new(logging.ColouredFormatter)), + ) + + // DEBUG ... + DEBUG = logger[logging.DEBUG] + // INFO ... + INFO = logger[logging.INFO] + // WARNING ... + WARNING = logger[logging.WARNING] + // ERROR ... + ERROR = logger[logging.ERROR] + // FATAL ... + FATAL = logger[logging.FATAL] +) +``` + +### Using the Logger + Then from your app you could do: ```go @@ -53,5 +100,7 @@ import ( func main() { log.INFO.Print("log message") + log.WARNING.Printf("formatted %s", "message") + log.ERROR.Println("error message") } ``` diff --git a/coloured_formatter.go b/coloured_formatter.go index c110500..48cb47b 100644 --- a/coloured_formatter.go +++ b/coloured_formatter.go @@ -11,7 +11,7 @@ const ( ) // Colour map -var colour = map[level]string{ +var colour = map[Level]string{ INFO: fmt.Sprintf(colourSeq, 94), // blue WARNING: fmt.Sprintf(colourSeq, 95), // pink ERROR: fmt.Sprintf(colourSeq, 91), // red @@ -25,16 +25,16 @@ type ColouredFormatter struct { } // GetPrefix returns colour escape code -func (f *ColouredFormatter) GetPrefix(lvl level) string { +func (f *ColouredFormatter) GetPrefix(lvl Level) string { return colour[lvl] } // GetSuffix returns reset sequence code -func (f *ColouredFormatter) GetSuffix(lvl level) string { +func (f *ColouredFormatter) GetSuffix(lvl Level) string { return resetSeq } // Format adds filename and line number before the log message -func (f *ColouredFormatter) Format(lvl level, v ...interface{}) []interface{} { +func (f *ColouredFormatter) Format(lvl Level, v ...interface{}) []interface{} { return append([]interface{}{header()}, v...) } diff --git a/coloured_formatter_test.go b/coloured_formatter_test.go index 6074979..e25bcb6 100644 --- a/coloured_formatter_test.go +++ b/coloured_formatter_test.go @@ -17,7 +17,7 @@ func TestColouredFormatter(t *testing.T) { var ( out, errOut = bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{}) - logger = logging.New(out, errOut, new(logging.ColouredFormatter)) + logger = logging.New(out, errOut, logging.WithFormatter(new(logging.ColouredFormatter))) now time.Time actual []byte expected string diff --git a/config.go b/config.go new file mode 100644 index 0000000..789b9aa --- /dev/null +++ b/config.go @@ -0,0 +1,34 @@ +package logging + +// Config holds configuration options for the logger. +type Config struct { + // LogLevel sets the minimum log level to output. If not set, INFO level is used. + LogLevel Level + + // Formatter sets the log formatter to use. If not set, DefaultFormatter is used. + Formatter Formatter +} + +func defaultConfig() *Config { + return &Config{ + LogLevel: INFO, + Formatter: new(DefaultFormatter), + } +} + +// ConfigOption defines a function type for configuring the logger. +type ConfigOption func(*Config) + +// WithLogLevel sets the log level in the logger configuration. +func WithLogLevel(lvl Level) ConfigOption { + return func(c *Config) { + c.LogLevel = lvl + } +} + +// WithFormatter sets the log formatter in the logger configuration. +func WithFormatter(f Formatter) ConfigOption { + return func(c *Config) { + c.Formatter = f + } +} diff --git a/default_formatter.go b/default_formatter.go index e33d91e..9fed07a 100644 --- a/default_formatter.go +++ b/default_formatter.go @@ -5,16 +5,16 @@ type DefaultFormatter struct { } // GetPrefix returns "" -func (f *DefaultFormatter) GetPrefix(lvl level) string { +func (f *DefaultFormatter) GetPrefix(lvl Level) string { return "" } // GetSuffix returns "" -func (f *DefaultFormatter) GetSuffix(lvl level) string { +func (f *DefaultFormatter) GetSuffix(lvl Level) string { return "" } // Format adds filename and line number before the log message -func (f *DefaultFormatter) Format(lvl level, v ...interface{}) []interface{} { +func (f *DefaultFormatter) Format(lvl Level, v ...interface{}) []interface{} { return append([]interface{}{header()}, v...) } diff --git a/formatter_interface.go b/formatter_interface.go index 5465699..ba116c7 100644 --- a/formatter_interface.go +++ b/formatter_interface.go @@ -13,9 +13,9 @@ const ( // Formatter interface type Formatter interface { - GetPrefix(lvl level) string - Format(lvl level, v ...interface{}) []interface{} - GetSuffix(lvl level) string + GetPrefix(lvl Level) string + Format(lvl Level, v ...interface{}) []interface{} + GetSuffix(lvl Level) string } // Returns header including filename and line number diff --git a/go.mod b/go.mod index 26e2e6a..bd0235f 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,11 @@ module github.com/RichardKnop/logging +go 1.22 + +require github.com/stretchr/testify v1.11.1 + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.2 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e03ee77..667b99b 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logger.go b/logger.go index ff356c7..d3dfd6f 100644 --- a/logger.go +++ b/logger.go @@ -7,25 +7,25 @@ import ( ) // Level type -type level int +type Level int const ( - // DEBUG level - DEBUG level = iota - // INFO level + // DEBUG Level + DEBUG Level = iota + // INFO Level INFO - // WARNING level + // WARNING Level WARNING - // ERROR level + // ERROR Level ERROR - // FATAL level + // FATAL Level FATAL flag = log.Ldate | log.Ltime ) -// Log level prefix map -var prefix = map[level]string{ +// Log Level prefix map +var prefix = map[Level]string{ DEBUG: "DEBUG: ", INFO: "INFO: ", WARNING: "WARNING: ", @@ -34,10 +34,25 @@ var prefix = map[level]string{ } // Logger ... -type Logger map[level]LoggerInterface +type Logger map[Level]LoggerInterface + +// New returns instance of Logger. +func New(out, errOut io.Writer, ops ...ConfigOption) Logger { + config := defaultConfig() + + for _, op := range ops { + op(config) + } + + // If log level is out of bounds, set to nearest valid level. + if config.LogLevel < DEBUG { + config.LogLevel = DEBUG + } + + if config.LogLevel > FATAL { + config.LogLevel = FATAL + } -// New returns instance of Logger -func New(out, errOut io.Writer, f Formatter) Logger { // Fall back to stdout if out not set if out == nil { out = os.Stdout @@ -48,24 +63,26 @@ func New(out, errOut io.Writer, f Formatter) Logger { errOut = os.Stderr } - // Fall back to DefaultFormatter if f not set - if f == nil { - f = new(DefaultFormatter) - } + l := make(map[Level]LoggerInterface, 5) - l := make(map[level]LoggerInterface, 5) - l[DEBUG] = &Wrapper{lvl: DEBUG, formatter: f, logger: log.New(out, f.GetPrefix(DEBUG)+prefix[DEBUG], flag)} - l[INFO] = &Wrapper{lvl: INFO, formatter: f, logger: log.New(out, f.GetPrefix(INFO)+prefix[INFO], flag)} - l[WARNING] = &Wrapper{lvl: INFO, formatter: f, logger: log.New(out, f.GetPrefix(WARNING)+prefix[WARNING], flag)} - l[ERROR] = &Wrapper{lvl: INFO, formatter: f, logger: log.New(errOut, f.GetPrefix(ERROR)+prefix[ERROR], flag)} - l[FATAL] = &Wrapper{lvl: INFO, formatter: f, logger: log.New(errOut, f.GetPrefix(FATAL)+prefix[FATAL], flag)} + for level := DEBUG; level <= FATAL; level++ { + l[level] = NewNoOp() + + if level >= config.LogLevel { + l[level] = &Wrapper{ + lvl: level, + formatter: config.Formatter, + logger: log.New(out, config.Formatter.GetPrefix(level)+prefix[level], flag), + } + } + } - return Logger(l) + return l } // Wrapper ... type Wrapper struct { - lvl level + lvl Level formatter Formatter logger LoggerInterface } diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 0000000..82af68e --- /dev/null +++ b/logger_test.go @@ -0,0 +1,51 @@ +package logging_test + +import ( + "testing" + + "github.com/RichardKnop/logging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + logLevel logging.Level + expectedLogs int + }{ + {"DEBUG", logging.DEBUG, 5}, + {"INFO", logging.INFO, 4}, + {"WARNING", logging.WARNING, 3}, + {"ERROR", logging.ERROR, 2}, + {"FATAL", logging.FATAL, 1}, + } + + for _, tt := range tests { + out := &stubWriter{} + + t.Run("GIVEN a logging constructor configured with min log level "+tt.name, func(t *testing.T) { + logger := logging.New(out, nil, logging.WithLogLevel(tt.logLevel)) + + t.Run("WHEN logging with all the log levels", func(t *testing.T) { + for level := logging.DEBUG; level <= logging.FATAL; level++ { + logger[level].Print("This is a test log") + } + + t.Run("THEN only those allowed logs are performed", func(t *testing.T) { + require.Len(t, out.loggedMessages, tt.expectedLogs) + assert.Contains(t, out.loggedMessages[0], "This is a test log") + }) + }) + }) + } +} + +type stubWriter struct { + loggedMessages []string +} + +func (s *stubWriter) Write(p []byte) (n int, err error) { + s.loggedMessages = append(s.loggedMessages, string(p)) + return len(p), nil +} diff --git a/noop.go b/noop.go new file mode 100644 index 0000000..e63d06a --- /dev/null +++ b/noop.go @@ -0,0 +1,28 @@ +package logging + +type noOp struct { +} + +// NewNoOp creates a no-op logger that implements LoggerInterface but does nothing. +// Useful for disabling logging. Also used with Config MinLogLevel on disabled levels. +func NewNoOp() LoggerInterface { + return &noOp{} +} + +func (n noOp) Print(i ...interface{}) {} + +func (n noOp) Printf(s string, i ...interface{}) {} + +func (n noOp) Println(i ...interface{}) {} + +func (n noOp) Fatal(i ...interface{}) {} + +func (n noOp) Fatalf(s string, i ...interface{}) {} + +func (n noOp) Fatalln(i ...interface{}) {} + +func (n noOp) Panic(i ...interface{}) {} + +func (n noOp) Panicf(s string, i ...interface{}) {} + +func (n noOp) Panicln(i ...interface{}) {}