Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(logger): add slog support (#407)
* feat(logger): add slog support * fix(logger): resolve codacy * doc(logger): update document for logger * fix(logger): write log to file * doc: fix log doc * refactor(logger): upgrade slog version
- Loading branch information
Showing
8 changed files
with
395 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package ylog_test | ||
|
||
import ( | ||
"io" | ||
"net" | ||
|
||
"github.com/yomorun/yomo/core/ylog" | ||
) | ||
|
||
func Example() { | ||
// text format logger | ||
logger := ylog.NewFromConfig(ylog.Config{ | ||
Level: "warn", | ||
Format: "text", | ||
ErrorOutput: "stdout", | ||
DisableTime: true, | ||
}) | ||
|
||
ylog.SetDefault(logger.With("hello", "yomo").WithGroup("syslog")) | ||
|
||
ylog.Debug("debug", "aaa", "bbb") | ||
ylog.Info("info", "ccc", "ddd") | ||
ylog.Warn("warn", "eee", "fff") | ||
ylog.Error("error", io.EOF, "eee", "fff") | ||
|
||
// json format logger | ||
sysLogger := ylog.NewFromConfig(ylog.Config{ | ||
Level: "error", | ||
Format: "json", | ||
ErrorOutput: "stdout", | ||
DisableTime: true, | ||
}) | ||
|
||
sysLogger = sysLogger.WithGroup("syslog") | ||
|
||
sysLogger.Error("sys error", net.ErrClosed, "ggg", "hhh") | ||
|
||
// Output: | ||
// level=WARN msg=warn hello=yomo syslog.eee=fff | ||
// level=ERROR msg=error hello=yomo syslog.eee=fff syslog.err=EOF | ||
// {"level":"ERROR","msg":"sys error","syslog":{"ggg":"hhh","err":"use of closed network connection"}} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// Package ylog provides a slog.Logger instance for logging. | ||
// ylog also provides a default slog.Logger, the default logger is build from environment. | ||
// | ||
// ylog allows to call log api directly, like: | ||
// | ||
// ylog.Debug("test", "name", "yomo") | ||
// ylog.Info("test", "name", "yomo") | ||
// ylog.Warn("test", "name", "yomo") | ||
// ylog.Error("test", "name", "yomo") | ||
package ylog | ||
|
||
import ( | ||
"io" | ||
"log" | ||
"os" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/caarlos0/env/v6" | ||
"golang.org/x/exp/slog" | ||
) | ||
|
||
var defaultLogger = Default() | ||
|
||
// SetDefault set global logger. | ||
func SetDefault(logger *slog.Logger) { defaultLogger = logger } | ||
|
||
// Debug logs a message at debug level. | ||
func Debug(msg string, keyvals ...interface{}) { | ||
defaultLogger.Debug(msg, keyvals...) | ||
} | ||
|
||
// Info logs a message at info level. | ||
func Info(msg string, keyvals ...interface{}) { | ||
defaultLogger.Info(msg, keyvals...) | ||
} | ||
|
||
// Warn logs a message at warn level. | ||
func Warn(msg string, keyvals ...interface{}) { | ||
defaultLogger.Warn(msg, keyvals...) | ||
} | ||
|
||
// Error logs a message at error level. | ||
func Error(msg string, err error, keyvals ...interface{}) { | ||
defaultLogger.Error(msg, err, keyvals...) | ||
} | ||
|
||
// Config is the config of slog, the config is from environment. | ||
type Config struct { | ||
// Verbose indicates if logger log code line, use false for production. | ||
Verbose bool `env:"YOMO_LOG_VERBOSE" envDefault:"false"` | ||
|
||
// the log level, It can be one of `debug`, `info`, `warn`, `error` | ||
Level string `env:"YOMO_LOG_LEVEL" envDefault:"info"` | ||
|
||
// log output file path, It's stdout if not set. | ||
Output string `env:"YOMO_LOG_OUTPUT"` | ||
|
||
// error log output file path, It's stderr if not set. | ||
ErrorOutput string `env:"YOMO_LOG_ERROR_OUTPUT"` | ||
|
||
// log format, support text and json. | ||
Format string `env:"YOMO_LOG_FORMAT" envDefault:"text"` | ||
|
||
// DisableTime disable time key, It's a pravited field, Just for testing. | ||
DisableTime bool | ||
} | ||
|
||
// DebugFrameSize is use for log dataFrame, | ||
// It means that only logs the first DebugFrameSize bytes if the data is large than DebugFrameSize bytes. | ||
// | ||
// DebugFrameSize is default to 16, | ||
// if env `YOMO_DEBUG_FRAME_SIZE` is setted and It's an int number, Set the env value to DebugFrameSize. | ||
var DebugFrameSize = 16 | ||
|
||
func init() { | ||
if e := os.Getenv("YOMO_DEBUG_FRAME_SIZE"); e != "" { | ||
if val, err := strconv.Atoi(e); err == nil { | ||
DebugFrameSize = val | ||
} | ||
} | ||
} | ||
|
||
// Default returns a slog.Logger according to enviroment. | ||
func Default() *slog.Logger { | ||
var conf Config | ||
if err := env.Parse(&conf); err != nil { | ||
log.Fatalf("%+v\n", err) | ||
} | ||
return NewFromConfig(conf) | ||
} | ||
|
||
// NewFromConfig returns a slog.Logger according to conf. | ||
func NewFromConfig(conf Config) *slog.Logger { | ||
return slog.New(NewHandlerFromConfig(conf)) | ||
} | ||
|
||
func parseToWriter(path string, defaultWriter io.Writer) (io.Writer, error) { | ||
switch strings.ToLower(path) { | ||
case "stdout": | ||
return os.Stdout, nil | ||
case "stderr": | ||
return os.Stderr, nil | ||
default: | ||
if path != "" { | ||
return os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | ||
} | ||
return defaultWriter, nil | ||
} | ||
} | ||
|
||
func mustParseToWriter(path string, defaultWriter io.Writer) io.Writer { | ||
w, err := parseToWriter(path, defaultWriter) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return w | ||
} | ||
|
||
func parseToSlogLevel(stringLevel string) slog.Level { | ||
var level = slog.LevelDebug | ||
switch strings.ToLower(stringLevel) { | ||
case "debug": | ||
level = slog.LevelDebug | ||
case "info": | ||
level = slog.LevelInfo | ||
case "warn": | ||
level = slog.LevelWarn | ||
case "error": | ||
level = slog.LevelError | ||
} | ||
|
||
return level | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package ylog | ||
|
||
import ( | ||
"io" | ||
"os" | ||
"path" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"golang.org/x/exp/slog" | ||
) | ||
|
||
func TestLogger(t *testing.T) { | ||
testdir := t.TempDir() | ||
|
||
var ( | ||
output = path.Join(testdir, "output.log") | ||
errOutput = path.Join(testdir, "err_output.log") | ||
) | ||
|
||
conf := Config{ | ||
Level: "info", | ||
Output: output, | ||
ErrorOutput: errOutput, | ||
DisableTime: true, | ||
} | ||
|
||
logger := slog.New(NewHandlerFromConfig(conf)) | ||
|
||
logger.Debug("some debug", "hello", "yomo") | ||
logger.Info("some info", "hello", "yomo") | ||
logger.Warn("some waring", "hello", "yomo") | ||
|
||
logger.Error("error", io.EOF, "hello", "yomo") | ||
|
||
log, err := os.ReadFile(output) | ||
|
||
assert.NoError(t, err) | ||
assert.FileExists(t, output) | ||
assert.Equal(t, "level=INFO msg=\"some info\" hello=yomo\nlevel=WARN msg=\"some waring\" hello=yomo\n", string(log)) | ||
|
||
errlog, err := os.ReadFile(errOutput) | ||
|
||
assert.NoError(t, err) | ||
assert.FileExists(t, errOutput) | ||
assert.Equal(t, "level=ERROR msg=error hello=yomo err=EOF\n", string(errlog)) | ||
|
||
os.Remove(output) | ||
os.Remove(errOutput) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// Package provides handler that supports spliting log stream to common log stream and error log stream. | ||
package ylog | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"os" | ||
"strings" | ||
"sync" | ||
|
||
"golang.org/x/exp/slog" | ||
) | ||
|
||
// handler supports spliting log stream to common log stream and error log stream. | ||
type handler struct { | ||
slog.Handler | ||
|
||
buf *asyncBuffer | ||
|
||
writer io.Writer | ||
errWriter io.Writer | ||
} | ||
|
||
type asyncBuffer struct { | ||
sync.Mutex | ||
underlying *bytes.Buffer | ||
} | ||
|
||
func newAsyncBuffer(cap int) *asyncBuffer { | ||
return &asyncBuffer{ | ||
underlying: bytes.NewBuffer(make([]byte, cap)), | ||
} | ||
} | ||
|
||
func (buf *asyncBuffer) Write(b []byte) (int, error) { | ||
buf.Lock() | ||
defer buf.Unlock() | ||
|
||
return buf.underlying.Write(b) | ||
} | ||
|
||
func (buf *asyncBuffer) Read(p []byte) (int, error) { | ||
buf.Lock() | ||
defer buf.Unlock() | ||
|
||
return buf.underlying.Read(p) | ||
} | ||
|
||
func (buf *asyncBuffer) Reset() { | ||
buf.Lock() | ||
defer buf.Unlock() | ||
|
||
buf.underlying.Reset() | ||
} | ||
|
||
// NewHandlerFromConfig creates a slog.Handler from conf | ||
func NewHandlerFromConfig(conf Config) slog.Handler { | ||
buf := newAsyncBuffer(0) | ||
|
||
h := bufferedSlogHandler( | ||
buf, | ||
conf.Format, | ||
parseToSlogLevel(conf.Level), | ||
conf.Verbose, | ||
conf.DisableTime, | ||
) | ||
|
||
return &handler{ | ||
Handler: h, | ||
buf: buf, | ||
writer: mustParseToWriter(conf.Output, os.Stdout), | ||
errWriter: mustParseToWriter(conf.ErrorOutput, os.Stderr), | ||
} | ||
} | ||
|
||
func (h *handler) Enabled(level slog.Level) bool { | ||
return h.Handler.Enabled(level) | ||
} | ||
|
||
func (h *handler) Handle(r slog.Record) error { | ||
err := h.Handler.Handle(r) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if r.Level == slog.LevelError { | ||
_, err = io.Copy(h.errWriter, h.buf) | ||
} else { | ||
_, err = io.Copy(h.writer, h.buf) | ||
} | ||
h.buf.Reset() | ||
|
||
return err | ||
} | ||
|
||
func (h *handler) WithAttrs(as []slog.Attr) slog.Handler { | ||
return &handler{ | ||
buf: h.buf, | ||
writer: h.writer, | ||
errWriter: h.errWriter, | ||
Handler: h.Handler.WithAttrs(as), | ||
} | ||
} | ||
|
||
func (h *handler) WithGroup(name string) slog.Handler { | ||
return &handler{ | ||
buf: h.buf, | ||
writer: h.writer, | ||
errWriter: h.errWriter, | ||
Handler: h.Handler.WithGroup(name), | ||
} | ||
} | ||
|
||
func bufferedSlogHandler(buf io.Writer, format string, level slog.Level, verbose, disableTime bool) slog.Handler { | ||
opt := slog.HandlerOptions{ | ||
AddSource: verbose, | ||
Level: level, | ||
} | ||
if disableTime { | ||
opt.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr { | ||
if a.Key == "time" { | ||
return slog.Attr{} | ||
} | ||
return a | ||
} | ||
} | ||
|
||
var h slog.Handler | ||
if strings.ToLower(format) == "json" { | ||
h = opt.NewJSONHandler(buf) | ||
} else { | ||
h = opt.NewTextHandler(buf) | ||
} | ||
|
||
return h | ||
} |
Oops, something went wrong.