From e9d16bf93cded9572cddc1ee88c1244f4cecf219 Mon Sep 17 00:00:00 2001 From: Jansen Date: Fri, 19 Jun 2020 14:34:21 +0800 Subject: [PATCH] refactoring log rotate logic --- log/file_writer.go | 156 +++++++-------------------- log/interface.go | 1 - log/log_dup_linux.go | 14 +-- log/log_writer.go | 242 ++++++++++++------------------------------ log/logger.go | 85 +-------------- log/option.go | 49 +++++++++ log/writer_manager.go | 113 ++++++++++++++++++++ test/log/log_test.go | 2 + 8 files changed, 278 insertions(+), 384 deletions(-) create mode 100644 log/option.go create mode 100644 log/writer_manager.go diff --git a/log/file_writer.go b/log/file_writer.go index 7a7a10e..cd982d7 100644 --- a/log/file_writer.go +++ b/log/file_writer.go @@ -2,7 +2,6 @@ package log import ( "bufio" - "bytes" "errors" "fmt" syslog "log" @@ -11,22 +10,23 @@ import ( "time" ) -var pathVariableTable map[byte]func(*time.Time) int - // fileWriter 文件输出器,将日志记录输出到文件中 type fileWriter struct { - filebasename string - pathFmt string - file *os.File - fileBufWriter *bufio.Writer - actions []func(*time.Time) int - variables []interface{} - RedirectError bool // 是否重定向错误信息到日志文件 + filePath string + lastRoateTail string + rotateTimeLayout string + file *os.File + fileBufWriter *bufio.Writer + variables []interface{} + redirectError bool // 是否重定向错误信息到日志文件 } // newFileWriter 构造一个文件输出器 -func newFileWriter() *fileWriter { - return &fileWriter{} +func newFileWriter(filePath string, rotateTimeLayout string) *fileWriter { + return &fileWriter{ + filePath: filePath, + rotateTimeLayout: rotateTimeLayout, + } } // Init 初始化文件输出器 @@ -34,47 +34,6 @@ func (w *fileWriter) Init() error { return w.Rotate() } -// SetPathPattern 设置文件路径 -func (w *fileWriter) SetPathPattern(filebasename string, pattern string) error { - n := 0 - for _, c := range pattern { - if c == '%' { - n++ - } - } - - if n == 0 { - w.filebasename = filebasename - w.pathFmt = pattern - return nil - } - - w.actions = make([]func(*time.Time) int, 0, n) - w.variables = make([]interface{}, n) - tmp := []byte(pattern) - - variable := 0 - for _, c := range tmp { - if variable == 1 { - act, ok := pathVariableTable[c] - if !ok { - return errors.New("Invalid rotate pattern (" + pattern + ")") - } - w.actions = append(w.actions, act) - variable = 0 - continue - } - if c == '%' { - variable = 1 - } - } - - w.filebasename = filebasename - w.pathFmt = convertPatternToFmt(tmp) - - return nil -} - // Write 写入一条日志记录 func (w *fileWriter) Write(r *Record) error { if w.fileBufWriter == nil { @@ -99,22 +58,13 @@ func (w *fileWriter) RotateByTime(t *time.Time) error { // doRotate 尝试转储文件,如果不需要进行转储,返回 nil func (w *fileWriter) doRotate(t *time.Time) error { - v := 0 - rotate := false - - for i, act := range w.actions { - v = act(t) - if v != w.variables[i] { - w.variables[i] = v - rotate = true - } + if w.rotateTimeLayout == "" { + return w.initFile(w.filePath) } - // fmt.Printf("start rotate file,actions:%d,%d,%d,%d,%d\n", len(w.actions), w.variables[0], w.variables[1], w.variables[2], w.variables[3]) - - if !rotate { + newRotateTail := t.Format(w.rotateTimeLayout) + if newRotateTail == w.lastRoateTail { return nil } - // fmt.Printf("start rotate file,actions:%d,%d,%d,%d,%d\n", len(w.actions), w.variables[0], w.variables[1], w.variables[2], w.variables[3]) if w.fileBufWriter != nil { if err := w.fileBufWriter.Flush(); err != nil { @@ -128,8 +78,16 @@ func (w *fileWriter) doRotate(t *time.Time) error { } } - filePath := fmt.Sprintf(w.pathFmt, w.variables...) + filePath := fmt.Sprint(w.filePath, ".", newRotateTail) + if err := w.initFile(filePath); err != nil { + syslog.Println(err) + return err + } + w.lastRoateTail = newRotateTail + return nil +} +func (w *fileWriter) initFile(filePath string) error { if err := os.MkdirAll(path.Dir(filePath), 0755); err != nil { if !os.IsExist(err) { return err @@ -139,11 +97,12 @@ func (w *fileWriter) doRotate(t *time.Time) error { // 这是真正的日志文件 file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { + syslog.Println(filePath, err) return err } w.file = file - if w.RedirectError { + if w.redirectError { // 把错误重定向到日志文件来 sysDup(int(w.file.Fd())) } @@ -153,66 +112,27 @@ func (w *fileWriter) doRotate(t *time.Time) error { } // 创建一个软链接 - // 检查文件是存在 - _, fileerr := os.Stat(w.filebasename) - if fileerr == nil { // 文件存在 - os.Remove(w.filebasename) - } - // Create a symlink - { - err := os.Symlink(path.Base(filePath), w.filebasename) + if filePath != w.filePath { + // 检查文件是存在 + _, fileerr := os.Stat(w.filePath) + if fileerr == nil { + // this file already exist + os.Remove(w.filePath) + } + // Create a symlink + err := os.Symlink(path.Base(filePath), w.filePath) if err != nil { - syslog.Println(err.Error()) + syslog.Println("os.Symlink error:", err.Error()) // return err } } - return nil } -// Flush 将文件缓冲区中的内容 Flush +// Flush 将文件缓冲区中的内容 func (w *fileWriter) Flush() error { if w.fileBufWriter != nil { return w.fileBufWriter.Flush() } return nil } - -func getYear(now *time.Time) int { - return now.Year() % 100 -} - -func getMonth(now *time.Time) int { - return int(now.Month()) -} - -func getDay(now *time.Time) int { - return now.Day() -} - -func getHour(now *time.Time) int { - return now.Hour() -} - -func getMin(now *time.Time) int { - return now.Minute() -} - -func convertPatternToFmt(pattern []byte) string { - pattern = bytes.Replace(pattern, []byte("%Y"), []byte("%d"), -1) - pattern = bytes.Replace(pattern, []byte("%M"), []byte("%02d"), -1) - pattern = bytes.Replace(pattern, []byte("%D"), []byte("%02d"), -1) - pattern = bytes.Replace(pattern, []byte("%H"), []byte("%02d"), -1) - pattern = bytes.Replace(pattern, []byte("%m"), []byte("%02d"), -1) - return string(pattern) -} - -// init 初始化获取指定时间元素的函数列表等 -func init() { - pathVariableTable = make(map[byte]func(*time.Time) int, 5) - pathVariableTable['Y'] = getYear - pathVariableTable['M'] = getMonth - pathVariableTable['D'] = getDay - pathVariableTable['H'] = getHour - pathVariableTable['m'] = getMin -} diff --git a/log/interface.go b/log/interface.go index da02cec..66e5fae 100644 --- a/log/interface.go +++ b/log/interface.go @@ -34,7 +34,6 @@ type Writer interface { type Rotater interface { Rotate() error RotateByTime(*time.Time) error - SetPathPattern(string, string) error } // Flusher 刷新输出 diff --git a/log/log_dup_linux.go b/log/log_dup_linux.go index 1744744..2c6e5be 100644 --- a/log/log_dup_linux.go +++ b/log/log_dup_linux.go @@ -7,13 +7,13 @@ import ( // sysDup 将进程致命错误转储 func sysDup(fd int) error { - // { - // err := syscall.Dup2(fd, 1) - // if err != nil { - // syslog.Println(err.Error()) - // // return err - // } - // } + { + err := syscall.Dup2(fd, 1) + if err != nil { + syslog.Println(err.Error()) + // return err + } + } { err := syscall.Dup2(fd, 2) if err != nil { diff --git a/log/log_writer.go b/log/log_writer.go index ab0eca3..1762a95 100644 --- a/log/log_writer.go +++ b/log/log_writer.go @@ -1,10 +1,8 @@ package log import ( - "errors" + "fmt" syslog "log" - "strings" - "sync" "time" ) @@ -15,176 +13,41 @@ const ( writerTypeFile = 2 ) -// writerCfg config of log writer -type writerCfg struct { - w Writer - f Flusher -} - -// writerManager writer manager -type writerManager struct { - ws []*writerCfg - l sync.Mutex -} - -func (wm *writerManager) AddWriter(w Writer) { - wm.l.Lock() - defer wm.l.Unlock() - - var f Flusher - if fi, ok := w.(Flusher); ok { - f = fi - } - - wm.ws = append(wm.ws, &writerCfg{ - w: w, - f: f, - }) -} - -func (wm *writerManager) Flush() error { - var errs []string - for _, w := range wm.ws { - if w.f != nil { - err := w.f.Flush() - if err != nil { - errs = append(errs, err.Error()) - } - } - } - if len(errs) <= 0 { - return nil - } - return errors.New("error list: " + strings.Join(errs, "; ")) -} - -func (wm *writerManager) Write(r *Record) error { - var errs []string - for _, w := range wm.ws { - if w.w != nil { - err := w.w.Write(r) - if err != nil { - errs = append(errs, err.Error()) - } - } - } - if len(errs) <= 0 { - return nil - } - return errors.New("error list: " + strings.Join(errs, "; ")) -} - -func (wm *writerManager) RenameFile(filebasename, pattern string) error { - wm.l.Lock() - defer wm.l.Unlock() - - var errs []string - for _, w := range wm.ws { - if r, ok := w.w.(Rotater); ok { - err := r.SetPathPattern(filebasename, pattern) - if err != nil { - errs = append(errs, err.Error()) - continue - } - err = r.Rotate() - if err != nil { - errs = append(errs, err.Error()) - continue - } - } - } - if len(errs) <= 0 { - return nil - } - return errors.New("error list: " + strings.Join(errs, "; ")) -} - -func (wm *writerManager) RotateByTime(t *time.Time) error { - wm.l.Lock() - defer wm.l.Unlock() - - var errs []string - for _, w := range wm.ws { - if r, ok := w.w.(Rotater); ok { - if err := r.RotateByTime(t); err != nil { - errs = append(errs, err.Error()) - continue - } - } - } - if len(errs) <= 0 { - return nil - } - return errors.New("error list: " + strings.Join(errs, "; ")) -} - -func (wm *writerManager) Rotate() error { - wm.l.Lock() - defer wm.l.Unlock() - - var errs []string - for _, w := range wm.ws { - if r, ok := w.w.(Rotater); ok { - if err := r.Rotate(); err != nil { - errs = append(errs, err.Error()) - continue - } - } - } - if len(errs) <= 0 { - return nil - } - return errors.New("error list: " + strings.Join(errs, "; ")) -} - // recordWriter log写入器 type recordWriter struct { wm *writerManager tunnel chan *Record c chan bool stopchan chan struct{} - opt *Options lastRecordTimeUnix60 int64 lastRecordTime time.Time + lastTime int64 + lastTimeStr string } // Init 初始化log写入器 func (rec *recordWriter) Init(opt *Options) { - rec.opt = opt rec.wm = &writerManager{} rec.tunnel = make(chan *Record, tunnelSizeDefault) rec.stopchan = make(chan struct{}) rec.c = make(chan bool, 1) - go rec.boostrapLogWriter() + go rec.boostrapLogWriter(opt) } // AddLogFile 增加一个文件输出器 -func (rec *recordWriter) AddLogFile(filename string) error { - // fmt.Printf("log filename,%s \n", filename) - filebasename := filename - filename += ".%Y%M%D-%H" - w := newFileWriter() - w.RedirectError = rec.opt.RedirectError - err := w.SetPathPattern(filebasename, filename) - if err != nil { - return err - } - err = w.Init() +func (rec *recordWriter) AddLogFile(filePath string, opt *Options) error { + w := newFileWriter(filePath, opt.RotateTimeLayout) + w.redirectError = opt.RedirectError + err := w.Init() if err != nil { + syslog.Println(err) return err } rec.registerLogWriter(w) return nil } -// ChangeLogFile 修改所有的文件输出器的目标文件 -func (rec *recordWriter) ChangeLogFile(filename string) { - filebasename := filename - filename += ".%Y%M%D-%H" - rec.wm.RenameFile(filebasename, filename) -} - // registerLogWriter 注册一个文件输出器到该 log 写入器中 func (rec *recordWriter) registerLogWriter(w Writer) { rec.wm.AddWriter(w) @@ -197,7 +60,6 @@ func (rec *recordWriter) Close() { return default: close(rec.stopchan) - // close(rec.tunnel) break } select { @@ -210,8 +72,42 @@ func (rec *recordWriter) Close() { } } +func (rec *recordWriter) deliverRecord(opt *Options, level Level, format string, args ...interface{}) { + var inf, code string + // 检查日志等级有效性 + if level < opt.Level { + return + } + // 连接主题 + if opt.Topic != "" { + inf += opt.Topic + " " + } + // 连接格式化内容 + if format != "" { + inf += fmt.Sprintf(format, args...) + } else { + inf += fmt.Sprint(args...) + } + // format time + now := time.Now() + if now.Unix() != rec.lastTime { + rec.lastTime = now.Unix() + rec.lastTimeStr = now.Format(opt.RecordTimeLayout) + } + // record to recorder + r := recordPool.Get().(*Record) + r.info = inf + r.code = code + r.time = rec.lastTimeStr + r.level = level + r.name = opt.Name + r.timeUnix = rec.lastTime + + rec.write(r, opt) +} + // write 写入一条日志记录,等待后续异步处理 -func (rec *recordWriter) write(r *Record) { +func (rec *recordWriter) write(r *Record, opt *Options) { select { case <-rec.stopchan: return @@ -219,8 +115,8 @@ func (rec *recordWriter) write(r *Record) { } // no async - if !rec.opt.AsyncWrite { - rec.doWriteRecord(r) + if !opt.AsyncWrite { + rec.doWriteRecord(r, opt) return } @@ -233,24 +129,14 @@ func (rec *recordWriter) write(r *Record) { } // boostrapLogWriter 日志写入线程 -func (rec *recordWriter) boostrapLogWriter() { +func (rec *recordWriter) boostrapLogWriter(opt *Options) { var ( r *Record ok bool ) - if r, ok = <-rec.tunnel; !ok { - rec.c <- true - return - } - - if err := rec.wm.Write(r); err != nil { - syslog.Println("boostrapLogWriter w.Write error", err) - } - - flushTimer := time.NewTimer(time.Millisecond * 500) + flushTimer := time.NewTimer(opt.AsyncWriteDuration) rotateTimer := time.NewTimer(time.Millisecond * 100) - // rotateTimer := time.NewTimer(time.Second * 10) for { select { case r, ok = <-rec.tunnel: @@ -258,28 +144,26 @@ func (rec *recordWriter) boostrapLogWriter() { rec.c <- true return } - rec.doWriteRecord(r) + rec.doWriteRecord(r, opt) case <-rec.stopchan: rec.c <- true return case <-flushTimer.C: - // for _, w := range rec.writers { - // if f, ok := w.(Flusher); ok { - // if err := f.Flush(); err != nil { - // syslog.Println("boostrapLogWriter f.Flush error", err) - // } - // } - // } - // flushTimer.Reset(time.Millisecond * 500) + if opt.AsyncWrite { + err := rec.wm.Flush() + if err != nil { + syslog.Println(err) + } + flushTimer.Reset(opt.AsyncWriteDuration) + } case <-rotateTimer.C: - // fmt.Printf("start rotate file,actions, 1111\n") rec.tryRotate() rotateTimer.Reset(time.Second * 60) } } } -func (rec *recordWriter) doWriteRecord(r *Record) error { +func (rec *recordWriter) doWriteRecord(r *Record, opt *Options) error { needTryRotate := false // 如果上一条记录的时间和这条记录的时间不是同一分钟,需要尝试增加log文件 nowTimeUnix60 := r.timeUnix / 60 @@ -298,13 +182,17 @@ func (rec *recordWriter) doWriteRecord(r *Record) error { } // 写入log if err := rec.wm.Write(r); err != nil { - syslog.Println("boostrapLogWriter for w.Write error", err) + syslog.Println("doWriteRecord for w.Write error", err) return err } recordPool.Put(r) - if err := rec.wm.Flush(); err != nil { - syslog.Println("boostrapLogWriter f.Flush error", err) - return err + + // flush + if !opt.AsyncWrite { + if err := rec.wm.Flush(); err != nil { + syslog.Println("doWriteRecord f.Flush error", err) + return err + } } return nil } diff --git a/log/logger.go b/log/logger.go index 4e5b17f..6cd176a 100644 --- a/log/logger.go +++ b/log/logger.go @@ -1,60 +1,13 @@ package log import ( - "fmt" "strings" - "time" ) // Logger 日志实例 type Logger struct { - logWriter *recordWriter - lastTime int64 - lastTimeStr string - opt Options -} - -// Options of log -type Options struct { - // NoConsole while remove console out put, default false. - NoConsole bool - // NoConsoleColor whil disable console output color, default false. - NoConsoleColor bool - // FilePaths is the log output file path, default none log file. - FilePaths []string - // RecordTimeLayout use for (time.Time).Format(layout string) record time field, default "060102-15:04:05", - // will not be empty. - RecordTimeLayout string - // Level log record level limit, only higer thie level log can be get reach Writer, default SYS. - Level Level - // Name is thie logger name, default "". - Name string - // Topic is thie logger topic, default "". - Topic string - // AsyncWrite while asynchronously output the log record to Write, it may be more performance, - // but if you exit(e.g. os.Exit(1), main() return) this process, it may be loss some log record, - // because they didn't have time to Write and flush to file. - AsyncWrite bool - // AsyncWriteDuration only effective when AsyncWrite is true, this is time duration of asynchronously - // check log output to write default 100ms. - AsyncWriteDuration time.Duration - // RedirectError duplicate stderr to log file, it will be call syscall.Dup2 in linux or syscall.DuplicateHandle - // in windows, default false. - RedirectError bool -} - -var defaultOptions = Options{ - RecordTimeLayout: "060102-15:04:05", -} - -// Check options -func (o *Options) Check() { - if o.RecordTimeLayout == "" { - o.RecordTimeLayout = "060102-15:04:05" - } - if o.AsyncWrite && o.AsyncWriteDuration.Milliseconds() == 0 { - o.AsyncWriteDuration = time.Millisecond * 100 - } + logWriter *recordWriter + opt Options } // NewLogger 构造一个日志 @@ -70,7 +23,7 @@ func NewLogger(optp *Options) *Logger { l.logWriter.Init(&l.opt) for _, path := range l.opt.FilePaths { - l.logWriter.AddLogFile(path) + l.logWriter.AddLogFile(path, &l.opt) } if !l.opt.NoConsole { @@ -218,37 +171,7 @@ func (l *Logger) deliverRecordToWriter(level Level, format string, args ...inter defaultLogger.deliverRecordToWriter(level, format, args...) return } - var inf, code string - // 检查日志等级有效性 - if level < l.opt.Level { - return - } - // 连接主题 - if l.opt.Topic != "" { - inf += l.opt.Topic + " " - } - // 连接格式化内容 - if format != "" { - inf += fmt.Sprintf(format, args...) - } else { - inf += fmt.Sprint(args...) - } - // format time - now := time.Now() - if now.Unix() != l.lastTime { - l.lastTime = now.Unix() - l.lastTimeStr = now.Format(l.opt.RecordTimeLayout) - } - // record to recorder - r := recordPool.Get().(*Record) - r.info = inf - r.code = code - r.time = l.lastTimeStr - r.level = level - r.name = l.opt.Name - r.timeUnix = l.lastTime - - l.logWriter.write(r) + l.logWriter.deliverRecord(&l.opt, level, format, args...) } // GetLogger 获取当前 Logger 的 Logger ,意义在于会进行接收器 Logger 是否为空的判断, diff --git a/log/option.go b/log/option.go new file mode 100644 index 0000000..055808c --- /dev/null +++ b/log/option.go @@ -0,0 +1,49 @@ +package log + +import "time" + +// Options of log +type Options struct { + // NoConsole while remove console out put, default false. + NoConsole bool + // NoConsoleColor whil disable console output color, default false. + NoConsoleColor bool + // FilePaths is the log output file path, default none log file. + FilePaths []string + // RecordTimeLayout use for (time.Time).Format(layout string) record time field, default "060102-15:04:05", + // will not be empty. + RecordTimeLayout string + // Level log record level limit, only higer thie level log can be get reach Writer, default SYS. + Level Level + // Name is thie logger name, default "". + Name string + // Topic is thie logger topic, default "". + Topic string + // AsyncWrite while asynchronously output the log record to Write, it may be more performance, + // but if you exit(e.g. os.Exit(1), main() return) this process, it may be loss some log record, + // because they didn't have time to Write and flush to file. + AsyncWrite bool + // AsyncWriteDuration only effective when AsyncWrite is true, this is time duration of asynchronously + // check log output to write default 100ms. + AsyncWriteDuration time.Duration + // RedirectError duplicate stderr to log file, it will be call syscall.Dup2 in linux or syscall.DuplicateHandle + // in windows, default false. + RedirectError bool + // RotateTimeLayout use of (time.Time).Format(layout string) to check if a roteta file is required. + // default "", will disable rotate. Highest accuracy is minutes. + RotateTimeLayout string +} + +var defaultOptions = Options{ + RecordTimeLayout: "060102-15:04:05", +} + +// Check options +func (o *Options) Check() { + if o.RecordTimeLayout == "" { + o.RecordTimeLayout = "060102-15:04:05" + } + if o.AsyncWrite && o.AsyncWriteDuration.Milliseconds() == 0 { + o.AsyncWriteDuration = time.Millisecond * 100 + } +} diff --git a/log/writer_manager.go b/log/writer_manager.go new file mode 100644 index 0000000..90d43c2 --- /dev/null +++ b/log/writer_manager.go @@ -0,0 +1,113 @@ +package log + +import ( + "errors" + "strings" + "sync" + "time" +) + +// writerCfg config of log writer +type writerCfg struct { + w Writer + f Flusher + r Rotater +} + +func newWriterCfg(w Writer) *writerCfg { + cfg := &writerCfg{ + w: w, + } + + if v, ok := w.(Flusher); ok { + cfg.f = v + } + + if v, ok := w.(Rotater); ok { + cfg.r = v + } + return cfg +} + +// writerManager writer manager +type writerManager struct { + ws []*writerCfg + l sync.Mutex +} + +func (wm *writerManager) AddWriter(w Writer) { + cfg := newWriterCfg(w) + wm.l.Lock() + defer wm.l.Unlock() + wm.ws = append(wm.ws, cfg) +} + +func (wm *writerManager) Flush() error { + var errs []string + for _, w := range wm.ws { + if w.f != nil { + err := w.f.Flush() + if err != nil { + errs = append(errs, err.Error()) + } + } + } + if len(errs) <= 0 { + return nil + } + return errors.New("error list: " + strings.Join(errs, "; ")) +} + +func (wm *writerManager) Write(r *Record) error { + var errs []string + for _, w := range wm.ws { + if w.w != nil { + err := w.w.Write(r) + if err != nil { + errs = append(errs, err.Error()) + } + } + } + if len(errs) <= 0 { + return nil + } + return errors.New("error list: " + strings.Join(errs, "; ")) +} + +func (wm *writerManager) RotateByTime(t *time.Time) error { + wm.l.Lock() + defer wm.l.Unlock() + + var errs []string + for _, w := range wm.ws { + if w.r != nil { + if err := w.r.RotateByTime(t); err != nil { + errs = append(errs, err.Error()) + continue + } + } + } + if len(errs) <= 0 { + return nil + } + return errors.New("error list: " + strings.Join(errs, "; ")) +} + +func (wm *writerManager) Rotate() error { + wm.l.Lock() + defer wm.l.Unlock() + + var errs []string + for _, w := range wm.ws { + if w.r != nil { + if err := w.r.Rotate(); err != nil { + errs = append(errs, err.Error()) + continue + } + } + } + if len(errs) <= 0 { + return nil + } + return errors.New("error list: " + strings.Join(errs, "; ")) +} diff --git a/test/log/log_test.go b/test/log/log_test.go index 2b4c19b..4ac2558 100644 --- a/test/log/log_test.go +++ b/test/log/log_test.go @@ -12,7 +12,9 @@ func Test_LogFileFlush(t *testing.T) { log.SetDefaultLogger(log.NewLogger(&log.Options{ FilePaths: []string{logPath}, // AsyncWrite: true, + RotateTimeLayout: "060102", })) + log.Syslog("test") log.Info("test") log.Debug("test")