vorteil/vorteil

View on GitHub
pkg/elog/logger.go

Summary

Maintainability
A
0 mins
Test Coverage
package elog

/**
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2020 vorteil.io Pty Ltd
 */

import (
    "bytes"
    "errors"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "sync"
    "time"

    "github.com/vorteil/vorteil/pkg/vio"

    "github.com/fatih/color"
    "github.com/sirupsen/logrus"
    "github.com/vbauerster/mpb/v5"
    "github.com/vbauerster/mpb/v5/decor"
)

// Logger is an interface that has the ability to hide debug/info
type Logger interface {
    Debugf(format string, x ...interface{})
    Errorf(format string, x ...interface{})
    Infof(format string, x ...interface{})
    Printf(format string, x ...interface{})
    Warnf(format string, x ...interface{})
    IsInfoEnabled() bool
    IsDebugEnabled() bool
}

// Progress is an interface to display progress bars for certain operations
type Progress interface {
    Finish(success bool)
    Increment(n int64)
    Write(p []byte) (n int, err error)
    Seek(offset int64, whence int) (int64, error)
    ProxyReader(r io.Reader) io.ReadCloser
}

// ProgressReporter is an interface that contains the ability to create a Progress bar object.
type ProgressReporter interface {
    NewProgress(label string, units string, total int64) Progress
}

// View is an interface that contains a logger and the ability to create progress objects
type View interface {
    Logger
    ProgressReporter
}

// CLI is a generic object setup for logging to terminal outputs
type CLI struct {
    DisableColors      bool
    DisableTTY         bool
    IsDebug            bool
    IsVerbose          bool
    lock               sync.Mutex
    isTrackingProgress bool
    bars               map[*mpb.Bar]bool
    buffer             *bytes.Buffer
    progressContainer  *mpb.Progress
}

// Debugf is a wrapper function that executes logrus.Tracef if debug is enabled.
func (log *CLI) Debugf(format string, x ...interface{}) {
    if log.IsDebug {
        logrus.Tracef(format, x...)
    }
}

// Errorf is a wrapper function that executes logrus.Errorf
func (log *CLI) Errorf(format string, x ...interface{}) {
    logrus.Errorf(format, x...)
}

// Infof is a wrapper function that executes logrus.Debugf only if verbose is enabled.
func (log *CLI) Infof(format string, x ...interface{}) {
    if log.IsVerbose {
        logrus.Debugf(format, x...)
    }
}

// Printf is a wrapper function that executes logrus.Printf
func (log *CLI) Printf(format string, x ...interface{}) {
    logrus.Printf(format, x...)
}

// Warnf is a wrapper function that executes logrus.Warnf
func (log *CLI) Warnf(format string, x ...interface{}) {
    logrus.Warnf(format, x...)
}

// IsInfoEnabled returns whether InfoLevel logging is enabled
func (log *CLI) IsInfoEnabled() bool {
    return logrus.IsLevelEnabled(logrus.InfoLevel)
}

// IsDebugEnabled returns whether DebugLevel logging is enabled
func (log *CLI) IsDebugEnabled() bool {
    return logrus.IsLevelEnabled(logrus.DebugLevel)
}

// NewProgress creates a progress object and returns
func (log *CLI) NewProgress(label string, units string, total int64) Progress {

    if log.DisableTTY {
        return &nilProgress{
            total: total,
        }
    }

    log.lock.Lock()
    defer log.lock.Unlock()

    if !log.isTrackingProgress {
        log.isTrackingProgress = true
        log.buffer = new(bytes.Buffer)
        logrus.SetOutput(log.buffer)
        log.progressContainer = mpb.New(mpb.WithWidth(80))
        log.bars = make(map[*mpb.Bar]bool)
    }

    var decorators []decor.Decorator
    switch units {
    default:
        fallthrough
    case "%":
        decorators = append(decorators, decor.Percentage())
    case "KiB":
        decorators = append(decorators, decor.Counters(decor.UnitKiB, "% .1f / % .1f"))
    }

    var p *mpb.Bar
    if total == 0 {
        p = log.progressContainer.AddSpinner(0, mpb.SpinnerOnLeft,
            mpb.PrependDecorators(
                decor.Name(label, decor.WC{W: len(label) + 1, C: decor.DidentRight}),
            ),
        )
    } else {
        p = log.progressContainer.AddBar(total,
            // mpb.BarStyle("╢▌▌░╟"),
            mpb.PrependDecorators(
                // display our name with one space on the right
                decor.Name(label, decor.WC{W: len(label) + 1, C: decor.DidentRight}),
                // replace ETA decorator with "done" message, OnComplete event
                decor.OnComplete(
                    decor.AverageETA(decor.ET_STYLE_GO, decor.WC{W: 4}), "done",
                ),
            ),
            mpb.AppendDecorators(decorators...),
        )
    }

    log.bars[p] = true

    pb := &pb{
        log:      log,
        p:        p,
        total:    total,
        interval: time.Millisecond * 100,
    }
    pb.nextUpdate = time.Now().Add(pb.interval)

    return pb

}

type nilProgress struct {
    cursor int64
    total  int64
}

// Increment nilProgress does nothing...
func (np *nilProgress) Increment(n int64) {

}

// Finish nilProgress does nothing...
func (np *nilProgress) Finish(success bool) {

}

