diff --git a/.gitignore b/.gitignore index 4b88bd8..8eb0085 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# logs +*.log + # ide .idea/ *.iml diff --git a/FUTURE.md b/FUTURE.md index 23b57c0..fa2dbb5 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -1,14 +1,14 @@ ## ✒ 未来版本的新特性 (Features in future version) -### v0.1.6 -* 完善 DurationRollingFile 结构,加入文件个数限制 -* 完善 SizeRollingFile 结构,加入文件个数限制 - -### v0.1.5 -* 更改内置日志处理器的换行符(\n)为系统的换行符 +### v0.2.0 * 继续完善配置文件,主要针对内置的日志处理器做适配 +* 合并目前的 DefaultHandler 和 JsonHandler * DefaultHandler 和 wrapper 进行配置文件的适配 -* JsonHandler 和 wrapper 进行配置文件的适配 +* 加入 FileHandler,专门负责文件相关的日志处理器 +* 增加分级日志处理器包装器,不同的级别可以使用不同的日志处理器 + +### v0.1.5 +* 完善 Json 处理器没有做字符转义的修复方案,详情查询 [issue/1](https://github.com/FishGoddess/logit/issues/1) ### v0.1.4 * 紧急修复 Json 处理器没有做字符转义的 bug,详情查询 [issue/1](https://github.com/FishGoddess/logit/issues/1) diff --git a/HISTORY.md b/HISTORY.md index 816a770..05b9f91 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ ## ✒ 历史版本的特性介绍 (Features in old version) +### v0.1.5 +> 此版本发布于 2020-04-19 +* 完善 Json 处理器没有做字符转义的修复方案,详情查询 [issue/1](https://github.com/FishGoddess/logit/issues/1) + ### v0.1.4 > 此版本发布于 2020-04-10 * 紧急修复 Json 处理器没有做字符转义的 bug,详情查询 [issue/1](https://github.com/FishGoddess/logit/issues/1) diff --git a/README.en.md b/README.en.md index 9bbb38c..174856b 100644 --- a/README.en.md +++ b/README.en.md @@ -24,6 +24,8 @@ _Check [HISTORY.md](./HISTORY.md) and [FUTURE.md](./FUTURE.md) to know about more information._ +> v0.1.x is an interim version and will not be updated but fixed from now on. Next version v0.2.x is a big update which will bring new features and user experience, also it will be updated and maintained for a long time! + ### 🚀 Installation The only requirement is the [Golang Programming Language](https://golang.org). @@ -42,7 +44,7 @@ module your_project_name go 1.14 require ( - github.com/FishGoddess/logit v0.1.4 + github.com/FishGoddess/logit v0.1.5 ) ``` diff --git a/README.md b/README.md index 2890c6c..52a28a5 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ _历史版本的特性请查看 [HISTORY.md](./HISTORY.md)。未来版本的新特性和计划请查看 [FUTURE.md](./FUTURE.md)。_ +> v0.1.x 版本只是一个特性过渡版本,目前已经停止更新,只进行维护和修复。下一个大更新版本 v0.2.x 将带来全新的特性和使用体验(其中一点就是做减法),并保持长期更新和维护! + ### 🚀 安装方式 唯一需要的依赖就是 [Golang 运行环境](https://golang.org). @@ -43,7 +45,7 @@ module your_project_name go 1.14 require ( - github.com/FishGoddess/logit v0.1.4 + github.com/FishGoddess/logit v0.1.5 ) ``` diff --git a/_examples/benchmarks_test.go b/_examples/benchmarks_test.go index c6ba4e9..9dee630 100644 --- a/_examples/benchmarks_test.go +++ b/_examples/benchmarks_test.go @@ -1,3 +1,17 @@ +// Copyright 2020 Ye Zi Jie. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// // Author: fish // Email: fishinlove@163.com // Created at 2020/03/02 20:51:29 diff --git a/_examples/config/logit-config-template.cfg b/_examples/config/logit-config-template.cfg index f8b1f1e..9cc15d3 100644 --- a/_examples/config/logit-config-template.cfg +++ b/_examples/config/logit-config-template.cfg @@ -1,4 +1,4 @@ -# logit 配置文件的模板 v0.1.4 +# logit 配置文件的模板 v0.1.5 # 日志级别,可取值有 debug,info,warn,error,off "level": "info", @@ -7,15 +7,46 @@ # 注意记录文件信息会有运行时操作,比较消耗性能,确保您是必须要记录才开启这个选项 "caller": false, -# 日志处理器,您可以添加多个日志处理器 +# 日志处理器,您可以添加多个日志处理器,可选值有 default,json # 只有当你使用 RegisterHandler 将你自定义的日志处理器注册进 logit 才可以在这里使用 -"handlers":{ +"handlers": { # 这里使用 Json 形式的日志处理器 # 注意:以下是每个日志处理器所需要的参数,并且会以键值对的形式注入到 RegisterHandler 的 newHandler 中 # 配置文件只负责把参数传递进去,要怎么使用这些参数是这个日志处理器自己的事情 # 所以不同的日志处理器的参数也不一定相同,需要参考具体日志处理器的文档描述 - "json":{ + "json": { # 时间格式化样板,如果是 "" 就说明不格式化时间,使用 unix 形式 - "timeFormat":"2006年01月02日" + "timeFormat": "2006年01月02日", + + # 日志写出器 + # 这个选项如果不设置就使用 os.Stdout 来输出日志 + "writer": { + # 当使用 duration 的时候,可以设置时间间隔和日志输出的目标文件夹 + # 当使用 size 的时候,可以设置文件最大大小和日志输出的目标文件夹 + # 当使用 off 的时候,可以设置日志文件的路径 + + # 使用 duration 的范例 + # rolling 是否需要使用滚动的日志机制,可选值有 duration,size,off + # 注意这个 duration 的 1 的单位是秒,也就是每隔一秒滚动一个日志文件 + # directory 表示日志存储的文件夹,因为会有很多日志文件需要存储,所以这里配置的是文件夹 + "rolling": "duration", + "duration": 1, + "directory": "D:/" + + # 使用 size 的范例 + # rolling 是否需要使用滚动的日志机制,可选值有 duration,size,off + # 注意这个 size 的 1 的单位是 MB,也就是日志文件达到 1 MB 之后就会滚动一个日志文件 + # directory 表示日志存储的文件夹,因为会有很多日志文件需要存储,所以这里配置的是文件夹 + #"rolling": "size", + #"size": 1, + #"directory": "D:/" + + # 使用 size 的范例 + # rolling 是否需要使用滚动的日志机制,可选值有 duration,size,off + # 注意这个 size 的 1 的单位是 MB,也就是日志文件达到 1 MB 之后就会滚动一个日志文件 + # directory 表示日志存储的文件夹,因为会有很多日志文件需要存储,所以这里配置的是文件夹 + #"rolling": "off", + #"file": "D:/me.log" + } } } \ No newline at end of file diff --git a/_examples/config/logit-config-template.en.cfg b/_examples/config/logit-config-template.en.cfg index 12e0ec5..7155238 100644 --- a/_examples/config/logit-config-template.en.cfg +++ b/_examples/config/logit-config-template.en.cfg @@ -1,4 +1,4 @@ -# logit config template v0.1.4 +# logit config template v0.1.5 # Logger level, all valid value is debug,info,warn,error,off "level": "info", @@ -13,11 +13,42 @@ "handlers":{ # Use provided Json handler here # Notice that what params can be set here is dependent to what handler you use - # Also, every params you set here will inject into newHandler in RegisterHandler in kv pair form + # Also, every params you set here will be injected into newHandler in RegisterHandler in kv pair form # Config file only be responsible for injection, so handler has responsibility to decide how to use these params # Different handler may have different params, check specific handler document to know about more information "json":{ - # how to format time, if this value is not set or is "", then unix format will be used + # How to format time, if this value is not set or is "", then unix format will be used "timeFormat":"2006-01-02" + + # Log writer + # Use os.Stdout if you do not set it up + "writer": { + # 当使用 duration 的时候,可以设置时间间隔和日志输出的目标文件夹 + # 当使用 size 的时候,可以设置文件最大大小和日志输出的目标文件夹 + # 当使用 off 的时候,可以设置日志文件的路径 + + # 使用 duration 的范例 + # rolling 是否需要使用滚动的日志机制,可选值有 duration,size,off + # 注意这个 duration 的 1 的单位是秒,也就是每隔一秒滚动一个日志文件 + # directory 表示日志存储的文件夹,因为会有很多日志文件需要存储,所以这里配置的是文件夹 + "rolling": "duration", + "duration": 1, + "directory": "D:/" + + # 使用 size 的范例 + # rolling 是否需要使用滚动的日志机制,可选值有 duration,size,off + # 注意这个 size 的 1 的单位是 MB,也就是日志文件达到 1 MB 之后就会滚动一个日志文件 + # directory 表示日志存储的文件夹,因为会有很多日志文件需要存储,所以这里配置的是文件夹 + #"rolling": "size", + #"size": 1, + #"directory": "D:/" + + # 使用 size 的范例 + # rolling 是否需要使用滚动的日志机制,可选值有 duration,size,off + # 注意这个 size 的 1 的单位是 MB,也就是日志文件达到 1 MB 之后就会滚动一个日志文件 + # directory 表示日志存储的文件夹,因为会有很多日志文件需要存储,所以这里配置的是文件夹 + #"rolling": "off", + #"file": "D:/me.log" + } } } \ No newline at end of file diff --git a/_examples/handler.go b/_examples/handler.go index 8d267db..9940394 100644 --- a/_examples/handler.go +++ b/_examples/handler.go @@ -19,49 +19,49 @@ package main import ( - "fmt" - "os" + "fmt" + "os" - "github.com/FishGoddess/logit" + "github.com/FishGoddess/logit" ) type myHandler struct{} // Customize your own handler. func (mh *myHandler) Handle(log *logit.Log) bool { - os.Stdout.Write([]byte("myHandler: ")) - os.Stdout.Write(logit.EncodeToJson(log, "")) // Try `os.Stdout.WriteString(log.Msg())` ? - return true + os.Stdout.Write([]byte("myHandler: ")) + os.Stdout.Write(logit.EncodeToJson(log, "")) // Try `os.Stdout.WriteString(log.Msg())` ? + return true } // We recommend you to register your handler to logit, so that // you can use your handler by logit.HandlerOf. // See logit.HandlerOf. func init() { - logit.RegisterHandler("myHandler", func(params map[string]string) logit.Handler { - return &myHandler{} - }) + logit.RegisterHandler("myHandler", func(params map[string]interface{}) logit.Handler { + return &myHandler{} + }) } func main() { - // Create a logger holder. - // Default handler is logit.DefaultHandler. - logger := logit.NewDevelopLogger() - logger.Info("before adding handlers...") + // Create a logger holder. + // Default handler is logit.DefaultHandler. + logger := logit.NewDevelopLogger() + logger.Info("before adding handlers...") - // Add handlers to logger. - // There are two handlers in logger because logger has a default handler inside after creating. - // See logit.DefaultHandler. - logger.AddHandlers(&myHandler{}, logit.HandlerOf("json", map[string]string{})) - fmt.Println("fmt =========================================") - logger.Info("after adding handlers...") + // Add handlers to logger. + // There are two handlers in logger because logger has a default handler inside after creating. + // See logit.DefaultHandler. + logger.AddHandlers(&myHandler{}, logit.HandlerOf("json", map[string]interface{}{})) + fmt.Println("fmt =========================================") + logger.Info("after adding handlers...") - // Set handlers to logger. - // There are two handlers in logger because the default handler inside was removed. - // If you register your handler to logit by logit.RegisterHandler, then you can - // use your handler everywhere like this: - logger.SetHandlers(logit.HandlerOf("myHandler", map[string]string{})) - fmt.Println("fmt =========================================") - logger.Info("after setting handlers...") + // Set handlers to logger. + // There are two handlers in logger because the default handler inside was removed. + // If you register your handler to logit by logit.RegisterHandler, then you can + // use your handler everywhere like this: + logger.SetHandlers(logit.HandlerOf("myHandler", map[string]interface{}{})) + fmt.Println("fmt =========================================") + logger.Info("after setting handlers...") } diff --git a/_examples/logger.cfg b/_examples/logger.cfg index 900371d..a80a4e8 100644 --- a/_examples/logger.cfg +++ b/_examples/logger.cfg @@ -1,10 +1,7 @@ -"level":"debug", +"level": "debug", -"handlers":{ - "default":{ - "timeFormat" : "2006-01-02 15:04" - }, - "json":{ - # "timeFormat" : "2006/01/02" +"handlers": { + "default": { + "timeFormat": "2006-01-02 15:04" } } \ No newline at end of file diff --git a/_examples/logger.go b/_examples/logger.go index 2cd6d8c..71b4e6c 100644 --- a/_examples/logger.go +++ b/_examples/logger.go @@ -19,58 +19,58 @@ package main import ( - "math/rand" - "os" - "strconv" - "time" + "math/rand" + "os" + "strconv" + "time" - "github.com/FishGoddess/logit" + "github.com/FishGoddess/logit" ) func main() { - // NewDevelopLogger creates a new Logger holder for developing, generally log to terminal or console. - // You can switch to logit.NewProductionLogger for production environment. - //logger := logit.NewProductionLogger(os.Stdout) - logger := logit.NewDevelopLogger() + // NewDevelopLogger creates a new Logger holder for developing, generally log to terminal or console. + // You can switch to logit.NewProductionLogger for production environment. + //logger := logit.NewProductionLogger(os.Stdout) + logger := logit.NewDevelopLogger() - // Then you will be easy to log! - logger.Debug("this is a debug message!") - logger.Info("this is an info message!") - logger.Warn("this is a warn message!") - logger.Error("this is an error message!") + // Then you will be easy to log! + logger.Debug("this is a debug message!") + logger.Info("this is an info message!") + logger.Warn("this is a warn message!") + logger.Error("this is an error message!") - // NewLoggerWithoutEncoder creates a new Logger holder with given level and handlers. - // As you know, file also can be written, just replace os.Stdout with your file! - // A logger is made of level and handlers, so we provide some handlers for use, see logit.Handler. - // This method is the most original way to create a logger for use. - logger = logit.NewLogger(logit.DebugLevel, logit.NewDefaultHandler(os.Stdout, "2006/01/02 15:04:05")) - logger.Info("What time is it now?") + // NewLoggerWithoutEncoder creates a new Logger holder with given level and handlers. + // As you know, file also can be written, just replace os.Stdout with your file! + // A logger is made of level and handlers, so we provide some handlers for use, see logit.Handler. + // This method is the most original way to create a logger for use. + logger = logit.NewLogger(logit.DebugLevel, logit.NewDefaultHandler(os.Stdout, "2006/01/02 15:04:05")) + logger.Info("What time is it now?") - // For convenience, we provide a register mechanism and you can use handlers like this: - logger = logit.NewLogger(logit.DebugLevel, logit.HandlerOf("default", map[string]string{})) - logger.Info("What handler is it now?") + // For convenience, we provide a register mechanism and you can use handlers like this: + logger = logit.NewLogger(logit.DebugLevel, logit.HandlerOf("default", map[string]interface{}{})) + logger.Info("What handler is it now?") - // NewLoggerFrom creates a new Logger holder with given config. - // The config has all the things to create a logger, such as level. - // We provide some encoders: default encoder and json encoder. - // See logit.Encoder to check more information. - logger = logit.NewLoggerFrom(logit.Config{ - Level: logit.DebugLevel, - Handlers: []logit.Handler{logit.NewJsonHandler(os.Stdout, "")}, - }) - logger.Info("I am a json log!") + // NewLoggerFrom creates a new Logger holder with given config. + // The config has all the things to create a logger, such as level. + // We provide some encoders: default encoder and json encoder. + // See logit.Encoder to check more information. + logger = logit.NewLoggerFrom(logit.Config{ + Level: logit.DebugLevel, + Handlers: []logit.Handler{logit.NewJsonHandler(os.Stdout, "")}, + }) + logger.Info("I am a json log!") - // If you want to output log with file info, try this: - logger.EnableFileInfo() - logger.Info("What file is it? Which line?") - logger.DisableFileInfo() + // If you want to output log with file info, try this: + logger.EnableFileInfo() + logger.Info("What file is it? Which line?") + logger.DisableFileInfo() - // If you have a long log and it is made of many variables, try this: - // The msg is the return value of msgGenerator. - logger.DebugFunc(func() string { - // Use time as the source of random number generator. - r := rand.New(rand.NewSource(time.Now().Unix())) - return "debug rand int: " + strconv.Itoa(r.Intn(100)) - }) + // If you have a long log and it is made of many variables, try this: + // The msg is the return value of msgGenerator. + logger.DebugFunc(func() string { + // Use time as the source of random number generator. + r := rand.New(rand.NewSource(time.Now().Unix())) + return "debug rand int: " + strconv.Itoa(r.Intn(100)) + }) } diff --git a/config.go b/config.go index 2b7369e..06a037e 100644 --- a/config.go +++ b/config.go @@ -56,7 +56,7 @@ type fileConfig struct { Caller bool `json:"caller"` // Handlers is the mapping to config file. - Handlers map[string]map[string]string `json:"handlers"` + Handlers map[string]map[string]interface{} `json:"handlers"` } // removeComments removes all comments of fileInBytes. diff --git a/config_test.go b/config_test.go index e821dd7..85612dd 100644 --- a/config_test.go +++ b/config_test.go @@ -38,7 +38,7 @@ func TestFileConfigToConfig(t *testing.T) { config, err := parseConfig(fileConfig{ Level: "info", - Handlers: map[string]map[string]string{ + Handlers: map[string]map[string]interface{}{ "json": { "timeFormat": "2006年01月02日 15点04分05秒", }, diff --git a/doc.go b/doc.go index ccc3927..c56a7de 100644 --- a/doc.go +++ b/doc.go @@ -212,4 +212,4 @@ Package logit provides an easy way to use foundation for your logging operations package logit // import "github.com/FishGoddess/logit" // Version is the version string representation of logit. -const Version = "v0.1.4" +const Version = "v0.1.5" diff --git a/encode.go b/encode.go new file mode 100644 index 0000000..c7db0b0 --- /dev/null +++ b/encode.go @@ -0,0 +1,105 @@ +// Copyright 2020 Ye Zi Jie. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: fish +// Email: fishinlove@163.com +// Created at 2020/04/14 21:06:56 + +package logit + +import ( + "bytes" + "strconv" + "strings" +) + +// EncodeToText encodes a log to a plain string like "[Info] [2020-03-06 16:10:44] msg" in bytes. +func EncodeToText(log *Log, timeFormat string) []byte { + + // 组装 log + buffer := bytes.NewBuffer(make([]byte, 0, 64)) + buffer.WriteString("[") + buffer.WriteString(log.Level().String()) + buffer.WriteString("] [") + buffer.WriteString(log.Now().Format(timeFormat)) + buffer.WriteString("] ") + + // 如果有文件信息,就把文件信息也加进去 + if log.file != "" && log.Line() != 0 { + buffer.WriteString("[") + buffer.WriteString(log.File() + ":" + strconv.Itoa(log.Line())) + buffer.WriteString("] ") + } + + buffer.WriteString(log.Msg()) + buffer.WriteString("\n") + return buffer.Bytes() +} + +// EncodeToJson encodes a log to a Json string like `{"level":"debug", "time":"2020-03-22 22:35:00", "msg":"log content..."}` in bytes. +// If timeFormat == "", then it will not format time and keep time in unix form. +func EncodeToJson(log *Log, timeFormat string) []byte { + + // 组装 log + buffer := bytes.NewBuffer(make([]byte, 0, 64)) + buffer.WriteString(`{"level":"`) + buffer.WriteString(log.Level().String()) + buffer.WriteString(`", "time":`) + + // 判断是否需要格式化时间 + if timeFormat != "" { + buffer.WriteString(strconv.Quote(log.Now().Format(timeFormat))) + } else { + buffer.WriteString(strconv.FormatInt(log.Now().Unix(), 10)) + } + + // 如果有文件信息,就把文件信息也加进去 + if log.file != "" && log.Line() != 0 { + buffer.WriteString(`, "file":"` + log.File()) + buffer.WriteString(`", "line":` + strconv.Itoa(log.Line())) + } + + buffer.WriteString(`, "msg":"`) + buffer.WriteString(escapeString(log.Msg())) + buffer.WriteString("\"}\n") + return buffer.Bytes() +} + +// escapeString is for escaping string from special character, such as double quotes. +// See issue: https://github.com/FishGoddess/logit/issues/1 +func escapeString(s string) string { + + builder := strings.Builder{} + runes := []rune(s) + for _, r := range runes { + + // Json 中需要进行转义的字符主要是 \ 和 ",还有控制字符(\u0020 以内的 ascii 字符) + switch r { + case '"', '\\': + builder.WriteRune('\\') + builder.WriteRune(r) + default: + // ascii 小于 16 需要在前面补 \u000,介于 16 和 32 之间的需要补 \u00 + if r < 16 { + builder.WriteString("\\u000" + strconv.FormatInt(int64(r), 16)) + } else if r < 32 { + builder.WriteString("\\u00" + strconv.FormatInt(int64(r), 16)) + } else { + builder.WriteRune(r) + } + } + } + + return builder.String() +} diff --git a/handler.go b/handler.go index f660ef7..4a0cd46 100644 --- a/handler.go +++ b/handler.go @@ -19,13 +19,14 @@ package logit import ( - "bytes" - "errors" - "io" - "os" - "strconv" - "strings" - "sync" + "errors" + "io" + "os" + "strconv" + "sync" + "time" + + "github.com/FishGoddess/logit/wrapper" ) // Handler is an interface representation of log handler. @@ -36,130 +37,161 @@ import ( // will not be used anymore. type Handler interface { - // Handle should handle this log in someway. - // If you don't want next handler to be used, just return false. - // Then all handlers after current handler will not be used. - Handle(log *Log) bool + // Handle should handle this log in someway. + // If you don't want next handler to be used, just return false. + // Then all handlers after current handler will not be used. + Handle(log *Log) bool +} + +// WriterOf returns a writer implement with given params. +// Different writer implement may have different params, so what params should +// be used is dependent to specific writer implement. +// An example of this method in config: +// +// "writer": { +// "rolling": "duration", +// "duration": 1, +// "directory": "D:/" +// } +// +func WriterOf(params map[string]interface{}) io.Writer { + + // 默认使用 os.Stdout + writer := os.Stdout + param, ok := params["writer"] + if !ok { + return writer + } + + // 下面这段代码有些 “肮脏”,但是大张旗鼓地重构这个又有点主次不分的感觉,所以先保持这样,后续再考虑这个点 + writerConfig := param.(map[string]interface{}) + rolling, ok := writerConfig["rolling"] + if !ok { + return writer + } + + switch rolling { + + // 以时间间隔进行滚动的日志写出器 + case "duration": + + // 滚动的时间间隔,单位是秒 + duration := 24 * 60 * 60 // 一天 + if param, ok := writerConfig["duration"]; ok { + duration = int(param.(float64)) + } + + // 写出的目标文件夹 + directory := "./" + if param, ok := writerConfig["directory"]; ok { + directory = param.(string) + } + + return wrapper.NewDurationRollingFile(time.Duration(duration)*time.Second, wrapper.NextFilename(directory)) + + // 以文件大小进行滚动的日志写出器 + case "size": + + // 滚动的文件大小,单位是 MB + size := 64 // 64MB + if param, ok := writerConfig["size"]; ok { + size = int(param.(float64)) + } + + // 写出的目标文件夹 + directory := "./" + if param, ok := writerConfig["directory"]; ok { + directory = param.(string) + } + + return wrapper.NewSizeRollingFile(int64(size)*wrapper.MB, wrapper.NextFilename(directory)) + + // 不滚动的日志写出器 + case "off": + + // 写出的目标文件 + if param, ok := writerConfig["file"]; ok { + file, err := wrapper.NewFile(param.(string)) + if err != nil { + panic(err) + } + return file + } + + file, err := wrapper.NewFile("./logit-" + strconv.FormatInt(time.Now().Unix(), 10) + wrapper.SuffixOfLogFile) + if err != nil { + panic(err) + } + return file + } + + return writer } func init() { - RegisterHandler("default", func(params map[string]string) Handler { - timeFormat := DefaultTimeFormat - if format, ok := params["timeFormat"]; ok && format != "" { - timeFormat = format - } - return NewDefaultHandler(os.Stdout, timeFormat) - }) - RegisterHandler("json", func(params map[string]string) Handler { - timeFormat := "" - if format, ok := params["timeFormat"]; ok { - timeFormat = format - } - return NewJsonHandler(os.Stdout, timeFormat) - }) + + // 注册默认日志处理器 + RegisterHandler("default", func(params map[string]interface{}) Handler { + timeFormat := DefaultTimeFormat + if format, ok := params["timeFormat"]; ok && format != "" { + timeFormat = format.(string) + } + return NewDefaultHandler(WriterOf(params), timeFormat) + }) + + // 注册 Json 格式日志处理器 + RegisterHandler("json", func(params map[string]interface{}) Handler { + timeFormat := "" + if format, ok := params["timeFormat"]; ok { + timeFormat = format.(string) + } + return NewJsonHandler(WriterOf(params), timeFormat) + }) } const ( - // DefaultTimeFormat is the default format for formatting time. - DefaultTimeFormat = "2006-01-02 15:04:05" + // DefaultTimeFormat is the default format for formatting time. + DefaultTimeFormat = "2006-01-02 15:04:05" ) var ( - // handlers stores all registered handlers. - // mutexOfHandlers is for concurrency. - handlers = map[string]func(params map[string]string) Handler{} - mutexOfHandlers = &sync.RWMutex{} + // handlers stores all registered handlers. + // mutexOfHandlers is for concurrency. + handlers = map[string]func(params map[string]interface{}) Handler{} + mutexOfHandlers = &sync.RWMutex{} - // HandlerIsExistedError is an error happens on repeat handler name. - HandlerIsExistedError = errors.New("the name of handler you want to register already exists! May be you should give it an another name") + // HandlerIsExistedError is an error happens on repeating handler name. + HandlerIsExistedError = errors.New("the name of handler you want to register already exists! May be you should give it an another name") ) // RegisterHandler registers your handler to logit so that you can use them easily. // Return an error if the name is existed, and you should change another name for your handler. -// Notice that newHandler has a parameter called params, which will inject into newHandler +// Notice that newHandler has a parameter called params, which will be injected into newHandler // by logit automatically. Different handler may have different params, so what params should -// inject into newHandler is dependent to specific handler. -func RegisterHandler(name string, newHandler func(params map[string]string) Handler) error { - mutexOfHandlers.Lock() - defer mutexOfHandlers.Unlock() - if _, ok := handlers[name]; ok { - return HandlerIsExistedError - } - handlers[name] = newHandler - return nil +// be injected into newHandler is dependent to specific handler. +func RegisterHandler(name string, newHandler func(params map[string]interface{}) Handler) error { + mutexOfHandlers.Lock() + defer mutexOfHandlers.Unlock() + if _, ok := handlers[name]; ok { + return HandlerIsExistedError + } + handlers[name] = newHandler + return nil } // HandlerOf returns handler whose name is given name and params. // Different handler may have different params, so what params should -// inject into newHandler is dependent to specific handler. +// be injected into newHandler is dependent to specific handler. // Notice that we don't use an error mechanism or ok mechanism to check the name but // a default handler returning mechanism. This is a more convenient way to use handlers (we think). -func HandlerOf(name string, params map[string]string) Handler { - mutexOfHandlers.RLock() - defer mutexOfHandlers.RUnlock() - newHandler, ok := handlers[name] - if !ok { - return NewDefaultHandler(os.Stdout, DefaultTimeFormat) - } - return newHandler(params) -} - -// EncodeToText encodes a log to a plain string like "[Info] [2020-03-06 16:10:44] msg" in bytes. -func EncodeToText(log *Log, timeFormat string) []byte { - - // 组装 log - buffer := bytes.NewBuffer(make([]byte, 0, 64)) - buffer.WriteString("[") - buffer.WriteString(log.Level().String()) - buffer.WriteString("] [") - buffer.WriteString(log.Now().Format(timeFormat)) - buffer.WriteString("] ") - - // 如果有文件信息,就把文件信息也加进去 - if log.file != "" && log.Line() != 0 { - buffer.WriteString("[") - buffer.WriteString(log.File() + ":" + strconv.Itoa(log.Line())) - buffer.WriteString("] ") - } - - buffer.WriteString(log.Msg()) - buffer.WriteString("\n") - return buffer.Bytes() -} - -// EncodeToJson encodes a log to a Json string like `{"level":"debug", "time":"2020-03-22 22:35:00", "msg":"log content..."}` in bytes. -// If timeFormat == "", then it will not format time and keep time in unix form. -func EncodeToJson(log *Log, timeFormat string) []byte { - - // 组装 log - buffer := bytes.NewBuffer(make([]byte, 0, 64)) - buffer.WriteString(`{"level":"`) - buffer.WriteString(log.Level().String()) - buffer.WriteString(`", "time":`) - - // 判断是否需要格式化时间 - if timeFormat != "" { - buffer.WriteString(strconv.Quote(log.Now().Format(timeFormat))) - } else { - buffer.WriteString(strconv.FormatInt(log.Now().Unix(), 10)) - } - - // 如果有文件信息,就把文件信息也加进去 - if log.file != "" && log.Line() != 0 { - buffer.WriteString(`, "file":"` + escapeString(log.File())) - buffer.WriteString(`", "line":` + strconv.Itoa(log.Line())) - } - - buffer.WriteString(`, "msg":"`) - buffer.WriteString(escapeString(log.Msg())) - buffer.WriteString("\"}\n") - return buffer.Bytes() -} - -// escapeString is for escaping string from special character, such as double quotes. -// See issue: https://github.com/FishGoddess/logit/issues/1 -func escapeString(s string) string { - return strings.ReplaceAll(s, `"`, `\"`) +func HandlerOf(name string, params map[string]interface{}) Handler { + mutexOfHandlers.RLock() + defer mutexOfHandlers.RUnlock() + newHandler, ok := handlers[name] + if !ok { + return NewDefaultHandler(os.Stdout, DefaultTimeFormat) + } + return newHandler(params) } // DefaultHandler is a default handler for use. @@ -185,23 +217,23 @@ func escapeString(s string) string { // } // type DefaultHandler struct { - writer io.Writer - timeFormat string + writer io.Writer + timeFormat string } // NewDefaultHandler returns a DefaultHandler holder with given writer. func NewDefaultHandler(writer io.Writer, timeFormat string) Handler { - return &DefaultHandler{ - writer: writer, - timeFormat: timeFormat, - } + return &DefaultHandler{ + writer: writer, + timeFormat: timeFormat, + } } // Handle will encode log and write log by internal writer. // Return true so that handlers after it will be used. func (dh *DefaultHandler) Handle(log *Log) bool { - dh.writer.Write(EncodeToText(log, dh.timeFormat)) - return true + dh.writer.Write(EncodeToText(log, dh.timeFormat)) + return true } // JsonHandler is a json handler for use. @@ -225,22 +257,22 @@ func (dh *DefaultHandler) Handle(log *Log) bool { // } // type JsonHandler struct { - writer io.Writer - timeFormat string + writer io.Writer + timeFormat string } // NewJsonHandler returns a JsonHandler holder with given writer. // If timeFormat == "", then it will not format time and keep time in unix form. func NewJsonHandler(writer io.Writer, timeFormat string) Handler { - return &JsonHandler{ - writer: writer, - timeFormat: timeFormat, - } + return &JsonHandler{ + writer: writer, + timeFormat: timeFormat, + } } // Handle will encode log and write log by internal writer. // Return true so that handlers after it will be used. func (jh *JsonHandler) Handle(log *Log) bool { - jh.writer.Write(EncodeToJson(log, jh.timeFormat)) - return true + jh.writer.Write(EncodeToJson(log, jh.timeFormat)) + return true } diff --git a/logger_extension.go b/logger_extension.go index a9d2df9..03f6c56 100644 --- a/logger_extension.go +++ b/logger_extension.go @@ -20,32 +20,12 @@ package logit import ( "io" - "math/rand" "os" - "path" - "strconv" "time" "github.com/FishGoddess/logit/wrapper" ) -const ( - // SuffixOfLogFile is the suffix of log file. - SuffixOfLogFile = ".log" -) - -// nextFilename creates a time-relative filename with given now time. -// Also, it uses random number to ensure this filename is available. -// The filename will be like "20200304-145246-45.log". -// Notice that directory stores all log files generated in this time. -func nextFilename(directory string) func(now time.Time) string { - rand.Seed(time.Now().UnixNano()) - return func(now time.Time) string { - name := now.Format("20060102-150405") + "-" + strconv.Itoa(rand.Intn(1000)) + SuffixOfLogFile - return path.Join(directory, name) - } -} - // NewLoggerFrom returns a logger with given config. // See logit.Config. func NewLoggerFrom(config Config) *Logger { @@ -116,7 +96,7 @@ func NewFileLogger(logFile string) *Logger { // If you want to appoint another filename, check this and do it by this way. // See wrapper.NewDurationRollingFile (it is an implement of io.writer). func NewDurationRollingLogger(directory string, duration time.Duration) *Logger { - file := wrapper.NewDurationRollingFile(duration, nextFilename(directory)) + file := wrapper.NewDurationRollingFile(duration, wrapper.NextFilename(directory)) return NewLoggerFrom(Config{ Level: InfoLevel, Handlers: []Handler{NewDefaultHandler(file, DefaultTimeFormat)}, @@ -138,7 +118,7 @@ func NewDayRollingLogger(directory string) *Logger { // If you want to appoint another filename, check this and do it by this way. // See wrapper.NewSizeRollingFile (it is an implement of io.writer). func NewSizeRollingLogger(directory string, limitedSize int64) *Logger { - file := wrapper.NewSizeRollingFile(limitedSize, nextFilename(directory)) + file := wrapper.NewSizeRollingFile(limitedSize, wrapper.NextFilename(directory)) return NewLoggerFrom(Config{ Level: InfoLevel, Handlers: []Handler{NewDefaultHandler(file, DefaultTimeFormat)}, diff --git a/logger_extension_test.go b/logger_extension_test.go index d5af0d4..e104831 100644 --- a/logger_extension_test.go +++ b/logger_extension_test.go @@ -19,100 +19,100 @@ package logit import ( - "math/rand" - "os" - "strconv" - "testing" - "time" + "math/rand" + "os" + "strconv" + "testing" + "time" - "github.com/FishGoddess/logit/wrapper" + "github.com/FishGoddess/logit/wrapper" ) // 测试创建文件日志记录器 func TestNewFileLogger(t *testing.T) { - defer func() { - err := recover() - if err == nil { - t.Fatal("创建文件日志记录器测试出现问题!") - } - }() + defer func() { + err := recover() + if err == nil { + t.Fatal("创建文件日志记录器测试出现问题!") + } + }() - logger := NewFileLogger("Z:/test.log") - for i := 0; i < 100; i++ { - logger.Info("我是第 " + strconv.Itoa(i) + " 条日志!") - } + logger := NewFileLogger("Z:/test.log") + for i := 0; i < 100; i++ { + logger.Info("我是第 " + strconv.Itoa(i) + " 条日志!") + } - logger = NewFileLogger("https://test.io") + logger = NewFileLogger("https://test.io") } // 测试创建随时间间隔滚动的文件日志记录器 func TestNewDurationRollingLogger(t *testing.T) { - logger := NewDurationRollingLogger("Z:/", time.Second) - for i := 0; i < 10; i++ { - logger.Info("1. info!!!!!!!! " + strconv.FormatInt(time.Now().Unix(), 10)) - time.Sleep(time.Second) - logger.Info("2. info!!!!!!!! " + strconv.FormatInt(time.Now().Unix(), 10)) - } + logger := NewDurationRollingLogger("Z:/", time.Second) + for i := 0; i < 10; i++ { + logger.Info("1. info!!!!!!!! " + strconv.FormatInt(time.Now().Unix(), 10)) + time.Sleep(time.Second) + logger.Info("2. info!!!!!!!! " + strconv.FormatInt(time.Now().Unix(), 10)) + } } // 测试按天自动划分日志文件的日志记录器 func TestNewDayRollingLogger(t *testing.T) { - logger := NewDayRollingLogger("Z:/") - logger.Info("1. info!!!!!!!! " + strconv.FormatInt(time.Now().Unix(), 10)) - time.Sleep(time.Second) - logger.Info("2. info!!!!!!!! " + strconv.FormatInt(time.Now().Unix(), 10)) + logger := NewDayRollingLogger("Z:/") + logger.Info("1. info!!!!!!!! " + strconv.FormatInt(time.Now().Unix(), 10)) + time.Sleep(time.Second) + logger.Info("2. info!!!!!!!! " + strconv.FormatInt(time.Now().Unix(), 10)) } // 测试按照文件大小自动划分日志文件的日志记录器 func TestNewSizeRollingLogger(t *testing.T) { - logger := NewSizeRollingLogger("Z:/", 64*wrapper.KB) - for i := 0; i < 1000; i++ { - logger.Debug("debug...") - logger.Info("info...") - logger.Warn("warn...") - logger.Error("error...") - } + logger := NewSizeRollingLogger("Z:/", 64*wrapper.KB) + for i := 0; i < 1000; i++ { + logger.Debug("debug...") + logger.Info("info...") + logger.Warn("warn...") + logger.Error("error...") + } } // 测试按照默认文件大小自动划分日志文件的日志记录器 func TestNewDefaultSizeRollingLogger(t *testing.T) { - logger := NewDefaultSizeRollingLogger("Z:/") - for i := 0; i < 1000; i++ { - logger.Debug("debug...") - logger.Info("info...") - logger.Warn("warn...") - logger.Error("error...") - } + logger := NewDefaultSizeRollingLogger("Z:/") + for i := 0; i < 1000; i++ { + logger.Debug("debug...") + logger.Info("info...") + logger.Warn("warn...") + logger.Error("error...") + } } // 测试输出日志是从函数中生成的几个方法 func TestLoggerLogFunction(t *testing.T) { - logger := NewProductionLogger(os.Stdout) - logger.ChangeLevelTo(DebugLevel) - logger.DebugFunc(func() string { - return "debug rand int: " + strconv.Itoa(rand.Intn(100)) - }) - logger.InfoFunc(func() string { - return "info rand int: " + strconv.Itoa(rand.Intn(100)) - }) - logger.WarnFunc(func() string { - return "warn rand int: " + strconv.Itoa(rand.Intn(100)) - }) - logger.ErrorFunc(func() string { - return "error rand int: " + strconv.Itoa(rand.Intn(100)) - }) - - // test double quotes - logger.Info(`test "double quotes" !!!!`) + logger := NewProductionLogger(os.Stdout) + logger.ChangeLevelTo(DebugLevel) + logger.DebugFunc(func() string { + return "debug rand int: " + strconv.Itoa(rand.Intn(100)) + }) + logger.InfoFunc(func() string { + return "info rand int: " + strconv.Itoa(rand.Intn(100)) + }) + logger.WarnFunc(func() string { + return "warn rand int: " + strconv.Itoa(rand.Intn(100)) + }) + logger.ErrorFunc(func() string { + return "error rand int: " + strconv.Itoa(rand.Intn(100)) + }) + + // test escaping + logger.Info(`test "double quotes"\t\b \u0003 \u0019 !!!!`) } // 测试从配置文件中创建一个 logger func TestNewLoggerFromConfigFile(t *testing.T) { - logger := NewLoggerFromConfigFile("./_examples/logger.cfg") - logger.Info("Does it work?") + logger := NewLoggerFromConfigFile("./_examples/logger.cfg") + logger.Info("Does it work?") } diff --git a/wrapper/file_helper.go b/wrapper/wrapper.go similarity index 69% rename from wrapper/file_helper.go rename to wrapper/wrapper.go index cec8243..b0d3f54 100644 --- a/wrapper/file_helper.go +++ b/wrapper/wrapper.go @@ -19,7 +19,10 @@ package wrapper import ( + "math/rand" "os" + "path" + "strconv" "time" ) @@ -34,6 +37,23 @@ const ( GB ) +const ( + // SuffixOfLogFile is the suffix of log file. + SuffixOfLogFile = ".log" +) + +// nextFilename creates a time-relative filename with given now time. +// Also, it uses random number to ensure this filename is available. +// The filename will be like "20200304-145246-45.log". +// Notice that directory stores all log files generated in this time. +func NextFilename(directory string) func(now time.Time) string { + rand.Seed(time.Now().UnixNano()) + return func(now time.Time) string { + name := now.Format("20060102-150405") + "-" + strconv.Itoa(rand.Intn(1000)) + SuffixOfLogFile + return path.Join(directory, name) + } +} + // NewFile creates a new file with given filePath. // Return a new File or an error if failed. // Notice that the permission of new file is 0644, which means rw-rw-r-- in unix-like os.