ccpaging/nxlog4go

View on GitHub
file/file.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright (C) 2017, ccpaging <ccpaging@gmail.com>.  All rights reserved.

package file

import (
    "bytes"
    "fmt"
    "os"
    "path/filepath"
    "strings"
    "sync"
    "time"

    l4g "github.com/ccpaging/nxlog4go"
    "github.com/ccpaging/nxlog4go/cast"
    "github.com/ccpaging/nxlog4go/driver"
    "github.com/ccpaging/nxlog4go/patt"
)

var (
    dayToSecs int64 = 86400
)

// Appender represents the log appender that sends output to a file
type Appender struct {
    mu       sync.Mutex            // ensures atomic writes; protects the following fields
    rec      chan *driver.Recorder // entry channel
    runOnce  sync.Once
    waitExit *sync.WaitGroup

    level  int
    layout driver.Layout // format entry for output

    out    *RotateFile
    rotate int // rolling number. -1, no rotate; 0, no backup; 1 ... n, backup n log files

    // If cycle < dayToSecs, check interval in seconds.
    // Otherwise, check every (cycle / dayToSecs) day
    cycle int64
    delay int64 // delay seconds since midnight
    reset chan time.Time
}

/* Bytes Buffer */
var bufferPool *sync.Pool

func init() {
    bufferPool = &sync.Pool{
        New: func() interface{} {
            return new(bytes.Buffer)
        },
    }

    driver.Register("file", &Appender{})
}

// NewAppender creates a new file appender which writes to the file
// named '<exe full path base name>.log'., and without rotating as default.
func NewAppender(filename string, args ...interface{}) (*Appender, error) {
    if filename == "" {
        base := filepath.Base(os.Args[0])
        filename = strings.TrimSuffix(base, filepath.Ext(base)) + ".log"
    }

    fa := &Appender{
        rec: make(chan *driver.Recorder, 32),

        layout: patt.NewLayout(""),

        out: NewRotateFile(filename),

        rotate: -1,

        cycle: dayToSecs,
        reset: make(chan time.Time, 8),
    }

    fa.SetOptions(args...)
    return fa, nil
}

// Open creates a new appender which writes to the file
// named '<exe full path base name>.log', and without rotating as default.
func (*Appender) Open(filename string, args ...interface{}) (driver.Appender, error) {
    return NewAppender(filename, args...)
}

// Layout returns the output layout for the appender.
func (fa *Appender) Layout() driver.Layout {
    fa.mu.Lock()
    defer fa.mu.Unlock()
    return fa.layout
}

// SetLayout sets the output layout for the appender.
func (fa *Appender) SetLayout(layout driver.Layout) *Appender {
    fa.mu.Lock()
    defer fa.mu.Unlock()
    fa.layout = layout
    return fa
}

// SetOptions sets name-value pair options.
//
// Return the appender.
func (fa *Appender) SetOptions(args ...interface{}) *Appender {
    ops, idx, _ := driver.ArgsToMap(args...)
    for _, k := range idx {
        fa.Set(k, ops[k])
    }
    return fa
}

// Enabled encodes log Recorder and output it.
func (fa *Appender) Enabled(r *driver.Recorder) bool {
    // r.Level < fa.level
    if fa.level != 0 && r.Level < fa.level {
        return false
    }

    fa.runOnce.Do(func() {
        fa.waitExit = &sync.WaitGroup{}
        fa.waitExit.Add(1)
        go fa.run(fa.waitExit)
    })

    if fa.waitExit == nil {
        // Closed alreay
        fa.output(r)
        return false
    }

    fa.rec <- r
    return false
}

// Write is the filter's output method. This will block if the output
// buffer is full.
func (fa *Appender) Write(b []byte) (int, error) {
    return 0, nil
}

func newRotateTimer(cycle int64, delay int64) *time.Timer {
    if cycle <= 0 {
        cycle = dayToSecs
    }

    if cycle < dayToSecs {
        d := time.Duration(cycle) * time.Second
        l4g.LogLogTrace("rotate after %v", d)
        return time.NewTimer(d)
    }

    // now + cycle
    t := time.Now().Add(time.Duration(cycle) * time.Second)
    // back to midnight
    t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
    // midnight + cycle % 86400
    t = t.Add(time.Duration(delay) * time.Second)
    d := t.Sub(time.Now())
    l4g.LogLogTrace("rotate after %v", d)
    return time.NewTimer(d)
}

func (fa *Appender) run(waitExit *sync.WaitGroup) {
    l4g.LogLogTrace("cycle %v", time.Duration(fa.cycle)*time.Second)
    fa.doRotate()

    t := newRotateTimer(fa.cycle, fa.delay)

    for {
        select {
        case <-t.C:
            t = newRotateTimer(fa.cycle, fa.delay)
            fa.doRotate()

        case r, ok := <-fa.rec:
            if !ok {
                waitExit.Done()
                return
            }
            fa.output(r)

        case _, ok := <-fa.reset:
            if !ok {
                return
            }
            t = newRotateTimer(fa.cycle, fa.delay)
            l4g.LogLogTrace("Reset cycle %d, delay %d", fa.cycle, fa.delay)
        }
    }
}