// Write nilProgress writes to the cursor
func (np *nilProgress) Write(p []byte) (n int, err error) {
    n = len(p)
    np.cursor += int64(n)
    return
}

// Seek nilProgress creates the infinite loader symbol
func (np *nilProgress) Seek(offset int64, whence int) (int64, error) {
    var abs int64

    switch whence {
    case io.SeekCurrent:
        abs = np.cursor + offset
    case io.SeekStart:
        abs = offset
    case io.SeekEnd:
        abs = np.total + offset
    default:
        return 0, errors.New("invalid whence")
    }

    np.cursor = abs
    return abs, nil
}

// ProxyReader nillProgress does nothing with the reader
func (np *nilProgress) ProxyReader(r io.Reader) io.ReadCloser {

    if rc, ok := r.(io.ReadCloser); ok {
        return rc
    }

    return ioutil.NopCloser(r)
}

type pb struct {
    log    *CLI
    p      *mpb.Bar
    closed bool
    total  int64
    cursor int64
    bar    int64

    buffered   int64
    interval   time.Duration
    nextUpdate time.Time
}

// Increment increaes the progress on the bar
func (pb *pb) Increment(n int64) {
    pb.buffered += n
    pb.bar += n
    if !time.Now().Before(pb.nextUpdate) {
        pb.flush()
    }
}

func (pb *pb) flush() {
    pb.nextUpdate = time.Now().Add(pb.interval)
    pb.p.IncrInt64(pb.buffered)
    pb.buffered = 0
}

// Finish closes the progress bar object
func (pb *pb) Finish(success bool) {
    if pb.closed {
        return
    }
    pb.flush()
    pb.closed = true
    if pb.bar != pb.total || pb.total == 0 || !success {
        pb.p.Abort(false)
    }

    pb.log.lock.Lock()
    defer pb.log.lock.Unlock()
    delete(pb.log.bars, pb.p)

    if len(pb.log.bars) == 0 {
        pb.log.bars = nil
        pb.log.isTrackingProgress = false
        pb.log.progressContainer.Wait()
        pb.log.progressContainer = nil
        logrus.SetOutput(os.Stdout)
        _, _ = pb.log.buffer.WriteTo(os.Stdout)
        pb.log.buffer = nil
    }
}

// Write writes to the progress bar object
func (pb *pb) Write(p []byte) (n int, err error) {
    n = len(p)
    pb.cursor += int64(n)
    if pb.bar < pb.cursor {
        pb.Increment(pb.cursor - pb.bar)
    }
    return
}

// Seek applies the offset to the progressbar cursor
func (pb *pb) Seek(offset int64, whence int) (int64, error) {
    var abs int64

    switch whence {
    case io.SeekCurrent:
        abs = pb.cursor + offset
    case io.SeekStart:
        abs = offset
    case io.SeekEnd:
        abs = pb.total + offset
    default:
        return 0, errors.New("invalid whence")
    }

    pb.cursor = abs
    if pb.bar < pb.cursor {
        pb.Increment(pb.cursor - pb.bar)
    }

    return abs, nil
}

// ProxyReader returns the ready of the progress bar
func (pb *pb) ProxyReader(r io.Reader) io.ReadCloser {

    pr := pb.p.ProxyReader(r)

    return vio.LazyReadCloser(
        func() (io.Reader, error) {
            return pr, nil
        },
        func() error {
            pb.flush()
            pb.Finish(pb.total == pb.bar)
            return pr.Close()
        },
    )

}

type mws struct {
    w []io.WriteSeeker
}

// MultiWriteSeeker returns a MultiWriteSeeker object
func MultiWriteSeeker(writeseekers ...io.WriteSeeker) io.WriteSeeker {
    return &mws{
        w: writeseekers,
    }
}

// Write writes to the multi write seekers
func (mws *mws) Write(p []byte) (n int, err error) {
    for _, w := range mws.w {
        n, err = w.Write(p)
        if err != nil {
            return
        }
        if n != len(p) {
            err = io.ErrShortWrite
            return
        }
    }
    return len(p), nil
}

// Seek moves to the offset
func (mws *mws) Seek(offset int64, whence int) (int64, error) {

    var abs int64
    abs, err := mws.w[0].Seek(offset, whence)
    if err != nil {
        return 0, err
    }

    for _, w := range mws.w[1:] {
        n, err := w.Seek(offset, whence)
        if err != nil {
            return 0, err
        }
        if n != abs {
            err = io.ErrShortWrite
            return 0, err
        }
    }
    return abs, nil
}

// Format formats our logger for terminal use
func (log *CLI) Format(entry *logrus.Entry) ([]byte, error) {

    faint := color.New(color.Faint).SprintFunc()
    yellow := color.New(color.FgYellow).SprintFunc()
    red := color.New(color.FgRed).SprintFunc()
    blue := color.New(color.FgBlue).SprintFunc()

    x := entry.Message
    if !log.DisableColors {
        switch entry.Level {
        case logrus.TraceLevel:
            x = fmt.Sprintf("%s\n", faint(x))
        case logrus.DebugLevel:
            x = fmt.Sprintf("%s\n", blue(x))
        case logrus.InfoLevel:
            x = fmt.Sprintf("%s\n", x)
        case logrus.WarnLevel:
            x = fmt.Sprintf("%s\n", yellow(x))
        case logrus.ErrorLevel:
            x = fmt.Sprintf("%s\n", red(x))
        default:
        }
    }

    return []byte(x), nil

}