From 3096ae5d707dc2769be0fbfe52430885f70c7e82 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sun, 5 Mar 2017 18:46:00 -0800 Subject: [PATCH 1/9] prepare for next dev iteration and travis build config update --- .travis.yml | 7 +------ default_test.go | 2 +- formatter.go | 2 +- log.go | 6 +++--- log_test.go | 2 +- receiver.go | 4 ++-- 6 files changed, 9 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 66841cc7..136c5076 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,12 +13,7 @@ go: - 1.8 - tip -before_install: - - bash <(curl -s https://aahframework.org/env.txt) "log" "v0" - -install: - - cd $GOPATH/src/aahframework.org/log.v0 - - go get -v -t ./... +go_import_path: aahframework.org/log.v0-unstable script: - bash go.test.sh diff --git a/default_test.go b/default_test.go index 5e3b95fa..97bb133f 100644 --- a/default_test.go +++ b/default_test.go @@ -8,7 +8,7 @@ import ( "fmt" "testing" - "aahframework.org/test.v0/assert" + "aahframework.org/test.v0-unstable/assert" ) func TestDefaultStandardLogger(t *testing.T) { diff --git a/formatter.go b/formatter.go index 9d21a7c4..c226d6b5 100644 --- a/formatter.go +++ b/formatter.go @@ -8,7 +8,7 @@ import ( "fmt" "strings" - "aahframework.org/essentials.v0" + "aahframework.org/essentials.v0-unstable" ) // FormatterFunc is the handler function to implement log entry diff --git a/log.go b/log.go index 7900f5ee..da229709 100644 --- a/log.go +++ b/log.go @@ -25,8 +25,8 @@ import ( "sync" "time" - "aahframework.org/config.v0" - "aahframework.org/essentials.v0" + "aahframework.org/config" + ess "aahframework.org/essentials" ) // Level type definition @@ -62,7 +62,7 @@ const ( var ( // Version no. of aahframework.org/log library - Version = "0.2" + Version = "0.3" // FmtFlags is the list of log format flags supported by aah/log library // Usage of flag order is up to format composition. diff --git a/log_test.go b/log_test.go index 9c3e232a..e285143c 100644 --- a/log_test.go +++ b/log_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "aahframework.org/test.v0/assert" + "aahframework.org/test.v0-unstable/assert" ) func TestNewCustomUTCConsoleReceiver(t *testing.T) { diff --git a/receiver.go b/receiver.go index d83ad39e..3fc4b563 100644 --- a/receiver.go +++ b/receiver.go @@ -13,8 +13,8 @@ import ( "sync" "time" - "aahframework.org/config.v0" - "aahframework.org/essentials.v0" + "aahframework.org/config.v0-unstable" + "aahframework.org/essentials.v0-unstable" ) // Receiver represents aah logger object that statisfy console, file, logging From d5d4db4efcd7e39fb781c89303920266488c3a22 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Sun, 5 Mar 2017 18:49:41 -0800 Subject: [PATCH 2/9] test case fix --- log.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/log.go b/log.go index da229709..ce6e7754 100644 --- a/log.go +++ b/log.go @@ -25,8 +25,8 @@ import ( "sync" "time" - "aahframework.org/config" - ess "aahframework.org/essentials" + "aahframework.org/config.v0-unstable" + "aahframework.org/essentials.v0-unstable" ) // Level type definition From 16171cf7b6fc5aba340930a0e47d415299ecc865 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Fri, 10 Mar 2017 18:49:59 -0800 Subject: [PATCH 3/9] build config update --- .travis.yml | 10 ++++++---- go.test.sh | 12 ------------ 2 files changed, 6 insertions(+), 16 deletions(-) delete mode 100755 go.test.sh diff --git a/.travis.yml b/.travis.yml index 136c5076..080e5571 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,11 @@ language: go sudo: false branches: - only: - # - master - # - integration + except: + # skip tags build, we are building branch and master that is enough for + # consistenty check and release. Let's use Travis CI resources optimally + # for aah framework. + - /^v[0-9]\.[0-9]/ go: - 1.6 @@ -16,7 +18,7 @@ go: go_import_path: aahframework.org/log.v0-unstable script: - - bash go.test.sh + - bash <(curl -s https://aahframework.org/go-test) after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/go.test.sh b/go.test.sh deleted file mode 100755 index 34dbbfb3..00000000 --- a/go.test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e -echo "" > coverage.txt - -for d in $(go list ./... | grep -v vendor); do - go test -race -coverprofile=profile.out -covermode=atomic $d - if [ -f profile.out ]; then - cat profile.out >> coverage.txt - rm profile.out - fi -done From 93263a47efb4dbbce9b436fdeb2276f012fc616d Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 17 Apr 2017 09:04:08 -0700 Subject: [PATCH 4/9] build config update --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 080e5571..f060352a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,12 +10,12 @@ branches: - /^v[0-9]\.[0-9]/ go: - - 1.6 - - 1.7 + - 1.7.x - 1.8 + - 1.8.x - tip -go_import_path: aahframework.org/log.v0-unstable +go_import_path: aahframework.org/log.v0 script: - bash <(curl -s https://aahframework.org/go-test) From cead8d0012b9cf166c6c3d9eb88ef2f60184f78f Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 17 Apr 2017 09:04:36 -0700 Subject: [PATCH 5/9] godoc update --- default.go | 2 +- formatter.go | 2 +- log.go | 16 +++++++++------- log_test.go | 4 ++-- receiver.go | 2 +- stats.go | 2 +- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/default.go b/default.go index 41775f45..16cf3ff1 100644 --- a/default.go +++ b/default.go @@ -1,4 +1,4 @@ -// Copyright (c) 2016 Jeevanandam M (https://github.com/jeevatkm) +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) // go-aah/log source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/formatter.go b/formatter.go index c226d6b5..4a3c20ad 100644 --- a/formatter.go +++ b/formatter.go @@ -1,4 +1,4 @@ -// Copyright (c) 2016 Jeevanandam M (https://github.com/jeevatkm) +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) // go-aah/log source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/log.go b/log.go index ce6e7754..a3c9da3d 100644 --- a/log.go +++ b/log.go @@ -1,13 +1,15 @@ -// Copyright (c) 2016 Jeevanandam M (https://github.com/jeevatkm) +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) // go-aah/log source code and usage is governed by a MIT style // license that can be found in the LICENSE file. -// Package log implements a simple, flexible & powerful logger. Supports -// console, file (rotation by size, daily, lines), logging receivers -// and Logging Stats. It also has a predefined 'standard' Logger accessible -// through helper functions Error{f}, Warn{f}, Info{f}, Debug{f}, Trace{f} -// which are easier to use than creating a Logger manually. That logger writes -// to standard error and prints log `Entry` details as per `DefaultPattern`. +// Package log implements a simple, flexible & powerful logger. +// Currently it supports `console`, `file` (rotation by daily, size, lines), +// logging receivers and logging stats. It also has a predefined 'standard' +// Logger accessible through helper functions `Error{f}`, `Warn{f}`, `Info{f}`, +// `Debug{f}`, `Trace{f}` which are easier to use than creating a Logger manually. +// That logger writes to standard error and prints log `Entry` details +// as per `DefaultPattern`. +// // log.Info("Welcome ", "to ", "aah ", "logger") // log.Infof("%v, %v, & %v", "simple", "flexible", "powerful logger") // diff --git a/log_test.go b/log_test.go index e285143c..aeee9fcc 100644 --- a/log_test.go +++ b/log_test.go @@ -163,10 +163,10 @@ file = "test-aah-filename.log" rotate { mode = "lines" - # this value is needed if rotate="lines"; default is unlimited + # this value is needed if rotate.mode="lines"; default is unlimited lines = 20 - # this value is needed in MB if rotate="size"; default is unlimited + # this value is needed in MB if rotate.mode="size"; default is 100 MB #size = 250 } ` diff --git a/receiver.go b/receiver.go index 3fc4b563..02b06220 100644 --- a/receiver.go +++ b/receiver.go @@ -1,4 +1,4 @@ -// Copyright (c) 2016 Jeevanandam M (https://github.com/jeevatkm) +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) // go-aah/log source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/stats.go b/stats.go index 5f3938a6..c4858ce9 100644 --- a/stats.go +++ b/stats.go @@ -1,4 +1,4 @@ -// Copyright (c) 2016 Jeevanandam M (https://github.com/jeevatkm) +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) // go-aah/log source code and usage is governed by a MIT style // license that can be found in the LICENSE file. From b8df3d21d876e503b907697269579e9cc10e4ac6 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Mon, 15 May 2017 20:02:35 -0700 Subject: [PATCH 6/9] improved design, non-blocking serial access logging --- console_receiver.go | 93 ++++++++++ console_receiver_test.go | 107 +++++++++++ default.go | 135 ++++++++------ default_test.go | 72 ++++---- discard_receiver.go | 33 ++++ entry.go | 80 ++++++++ file_receiver.go | 194 ++++++++++++++++++++ file_receiver_test.go | 146 +++++++++++++++ formatter.go | 157 ++++++++-------- log.go | 382 +++++++++++++++++++++------------------ log_test.go | 363 +++++++------------------------------ receiver.go | 327 --------------------------------- stats.go | 8 +- util.go | 94 ++++++++++ 14 files changed, 1202 insertions(+), 989 deletions(-) create mode 100644 console_receiver.go create mode 100644 console_receiver_test.go create mode 100644 discard_receiver.go create mode 100644 entry.go create mode 100644 file_receiver.go create mode 100644 file_receiver_test.go delete mode 100644 receiver.go create mode 100644 util.go diff --git a/console_receiver.go b/console_receiver.go new file mode 100644 index 00000000..022820bb --- /dev/null +++ b/console_receiver.go @@ -0,0 +1,93 @@ +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) +// go-aah/log source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package log + +import ( + "fmt" + "io" + "os" + "runtime" + + "aahframework.org/config.v0" +) + +var ( + // ANSI color codes + resetColor = []byte("\033[0m") + levelToColor = [][]byte{ + levelFatal: []byte("\033[0;31m"), // red + levelPanic: []byte("\033[0;31m"), // red + LevelError: []byte("\033[0;31m"), // red + LevelWarn: []byte("\033[0;33m"), // yellow + LevelInfo: []byte("\033[0;37m"), // white + LevelDebug: []byte("\033[0;34m"), // blue + LevelTrace: []byte("\033[0;35m"), // magenta (purple) + } + + _ Receiver = &ConsoleReceiver{} +) + +// ConsoleReceiver writes the log entry into os.Stderr. +// For non-windows it writes with color. +type ConsoleReceiver struct { + out io.Writer + formatter string + flags *[]FlagPart + isCallerInfo bool + isColor bool +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// ConsoleReceiver methods +//___________________________________ + +// Init method initializes the console logger. +func (c *ConsoleReceiver) Init(cfg *config.Config) error { + c.out = os.Stderr + c.isColor = runtime.GOOS != "windows" + + c.formatter = cfg.StringDefault("log.format", "text") + if !(c.formatter == textFmt || c.formatter == jsonFmt) { + return fmt.Errorf("log: unsupported format '%s'", c.formatter) + } + + return nil +} + +// SetPattern method initializes the logger format pattern. +func (c *ConsoleReceiver) SetPattern(pattern string) error { + flags, err := parseFlag(pattern) + if err != nil { + return err + } + c.flags = flags + if c.formatter == textFmt { + c.isCallerInfo = isCallerInfo(c.flags) + } + return nil +} + +// IsCallerInfo method returns true if log receiver is configured with caller info +// otherwise false. +func (c *ConsoleReceiver) IsCallerInfo() bool { + return c.isCallerInfo +} + +// Log method writes the log entry into os.Stderr. +func (c *ConsoleReceiver) Log(entry *Entry) { + if c.isColor { + _, _ = c.out.Write(levelToColor[entry.Level]) + } + + msg := applyFormatter(c.formatter, c.flags, entry) + if len(msg) == 0 || msg[len(msg)-1] != '\n' { + msg = append(msg, '\n') + } + _, _ = c.out.Write(msg) + + if c.isColor { + _, _ = c.out.Write(resetColor) + } +} diff --git a/console_receiver_test.go b/console_receiver_test.go new file mode 100644 index 00000000..b11b426f --- /dev/null +++ b/console_receiver_test.go @@ -0,0 +1,107 @@ +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) +// go-aah/log source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package log + +import ( + "testing" + "time" + + "aahframework.org/config.v0" + "aahframework.org/test.v0/assert" +) + +func TestConsoleLoggerTextJSON(t *testing.T) { + // Text 1 + textConfigStr1 := ` + log { + receiver = "console" + level = "debug" + pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message" + } + ` + testConsoleLogger(t, textConfigStr1) + + // Text 2 + textConfigStr2 := ` + log { + receiver = "console" + level = "debug" + pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %shortfile %line %custom:- %message" + } + ` + testConsoleLogger(t, textConfigStr2) + + // JSON + jsonConfigStr := ` + log { + receiver = "console" + level = "debug" + format = "json" + } + ` + testConsoleLogger(t, jsonConfigStr) +} + +func TestConsoleLoggerUnsupportedFormat(t *testing.T) { + configStr := ` + log { + # default config plus + pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message" + format = "xml" + } + ` + cfg, _ := config.ParseString(configStr) + logger, err := New(cfg) + assert.Nil(t, logger) + assert.Equal(t, "log: unsupported format 'xml'", err.Error()) +} + +func TestConsoleLoggerUnknownFormatFlag(t *testing.T) { + configStr := ` + log { + # default config plus + pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %myfile %line %custom:- %message" + } + ` + cfg, _ := config.ParseString(configStr) + logger, err := New(cfg) + assert.Nil(t, logger) + assert.Equal(t, "unrecognized log format flag: myfile", err.Error()) +} + +func TestConsoleLoggerDefaults(t *testing.T) { + configStr := ` + log { + # default config + } + ` + cfg, _ := config.ParseString(configStr) + logger, err := New(cfg) + assert.NotNil(t, logger) + assert.Nil(t, err) +} + +func testConsoleLogger(t *testing.T, cfgStr string) { + cfg, _ := config.ParseString(cfgStr) + logger, err := New(cfg) + assert.FailNowOnError(t, err, "unexpected error") + + logger.Trace("I shoudn't see this msg, because standard logger level is DEBUG") + logger.Tracef("I shoudn't see this msg, because standard logger level is DEBUG: %v", 4) + + logger.Debug("I would like to see this message, debug is useful for dev") + logger.Debugf("I would like to see this message, debug is useful for %v", "dev") + + logger.Info("Yes, I would love to see") + logger.Infof("Yes, I would love to %v", "see") + + logger.Warn("Yes, yes it's an warning") + logger.Warnf("Yes, yes it's an %v", "warning") + + logger.Error("Yes, yes, yes - finally an error") + logger.Errorf("Yes, yes, yes - %v", "finally an error") + + time.Sleep(1 * time.Millisecond) +} diff --git a/default.go b/default.go index 16cf3ff1..7fdb6cc4 100644 --- a/default.go +++ b/default.go @@ -7,106 +7,123 @@ package log import ( "fmt" "os" -) -var stdLogger Logger + "aahframework.org/config.v0" +) -// Fatal logs message as `FATAL` and calls os.Exit(1) -func Fatal(v ...interface{}) { - _ = stdLogger.Output(levelFatal, 2, nil, v...) - os.Exit(1) -} +var std *Logger -// Fatalf logs message as `FATAL` and calls os.Exit(1) -func Fatalf(format string, v ...interface{}) { - _ = stdLogger.Output(levelFatal, 2, &format, v...) - os.Exit(1) -} +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Logger methods +//_______________________________________ -// Panic logs message as `PANIC` and calls panic() -func Panic(v ...interface{}) { - _ = stdLogger.Output(levelPanic, 2, nil, v...) - panic("") -} - -// Panicf logs message as `PANIC` and calls panic() -func Panicf(format string, v ...interface{}) { - _ = stdLogger.Output(levelPanic, 2, &format, v...) - panic(fmt.Sprintf(format, v...)) -} - -// Error logs message as `LevelError` +// Error logs message as `ERROR`. Arguments handled in the mananer of `fmt.Print`. func Error(v ...interface{}) { - _ = stdLogger.Output(LevelError, 2, nil, v...) + std.output(LevelError, 3, nil, v...) } -// Errorf logs message as `LevelError` +// Errorf logs message as `ERROR`. Arguments handled in the mananer of `fmt.Printf`. func Errorf(format string, v ...interface{}) { - _ = stdLogger.Output(LevelError, 2, &format, v...) + std.output(LevelError, 3, &format, v...) } -// Warn logs message as `LevelWarn` +// Warn logs message as `WARN`. Arguments handled in the mananer of `fmt.Print`. func Warn(v ...interface{}) { - _ = stdLogger.Output(LevelWarn, 2, nil, v...) + std.output(LevelWarn, 3, nil, v...) } -// Warnf logs message as `LevelWarn` +// Warnf logs message as `WARN`. Arguments handled in the mananer of `fmt.Printf`. func Warnf(format string, v ...interface{}) { - _ = stdLogger.Output(LevelWarn, 2, &format, v...) + std.output(LevelWarn, 3, &format, v...) } -// Info logs message as `LevelInfo` +// Info logs message as `INFO`. Arguments handled in the mananer of `fmt.Print`. func Info(v ...interface{}) { - _ = stdLogger.Output(LevelInfo, 2, nil, v...) + std.output(LevelInfo, 3, nil, v...) } -// Infof logs message as `LevelInfo` +// Infof logs message as `INFO`. Arguments handled in the mananer of `fmt.Printf`. func Infof(format string, v ...interface{}) { - _ = stdLogger.Output(LevelInfo, 2, &format, v...) + std.output(LevelInfo, 3, &format, v...) } -// Debug logs message as `LevelDebug` +// Debug logs message as `DEBUG`. Arguments handled in the mananer of `fmt.Print`. func Debug(v ...interface{}) { - _ = stdLogger.Output(LevelDebug, 2, nil, v...) + std.output(LevelDebug, 3, nil, v...) } -// Debugf logs message as `LevelDebug` +// Debugf logs message as `DEBUG`. Arguments handled in the mananer of `fmt.Printf`. func Debugf(format string, v ...interface{}) { - _ = stdLogger.Output(LevelDebug, 2, &format, v...) + std.output(LevelDebug, 3, &format, v...) } -// Trace logs message as `LevelTrace` +// Trace logs message as `TRACE`. Arguments handled in the mananer of `fmt.Print`. func Trace(v ...interface{}) { - _ = stdLogger.Output(LevelTrace, 2, nil, v...) + std.output(LevelTrace, 3, nil, v...) } -// Tracef logs message as `LevelTrace` +// Tracef logs message as `TRACE`. Arguments handled in the mananer of `fmt.Printf`. func Tracef(format string, v ...interface{}) { - _ = stdLogger.Output(LevelTrace, 2, &format, v...) + std.output(LevelTrace, 3, &format, v...) } -// Stats returns current logger statistics like number of lines written, -// number of bytes written, etc. -func Stats() *ReceiverStats { - return stdLogger.Stats() +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Logger methods - Drop-in replacement +// for Go standard logger +//_______________________________________ + +// Print logs message as `INFO`. Arguments handled in the mananer of `fmt.Print`. +func Print(v ...interface{}) { + std.output(LevelInfo, 3, nil, v...) } -// SetPattern sets the log entry format -func SetPattern(pattern string) error { - return stdLogger.SetPattern(pattern) +// Printf logs message as `INFO`. Arguments handled in the mananer of `fmt.Printf`. +func Printf(format string, v ...interface{}) { + std.output(LevelInfo, 3, &format, v...) } -// SetLevel allows to set log level dynamically -func SetLevel(level Level) { - stdLogger.SetLevel(level) +// Println logs message as `INFO`. Arguments handled in the mananer of `fmt.Printf`. +func Println(format string, v ...interface{}) { + std.output(LevelInfo, 3, &format, v...) } -// SetOutput allows to set standard logger implementation -// which statisfies `Logger` interface -func SetOutput(logger Logger) { - stdLogger = logger +// Fatal logs message as `FATAL` and call to os.Exit(1). +func Fatal(v ...interface{}) { + std.output(levelFatal, 3, nil, v...) + os.Exit(1) +} + +// Fatalf logs message as `FATAL` and call to os.Exit(1). +func Fatalf(format string, v ...interface{}) { + std.output(levelFatal, 3, &format, v...) + os.Exit(1) +} + +// Fatalln logs message as `FATAL` and call to os.Exit(1). +func Fatalln(format string, v ...interface{}) { + std.output(levelFatal, 3, &format, v...) + os.Exit(1) +} + +// Panic logs message as `PANIC` and call to panic(). +func Panic(v ...interface{}) { + std.output(levelPanic, 3, nil, v...) + panic("") +} + +// Panicf logs message as `PANIC` and call to panic(). +func Panicf(format string, v ...interface{}) { + std.output(levelPanic, 3, &format, v...) + panic(fmt.Sprintf(format, v...)) +} + +// Panicln logs message as `PANIC` and call to panic(). +func Panicln(format string, v ...interface{}) { + std.output(levelPanic, 3, &format, v...) + panic(fmt.Sprintf(format, v...)) } func init() { - stdLogger, _ = New(`receiver = "CONSOLE"; level = "DEBUG";`) + cfg, _ := config.ParseString("log { }") + std, _ = New(cfg) } diff --git a/default_test.go b/default_test.go index 97bb133f..d84bce8c 100644 --- a/default_test.go +++ b/default_test.go @@ -5,60 +5,58 @@ package log import ( - "fmt" "testing" + "time" - "aahframework.org/test.v0-unstable/assert" + "aahframework.org/config.v0" ) -func TestDefaultStandardLogger(t *testing.T) { - SetLevel(LevelInfo) - _ = SetPattern("%time:2006-01-02 15:04:05.000 %level:-5 %line %custom:- %message") - Trace("I shoudn't see this msg, because standard logger level is DEBUG") - Debug("I would like to see this message, debug is useful for dev") - Info("Yes, I would love to") - Warn("Yes, yes it's an warning") - Error("Yes, yes, yes - finally an error") - fmt.Println() +func TestDefaultLogger(t *testing.T) { + cfg, _ := config.ParseString(` + log { + pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message" + } + `) + std, _ = New(cfg) - t.Logf("First round: %#v\n\n", Stats()) + Print("welcome print") + Printf("welcome printf") + Println("welcome println") - SetLevel(LevelDebug) - _ = SetPattern("%time:2006-01-02 15:04:05.000 %level:-5 %shortfile %line %custom:- %message") - Tracef("I shoudn't see this msg: %v", 4) - Debugf("I would like to see this message, debug is useful for dev: %v", 3) - Infof("Yes, I would love to: %v", 2) - Warnf("Yes, yes it's an warning: %v", 1) - Errorf("Yes, yes, yes - finally an error: %v", 0) + Trace("I shoudn't see this msg, because standard logger level is DEBUG") + Tracef("I shoudn't see this msg, because standard logger level is DEBUG: %v", 4) - t.Logf("Second round: %#v\n\n", Stats()) + Debug("I would like to see this message, debug is useful for dev") + Debugf("I would like to see this message, debug is useful for %v", "dev") - err := SetPattern("%level:-5 %shortfile %line %unknown") - assert.NotNil(t, err) + Info("Yes, I would love to see") + Infof("Yes, I would love to %v", "see") - newLogger, _ := New(`receiver = "CONSOLE"; level = "DEBUG";`) - SetOutput(newLogger) - Info("Fresh new face ...") -} + Warn("Yes, yes it's an warning") + Warnf("Yes, yes it's an %v", "warning") -func TestPanicDefaultStandardLogger(t *testing.T) { - defer func() { - if r := recover(); r != nil { - _ = r - } - }() + Error("Yes, yes, yes - finally an error") + Errorf("Yes, yes, yes - %v", "finally an error") + + testStdPanic("panic", "this is panic") + testStdPanic("panicf", "this is panicf") + testStdPanic("panicln", "this is panicln") - SetLevel(levelPanic) - Panic("This is panic message") + time.Sleep(1 * time.Millisecond) } -func TestPanicfDefaultStandardLogger(t *testing.T) { +func testStdPanic(method, msg string) { defer func() { if r := recover(); r != nil { _ = r } }() - SetLevel(levelPanic) - Panicf("This is panic %v", "message from param") + if method == "panic" { + Panic(msg) + } else if method == "panicf" { + Panicf("%s", msg) + } else if method == "panicln" { + Panicln("%s", msg) + } } diff --git a/discard_receiver.go b/discard_receiver.go new file mode 100644 index 00000000..59caa582 --- /dev/null +++ b/discard_receiver.go @@ -0,0 +1,33 @@ +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) +// go-aah/log source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package log + +import "aahframework.org/config.v0" + +var _ Receiver = &DiscardReceiver{} + +// DiscardReceiver is to throw the log entry. +type DiscardReceiver struct { +} + +// Init method initializes the console logger. +func (d *DiscardReceiver) Init(_ *config.Config) error { + return nil +} + +// SetPattern method initializes the logger format pattern. +func (d *DiscardReceiver) SetPattern(_ string) error { + return nil +} + +// IsCallerInfo method returns true if log receiver is configured with caller info +// otherwise false. +func (d *DiscardReceiver) IsCallerInfo() bool { + return false +} + +// Log method writes the buf to +func (d *DiscardReceiver) Log(_ *Entry) { +} diff --git a/entry.go b/entry.go new file mode 100644 index 00000000..b45476e0 --- /dev/null +++ b/entry.go @@ -0,0 +1,80 @@ +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) +// go-aah/log source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package log + +import ( + "bytes" + "encoding/json" + "sync" + "time" +) + +var ( + entryPool = &sync.Pool{New: func() interface{} { return &Entry{} }} + bufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} +) + +// Entry represents a log entry and contains the timestamp when the entry +// was created, level, etc. +type Entry struct { + Level Level `json:"level,omitempty"` + Time time.Time `json:"timestamp,omitempty"` + Message string `json:"message,omitempty"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Entry methods +//___________________________________ + +// MarshalJSON method for formating entry to JSON. +func (e *Entry) MarshalJSON() ([]byte, error) { + type Alias Entry + return json.Marshal(&struct { + Level string `json:"level,omitempty"` + Time string `json:"timestamp,omitempty"` + *Alias + }{ + Level: levelToLevelName[e.Level], + Time: formatTime(e.Time), + Alias: (*Alias)(e), + }) +} + +// Reset method resets the `Entry` values for reuse. +func (e *Entry) Reset() { + e.Level = LevelUnknown + e.Time = time.Time{} + e.Message = "" + e.File = "" + e.Line = 0 +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Unexported methods +//___________________________________ + +// getEntry gets entry object from pool. +func getEntry() *Entry { + return entryPool.Get().(*Entry) +} + +// putEntry reset the entry and puts it into Pool. +func putEntry(e *Entry) { + e.Reset() + entryPool.Put(e) +} + +// getBuffer gets buffer object from pool. +func getBuffer() *bytes.Buffer { + return bufPool.Get().(*bytes.Buffer) +} + +// putBuffer reset the buffer and puts it into Pool. +func putBuffer(buf *bytes.Buffer) { + buf.Reset() + bufPool.Put(buf) +} diff --git a/file_receiver.go b/file_receiver.go new file mode 100644 index 00000000..391f0e21 --- /dev/null +++ b/file_receiver.go @@ -0,0 +1,194 @@ +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) +// go-aah/log source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package log + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "aahframework.org/config.v0" + "aahframework.org/essentials.v0" +) + +var _ Receiver = &FileReceiver{} + +// FileReceiver writes the log entry into file. +type FileReceiver struct { + filename string + out io.Writer + formatter string + flags *[]FlagPart + isCallerInfo bool + stats *receiverStats + isClosed bool + rotatePolicy string + openDay int + isUTC bool + maxSize int64 + maxLines int64 +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// FileReceiver methods +//___________________________________ + +// Init method initializes the file receiver instance. +func (f *FileReceiver) Init(cfg *config.Config) error { + // File + f.filename = cfg.StringDefault("log.file", "") + if ess.IsStrEmpty(f.filename) { + return errors.New("log: file value is required") + } + + if err := f.openFile(); err != nil { + return err + } + + f.formatter = cfg.StringDefault("log.format", "text") + if !(f.formatter == textFmt || f.formatter == jsonFmt) { + return fmt.Errorf("log: unsupported format '%s'", f.formatter) + } + + f.rotatePolicy = cfg.StringDefault("log.rotate.policy", "daily") + switch f.rotatePolicy { + case "daily": + f.openDay = time.Now().Day() + case "lines": + f.maxLines = int64(cfg.IntDefault("log.rotate.lines", 0)) + case "size": + maxSize, err := ess.StrToBytes(cfg.StringDefault("log.rotate.size", "512mb")) + if err != nil { + return err + } + f.maxSize = maxSize + } + + return nil +} + +// SetPattern method initializes the logger format pattern. +func (f *FileReceiver) SetPattern(pattern string) error { + flags, err := parseFlag(pattern) + if err != nil { + return err + } + f.flags = flags + if f.formatter == textFmt { + f.isCallerInfo = isCallerInfo(f.flags) + } + f.isUTC = isFmtFlagExists(f.flags, FmtFlagUTCTime) + if f.isUTC { + f.openDay = time.Now().UTC().Day() + } + return nil +} + +// IsCallerInfo method returns true if log receiver is configured with caller info +// otherwise false. +func (f *FileReceiver) IsCallerInfo() bool { + return f.isCallerInfo +} + +// Log method logs the given entry values into file. +func (f *FileReceiver) Log(entry *Entry) { + if f.isRotate() { + _ = f.rotateFile() + } + + msg := applyFormatter(f.formatter, f.flags, entry) + if len(msg) == 0 || msg[len(msg)-1] != '\n' { + msg = append(msg, '\n') + } + + size, _ := f.out.Write(msg) + if size == 0 { + return + } + + // calculate receiver stats + f.stats.bytes += int64(size) + f.stats.lines++ +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// FileReceiver Unexported methods +//___________________________________ + +func (f *FileReceiver) isRotate() bool { + switch f.rotatePolicy { + case "daily": + if f.isUTC { + return time.Now().UTC().Day() != f.openDay + } + return time.Now().Day() != f.openDay + case "lines": + return f.maxLines != 0 && f.stats.lines >= f.maxLines + case "size": + return f.maxSize != 0 && f.stats.bytes >= f.maxSize + } + return false +} + +func (f *FileReceiver) rotateFile() error { + if _, err := os.Lstat(f.filename); err == nil { + f.close() + if err = os.Rename(f.filename, f.backupFileName()); err != nil { + return err + } + } + + if err := f.openFile(); err != nil { + return err + } + + return nil +} + +func (f *FileReceiver) openFile() error { + dir := filepath.Dir(f.filename) + _ = ess.MkDirAll(dir, filePermission) + + file, err := os.OpenFile(f.filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, filePermission) + if err != nil { + return err + } + + fileStat, err := file.Stat() + if err != nil { + return err + } + + f.out = file + f.isClosed = false + f.stats = &receiverStats{} + f.stats.bytes = fileStat.Size() + f.stats.lines = int64(ess.LineCntr(file)) + + return nil +} + +func (f *FileReceiver) close() { + if f.isClosed { + return + } + ess.CloseQuietly(f.out) + f.isClosed = true +} + +func (f *FileReceiver) backupFileName() string { + dir := filepath.Dir(f.filename) + fileName := filepath.Base(f.filename) + ext := filepath.Ext(fileName) + baseName := ess.StripExt(fileName) + t := time.Now() + if f.isUTC { + t = t.UTC() + } + return filepath.Join(dir, fmt.Sprintf("%s-%s%s", baseName, t.Format(BackupTimeFormat), ext)) +} diff --git a/file_receiver_test.go b/file_receiver_test.go new file mode 100644 index 00000000..42f2cbd0 --- /dev/null +++ b/file_receiver_test.go @@ -0,0 +1,146 @@ +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) +// go-aah/log source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package log + +import ( + "testing" + "time" + + "aahframework.org/config.v0" + "aahframework.org/test.v0/assert" +) + +func TestFileLoggerDailyRotation(t *testing.T) { + cleaupFiles("*.log") + fileConfigStr1 := ` + log { + receiver = "file" + level = "debug" + pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message" + file = "daily-aah-filename.log" + rotate { + policy = "daily" + } + } + ` + + testFileLogger(t, fileConfigStr1, 5000) + cleaupFiles("*.log") + + fileConfigStr2 := ` + log { + receiver = "file" + level = "debug" + pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message" + file = "daily-aah-filename.log" + rotate { + policy = "lines" + lines = 10000 + } + } + ` + testFileLogger(t, fileConfigStr2, 5000) + cleaupFiles("*.log") + + fileConfigStr3 := ` + log { + receiver = "file" + level = "debug" + pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %shortfile %line %custom:- %message" + file = "daily-aah-filename.log" + rotate { + policy = "size" + size = "500kb" + } + } + ` + testFileLogger(t, fileConfigStr3, 5000) + cleaupFiles("*.log") + + // JSON + fileConfigStrJSON := ` + log { + receiver = "file" + level = "debug" + format = "json" + file = "daily-aah-filename.log" + } + ` + testFileLogger(t, fileConfigStrJSON, 1000) + cleaupFiles("*.log") +} + +func TestFileLoggerFileRequired(t *testing.T) { + fileConfigStr := ` + log { + receiver = "file" + level = "debug" + pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message" + rotate { + policy = "daily" + } + } + ` + cfg, _ := config.ParseString(fileConfigStr) + logger, err := New(cfg) + assert.Nil(t, logger) + assert.Equal(t, "log: file value is required", err.Error()) +} + +func TestFileLoggerUnsupportedFormat(t *testing.T) { + defer cleaupFiles("*.log") + configStr := ` + log { + receiver = "file" + file = "daily-aah-filename.log" + format = "xml" + } + ` + cfg, _ := config.ParseString(configStr) + logger, err := New(cfg) + assert.Equal(t, "log: unsupported format 'xml'", err.Error()) + assert.Nil(t, logger) + +} + +func TestFileLoggerUnknownFormatFlag(t *testing.T) { + defer cleaupFiles("*.log") + configStr := ` + log { + receiver = "file" + file = "daily-aah-filename.log" + pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %myfile %line %custom:- %message" + } + ` + cfg, _ := config.ParseString(configStr) + logger, err := New(cfg) + assert.Nil(t, logger) + assert.Equal(t, "unrecognized log format flag: myfile", err.Error()) +} + +func testFileLogger(t *testing.T, cfgStr string, loop int) { + cfg, _ := config.ParseString(cfgStr) + logger, err := New(cfg) + assert.FailNowOnError(t, err, "unexpected error") + + for i := 0; i < loop; i++ { + logger.Trace("I shoudn't see this msg, because standard logger level is DEBUG") + logger.Tracef("I shoudn't see this msg, because standard logger level is DEBUG: %v", 4) + + logger.Debug("I would like to see this message, debug is useful for dev") + logger.Debugf("I would like to see this message, debug is useful for %v", "dev") + + logger.Info("Yes, I would love to see") + logger.Infof("Yes, I would love to %v", "see") + + logger.Warn("Yes, yes it's an warning") + logger.Warnf("Yes, yes it's an %v", "warning") + + logger.Error("Yes, yes, yes - finally an error") + logger.Errorf("Yes, yes, yes - %v", "finally an error") + } + + time.Sleep(2 * time.Millisecond) +} diff --git a/formatter.go b/formatter.go index 4a3c20ad..dea217de 100644 --- a/formatter.go +++ b/formatter.go @@ -5,15 +5,16 @@ package log import ( + "encoding/json" "fmt" + "path/filepath" "strings" - - "aahframework.org/essentials.v0-unstable" ) -// FormatterFunc is the handler function to implement log entry -// formatting log entry based on log format flags. -type FormatterFunc func(flags *[]FlagPart, entry *Entry, isColor bool) (*[]byte, error) +const ( + textFmt = "text" + jsonFmt = "json" +) // FlagPart is indiviual flag details // For e.g.: @@ -28,116 +29,102 @@ type FlagPart struct { Format string } -// parseFlag it parses the log message formart into flag parts -// For e.g.: -// %time:2006-01-02 15:04:05.000 %level %custom:- %msg -func parseFlag(format string) (*[]FlagPart, error) { - if ess.IsStrEmpty(format) { - return nil, ErrFormatStringEmpty - } - - var flagParts []FlagPart - format = strings.TrimSpace(format) - formatFlags := strings.Split(format, flagSeparator)[1:] - for _, f := range formatFlags { - parts := strings.SplitN(strings.TrimSpace(f), flagValueSeparator, 2) - flag := getFmtFlagByName(parts[0]) - if flag == FmtFlagUnknown { - return nil, fmt.Errorf("unrecognized log format flag: %v", f) - } - - part := FlagPart{Flag: flag, Name: parts[0]} - switch len(parts) { - case 2: - if flag == FmtFlagTime || flag == FmtFlagUTCTime || - flag == FmtFlagCustom { - part.Format = parts[1] - } else { - part.Format = "%" + parts[1] + "v" - } - default: - part.Format = defaultFormat - if flag == FmtFlagLine { - part.Format = "L" + defaultFormat - } - } - - flagParts = append(flagParts, part) - } - - return &flagParts, nil -} +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// textFormatter +//___________________________________ -// DefaultFormatter formats the `Entry` object details as per log `pattern` +// textFormatter formats the `Entry` object details as per log `pattern` // For e.g.: // 2016-07-02 22:26:01.530 INFO formatter_test.go L29 - Yes, I would love to see -func DefaultFormatter(flags *[]FlagPart, entry *Entry, isColor bool) (*[]byte, error) { - var buf []byte - - if isColor { - buf = append(buf, levelToColor[entry.Level]...) - } +func textFormatter(flags *[]FlagPart, entry *Entry) []byte { + buf := getBuffer() + defer putBuffer(buf) for _, part := range *flags { switch part.Flag { case FmtFlagLevel: - buf = append(buf, fmt.Sprintf(part.Format, entry.Level)...) + buf.WriteString(fmt.Sprintf(part.Format, levelToLevelName[entry.Level])) case FmtFlagTime: - buf = append(buf, entry.Time.Format(part.Format)...) + buf.WriteString(entry.Time.Format(part.Format)) case FmtFlagUTCTime: - buf = append(buf, entry.Time.UTC().Format(part.Format)...) + buf.WriteString(entry.Time.UTC().Format(part.Format)) case FmtFlagLongfile, FmtFlagShortfile: if part.Flag == FmtFlagShortfile { - if slash := strings.LastIndex(entry.File, "/"); slash >= 0 { - entry.File = entry.File[slash+1:] - } + entry.File = filepath.Base(entry.File) } - buf = append(buf, fmt.Sprintf(part.Format, entry.File)...) + buf.WriteString(fmt.Sprintf(part.Format, entry.File)) case FmtFlagLine: - buf = append(buf, fmt.Sprintf(part.Format, entry.Line)...) + buf.WriteString(fmt.Sprintf(part.Format, entry.Line)) case FmtFlagMessage: - if entry.Format == nil { - buf = append(buf, fmt.Sprint(*entry.Values...)...) - } else { - buf = append(buf, fmt.Sprintf(*entry.Format, *entry.Values...)...) - } + buf.WriteString(entry.Message) case FmtFlagCustom: - buf = append(buf, part.Format...) + buf.WriteString(part.Format) } - buf = append(buf, ' ') + buf.WriteByte(' ') } - if isColor { - buf = append(buf, resetColor...) - } + buf.WriteByte('\n') + return buf.Bytes() +} - buf = append(buf, '\n') +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// jsonFormatter +//___________________________________ - return &buf, nil +func jsonFormatter(entry *Entry) ([]byte, error) { + return json.Marshal(entry) } -// unexported methods +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Unexported methods +//___________________________________ -func getFmtFlagByName(name string) FmtFlag { - if flag, ok := FmtFlags[name]; ok { - return flag +func applyFormatter(formatter string, flags *[]FlagPart, entry *Entry) []byte { + if formatter == textFmt { + return textFormatter(flags, entry) } - return FmtFlagUnknown -} + lm, err := jsonFormatter(entry) + if err == nil { + return lm + } -func isFileFlagExists(flags *[]FlagPart) bool { - return (isFmtFlagExists(flags, FmtFlagShortfile) || - isFmtFlagExists(flags, FmtFlagLongfile)) + return []byte(err.Error()) } -func isFmtFlagExists(flags *[]FlagPart, flag FmtFlag) bool { - for _, f := range *flags { - if f.Flag == flag { - return true +// parseFlag it parses the log message formart into flag parts +// For e.g.: +// %time:2006-01-02 15:04:05.000 %level %custom:- %msg +func parseFlag(format string) (*[]FlagPart, error) { + var flagParts []FlagPart + format = strings.TrimSpace(format) + formatFlags := strings.Split(format, flagSeparator)[1:] + for _, f := range formatFlags { + parts := strings.SplitN(strings.TrimSpace(f), flagValueSeparator, 2) + flag := fmtFlagByName(parts[0]) + if flag == FmtFlagUnknown { + return nil, fmt.Errorf("unrecognized log format flag: %v", strings.TrimSpace(f)) } + + part := FlagPart{Flag: flag, Name: parts[0]} + switch len(parts) { + case 2: + if flag == FmtFlagTime || flag == FmtFlagUTCTime || + flag == FmtFlagCustom { + part.Format = parts[1] + } else { + part.Format = "%" + parts[1] + "v" + } + default: + part.Format = defaultFormat + if flag == FmtFlagLine { + part.Format = "L" + defaultFormat + } + } + + flagParts = append(flagParts, part) } - return false + return &flagParts, nil } diff --git a/log.go b/log.go index a3c9da3d..01d25ecd 100644 --- a/log.go +++ b/log.go @@ -10,6 +10,8 @@ // That logger writes to standard error and prints log `Entry` details // as per `DefaultPattern`. // +// Note: aah log package is drop-in replacement for standard go logger with features. +// // log.Info("Welcome ", "to ", "aah ", "logger") // log.Infof("%v, %v, & %v", "simple", "flexible", "powerful logger") // @@ -22,13 +24,11 @@ import ( "errors" "fmt" "os" - "runtime" "strings" "sync" "time" - "aahframework.org/config.v0-unstable" - "aahframework.org/essentials.v0-unstable" + "aahframework.org/config.v0" ) // Level type definition @@ -87,245 +87,267 @@ var ( "custom": FmtFlagCustom, } - // DefaultPattern is default log entry pattern in aah/log + // DefaultPattern is default log entry pattern in aah/log. Only applicable to + // text formatter. // For e.g: - // 2006-01-02 15:04:05.000 INFO - This is my message - DefaultPattern = "%time:2006-01-02 15:04:05.000 %level:-5 %custom:- %message" + // 2006-01-02 15:04:05.000 INFO This is my message + DefaultPattern = "%time:2006-01-02 15:04:05.000 %level:-5 %message" // BackupTimeFormat is used for timestamp with filename on rotation BackupTimeFormat = "2006-01-02-15-04-05.000" - // ErrFormatStringEmpty returned when log format parameter is empty - ErrFormatStringEmpty = errors.New("log format string is empty") - - // ErrWriterIsClosed returned when log writer is closed - ErrWriterIsClosed = errors.New("log writer is closed") + // ErrLogReceiverIsNil returned when suppiled receiver is nil. + ErrLogReceiverIsNil = errors.New("log: receiver is nil") flagSeparator = "%" flagValueSeparator = ":" defaultFormat = "%v" filePermission = os.FileMode(0755) +) - levelNameToLevel = map[string]Level{ - "FATAL": levelFatal, - "PANIC": levelPanic, - "ERROR": LevelError, - "WARN": LevelWarn, - "INFO": LevelInfo, - "DEBUG": LevelDebug, - "TRACE": LevelTrace, +type ( + // Receiver is the interface for pluggable log receiver. + // For e.g: Console, File, HTTP, etc + Receiver interface { + Init(cfg *config.Config) error + SetPattern(pattern string) error + IsCallerInfo() bool + Log(e *Entry) } - levelToLevelName = map[Level]string{ - levelFatal: "FATAL", - levelPanic: "PANIC", - LevelError: "ERROR", - LevelWarn: "WARN", - LevelInfo: "INFO", - LevelDebug: "DEBUG", - LevelTrace: "TRACE", + // Logger is the object which logs the given message into recevier as per deifned + // format flags. Logger can be used simultaneously from multiple goroutines; + // it guarantees to serialize access to the Receivers. + Logger struct { + cfg *config.Config + m *sync.Mutex + level Level + receiver Receiver + ec chan *Entry } +) - // ANSI color codes - resetColor = []byte("\033[0m") - levelToColor = [][]byte{ - levelFatal: []byte("\033[0;31m"), // red - levelPanic: []byte("\033[0;31m"), // red - LevelError: []byte("\033[0;31m"), // red - LevelWarn: []byte("\033[0;33m"), // yellow - LevelInfo: []byte("\033[0;37m"), // white - LevelDebug: []byte("\033[0;34m"), // blue - LevelTrace: []byte("\033[0;35m"), // magenta (purple) +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Global methods +//___________________________________ + +// New method creates the aah logger based on supplied `config.Config`. +func New(cfg *config.Config) (*Logger, error) { + if cfg == nil { + return nil, errors.New("log: config is nil") } - _ Logger = &Receiver{} -) + logger := &Logger{m: &sync.Mutex{}, cfg: cfg} -// Entry represents a log entry and contains the timestamp when the entry -// was created, level, etc. -type Entry struct { - Level Level - Time time.Time - Format *string - Values *[]interface{} - File string - Line int -} + // Receiver + receiverType := strings.ToUpper(cfg.StringDefault("log.receiver", "CONSOLE")) + if err := logger.SetReceiver(getReceiverByName(receiverType)); err != nil { + return nil, err + } -// Logger is interface for `aah/log` package -type Logger interface { - // Output writes the entry data into receiver - Output(level Level, calldepth int, format *string, v ...interface{}) error + // Pattern + if err := logger.SetPattern(cfg.StringDefault("log.pattern", DefaultPattern)); err != nil { + return nil, err + } - // Close closes the log writer. It cannot be used after this operation - Close() + // Level + if err := logger.SetLevel(cfg.StringDefault("log.level", "DEBUG")); err != nil { + return nil, err + } - // Closed returns true if the logger was previously closed - Closed() bool + logger.ec = make(chan *Entry, 1000) // TODO make it configurable + go logger.listenToEntry() - // Stats returns current logger statistics like number of lines written, - // number of bytes written, etc. - Stats() *ReceiverStats + return logger, nil +} - // SetPattern sets the log entry format - SetPattern(pattern string) error +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Logger methods +//___________________________________ - // SetLevel allows to set log level dynamically - SetLevel(level Level) +// Level method returns currently enabled logging level. +func (l *Logger) Level() string { + return levelToLevelName[l.level] +} - Fatal(v ...interface{}) - Fatalf(format string, v ...interface{}) +// SetLevel method sets the given logging level for the logger. +// For e.g.: INFO, WARN, DEBUG, etc. Case-insensitive. +func (l *Logger) SetLevel(level string) error { + l.m.Lock() + defer l.m.Unlock() + levelFlag := levelByName(level) + if levelFlag == LevelUnknown { + return fmt.Errorf("log: unknown log level '%s'", level) + } + l.level = levelFlag + return nil +} - Panic(v ...interface{}) - Panicf(format string, v ...interface{}) +// SetPattern methods sets the log format pattern. +func (l *Logger) SetPattern(pattern string) error { + if l.receiver == nil { + return ErrLogReceiverIsNil + } - Error(v ...interface{}) - Errorf(format string, v ...interface{}) + l.m.Lock() + defer l.m.Unlock() + return l.receiver.SetPattern(pattern) +} - Warn(v ...interface{}) - Warnf(format string, v ...interface{}) +// SetReceiver sets the given receiver into logger instance. +func (l *Logger) SetReceiver(receiver Receiver) error { + if receiver == nil { + return ErrLogReceiverIsNil + } + l.m.Lock() + defer l.m.Unlock() - Info(v ...interface{}) - Infof(format string, v ...interface{}) + l.receiver = receiver + return l.receiver.Init(l.cfg) +} - Debug(v ...interface{}) - Debugf(format string, v ...interface{}) +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Logger methods +//_______________________________________ - Trace(v ...interface{}) - Tracef(format string, v ...interface{}) +// Error logs message as `ERROR`. Arguments handled in the mananer of `fmt.Print`. +func (l *Logger) Error(v ...interface{}) { + l.output(LevelError, 3, nil, v...) } -// New creates the aah logger based on supplied config string -func New(configStr string) (Logger, error) { - if ess.IsStrEmpty(configStr) { - return nil, errors.New("logger config is empty") - } - - cfg, err := config.ParseString(configStr) - if err != nil { - return nil, err - } +// Errorf logs message as `ERROR`. Arguments handled in the mananer of `fmt.Printf`. +func (l *Logger) Errorf(format string, v ...interface{}) { + l.output(LevelError, 3, &format, v...) +} - return Newc(cfg) +// Warn logs message as `WARN`. Arguments handled in the mananer of `fmt.Print`. +func (l *Logger) Warn(v ...interface{}) { + l.output(LevelWarn, 3, nil, v...) } -// Newc creates the aah logger based on supplied `config.Config` -func Newc(cfg *config.Config) (Logger, error) { - if cfg == nil { - return nil, errors.New("logger config is nil") - } +// Warnf logs message as `WARN`. Arguments handled in the mananer of `fmt.Printf`. +func (l *Logger) Warnf(format string, v ...interface{}) { + l.output(LevelWarn, 3, &format, v...) +} - receiverType := strings.ToUpper(cfg.StringDefault("receiver", "CONSOLE")) +// Info logs message as `INFO`. Arguments handled in the mananer of `fmt.Print`. +func (l *Logger) Info(v ...interface{}) { + l.output(LevelInfo, 3, nil, v...) +} - levelName := cfg.StringDefault("level", "DEBUG") - level := levelByName(levelName) - if level == LevelUnknown { - return nil, fmt.Errorf("unrecognized log level: %v", levelName) - } +// Infof logs message as `INFO`. Arguments handled in the mananer of `fmt.Printf`. +func (l *Logger) Infof(format string, v ...interface{}) { + l.output(LevelInfo, 3, &format, v...) +} - pattern := cfg.StringDefault("pattern", DefaultPattern) - flags, err := parseFlag(pattern) - if err != nil { - return nil, err - } +// Debug logs message as `DEBUG`. Arguments handled in the mananer of `fmt.Print`. +func (l *Logger) Debug(v ...interface{}) { + l.output(LevelDebug, 3, nil, v...) +} - var alogger interface{} - switch receiverType { - case "CONSOLE": - alogger, err = newConsoleReceiver(cfg, receiverType, level, flags) - case "FILE": - alogger, err = newFileReceiver(cfg, receiverType, level, flags) - default: - return nil, errors.New("unsupported receiver") - } +// Debugf logs message as `DEBUG`. Arguments handled in the mananer of `fmt.Printf`. +func (l *Logger) Debugf(format string, v ...interface{}) { + l.output(LevelDebug, 3, &format, v...) +} - if err != nil { - return nil, err - } else if logger, ok := alogger.(Logger); ok { - return logger, nil - } +// Trace logs message as `TRACE`. Arguments handled in the mananer of `fmt.Print`. +func (l *Logger) Trace(v ...interface{}) { + l.output(LevelTrace, 3, nil, v...) +} - return nil, errors.New("unable to create logger") +// Tracef logs message as `TRACE`. Arguments handled in the mananer of `fmt.Printf`. +func (l *Logger) Tracef(format string, v ...interface{}) { + l.output(LevelTrace, 3, &format, v...) } -func (level Level) String() string { - if name, ok := levelToLevelName[level]; ok { - return name - } +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Logger methods - Drop-in replacement +// for Go standard logger +//_______________________________________ - return "Unknown" +// Print logs message as `INFO`. Arguments handled in the mananer of `fmt.Print`. +func (l *Logger) Print(v ...interface{}) { + l.output(LevelInfo, 3, nil, v...) } -// unexported methods +// Printf logs message as `INFO`. Arguments handled in the mananer of `fmt.Printf`. +func (l *Logger) Printf(format string, v ...interface{}) { + l.output(LevelInfo, 3, &format, v...) +} -func levelByName(name string) Level { - if level, ok := levelNameToLevel[strings.ToUpper(name)]; ok { - return level - } +// Println logs message as `INFO`. Arguments handled in the mananer of `fmt.Printf`. +func (l *Logger) Println(format string, v ...interface{}) { + l.output(LevelInfo, 3, &format, v...) +} - return LevelUnknown +// Fatal logs message as `FATAL` and call to os.Exit(1). +func (l *Logger) Fatal(v ...interface{}) { + l.output(levelFatal, 3, nil, v...) + os.Exit(1) } -func fetchCallerInfo(calldepth int) (string, int) { - _, file, line, ok := runtime.Caller(calldepth + 1) - if !ok { - file = "???" - line = 0 - } +// Fatalf logs message as `FATAL` and call to os.Exit(1). +func (l *Logger) Fatalf(format string, v ...interface{}) { + l.output(levelFatal, 3, &format, v...) + os.Exit(1) +} - return file, line +// Fatalln logs message as `FATAL` and call to os.Exit(1). +func (l *Logger) Fatalln(format string, v ...interface{}) { + l.output(levelFatal, 3, &format, v...) + os.Exit(1) } -func newConsoleReceiver(cfg *config.Config, receiverType string, level Level, flags *[]FlagPart) (*Receiver, error) { - receiver := Receiver{ - Config: cfg, - Type: receiverType, - Flags: flags, - Format: DefaultFormatter, - m: sync.Mutex{}, - level: level, - out: os.Stderr, - stats: &ReceiverStats{}, - isFileInfo: isFileFlagExists(flags), - isLineInfo: isFmtFlagExists(flags, FmtFlagLine), - isColor: runtime.GOOS != "windows", - } +// Panic logs message as `PANIC` and call to panic(). +func (l *Logger) Panic(v ...interface{}) { + l.output(levelPanic, 3, nil, v...) + panic("") +} - return &receiver, nil +// Panicf logs message as `PANIC` and call to panic(). +func (l *Logger) Panicf(format string, v ...interface{}) { + l.output(levelPanic, 3, &format, v...) + panic(fmt.Sprintf(format, v...)) } -func newFileReceiver(cfg *config.Config, receiverType string, level Level, flags *[]FlagPart) (*Receiver, error) { - maxSize := cfg.IntDefault("rotate.size", 100) - if maxSize > 2048 { // maximum 2GB file size - return nil, errors.New("max size > 2GB, please set it to 2048 for size rotation") - } +// Panicln logs message as `PANIC` and call to panic(). +func (l *Logger) Panicln(format string, v ...interface{}) { + l.output(levelPanic, 3, &format, v...) + panic(fmt.Sprintf(format, v...)) +} - receiver := Receiver{ - Config: cfg, - Type: receiverType, - Flags: flags, - Format: DefaultFormatter, - level: level, - stats: &ReceiverStats{}, - isFileInfo: isFileFlagExists(flags), - isLineInfo: isFmtFlagExists(flags, FmtFlagLine), - isUTC: isFmtFlagExists(flags, FmtFlagUTCTime), +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Unexported methods +//___________________________________ + +// output method checks the level, formats the arguments and call to configured +// Log receivers. +func (l *Logger) output(level Level, calldepth int, format *string, v ...interface{}) { + if level > l.level { + return } - err := receiver.openFile() - if err != nil { - return nil, err + entry := getEntry() + entry.Time = time.Now() + entry.Level = level + if format == nil { + entry.Message = fmt.Sprint(v...) + } else { + entry.Message = fmt.Sprintf(*format, v...) } - receiver.rotate = cfg.StringDefault("rotate.mode", "daily") - switch receiver.rotate { - case "daily": - receiver.setOpenDay() - case "lines": - receiver.maxLines = int64(cfg.IntDefault("rotate.lines", 0)) - case "size": - receiver.maxSize = int64(maxSize * 1024 * 1024) + if l.receiver.IsCallerInfo() { + entry.File, entry.Line = fetchCallerInfo(calldepth) } - return &receiver, nil + l.ec <- entry +} + +// listenToEntry method listens to entry channel and log the entry into receiver. +func (l Logger) listenToEntry() { + for { + entry := <-l.ec + l.receiver.Log(entry) + putEntry(entry) + } } diff --git a/log_test.go b/log_test.go index aeee9fcc..649f924a 100644 --- a/log_test.go +++ b/log_test.go @@ -7,328 +7,97 @@ package log import ( "os" "path/filepath" - "strings" "testing" + "time" - "aahframework.org/test.v0-unstable/assert" + "aahframework.org/config.v0" + "aahframework.org/test.v0/assert" ) -func TestNewCustomUTCConsoleReceiver(t *testing.T) { - config := ` -# console logger configuration -# "CONSOLE" uppercasse works too -receiver = "console" - -# "debug" lowercase works too and if not supplied then defaults to DEBUG -level = "debug" - -# if not suppiled then default pattern is used -pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %line %shortfile:-25 %custom:- %message" - ` - logger, err := New(config) - assert.FailNowOnError(t, err, "unexpected error") - - logger.Trace("I shoudn't see this msg, because standard logger level is DEBUG") - logger.Debug("I would like to see this message, debug is useful for dev") - logger.Info("Yes, I would love to") - logger.Warn("Yes, yes it's an warning") - logger.Error("Yes, yes, yes - finally an error") - - stats := logger.Stats() - t.Logf("First round: %#v\n", stats) - assert.Equal(t, int64(433), stats.bytes) - - logger.Tracef("I shoudn't see this msg, because standard logger level is DEBUG: %v", 4) - logger.Debugf("I would like to see this message, debug is useful for dev: %v", 3) - logger.Infof("Yes, I would love to: %v", 2) - logger.Warnf("Yes, yes it's an warning: %v", 1) - logger.Errorf("Yes, yes, yes - finally an error: %v", 0) - - stats = logger.Stats() - t.Logf("Second round: %#v\n", stats) - assert.Equal(t, int64(878), stats.bytes) - - Tracef("I shoudn't see this msg: %v", 46583) - Debugf("I would like to see this message, debug is useful for dev: %v", 334545) - - stats = logger.Stats() - t.Logf("Third round: %#v\n", stats) - assert.Equal(t, int64(878), stats.bytes) -} - -func TestNewCustomConsoleReceiver(t *testing.T) { - config := ` -# console logger configuration -# "CONSOLE" uppercasse works too -receiver = "CONSOLE" - ` - logger, err := New(config) - assert.FailNowOnError(t, err, "unexpected error") - - logger.Trace("I shoudn't see this msg, because standard logger level is DEBUG") - logger.Debug("I would like to see this message, debug is useful for dev") - logger.Info("Yes, I would love to") - logger.Warn("Yes, yes it's an warning") - logger.Error("Yes, yes, yes - finally an error") - - stats := logger.Stats() - t.Logf("First round: %#v\n", stats) - assert.Equal(t, int64(313), stats.bytes) - - logger.Tracef("I shoudn't see this msg, because standard logger level is DEBUG: %v", 4) - logger.Debugf("I would like to see this message, debug is useful for dev: %v", 3) - logger.Infof("Yes, I would love to: %v", 2) - logger.Warnf("Yes, yes it's an warning: %v", 1) - logger.Errorf("Yes, yes, yes - finally an error: %v", 0) - - stats = logger.Stats() - t.Logf("Second round: %#v\n", stats) - assert.Equal(t, int64(638), stats.bytes) - - Tracef("I shoudn't see this msg: %v", 46583) - Debugf("I would like to see this message, debug is useful for dev: %v", 334545) - - stats = logger.Stats() - t.Logf("Third round: %#v\n", stats) - assert.Equal(t, int64(638), stats.bytes) -} - -func TestNewCustomFileReceiverDailyRotation(t *testing.T) { - defer cleaupFiles("*.log") - - fileLoggerConfig := ` -# file logger configuration -# "FILE" uppercasse works too -receiver = "file" - -# "debug" lowercase works too and if not supplied then defaults to DEBUG -level = "info" - -# if not suppiled then default pattern is used -pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message" - -file = "daily-aah-filename.log" - -rotate { - mode = "daily" -} - ` - - logger, err := New(fileLoggerConfig) - assert.FailNowOnError(t, err, "unexpected error") - - for i := 0; i < 25; i++ { - logger.Trace("I shoudn't see this msg, because standard logger level is DEBUG") - logger.Debug("I would like to see this message, debug is useful for dev") - logger.Info("Yes, I would love to") - logger.Warn("Yes, yes it's an warning") - logger.Error("Yes, yes, yes - finally an error") - } - - _ = logger.SetPattern("%time:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message") - for i := 0; i < 25; i++ { - logger.Tracef("I shoudn't see this msg, because standard logger level is DEBUG: %v", 4) - logger.Debugf("I would like to see this message, debug is useful for dev: %v", 3) - logger.Infof("Yes, I would love to: %v", 2) - logger.Warnf("Yes, yes it's an warning: %v", 1) - logger.Errorf("Yes, yes, yes - finally an error: %v", 0) - } - - // Close scenario - logger.Close() - assert.Equal(t, true, logger.Closed()) - - logger.Info("This won't be written to file") - - // once again - logger.Close() -} - -func TestNewCustomFileReceiverLinesRotation(t *testing.T) { - defer cleaupFiles("*.log") - - fileLoggerConfig := ` -# file logger configuration -# "FILE" uppercasse works too -receiver = "file" - -# "debug" lowercase works too and if not supplied then defaults to DEBUG -level = "trace" - -# if not suppiled then default pattern is used -pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %shortfile %line %custom:- %message" - -file = "test-aah-filename.log" - -rotate { - mode = "lines" - - # this value is needed if rotate.mode="lines"; default is unlimited - lines = 20 - - # this value is needed in MB if rotate.mode="size"; default is 100 MB - #size = 250 -} - ` - - logger, err := New(fileLoggerConfig) - assert.FailNowOnError(t, err, "unexpected error") - - for i := 0; i < 25; i++ { - logger.Trace("I shoudn't see this msg, because standard logger level is DEBUG") - logger.Debug("I would like to see this message, debug is useful for dev") - logger.Info("Yes, I would love to") - logger.Warn("Yes, yes it's an warning") - logger.Error("Yes, yes, yes - finally an error") - - logger.Tracef("I shoudn't see this msg, because standard logger level is DEBUG: %v", 4) - logger.Debugf("I would like to see this message, debug is useful for dev: %v", 3) - logger.Infof("Yes, I would love to: %v", 2) - logger.Warnf("Yes, yes it's an warning: %v", 1) - logger.Errorf("Yes, yes, yes - finally an error: %v", 0) - } -} - -func TestNewCustomFileReceiverSizeRotation(t *testing.T) { - defer cleaupFiles("*.log") - - fileLoggerConfig := ` -# file logger configuration -# "FILE" uppercasse works too -receiver = "file" - -# if not suppiled then default pattern is used -pattern = "%utctime:2006-01-02 15:04:05.000 %level:-5 %longfile %line %custom:- %message" -rotate { - mode = "size" - - # this value is needed in MB if rotate="size"; default is unlimited - size = 1 +func TestLogDefault(t *testing.T) { + configStr := ` + log { + # default config + } + ` + cfg, _ := config.ParseString(configStr) + logger, err := New(cfg) + assert.NotNil(t, logger) + assert.Nil(t, err) + assert.Equal(t, "DEBUG", logger.Level()) + + logger.Print("This is print") + logger.Printf("This is print - %s", "yes") + logger.Println("This is print - %s", "yes") + + testPanic(logger, "panic", "this is panic") + testPanic(logger, "panicf", "this is panicf") + testPanic(logger, "panicln", "this is panicln") + time.Sleep(1 * time.Millisecond) } - ` - - logger, err := New(fileLoggerConfig) - assert.FailNowOnError(t, err, "unexpected error") - - // Size based rotation, dump more value into receiver - for i := 0; i < 5000; i++ { - logger.Trace("I shoudn't see this msg, because standard logger level is DEBUG") - logger.Debug("I would like to see this message, debug is useful for dev") - logger.Info("Yes, I would love to, Yes, I would love to, Yes, I would love to, Yes, I would love to") - logger.Warn("Yes, yes it's an warning, Yes, yes it's an warning,Yes, yes it's an warning, Yes, yes it's an warning") - logger.Error("Yes, yes, yes - finally an error") - - logger.Tracef("I shoudn't see this msg, because standard logger level is DEBUG: %v", 4) - logger.Debugf("I would like to see this message, debug is useful for dev: %v, %d", 3, 333) - logger.Infof("Yes, I would love to: %v, Yes, I would love to: %v", 2, 22, 222) - logger.Warnf("Yes, yes it's an warning: %v, Yes, yes it's an warning: %v, Yes, yes it's an warning: %v", 1, 11, 111) - logger.Errorf("Yes, yes, yes - finally an error: %v, finally an error: %v, finally an error: %v", 0, 000, 0000) - } -} - -func TestUnknownFormatFlag(t *testing.T) { - _, err := parseFlag("") - assert.Equal(t, ErrFormatStringEmpty, err) - _, err = parseFlag("%time:2006-01-02 15:04:05.000 %level:-5 %longfile %unknown %custom:- %message") - if !strings.Contains(err.Error(), "unrecognized log format flag") { - t.Errorf("Unexpected error: %v", err) - t.FailNow() - } -} - -func TestNewMisc(t *testing.T) { - _, err := New("") - assert.Equal(t, "logger config is empty", err.Error()) - - _, err = New(`receiver = "file" level="info"`) - if !strings.HasPrefix(err.Error(), "syntax error") { - t.Errorf("Unexpected error: %v", err) - } - - _, err = New(`receiver = "file"; level="unknown";`) - if !strings.HasPrefix(err.Error(), "unrecognized log level") { - t.Errorf("Unexpected error: %v", err) - } - - _, err = New(`receiver = "remote"; level="debug";`) - if !strings.HasPrefix(err.Error(), "unsupported receiver") { - t.Errorf("Unexpected error: %v", err) - t.FailNow() - } - - _, err = New(`receiver = "file"; level="debug"; rotate { mode="size"; size=2500; }`) - if !strings.HasPrefix(err.Error(), "max size > 2GB") { - t.Errorf("Unexpected error: %v", err) - } - - _, err = New(`receiver = "console"; level="debug"; pattern="%time:2006-01-02 15:04:05.000 %level:-5 %unknown %message";`) - if !strings.HasPrefix(err.Error(), "unrecognized log format flag") { - t.Errorf("Unexpected error: %v", err) - } -} - -func TestLevelUnknown(t *testing.T) { - var level Level - assert.Equal(t, "FATAL", level.String()) - - level = 9 // Unknown log level - assert.Equal(t, "Unknown", level.String()) -} - -func TestStats(t *testing.T) { - stats := ReceiverStats{ +func TestMisc(t *testing.T) { + stats := receiverStats{ lines: 200, bytes: 764736, } assert.Equal(t, int64(764736), stats.Bytes()) assert.Equal(t, int64(200), stats.Lines()) -} - -func TestPanicCustomConsoleReceiver(t *testing.T) { - config := ` -# console logger configuration -# "CONSOLE" uppercasse works too -receiver = "CONSOLE" - -level = "panic" - ` - defer func() { - if r := recover(); r != nil { - _ = r - } - }() - - logger, err := New(config) - assert.FailNowOnError(t, err, "unexpected error") - logger.Panic("This is panic message") + configStr := ` + log { + # default config + } + ` + cfg, _ := config.ParseString(configStr) + logger, err := New(cfg) + assert.NotNil(t, logger) + assert.Nil(t, err) + assert.Equal(t, "DEBUG", logger.Level()) + + err = logger.SetReceiver(nil) + assert.Equal(t, "log: receiver is nil", err.Error()) + + err = logger.SetLevel("MYLEVEL") + assert.Equal(t, "log: unknown log level 'MYLEVEL'", err.Error()) + + logger, err = New(nil) + assert.Nil(t, logger) + assert.NotNil(t, err) + assert.Equal(t, "log: config is nil", err.Error()) + + // Discard + discard := DiscardReceiver{} + _ = discard.Init(nil) + discard.Log(&Entry{}) + _ = discard.SetPattern("nothing") + assert.False(t, discard.IsCallerInfo()) } -func TestPanicfCustomConsoleReceiver(t *testing.T) { - config := ` -# console logger configuration -# "CONSOLE" uppercasse works too -receiver = "CONSOLE" - -level = "panic" - ` +func testPanic(logger *Logger, method, msg string) { defer func() { if r := recover(); r != nil { _ = r } }() - logger, err := New(config) - assert.FailNowOnError(t, err, "unexpected error") - - logger.Panicf("This is panic %v", "message from param") + if method == "panic" { + logger.Panic(msg) + } else if method == "panicf" { + logger.Panicf("%s", msg) + } else if method == "panicln" { + logger.Panicln("%s", msg) + } } -func cleaupFiles(match string) { +func getPwd() string { pwd, _ := os.Getwd() + return pwd +} +func cleaupFiles(match string) { + pwd := getPwd() dir, err := os.Open(pwd) if err != nil { return diff --git a/receiver.go b/receiver.go deleted file mode 100644 index 02b06220..00000000 --- a/receiver.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) -// go-aah/log source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package log - -import ( - "fmt" - "io" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "aahframework.org/config.v0-unstable" - "aahframework.org/essentials.v0-unstable" -) - -// Receiver represents aah logger object that statisfy console, file, logging -// and Logging Stats. Each logging operation makes a single call to the -// Writer's Write method. `Receiver` guarantees serialize access and -// it can be used from multiple goroutines like Go standard logger. -type Receiver struct { - Config *config.Config - Type string - Flags *[]FlagPart - Format FormatterFunc - - m sync.Mutex - level Level - out io.Writer - stats *ReceiverStats - isFileInfo bool - isLineInfo bool - isUTC bool - - // Console Receiver - isColor bool - - // File Receiver - isClosed bool - rotate string - openDay int - maxSize int64 - maxLines int64 -} - -// Output formats the give log inputs, caller info and writes to console -func (r *Receiver) Output(level Level, calldepth int, format *string, v ...interface{}) error { - if level > r.level { - return nil - } - - now := time.Now() // get this early - - if r.Closed() { - return ErrWriterIsClosed - } - - var ( - file string - line int - ) - if r.isFileInfo || r.isLineInfo { - file, line = fetchCallerInfo(calldepth) - } - - r.m.Lock() - defer r.m.Unlock() - - // Check log rotation is required - if r.isRotateRequired() { - if err := r.rotateFile(); err != nil { - return err - } - } - - entry := &Entry{ - Level: level, - Time: now, - Format: format, - Values: &v, - File: file, - Line: line, - } - - // format the log entry message as per pattern - buf, err := r.Format(r.Flags, entry, r.isColor) - if err != nil { - return err - } - - // writes bytes into writer - size, err := r.out.Write(*buf) - if err != nil { - return err - } - - // calculate receiver stats - r.stats.bytes += int64(size) - r.stats.lines++ - - return nil -} - -// Stats returns current logger statistics like number of lines written, -// number of bytes written, etc. -func (r *Receiver) Stats() *ReceiverStats { - return r.stats -} - -// Close closes the log writer. It cannot be used after this operation -func (r *Receiver) Close() { - if r.isClosed { - return - } - - if out, ok := r.out.(io.Closer); ok { - r.isClosed = true - _ = out.Close() - } -} - -// Closed returns true if the logger was previously closed -func (r *Receiver) Closed() bool { - return r.isClosed -} - -// SetPattern sets the pattern to log entry format -func (r *Receiver) SetPattern(pattern string) error { - r.m.Lock() - defer r.m.Unlock() - flags, err := parseFlag(pattern) - if err != nil { - return err - } - r.Flags = flags - r.isFileInfo = isFileFlagExists(flags) - r.isLineInfo = isFmtFlagExists(flags, FmtFlagLine) - r.isUTC = isFmtFlagExists(flags, FmtFlagUTCTime) - - return nil -} - -// SetLevel allows to set log level dynamically -func (r *Receiver) SetLevel(level Level) { - r.m.Lock() - defer r.m.Unlock() - r.level = level -} - -func (r *Receiver) isFileReceiver() bool { - return r.Type == "FILE" -} - -// Fatal logs message as `FATAL` and calls os.Exit(1) -func (r *Receiver) Fatal(v ...interface{}) { - _ = r.Output(levelFatal, 2, nil, v...) - os.Exit(1) -} - -// Fatalf logs message as `FATAL` and calls os.Exit(1) -func (r *Receiver) Fatalf(format string, v ...interface{}) { - _ = r.Output(levelFatal, 2, &format, v...) - os.Exit(1) -} - -// Panic logs message as `PANIC` and calls panic() -func (r *Receiver) Panic(v ...interface{}) { - _ = r.Output(levelPanic, 2, nil, v...) - panic("") -} - -// Panicf logs message as `PANIC` and calls panic() -func (r *Receiver) Panicf(format string, v ...interface{}) { - _ = r.Output(levelPanic, 2, &format, v...) - panic(fmt.Sprintf(format, v...)) -} - -// Error logs message as `LevelError` -func (r *Receiver) Error(v ...interface{}) { - _ = r.Output(LevelError, 2, nil, v...) -} - -// Errorf logs message as `LevelError` -func (r *Receiver) Errorf(format string, v ...interface{}) { - _ = r.Output(LevelError, 2, &format, v...) -} - -// Warn logs message as `LevelWarn` -func (r *Receiver) Warn(v ...interface{}) { - _ = r.Output(LevelWarn, 2, nil, v...) -} - -// Warnf logs message as `LevelWarn` -func (r *Receiver) Warnf(format string, v ...interface{}) { - _ = r.Output(LevelWarn, 2, &format, v...) -} - -// Info logs message as `LevelInfo` -func (r *Receiver) Info(v ...interface{}) { - _ = r.Output(LevelInfo, 2, nil, v...) -} - -// Infof logs message as `LevelInfo` -func (r *Receiver) Infof(format string, v ...interface{}) { - _ = r.Output(LevelInfo, 2, &format, v...) -} - -// Debug logs message as `LevelDebug` -func (r *Receiver) Debug(v ...interface{}) { - _ = r.Output(LevelDebug, 2, nil, v...) -} - -// Debugf logs message as `LevelDebug` -func (r *Receiver) Debugf(format string, v ...interface{}) { - _ = r.Output(LevelDebug, 2, &format, v...) -} - -// Trace logs message as `LevelTrace` -func (r *Receiver) Trace(v ...interface{}) { - _ = r.Output(LevelTrace, 2, nil, v...) -} - -// Tracef logs message as `LevelTrace` -func (r *Receiver) Tracef(format string, v ...interface{}) { - _ = r.Output(LevelTrace, 2, &format, v...) -} - -// unexported methods - -func (r *Receiver) openFile() error { - if !r.isFileReceiver() { - return nil - } - - name := r.fileName() - dir := filepath.Dir(name) - _ = ess.MkDirAll(dir, 0755) - - file, err := os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, filePermission) - if err != nil { - return err - } - - fileStat, err := file.Stat() - if err != nil { - return err - - } - - r.isClosed = false - r.setOpenDay() - r.stats.bytes = fileStat.Size() - r.stats.lines = int64(ess.LineCntr(file)) - r.out = file - - return nil -} - -func (r *Receiver) fileName() string { - return r.Config.StringDefault("file", "aah-log-file.log") -} - -func (r *Receiver) backupFileName() string { - name := r.fileName() - dir := filepath.Dir(name) - fileName := filepath.Base(name) - ext := filepath.Ext(fileName) - baseName := strings.TrimSuffix(fileName, ext) - - t := time.Now() - if r.isUTC { - t = t.UTC() - } - - return filepath.Join(dir, fmt.Sprintf("%s-%s%s", baseName, t.Format(BackupTimeFormat), ext)) -} - -func (r *Receiver) setOpenDay() { - if r.isUTC { - r.openDay = time.Now().UTC().Day() - } else { - r.openDay = time.Now().Day() - } -} - -func (r *Receiver) isRotateRequired() bool { - if !r.isFileReceiver() { - return false - } - - switch r.rotate { - case "daily": - if r.isUTC { - return time.Now().UTC().Day() != r.openDay - } - return time.Now().Day() != r.openDay - case "lines": - return r.maxLines != 0 && r.stats.lines >= r.maxLines - case "size": - return r.maxSize != 0 && r.stats.bytes >= r.maxSize - } - - return false -} - -func (r *Receiver) rotateFile() error { - if !r.isFileReceiver() { - return nil - } - - fileName := r.fileName() - if _, err := os.Lstat(fileName); err == nil { - r.Close() - if err = os.Rename(fileName, r.backupFileName()); err != nil { - return err - } - } - - if err := r.openFile(); err != nil { - return err - } - - return nil -} diff --git a/stats.go b/stats.go index c4858ce9..520136d9 100644 --- a/stats.go +++ b/stats.go @@ -4,18 +4,18 @@ package log -// ReceiverStats tracks the number of output lines and bytes written. -type ReceiverStats struct { +// receiverStats tracks the number of output lines and bytes written. +type receiverStats struct { lines int64 bytes int64 } // Lines returns the number of lines written. -func (s *ReceiverStats) Lines() int64 { +func (s *receiverStats) Lines() int64 { return s.lines } // Bytes returns the number of bytes written. -func (s *ReceiverStats) Bytes() int64 { +func (s *receiverStats) Bytes() int64 { return s.bytes } diff --git a/util.go b/util.go new file mode 100644 index 00000000..c26b8040 --- /dev/null +++ b/util.go @@ -0,0 +1,94 @@ +// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) +// go-aah/log source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package log + +import ( + "runtime" + "strings" + "time" +) + +var ( + levelNameToLevel = map[string]Level{ + "FATAL": levelFatal, + "PANIC": levelPanic, + "ERROR": LevelError, + "WARN": LevelWarn, + "INFO": LevelInfo, + "DEBUG": LevelDebug, + "TRACE": LevelTrace, + } + + levelToLevelName = map[Level]string{ + levelFatal: "FATAL", + levelPanic: "PANIC", + LevelError: "ERROR", + LevelWarn: "WARN", + LevelInfo: "INFO", + LevelDebug: "DEBUG", + LevelTrace: "TRACE", + } +) + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// Unexported methods +//___________________________________ + +func levelByName(name string) Level { + if level, ok := levelNameToLevel[strings.ToUpper(name)]; ok { + return level + } + + return LevelUnknown +} + +func fmtFlagByName(name string) FmtFlag { + if flag, ok := FmtFlags[name]; ok { + return flag + } + + return FmtFlagUnknown +} + +func isFmtFlagExists(flags *[]FlagPart, flag FmtFlag) bool { + for _, f := range *flags { + if f.Flag == flag { + return true + } + } + return false +} + +func fetchCallerInfo(calldepth int) (string, int) { + _, file, line, ok := runtime.Caller(calldepth) + if !ok { + file = "???" + line = 0 + } + return file, line +} + +// isCallerInfo method to identify to fetch caller or not. +func isCallerInfo(flags *[]FlagPart) bool { + return (isFmtFlagExists(flags, FmtFlagShortfile) || + isFmtFlagExists(flags, FmtFlagLongfile) || + isFmtFlagExists(flags, FmtFlagLine)) +} + +func getReceiverByName(name string) Receiver { + if name == "FILE" { + return &FileReceiver{} + } else if name == "CONSOLE" { + return &ConsoleReceiver{} + } + return nil +} + +func formatTime(t time.Time) string { + if t.IsZero() { + return "" + } + return t.Format(time.RFC3339) +} From 7e82610a4505ba8e628851576db4f3fe78277437 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Tue, 16 May 2017 01:06:12 -0700 Subject: [PATCH 7/9] godoc update and improvements --- console_receiver.go | 6 ++++++ default.go | 15 +++++++++++++++ file_receiver.go | 6 ++++++ formatter.go | 8 ++------ log.go | 31 +++++++++++++++---------------- util.go | 5 +++-- 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/console_receiver.go b/console_receiver.go index 022820bb..e84524e7 100644 --- a/console_receiver.go +++ b/console_receiver.go @@ -9,6 +9,7 @@ import ( "io" "os" "runtime" + "sync" "aahframework.org/config.v0" ) @@ -32,6 +33,7 @@ var ( // ConsoleReceiver writes the log entry into os.Stderr. // For non-windows it writes with color. type ConsoleReceiver struct { + rw *sync.RWMutex out io.Writer formatter string flags *[]FlagPart @@ -58,6 +60,8 @@ func (c *ConsoleReceiver) Init(cfg *config.Config) error { // SetPattern method initializes the logger format pattern. func (c *ConsoleReceiver) SetPattern(pattern string) error { + c.rw.Lock() + defer c.rw.Unlock() flags, err := parseFlag(pattern) if err != nil { return err @@ -77,6 +81,8 @@ func (c *ConsoleReceiver) IsCallerInfo() bool { // Log method writes the log entry into os.Stderr. func (c *ConsoleReceiver) Log(entry *Entry) { + c.rw.RLock() + defer c.rw.RUnlock() if c.isColor { _, _ = c.out.Write(levelToColor[entry.Level]) } diff --git a/default.go b/default.go index 7fdb6cc4..c6517d99 100644 --- a/default.go +++ b/default.go @@ -123,6 +123,21 @@ func Panicln(format string, v ...interface{}) { panic(fmt.Sprintf(format, v...)) } +// SetDefaultLogger method sets the given logger instance as default logger. +func SetDefaultLogger(l *Logger) { + std = l +} + +// SetLevel method sets log level for default logger. +func SetLevel(level string) error { + return std.SetLevel(level) +} + +// SetPattern method sets the log format pattern for default logger. +func SetPattern(pattern string) error { + return std.SetPattern(pattern) +} + func init() { cfg, _ := config.ParseString("log { }") std, _ = New(cfg) diff --git a/file_receiver.go b/file_receiver.go index 391f0e21..b0d06201 100644 --- a/file_receiver.go +++ b/file_receiver.go @@ -10,6 +10,7 @@ import ( "io" "os" "path/filepath" + "sync" "time" "aahframework.org/config.v0" @@ -20,6 +21,7 @@ var _ Receiver = &FileReceiver{} // FileReceiver writes the log entry into file. type FileReceiver struct { + rw *sync.RWMutex filename string out io.Writer formatter string @@ -74,6 +76,8 @@ func (f *FileReceiver) Init(cfg *config.Config) error { // SetPattern method initializes the logger format pattern. func (f *FileReceiver) SetPattern(pattern string) error { + f.rw.Lock() + defer f.rw.Unlock() flags, err := parseFlag(pattern) if err != nil { return err @@ -97,6 +101,8 @@ func (f *FileReceiver) IsCallerInfo() bool { // Log method logs the given entry values into file. func (f *FileReceiver) Log(entry *Entry) { + f.rw.RLock() + defer f.rw.RUnlock() if f.isRotate() { _ = f.rotateFile() } diff --git a/formatter.go b/formatter.go index dea217de..d6cd3d57 100644 --- a/formatter.go +++ b/formatter.go @@ -85,12 +85,8 @@ func applyFormatter(formatter string, flags *[]FlagPart, entry *Entry) []byte { return textFormatter(flags, entry) } - lm, err := jsonFormatter(entry) - if err == nil { - return lm - } - - return []byte(err.Error()) + lm, _ := jsonFormatter(entry) + return lm } // parseFlag it parses the log message formart into flag parts diff --git a/log.go b/log.go index 01d25ecd..5a779f57 100644 --- a/log.go +++ b/log.go @@ -2,22 +2,23 @@ // go-aah/log source code and usage is governed by a MIT style // license that can be found in the LICENSE file. -// Package log implements a simple, flexible & powerful logger. -// Currently it supports `console`, `file` (rotation by daily, size, lines), -// logging receivers and logging stats. It also has a predefined 'standard' -// Logger accessible through helper functions `Error{f}`, `Warn{f}`, `Info{f}`, -// `Debug{f}`, `Trace{f}` which are easier to use than creating a Logger manually. -// That logger writes to standard error and prints log `Entry` details -// as per `DefaultPattern`. +// Package log implements a simple, flexible, non-blocking logger. +// It supports `console`, `file` (rotation by daily, size, lines). +// It also has a predefined 'standard' Logger accessible through helper +// functions `Error{f}`, `Warn{f}`, `Info{f}`, `Debug{f}`, `Trace{f}`, +// `Print{f,ln}`, `Fatal{f,ln}`, `Panic{f,ln}` which are easier to use than creating +// a Logger manually. Default logger writes to standard error and prints log +// `Entry` details as per `DefaultPattern`. // -// Note: aah log package is drop-in replacement for standard go logger with features. +// aah log package can be used as drop-in replacement for standard go logger +// with features. // // log.Info("Welcome ", "to ", "aah ", "logger") -// log.Infof("%v, %v, & %v", "simple", "flexible", "powerful logger") +// log.Infof("%v, %v, %v", "simple", "flexible", "non-blocking logger") // // // Output: -// 2016-07-03 19:22:11.504 INFO - Welcome to aah logger -// 2016-07-03 19:22:11.504 INFO - simple, flexible, & powerful logger +// 2016-07-03 19:22:11.504 INFO Welcome to aah logger +// 2016-07-03 19:22:11.504 INFO simple, flexible, non-blocking logger package log import ( @@ -188,19 +189,17 @@ func (l *Logger) SetPattern(pattern string) error { if l.receiver == nil { return ErrLogReceiverIsNil } - - l.m.Lock() - defer l.m.Unlock() return l.receiver.SetPattern(pattern) } // SetReceiver sets the given receiver into logger instance. func (l *Logger) SetReceiver(receiver Receiver) error { + l.m.Lock() + defer l.m.Unlock() + if receiver == nil { return ErrLogReceiverIsNil } - l.m.Lock() - defer l.m.Unlock() l.receiver = receiver return l.receiver.Init(l.cfg) diff --git a/util.go b/util.go index c26b8040..e95bfad6 100644 --- a/util.go +++ b/util.go @@ -7,6 +7,7 @@ package log import ( "runtime" "strings" + "sync" "time" ) @@ -79,9 +80,9 @@ func isCallerInfo(flags *[]FlagPart) bool { func getReceiverByName(name string) Receiver { if name == "FILE" { - return &FileReceiver{} + return &FileReceiver{rw: &sync.RWMutex{}} } else if name == "CONSOLE" { - return &ConsoleReceiver{} + return &ConsoleReceiver{rw: &sync.RWMutex{}} } return nil } From fc98834f9d327aaabbdb1b6dfa224388d0b6c132 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Tue, 16 May 2017 01:06:33 -0700 Subject: [PATCH 8/9] test case update --- console_receiver_test.go | 19 +++++++++++++++++++ default_test.go | 14 ++++++++++++++ file_receiver_test.go | 19 +++++++++++++++++++ log_test.go | 4 ++++ 4 files changed, 56 insertions(+) diff --git a/console_receiver_test.go b/console_receiver_test.go index b11b426f..79ccacd3 100644 --- a/console_receiver_test.go +++ b/console_receiver_test.go @@ -71,6 +71,20 @@ func TestConsoleLoggerUnknownFormatFlag(t *testing.T) { assert.Equal(t, "unrecognized log format flag: myfile", err.Error()) } +func TestConsoleLoggerUnknownLevel(t *testing.T) { + configStr := ` + log { + # default config plus + level = "MYLEVEL" + pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %message" + } + ` + cfg, _ := config.ParseString(configStr) + logger, err := New(cfg) + assert.Nil(t, logger) + assert.Equal(t, "log: unknown log level 'MYLEVEL'", err.Error()) +} + func TestConsoleLoggerDefaults(t *testing.T) { configStr := ` log { @@ -81,6 +95,11 @@ func TestConsoleLoggerDefaults(t *testing.T) { logger, err := New(cfg) assert.NotNil(t, logger) assert.Nil(t, err) + + // receiver nil scenario + logger.receiver = nil + err = logger.SetPattern("%time:2006-01-02 15:04:05.000 %level:-5 %message") + assert.Equal(t, "log: receiver is nil", err.Error()) } func testConsoleLogger(t *testing.T, cfgStr string) { diff --git a/default_test.go b/default_test.go index d84bce8c..a6c942bd 100644 --- a/default_test.go +++ b/default_test.go @@ -9,6 +9,7 @@ import ( "time" "aahframework.org/config.v0" + "aahframework.org/test.v0/assert" ) func TestDefaultLogger(t *testing.T) { @@ -45,6 +46,19 @@ func TestDefaultLogger(t *testing.T) { time.Sleep(1 * time.Millisecond) } +func TestDefaultLoggerMisc(t *testing.T) { + cfg, _ := config.ParseString("log { }") + newStd, _ := New(cfg) + SetDefaultLogger(newStd) + Print("welcome 2 print") + Printf("welcome 2 printf") + Println("welcome 2 println") + + assert.Nil(t, SetLevel("trace")) + assert.Nil(t, SetPattern("%level:-5 %message")) + time.Sleep(1 * time.Millisecond) +} + func testStdPanic(method, msg string) { defer func() { if r := recover(); r != nil { diff --git a/file_receiver_test.go b/file_receiver_test.go index 42f2cbd0..611d3226 100644 --- a/file_receiver_test.go +++ b/file_receiver_test.go @@ -120,6 +120,25 @@ func TestFileLoggerUnknownFormatFlag(t *testing.T) { assert.Equal(t, "unrecognized log format flag: myfile", err.Error()) } +func TestFileLoggerIncorrectSizeValue(t *testing.T) { + defer cleaupFiles("*.log") + configStr := ` + log { + receiver = "file" + level = "debug" + pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %shortfile %line %custom:- %message" + file = "daily-aah-filename.log" + rotate { + policy = "size" + size = "500kbs" + } + } + ` + cfg, _ := config.ParseString(configStr) + _, err := New(cfg) + assert.Equal(t, "format: invalid input '500kbs'", err.Error()) +} + func testFileLogger(t *testing.T, cfgStr string, loop int) { cfg, _ := config.ParseString(cfgStr) logger, err := New(cfg) diff --git a/log_test.go b/log_test.go index 649f924a..947bbb73 100644 --- a/log_test.go +++ b/log_test.go @@ -73,6 +73,10 @@ func TestMisc(t *testing.T) { discard.Log(&Entry{}) _ = discard.SetPattern("nothing") assert.False(t, discard.IsCallerInfo()) + + // util + assert.Nil(t, getReceiverByName("SMTP")) + assert.Equal(t, "", formatTime(time.Time{})) } func testPanic(logger *Logger, method, msg string) { From 27c8bd2c098d0cfa46f1b2dd5c33525efc006926 Mon Sep 17 00:00:00 2001 From: Jeevanandam M Date: Tue, 16 May 2017 01:11:48 -0700 Subject: [PATCH 9/9] readme update for v0.3 release [ci skip] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0291268..5c2cd9e7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # log - aah framework [![Build Status](https://travis-ci.org/go-aah/log.svg?branch=master)](https://travis-ci.org/go-aah/log) [![codecov](https://codecov.io/gh/go-aah/log/branch/master/graph/badge.svg)](https://codecov.io/gh/go-aah/log/branch/master) [![Go Report Card](https://goreportcard.com/badge/aahframework.org/log.v0)](https://goreportcard.com/report/aahframework.org/log.v0) -[![Version](https://img.shields.io/badge/version-0.2-blue.svg)](https://github.com/go-aah/log/releases/latest) [![GoDoc](https://godoc.org/aahframework.org/log.v0?status.svg)](https://godoc.org/aahframework.org/log.v0) +[![Version](https://img.shields.io/badge/version-0.3-blue.svg)](https://github.com/go-aah/log/releases/latest) [![GoDoc](https://godoc.org/aahframework.org/log.v0?status.svg)](https://godoc.org/aahframework.org/log.v0) [![License](https://img.shields.io/github/license/go-aah/log.svg)](LICENSE) -***v0.2 [released](https://github.com/go-aah/log/releases/latest) and tagged on Mar 04, 2017*** +***v0.3 [released](https://github.com/go-aah/log/releases/latest) and tagged on May 16, 2017*** Simple, flexible & powerful `Go` logger inspired by standard logger & Google glog. aah framework utilizes `log` library across.