func (fa *Appender) closeChannel() {
    // notify closing. See run()
    close(fa.rec)
    // waiting for running channel closed
    fa.waitExit.Wait()
    fa.waitExit = nil
    // drain channel
    for left := range fa.rec {
        fa.output(left)
    }
}

// Close is nothing to do here.
func (fa *Appender) Close() {
    if fa.waitExit == nil {
        return
    }
    fa.closeChannel()

    fa.mu.Lock()
    defer fa.mu.Unlock()

    close(fa.reset)
    fa.out.Close()
}

func (fa *Appender) output(r *driver.Recorder) {
    if r == nil {
        return
    }

    buf := bufferPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufferPool.Put(buf)

    fa.mu.Lock()
    fa.layout.Encode(buf, r)
    fa.out.Write(buf.Bytes())
    fa.mu.Unlock()

    if fa.cycle <= 0 {
        // rotating on demand
        fa.doRotate()
    }
}

func (fa *Appender) doRotate() {
    if fa.rotate < 0 {
        return
    }

    fa.mu.Lock()
    defer fa.mu.Unlock()

    fa.out.Rotate(fa.rotate)
}

func (fa *Appender) setFileOption(k string, v interface{}) (err error) {
    var (
        s   string
        i64 int64
    )
    switch k {
    case "filename": // DEPRECATED. See Open function's dsn argument
        if s, err = cast.ToString(v); err == nil && len(s) >= 0 {
            fa.out = NewRotateFile(s)
        }
    case "head":
        if s, err = cast.ToString(v); err == nil {
            fa.out.Header = s
        }
    case "foot":
        if s, err = cast.ToString(v); err == nil {
            fa.out.Footer = s
        }
    case "maxsize":
        if i64, err = cast.ToInt64(v); err == nil {
            fa.out.Maxsize = i64
        }
    case "maxlines", "maxrecords":
        if i64, err := cast.ToInt64(v); err == nil {
            fa.out.Maxlines = i64
        }
    default:
        return fmt.Errorf("unknown option name %s, value %#v of type %T", k, v, v)
    }
    return
}

func (fa *Appender) setCycleOption(k string, v interface{}) (err error) {
    var i64 int64

    switch k {
    case "cycle":
        if i64, err = cast.ToSeconds(v); err == nil {
            fa.cycle = i64
        }
    case "delay", "clock":
        if i64, err = cast.ToSeconds(v); err == nil && i64 >= 0 {
            fa.delay = i64
        }
    default:
        return fmt.Errorf("unknown option name %s, value %#v of type %T", k, v, v)
    }
    if err == nil {
        // send notify to reset channel
        if fa.waitExit != nil {
            fa.reset <- time.Now()
        }
    }
    return
}

// Set sets name-value option with:
//  level    - The output level
//  head     - The header of log file. May includes %D (date) and %T (time).
//  foot     - The trailer of log file.
//  maxsize  - Rotating while size > maxsize
//  maxlines - Rotating while lines > maxsize
//  rotate   - -1, no rotating;
//               0, rotating but no backup;
//               n, backup n files
//  cycle    - Rotating cycle inn seconds.
//               <= 0s, checking and rotating on demand;
//               < 86400s, checking and rotating at every cycle;
//               >= 86400, checking and rotating at <delay> seconds since midnight.
//  delay    - Rotating seconds since midnight
//
// Pattern layout options:
//    pattern     - Layout format string
//  ...
//
// Return error
func (fa *Appender) Set(k string, v interface{}) (err error) {
    fa.mu.Lock()
    defer fa.mu.Unlock()

    var (
        n  int
        ok bool
    )

    switch k {
    case "level":
        if n, err = l4g.Level(l4g.INFO).IntE(v); err == nil {
            fa.level = n
        }
    case "filename", "head", "foot", "maxsize", "maxlines", "maxrecords":
        err = fa.setFileOption(k, v)
    case "rotate", "maxbackup":
        if n, err = cast.ToInt(v); err == nil {
            fa.rotate = n
        } else if ok, err = cast.ToBool(v); err == nil {
            if ok {
                fa.rotate = 1
            } else {
                fa.rotate = -1
            }
        }
    case "daily": // DEPRECATED. Replaced with cycle and delay
        if ok, err = cast.ToBool(v); err == nil {
            if ok {
                fa.cycle = dayToSecs
                fa.delay = 0
            } else {
                fa.cycle = 5
                fa.delay = 0
            }
        }
    case "cycle", "delay", "clock":
        err = fa.setCycleOption(k, v)
    default:
        return fa.layout.Set(k, v)
    }
    return
}