├── README.md ├── file.go ├── log ├── README.md └── log.go ├── logger.go ├── logger_test.go ├── switcher.go └── tools ├── gzdog ├── README.md └── main.go └── logfmt ├── README.md └── main.go /README.md: -------------------------------------------------------------------------------- 1 | 说明 2 | ==== 3 | 4 | 一行一条记录的json日志系统,支持按天和按小时切换日志文件。 5 | 6 | 用json格式存储日志数据的好处是方便其它的分析工具进行分析和数据挖掘。 7 | 8 | 对于json导致的数据膨胀可以在文件备份阶段通过打包压缩解决。 9 | 10 | 项目做了几个配套的命令行工具,在tools目录下,具体文档看各个工具的README文件。 -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package jsonlog 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | "sync" 10 | ) 11 | 12 | type M map[string]interface{} 13 | 14 | func fexists(name string) bool { 15 | _, err := os.Stat(name) 16 | return err == nil 17 | } 18 | 19 | type File struct { 20 | mutex sync.Mutex 21 | f *os.File 22 | w *bufio.Writer 23 | json *json.Encoder 24 | changed bool 25 | } 26 | 27 | func NewFile(fileName, fileType string, writeBufferSize int) (*File, error) { 28 | fullName := fileName + ".01" + fileType 29 | 30 | for fileID := 2; fexists(fullName); fileID++ { 31 | fullName = fileName + fmt.Sprintf(".%02d", fileID) + fileType 32 | } 33 | 34 | f, err := os.OpenFile(fullName, os.O_WRONLY|os.O_CREATE, 0755) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | w := bufio.NewWriterSize(f, writeBufferSize) 40 | return &File{ 41 | f: f, 42 | w: w, 43 | json: json.NewEncoder(w), 44 | }, nil 45 | } 46 | 47 | func (file *File) Write(r M) { 48 | file.mutex.Lock() 49 | defer file.mutex.Unlock() 50 | 51 | if err := file.json.Encode(r); err != nil { 52 | log.Println("jsonlog encode failed:", err.Error()) 53 | } 54 | file.changed = true 55 | } 56 | 57 | func (file *File) Flush() error { 58 | file.mutex.Lock() 59 | defer file.mutex.Unlock() 60 | 61 | if !file.changed { 62 | return nil 63 | } 64 | 65 | if err := file.w.Flush(); err != nil { 66 | return err 67 | } 68 | 69 | if err := file.f.Sync(); err != nil { 70 | return err 71 | } 72 | 73 | file.changed = false 74 | return nil 75 | } 76 | 77 | func (file *File) Close() error { 78 | if err := file.Flush(); err != nil { 79 | return err 80 | } 81 | return file.f.Close() 82 | } 83 | -------------------------------------------------------------------------------- /log/README.md: -------------------------------------------------------------------------------- 1 | 介绍 2 | ==== 3 | 4 | 基于jsonlog实现的系统信息日志模块,在指定目录记录日志文件,每天凌晨自动切换到新文件写入,日志以JSON格式记录,方便使用工具分析。 5 | 6 | 用法 7 | ==== 8 | 9 | log模块可以以全局的方式使用也可以以多个实例的方式使用。 10 | 11 | 全局化使用log模块需要在程序启动的时候调用`log.Init(dir)`来初始化全局日志系统,程序退出时调用`log.Close()`来关闭全局日志系统。 12 | 13 | 实例化的方式则是根据不同情况的需要,调用`log.New(dir)`来实例化日志记录器,不在使用时需要调用具体实例的`Close()`方法关闭记录器。 14 | 15 | 全局用法和实例用法接口是一致的,所以以下内容使用全局用法做示例。 16 | 17 | 记录日志的接口分为以下几种: 18 | 19 | 1. Info() -- 用于记录消息,通常是一些跟系统运行情况相关的数据,比如程序启动时间 20 | 2. Warn() -- 用于记录警告,通常是一些不影响业务但是需要注意的消息,比如数据库连接失败但重试成功 21 | 3. Error() -- 用于记录错误,通常是一些业务失败或操作失败,比如非法请求或请求处理失败 22 | 4. Debug() -- 用于记录调试信息,通常是开发时才关心的一些调试数据 23 | 24 | 以上接口都接受两个参数,第一个参数为所要记录的消息,第二个参数则是变长的日志数据列表,可以传任意多个对象,日志数据会一起被序列化为JSON格式。 25 | 26 | 在很多时候我们需要用到key-value的形式来记录日志数据,所以log模块内置了一个M类型,用来做这件事情。 27 | 28 | M类型是一个用字符串做key,存放`interface{}`类型的map,所以它可以存放任何可以参与JSON序列化的数据。 29 | 30 | 以下是一些用法示例: 31 | 32 | ```go 33 | log.Info("Hello World") 34 | 35 | log.Info("This is data", 123, 456, "string data") 36 | 37 | log.Info("This is key-value", log.M{ 38 | "error": error.Error(), 39 | "user": username, 40 | "email": email, 41 | }) 42 | ``` 43 | 44 | 有一些项目需要对不同模块的日志做划分,可以为每个模块的日志单独实例化日志系统: 45 | 46 | ```go 47 | logger1 := log.New("dir1", true) 48 | logger2 := log.New("dir2", true) 49 | ``` 50 | 51 | 在平时我们需要关闭生产环境上的调试信息输出,在出现一些异常情况时我们又会需要动态开启,所以log模块可以动态设置调试信息的输出与否: 52 | 53 | ```go 54 | log.SetDebug(true) 55 | 56 | logger1 := log.New("dir1", true) 57 | logger1.SetDebug(true) 58 | ``` -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/funny/jsonlog" 5 | "time" 6 | ) 7 | 8 | var gLogger *L 9 | 10 | // 初始化全局日志 11 | func Init(dir string) { 12 | l, err := New(dir) 13 | if err != nil { 14 | panic(err) 15 | } 16 | gLogger = l 17 | } 18 | 19 | // 关闭全局日志系统 20 | func Close() { 21 | gLogger.Close() 22 | } 23 | 24 | // 在全局日志中输出信息 25 | func Info(msg string, data ...interface{}) { 26 | gLogger.Info(msg, data...) 27 | } 28 | 29 | // 在全局日志中输出警告信息 30 | func Warn(msg string, data ...interface{}) { 31 | gLogger.Warn(msg, data...) 32 | } 33 | 34 | // 在全局日志中输出错误信息 35 | func Error(msg string, data ...interface{}) { 36 | gLogger.Error(msg, data...) 37 | } 38 | 39 | // 在全局日志中输出调试信息 40 | func Debug(msg string, data ...interface{}) { 41 | gLogger.Debug(msg, data...) 42 | } 43 | 44 | // 全局日志开启或关闭调试信息的输出 45 | func SetDebug(debug bool) { 46 | gLogger.SetDebug(debug) 47 | } 48 | 49 | type M map[string]interface{} 50 | 51 | // 日志记录器 52 | type L struct { 53 | l *jsonlog.L 54 | debug bool 55 | } 56 | 57 | // 新建一个日志记录器 58 | func New(dir string) (*L, error) { 59 | l, err := jsonlog.New(jsonlog.Config{ 60 | Dir: dir, 61 | Switcher: jsonlog.DAY_SWITCHER, 62 | FileType: ".log", 63 | }) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return &L{l, true}, nil 68 | } 69 | 70 | // 关闭日志系统 71 | func (logger *L) Close() { 72 | logger.l.Close() 73 | } 74 | 75 | func (logger *L) Log(msg string, typ string, data ...interface{}) { 76 | m := jsonlog.M{ 77 | "Time": time.Now().UnixNano(), 78 | "Type": typ, 79 | "Message": msg, 80 | } 81 | if data != nil { 82 | m["Data"] = data 83 | } 84 | logger.l.Log(m) 85 | } 86 | 87 | // 在日志文件中输出信息 88 | func (logger *L) Info(msg string, data ...interface{}) { 89 | logger.Log(msg, "info", data...) 90 | } 91 | 92 | // 在日志文件中输出警告信息 93 | func (logger *L) Warn(msg string, data ...interface{}) { 94 | logger.Log(msg, "warn", data...) 95 | } 96 | 97 | // 在日志文件中输出错误信息 98 | func (logger *L) Error(msg string, data ...interface{}) { 99 | logger.Log(msg, "error", data...) 100 | } 101 | 102 | // 在日志文件中输出调试信息 103 | func (logger *L) Debug(msg string, data ...interface{}) { 104 | if logger.debug { 105 | logger.Log(msg, "debug", data...) 106 | } 107 | } 108 | 109 | // 开启或关闭调试信息的输出 110 | func (logger *L) SetDebug(debug bool) { 111 | logger.debug = debug 112 | } 113 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package jsonlog 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | type Config struct { 12 | Dir string 13 | Switcher Switcher 14 | FileType string 15 | WriteBufferSize int 16 | FlushTick time.Duration 17 | LogChanSize int 18 | } 19 | 20 | // 日志记录器 21 | type L struct { 22 | config Config 23 | logChan chan M 24 | closeChan chan int 25 | closeWait sync.WaitGroup 26 | closeMutex sync.RWMutex 27 | closeFlag int32 28 | file *File 29 | } 30 | 31 | // 新建一个日志记录器 32 | func New(config Config) (*L, error) { 33 | if config.FileType[0] != '.' { 34 | config.FileType = "." + config.FileType 35 | } 36 | 37 | if config.WriteBufferSize <= 0 { 38 | config.WriteBufferSize = 4096 39 | } 40 | 41 | if config.FlushTick <= 0 { 42 | config.FlushTick = 2 * time.Second 43 | } 44 | 45 | if config.LogChanSize <= 0 { 46 | config.LogChanSize = 2000 47 | } 48 | 49 | logger := &L{ 50 | config: config, 51 | logChan: make(chan M, config.LogChanSize), 52 | } 53 | 54 | if err := logger.switchFile(); err != nil { 55 | return nil, err 56 | } 57 | 58 | logger.closeWait.Add(1) 59 | go logger.loop() 60 | 61 | return logger, nil 62 | } 63 | 64 | func (logger *L) loop() { 65 | defer func() { 66 | if logger.file != nil { 67 | logger.file.Close() 68 | } 69 | logger.closeWait.Done() 70 | }() 71 | 72 | // 定时刷新文件 73 | flushTicker := time.NewTicker(logger.config.FlushTick) 74 | defer flushTicker.Stop() 75 | 76 | // 定时切换文件 77 | switchTimer := time.NewTimer(logger.config.Switcher.FirstSwitchTime()) 78 | defer switchTimer.Stop() 79 | 80 | for { 81 | select { 82 | case r, ok := <-logger.logChan: 83 | if ok { 84 | logger.file.Write(r) 85 | } else { 86 | for r := range logger.logChan { 87 | logger.file.Write(r) 88 | } 89 | return 90 | } 91 | case <-flushTicker.C: 92 | if err := logger.file.Flush(); err != nil { 93 | log.Println("jsonlog flush failed:", err.Error()) 94 | panic(err) 95 | } 96 | case <-switchTimer.C: 97 | if err := logger.switchFile(); err != nil { 98 | log.Println("jsonlog switch failed:", err.Error()) 99 | panic(err) 100 | } 101 | switchTimer.Reset(logger.config.Switcher.NextSwitchTime()) 102 | } 103 | } 104 | } 105 | 106 | // 切换文件 107 | func (logger *L) switchFile() error { 108 | dir, fileName := logger.config.Switcher.DirAndFileName(logger.config.Dir) 109 | 110 | // 确认目录存在 111 | if err := os.MkdirAll(dir, 0755); err != nil { 112 | return err 113 | } 114 | 115 | // 先关闭旧文件再切换 116 | if logger.file != nil { 117 | if err := logger.file.Close(); err != nil { 118 | return err 119 | } 120 | } 121 | 122 | // 创建或者打开已存在文件 123 | file, err := NewFile(fileName, logger.config.FileType, logger.config.WriteBufferSize) 124 | if err != nil { 125 | return err 126 | } 127 | logger.file = file 128 | return nil 129 | } 130 | 131 | // 关闭日志系统 132 | func (logger *L) Close() { 133 | if atomic.LoadInt32(&logger.closeFlag) == 1 { 134 | return 135 | } 136 | 137 | logger.closeMutex.Lock() 138 | defer logger.closeMutex.Unlock() 139 | 140 | if atomic.LoadInt32(&logger.closeFlag) == 1 { 141 | return 142 | } 143 | 144 | atomic.StoreInt32(&logger.closeFlag, 1) 145 | close(logger.logChan) 146 | logger.closeWait.Wait() 147 | } 148 | 149 | // 在日志文件中输出信息 150 | func (logger *L) Log(r M) { 151 | if atomic.LoadInt32(&logger.closeFlag) == 1 { 152 | return 153 | } 154 | 155 | logger.closeMutex.RLock() 156 | defer logger.closeMutex.RUnlock() 157 | 158 | if atomic.LoadInt32(&logger.closeFlag) == 1 { 159 | return 160 | } 161 | 162 | logger.logChan <- r 163 | } 164 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package jsonlog 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/funny/utest" 8 | ) 9 | 10 | func Test_SwitchByDay(t *testing.T) { 11 | log, err := New(Config{ 12 | Dir: ".", 13 | Switcher: DAY_SWITCHER, 14 | FileType: ".log", 15 | }) 16 | utest.IsNilNow(t, err) 17 | log.Log(M{"Time": time.Now()}) 18 | log.Log(M{"Time": time.Now()}) 19 | log.Log(M{"Time": time.Now()}) 20 | log.Close() 21 | } 22 | 23 | func Test_SwitchByHours(t *testing.T) { 24 | log, err := New(Config{ 25 | Dir: ".", 26 | Switcher: HOURS_SWITCHER, 27 | FileType: ".log", 28 | }) 29 | utest.IsNilNow(t, err) 30 | log.Log(M{"Time": time.Now()}) 31 | log.Log(M{"Time": time.Now()}) 32 | log.Log(M{"Time": time.Now()}) 33 | log.Close() 34 | } 35 | -------------------------------------------------------------------------------- /switcher.go: -------------------------------------------------------------------------------- 1 | package jsonlog 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var ( 8 | DAY_SWITCHER = daySwitch{} // 按天切换文件 9 | HOURS_SWITCHER = hoursSwitch{} // 按小时切换文件 10 | ) 11 | 12 | type Switcher interface { 13 | FirstSwitchTime() time.Duration 14 | NextSwitchTime() time.Duration 15 | DirAndFileName(base string) (dir, file string) 16 | } 17 | 18 | type daySwitch struct{} 19 | 20 | func (_ daySwitch) FirstSwitchTime() time.Duration { 21 | // 到明天凌晨间隔多长时间 22 | now := time.Now() 23 | return time.Date( 24 | now.Year(), now.Month(), now.Day(), 25 | 0, 0, 0, 0, now.Location(), 26 | ).Add(24 * time.Hour).Sub(now) 27 | } 28 | 29 | func (_ daySwitch) NextSwitchTime() time.Duration { 30 | return 24 * time.Hour 31 | } 32 | 33 | func (_ daySwitch) DirAndFileName(base string) (dir, file string) { 34 | now := time.Now() 35 | dir = base + "/" + now.Format("2006-01/") 36 | file = dir + now.Format("2006-01-02") 37 | return 38 | } 39 | 40 | type hoursSwitch struct{} 41 | 42 | func (_ hoursSwitch) FirstSwitchTime() time.Duration { 43 | // 到下一个整点间隔多长时间 44 | now := time.Now() 45 | return time.Date( 46 | now.Year(), now.Month(), now.Day(), 47 | now.Hour(), 0, 0, 0, now.Location(), 48 | ).Add(time.Hour).Sub(now) 49 | } 50 | 51 | func (_ hoursSwitch) NextSwitchTime() time.Duration { 52 | return time.Hour 53 | } 54 | 55 | func (_ hoursSwitch) DirAndFileName(base string) (dir, file string) { 56 | now := time.Now() 57 | dir = base + "/" + now.Format("2006-01/2006-01-02/") 58 | file = dir + now.Format("2006-01-02_15") 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /tools/gzdog/README.md: -------------------------------------------------------------------------------- 1 | 说明 2 | ==== 3 | 4 | 在进程异常退出的时候,有可能出现日志文件没有正常关闭的情况,对于gz压缩的日志文件,会导致丢失最后没写入的部分数据和gzip的校验和。 5 | 6 | gzip校验和丢失会导致gunzip或gzcat等程序提示文件损坏而拒绝对文件进行加压操作。 7 | 8 | 但是实际上文件损坏的部分只有末尾没写入完整的部分,gzip算法有能力解压前面完整的数据。 9 | 10 | 所以我做了这个叫做gzdog的工具,一开始想叫gzcat但是名字被用了,cat不能叫咱叫dog还不成。 11 | 12 | 它和gzcat不一样的是它会尝试尽量的解压数据,直到gzip流解压失败,不会因为文件结尾有问题就完全不输出东西。 13 | 14 | 用法1,一次多个文件: 15 | 16 | ``` 17 | gzdog file1.log.gz file2.log.gz file3.log.gz 18 | ``` 19 | 20 | 用法2,从标准输入读取数据并解压: 21 | 22 | ``` 23 | gzdog < file1.log.gz 24 | ``` 25 | 26 | 默认会输出到标准输出,可以通过重定向输出把结果保存到文件或者传递给下个处理程序。 27 | -------------------------------------------------------------------------------- /tools/gzdog/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "compress/gzip" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | flag.Parse() 14 | files := flag.Args() 15 | bufWriter := bufio.NewWriter(os.Stdout) 16 | if len(files) > 0 { 17 | for _, file := range files { 18 | f, err := os.OpenFile(file, os.O_RDONLY, 0744) 19 | if err != nil { 20 | fmt.Errorf("Couldn't open file: %s\n", f) 21 | os.Exit(-1) 22 | } 23 | bufReader := bufio.NewReader(f) 24 | printGzip(bufReader, bufWriter) 25 | bufWriter.Flush() 26 | f.Close() 27 | } 28 | } else { 29 | bufReader := bufio.NewReader(os.Stdin) 30 | printGzip(bufReader, bufWriter) 31 | bufWriter.Flush() 32 | } 33 | } 34 | 35 | func printGzip(in io.Reader, out io.Writer) { 36 | gzReader, err := gzip.NewReader(in) 37 | if err != nil { 38 | panic(err) 39 | } 40 | defer gzReader.Close() 41 | 42 | p := make([]byte, 4096) 43 | for { 44 | n, err := gzReader.Read(p) 45 | if n > 0 { 46 | out.Write(p[0:n]) 47 | } 48 | if err != nil { 49 | break 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tools/logfmt/README.md: -------------------------------------------------------------------------------- 1 | 说明 2 | ==== 3 | 4 | jsonlog保存下来的json日志文件是一行一条json数据,当日志数据结构复杂的时候不方便肉眼阅读,所以我做了这个工具用来格式化json日志文件。 5 | 6 | 用法跟cat一样,可以一次输出多个文件也可以从标准输入读取数据并格式化输出。 7 | 8 | 用法示例: 9 | 10 | ``` 11 | logfmt file1.log file2.log 12 | 13 | logfmt < file1.log 14 | ``` 15 | 16 | 也可以配合gzdog命令一起使用: 17 | 18 | ``` 19 | gzdog file1.log.gz file2.log.gz | logfmt 20 | ``` 21 | -------------------------------------------------------------------------------- /tools/logfmt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | flag.Parse() 15 | files := flag.Args() 16 | bufWriter := bufio.NewWriter(os.Stdout) 17 | if len(files) > 0 { 18 | for _, file := range files { 19 | f, err := os.OpenFile(file, os.O_RDONLY, 0744) 20 | if err != nil { 21 | fmt.Errorf("Couldn't open file: %s\n", f) 22 | os.Exit(-1) 23 | } 24 | bufReader := bufio.NewReader(f) 25 | formatJson(bufReader, bufWriter) 26 | bufWriter.Flush() 27 | f.Close() 28 | } 29 | } else { 30 | bufReader := bufio.NewReader(os.Stdin) 31 | formatJson(bufReader, bufWriter) 32 | bufWriter.Flush() 33 | } 34 | } 35 | 36 | func formatJson(in *bufio.Reader, out io.Writer) { 37 | buf := new(bytes.Buffer) 38 | for { 39 | line, err := in.ReadBytes('\n') 40 | if err := json.Indent(buf, line, "", " "); err != nil { 41 | break 42 | } 43 | out.Write(buf.Bytes()) 44 | buf.Reset() 45 | if err != nil { 46 | break 47 | } 48 | } 49 | } 50 | --------------------------------------------------------------------------------