diff --git a/core/ylog/example_test.go b/core/ylog/example_test.go new file mode 100644 index 000000000..01a86d2db --- /dev/null +++ b/core/ylog/example_test.go @@ -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"}} +} diff --git a/core/ylog/logger.go b/core/ylog/logger.go new file mode 100644 index 000000000..887bb5ae7 --- /dev/null +++ b/core/ylog/logger.go @@ -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 +} diff --git a/core/ylog/logger_test.go b/core/ylog/logger_test.go new file mode 100644 index 000000000..a34f68ee0 --- /dev/null +++ b/core/ylog/logger_test.go @@ -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) +} diff --git a/core/ylog/slog_handler.go b/core/ylog/slog_handler.go new file mode 100644 index 000000000..ebd7eda6f --- /dev/null +++ b/core/ylog/slog_handler.go @@ -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 +} diff --git a/docs/log.md b/docs/log.md index a541d4883..b526eb4a8 100644 --- a/docs/log.md +++ b/docs/log.md @@ -1,36 +1,17 @@ ## Application Log -### Log +Applications can use loggers to record log messages. -Applications can use loggers to record log messages, we define a logging interface`logger` +yomo suggests using [slog](https://pkg.go.dev/golang.org/x/exp/slog) to output **structured log** -```go -// Logger is the interface for logger. -type Logger interface { - // SetLevel sets the logger level - SetLevel(Level) - // SetEncoding sets the logger's encoding - SetEncoding(encoding string) - // Printf logs a message wihout level - Printf(template string, args ...interface{}) - // Debugf logs a message at DebugLevel - Debugf(template string, args ...interface{}) - // Infof logs a message at InfoLevel - Infof(template string, args ...interface{}) - // Warnf logs a message at WarnLevel - Warnf(template string, args ...interface{}) - // Errorf logs a message at ErrorLevel - Errorf(template string, args ...interface{}) - // Output file path to write log message - Output(file string) - // ErrorOutput file path to write error message - ErrorOutput(file string) -} -``` +Structured logging is the ability to output logs with machine-readable structure, typically key-value pairs or json, in addition to a human-readable message. + +More detailed instructions can be found in the documentation:`core/ylog/logger.go` -More detailed instructions can be found in the documentation:`core/log/logger.go` +Yomo provide a default implementation of the logger, The default loads config from environment. -We provide a default implementation of the logger, you can directly refer to the `github.com/yomorun/yomo/pkg/logger` package to use, if the default implementation can not meet your requirements, you can implement the above interface, and then use the `yomo.WithLogger ` option , for example: +If the default implementation can not meet your requirements, +you can import `slog` directly, you can also implement interface to `slog.Handler`, and then use the `yomo.WithLogger ` option , for example: ```go sfn := yomo.NewStreamFunction ( @@ -40,32 +21,17 @@ sfn := yomo.NewStreamFunction ( ) ``` -#### Methods: - -- `Printf` Output log messages regardless of log level settings -- `Debugf` Output debug messages -- `Infof` Output information message -- `Warnf` Output warning message -- `Errorf` Output error message - -**Example of use:** - -```go -import "github.com/yomorun/yomo/pkg/logger" -... -logger.Infof("%s doesn't grow on trees. ","Money") -``` - #### Environment Variables -- `YOMO_LOG_LEVEL` Set the log level, default: `error` , optional values are as follows: +- `YOMO_LOG_LEVEL` Set the log level, default: `info` , optional values are as follows: - debug - info - warn - error -- `YOMO_LOG_OUTPUT` Set the log output file, the default is not output +- `YOMO_LOG_OUTPUT` Set the log output file, the default is stdout -- `YOMO_LOG_ERROR_OUTPUT` When an error occurs, output the message to the specified file, the default is not output +- `YOMO_LOG_ERROR_OUTPUT` When an error occurs, output the message to the specified file, the default is stderr - `YOMO_DEBUG_FRAME_SIZE` Set the output size in debug mode `Frame`, the default is 16 bytes +- - `YOMO_LOG_VERBOSE` enable or disable the debug mode of logger, logger outputs the source code position of the log statement if enable it, Do not enable it in production, default is false diff --git a/docs/zh-cn/log.md b/docs/zh-cn/log.md index c2c7b5243..4d5daa499 100644 --- a/docs/zh-cn/log.md +++ b/docs/zh-cn/log.md @@ -1,70 +1,35 @@ ## 应用程序日志处理 -### 日志 -应用程序可以使用日志记录器,记录日志消息,我们定义了一个日志接口 `logger` +应用程序可以使用日志记录器,记录日志消息。 -```go -// Logger is the interface for logger. -type Logger interface { - // SetLevel sets the logger level - SetLevel(Level) - // SetEncoding sets the logger's encoding - SetEncoding(encoding string) - // Printf logs a message wihout level - Printf(template string, args ...interface{}) - // Debugf logs a message at DebugLevel - Debugf(template string, args ...interface{}) - // Infof logs a message at InfoLevel - Infof(template string, args ...interface{}) - // Warnf logs a message at WarnLevel - Warnf(template string, args ...interface{}) - // Errorf logs a message at ErrorLevel - Errorf(template string, args ...interface{}) - // Output file path to write log message - Output(file string) - // ErrorOutput file path to write error message - ErrorOutput(file string) -} +yomo 推荐使用 [slog](https://pkg.go.dev/golang.org/x/exp/slog) 打印**结构化日志** -``` +结构化日志: 是一种人类可读的,并且机器可读的日志,通常是键-值对结构,或者 json 结构。 + +更详细的说明可以查看文件:`core/ylog/logger.go` -更详细的说明可以查看文件:`core/log/logger.go` +Yomo 提供了一个日志记录器的默认实现,默认的 logger 是从环境变量中加载配置。 -我们提供了一个日志记录器的默认实现,您可以直接引用`github.com/yomorun/yomo/pkg/logger` 包使用,如果默认实现不能满足您的要求,你可以实现上面的接口,然后在编写应用时使用 `yomo.WithLogger ` 选项,例: +如果默认实现不能满足你的要求,你也可以直接引用 `slog` 包使用, +或者你也可以实现 `slog.Handler` 接口,然后在编写应用时使用 `yomo.WithLogger ` 选项,例: ```go sfn := yomo.NewStreamFunction( "Name", .... - yomo.WithLogger(customLogger), // customLogger 是您自己的日志记录器实现 + yomo.WithLogger(customLogger), // customLogger 是你自己的日志记录器实现 ) ``` -#### 主要方法 - -- `Printf` 无视日志级别设置,输出日志消息 -- `Debugf` 输出调试消息 -- `Infof` 输出通知消息 -- `Warnf` 输出警告消息 -- `Errorf` 输出错误消息 - -**使用示例:** - -```go -import "github.com/yomorun/yomo/pkg/logger" -... -logger.Infof("%s doesn't grow on trees. ","Money") -``` - #### 环境变量 -- `YOMO_LOG_LEVEL` 设置日志级别,默认:`error`,可选值如下: +- `YOMO_LOG_LEVEL` 设置日志级别,默认:`info`,可选值如下: - debug - info - warn - error -- `YOMO_LOG_OUTPUT` 设置日志输出文件,默认不输出 -- `YOMO_LOG_ERROR_OUTPUT` 设置发生错误时,将消息输出到指定文件,默认不输出 +- `YOMO_LOG_OUTPUT` 设置日志输出文件,默认输出到 stdout +- `YOMO_LOG_ERROR_OUTPUT` 设置发生错误时,将消息输出到指定文件,默认输出到 stderr - `YOMO_DEBUG_FRAME_SIZE` 设置调试模式下输出`Frame`大小,默认 16 个字节 - +- `YOMO_LOG_VERBOSE` 设置是否打开 log 的 debug 模式,debug 模式下,日志会输出打印日志的代码行数,不建议在生产环境打开,默认是 false diff --git a/go.mod b/go.mod index 0c7933c36..9150c5748 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/briandowns/spinner v1.19.0 github.com/bytecodealliance/wasmtime-go v1.0.0 + github.com/caarlos0/env/v6 v6.10.1 github.com/cenkalti/backoff/v4 v4.1.3 github.com/fatih/color v1.13.0 github.com/joho/godotenv v1.4.0 @@ -18,6 +19,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/yomorun/y3 v1.0.5 go.uber.org/zap v1.23.0 + golang.org/x/exp v0.0.0-20221212164502-fae10dda9338 golang.org/x/tools v0.3.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 @@ -51,7 +53,6 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect golang.org/x/crypto v0.1.0 // indirect - golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect golang.org/x/mod v0.7.0 // indirect golang.org/x/net v0.2.0 // indirect golang.org/x/sys v0.2.0 // indirect diff --git a/go.sum b/go.sum index 67a6f741f..5ebd40ed3 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh github.com/briandowns/spinner v1.19.0/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/bytecodealliance/wasmtime-go v1.0.0 h1:9u9gqaUiaJeN5IoD1L7egD8atOnTGyJcNp8BhkL9cUU= github.com/bytecodealliance/wasmtime-go v1.0.0/go.mod h1:jjlqQbWUfVSbehpErw3UoWFndBXRRMvfikYH6KsCwOg= +github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= +github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -274,8 +276,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb h1:QIsP/NmClBICkqnJ4rSIhnrGiGR7Yv9ZORGGnmmLTPk= +golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20221212164502-fae10dda9338 h1:OvjRkcNHnf6/W5FZXSxODbxwD+X7fspczG7Jn/xQVD4= +golang.org/x/exp v0.0.0-20221212164502-fae10dda9338/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=