package blog4go
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
)
const (
// EOL end of a line
EOL = '\n'
// ESCAPE escape character
ESCAPE = '\\'
// PLACEHOLDER placeholder
PLACEHOLDER = '%'
)
var (
// blog is the singleton instance use for blog.write/writef
blog Writer
// global mutex log used for singlton
singltonLock *sync.Mutex
// DefaultBufferSize bufio buffer size
DefaultBufferSize = 4096 // default memory page size
// ErrInvalidFormat invalid format error
ErrInvalidFormat = errors.New("Invalid format type")
// ErrAlreadyInit show that blog is already initialized once
ErrAlreadyInit = errors.New("blog4go has been already initialized")
)
// Writer interface is a common definition of any writers in this package.
// Any struct implements Writer interface must implement functions below.
// Close is used for close the writer and free any elements if needed.
// write is an internal function that write pure message with specific
// logging level.
// writef is an internal function that formatting message with specific
// logging level. Placeholders in the format string will be replaced with
// args given.
// Both write and writef may have an asynchronous call of user defined
// function before write and writef function end..
type Writer interface {
// Close do anything end before program end
Close()
// SetLevel set logging level threshold
SetLevel(level LevelType)
// Level get log level
Level() LevelType
// write/writef functions with different levels
write(level LevelType, args ...interface{})
writef(level LevelType, format string, args ...interface{})
Debug(args ...interface{})
Debugf(format string, args ...interface{})
Trace(args ...interface{})
Tracef(format string, args ...interface{})
Info(args ...interface{})
Infof(format string, args ...interface{})
Warn(args ...interface{})
Warnf(format string, args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
Critical(args ...interface{})
Criticalf(format string, args ...interface{})
// flush log to disk
flush()
// hook
SetHook(hook Hook)
SetHookLevel(level LevelType)
SetHookAsync(async bool)
// logrotate
SetTimeRotated(timeRotated bool)
TimeRotated() bool
SetRotateSize(rotateSize int64)
RotateSize() int64
SetRotateLines(rotateLines int)
RotateLines() int
SetRetentions(retentions int64)
Retentions() int64
SetColored(colored bool)
Colored() bool
}
func init() {
singltonLock = new(sync.Mutex)
DefaultBufferSize = os.Getpagesize()
}
// NewWriterFromConfigAsFile initialize a writer according to given config file
// configFile must be the path to the config file
func NewWriterFromConfigAsFile(configFile string) (err error) {
singltonLock.Lock()
defer singltonLock.Unlock()
if nil != blog {
return ErrAlreadyInit
}
// read config from file
config, err := readConfig(configFile)
if nil != err {
return
}
if err = config.valid(); nil != err {
return
}
multiWriter := new(MultiWriter)
multiWriter.level = DEBUG
if level := LevelFromString(config.MinLevel); level.valid() {
multiWriter.level = level
}
multiWriter.closed = false
multiWriter.writers = make(map[LevelType]Writer)
for _, filter := range config.Filters {
var rotate = false
var timeRotate = false
var isSocket = false
var isConsole = false
var f *os.File
var blog *BLog
var fileLock *sync.RWMutex
// get file path
var filePath string
if (file{}) != filter.File {
// file do not need logrotate
filePath = filter.File.Path
rotate = false
f, err = os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0644))
if nil != err {
return err
}
blog = NewBLog(f)
fileLock = new(sync.RWMutex)
} else if (rotateFile{}) != filter.RotateFile {
// file need logrotate
filePath = filter.RotateFile.Path
rotate = true
timeRotate = TypeTimeBaseRotate == filter.RotateFile.Type
fileName := filePath
if timeRotate {
fileName = fmt.Sprintf("%s.%s", fileName, timeCache.Date())
}
f, err = os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0644))
if nil != err {
return err
}
blog = NewBLog(f)
fileLock = new(sync.RWMutex)
} else if (socket{}) != filter.Socket {
isSocket = true
} else {
// use console writer as default
isConsole = true
}
levels := strings.Split(filter.Levels, ",")
for _, levelStr := range levels {
var level LevelType
if level = LevelFromString(levelStr); !level.valid() {
return ErrInvalidLevel
}
if isConsole {
// console writer
writer, err := newConsoleWriter(filter.Console.Redirect)
if nil != err {
return err
}
multiWriter.writers[level] = writer
continue
}
if isSocket {
// socket writer
writer, err := newSocketWriter(filter.Socket.Network, filter.Socket.Address)
if nil != err {
return err
}
multiWriter.writers[level] = writer
continue
}
// init a base file writer
writer, err := newBaseFileWriter(filePath, timeRotate)
if nil != err {
return err
}
if rotate {
// set logrotate strategy
if TypeTimeBaseRotate == filter.RotateFile.Type {
writer.SetTimeRotated(true)
writer.SetRetentions(filter.RotateFile.Retentions)
} else if TypeSizeBaseRotate == filter.RotateFile.Type {
writer.SetRotateSize(filter.RotateFile.RotateSize)
writer.SetRotateLines(filter.RotateFile.RotateLines)
writer.SetRetentions(filter.RotateFile.Retentions)
} else {
return ErrInvalidRotateType
}
}
writer.file = f
writer.blog = blog
writer.lock = fileLock
// set color
multiWriter.SetColored(filter.Colored)
multiWriter.writers[level] = writer
}
}
blog = multiWriter
return
}
// BLog struct is a threadsafe log writer inherit bufio.Writer
type BLog struct {
// logging level
// every message level exceed this level will be written
level LevelType
// input io
in io.Writer
// bufio.Writer object of the input io
writer *bufio.Writer
// exclusive lock while calling write function of bufio.Writer
lock *sync.Mutex
// closed tag
closed bool
}
// NewBLog create a BLog instance and return the pointer of it.
// fileName must be an absolute path to the destination log file
func NewBLog(in io.Writer) (blog *BLog) {
blog = new(BLog)
blog.in = in
blog.level = TRACE
blog.lock = new(sync.Mutex)
blog.closed = false
blog.writer = bufio.NewWriterSize(in, DefaultBufferSize)
return
}
// write writes pure message with specific level
func (blog *BLog) write(level LevelType, args ...interface{}) int {
blog.lock.Lock()
defer blog.lock.Unlock()
// 统计日志size
var size = 0
format := fmt.Sprint(args...)
blog.writer.Write(timeCache.Format())
blog.writer.WriteString(level.prefix())
blog.writer.WriteString(format)
blog.writer.WriteByte(EOL)
size = len(timeCache.Format()) + len(level.prefix()) + len(format) + 1
return size
}
// write formats message with specific level and write it
func (blog *BLog) writef(level LevelType, format string, args ...interface{}) int {
// 格式化构造message
// 边解析边输出
// 使用 % 作占位符
blog.lock.Lock()
defer blog.lock.Unlock()
// 统计日志size
var size = 0
// 识别占位符标记
var tag = false
var tagPos int
// 转义字符标记
var escape = false
// 在处理的args 下标
var n int
// 未输出的,第一个普通字符位置
var last int
var s int
blog.writer.Write(timeCache.Format())
blog.writer.WriteString(level.prefix())
size += len(timeCache.Format()) + len(level.prefix())
for i, v := range format {
if tag {
switch v {
case 'd', 'f', 'v', 'b', 'o', 'x', 'X', 'c', 'p', 't', 's', 'T', 'q', 'U', 'e', 'E', 'g', 'G':
if escape {
escape = false
}
s, _ = blog.writer.WriteString(fmt.Sprintf(format[tagPos:i+1], args[n]))
size += s
n++
last = i + 1
tag = false
//转义符
case ESCAPE:
if escape {
blog.writer.WriteByte(ESCAPE)
size++
}
escape = !escape
//默认
default:
}
} else {
// 占位符,百分号
if PLACEHOLDER == format[i] && !escape {
tag = true
tagPos = i
s, _ = blog.writer.WriteString(format[last:i])
size += s
escape = false
}
}
}
blog.writer.WriteString(format[last:])
blog.writer.WriteByte(EOL)
size += len(format[last:]) + 1
return size
}
// Flush flush buffer to disk
func (blog *BLog) flush() {
blog.lock.Lock()
defer blog.lock.Unlock()
if blog.closed {
return
}
blog.writer.Flush()
}
// Close close file writer
func (blog *BLog) Close() {
blog.lock.Lock()
defer blog.lock.Unlock()
if nil == blog || blog.closed {
return
}
blog.closed = true
blog.writer.Flush()
blog.writer = nil
}
// In return the input io.Writer
func (blog *BLog) In() io.Writer {
return blog.in
}
// Level return logging level threshold
func (blog *BLog) Level() LevelType {
return blog.level
}
// SetLevel set logging level threshold
func (blog *BLog) SetLevel(level LevelType) *BLog {
blog.level = level
return blog
}
// resetFile resets file descriptor of the writer with specific file name
func (blog *BLog) resetFile(in io.Writer) (err error) {
blog.lock.Lock()
defer blog.lock.Unlock()
blog.writer.Flush()
blog.in = in
blog.writer.Reset(in)
return
}
// SetBufferSize set bufio buffer size in bytes
func SetBufferSize(size int) {
DefaultBufferSize = size
}
// Level get log level
func Level() LevelType {
return blog.Level()
}
// SetLevel set level for logging action
func SetLevel(level LevelType) {
blog.SetLevel(level)
}
// SetHook set hook for logging action
func SetHook(hook Hook) {
blog.SetHook(hook)
}
// SetHookLevel set when hook will be called
func SetHookLevel(level LevelType) {
blog.SetHookLevel(level)
}
// SetHookAsync set whether hook is called async
func SetHookAsync(async bool) {
blog.SetHookAsync(async)
}
// Colored get whether it is log with colored
func Colored() bool {
return blog.Colored()
}
// SetColored set logging color
func SetColored(colored bool) {
blog.SetColored(colored)
}
// TimeRotated get timeRotated
func TimeRotated() bool {
return blog.TimeRotated()
}
// SetTimeRotated toggle time base logrotate on the fly
func SetTimeRotated(timeRotated bool) {
blog.SetTimeRotated(timeRotated)
}
// Retentions get retentions
func Retentions() int64 {
return blog.Retentions()
}
// SetRetentions set how many logs will keep after logrotate
func SetRetentions(retentions int64) {
blog.SetRetentions(retentions)
}
// RotateSize get rotateSize
func RotateSize() int64 {
return blog.RotateSize()
}
// SetRotateSize set size when logroatate
func SetRotateSize(rotateSize int64) {
blog.SetRotateSize(rotateSize)
}
// RotateLines get rotateLines
func RotateLines() int {
return blog.RotateLines()
}
// SetRotateLines set line number when logrotate
func SetRotateLines(rotateLines int) {
blog.SetRotateLines(rotateLines)
}
// Flush flush logs to disk
func Flush() {
blog.flush()
}
// Trace static function for Trace
func Trace(args ...interface{}) {
blog.Trace(args...)
}
// Tracef static function for Tracef
func Tracef(format string, args ...interface{}) {
blog.Tracef(format, args...)
}
// Debug static function for Debug
func Debug(args ...interface{}) {
blog.Debug(args...)
}
// Debugf static function for Debugf
func Debugf(format string, args ...interface{}) {
blog.Debugf(format, args...)
}
// Info static function for Info
func Info(args ...interface{}) {
blog.Info(args...)
}
// Infof static function for Infof
func Infof(format string, args ...interface{}) {
blog.Infof(format, args...)
}
// Warn static function for Warn
func Warn(args ...interface{}) {
blog.Warn(args...)
}
// Warnf static function for Warnf
func Warnf(format string, args ...interface{}) {
blog.Warnf(format, args...)
}
// Error static function for Error
func Error(args ...interface{}) {
blog.Error(args...)
}
// Errorf static function for Errorf
func Errorf(format string, args ...interface{}) {
blog.Errorf(format, args...)
}
// Critical static function for Critical
func Critical(args ...interface{}) {
blog.Critical(args...)
}
// Criticalf static function for Criticalf
func Criticalf(format string, args ...interface{}) {
blog.Criticalf(format, args...)
}
// Close close the logger
func Close() {
singltonLock.Lock()
defer singltonLock.Unlock()
if nil == blog {
return
}
blog.Close()
blog = nil
}