matterbridge/vendor/github.com/mattermost/logr/v2/logr.go

472 lines
12 KiB
Go
Raw Normal View History

2021-10-16 21:11:32 +00:00
package logr
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"sync"
"sync/atomic"
"time"
"github.com/wiggin77/merror"
)
// Logr maintains a list of log targets and accepts incoming
// log records. Use `New` to create instances.
type Logr struct {
tmux sync.RWMutex // targetHosts mutex
targetHosts []*TargetHost
in chan *LogRec
quit chan struct{} // closed by Shutdown to exit read loop
done chan struct{} // closed when read loop exited
lvlCache levelCache
bufferPool sync.Pool
options *options
metricsMux sync.RWMutex
metrics *metrics
shutdown int32
}
// New creates a new Logr instance with one or more options specified.
// Some options with invalid values can cause an error to be returned,
// however `logr.New()` using just defaults never errors.
func New(opts ...Option) (*Logr, error) {
options := &options{
maxQueueSize: DefaultMaxQueueSize,
enqueueTimeout: DefaultEnqueueTimeout,
shutdownTimeout: DefaultShutdownTimeout,
flushTimeout: DefaultFlushTimeout,
maxPooledBuffer: DefaultMaxPooledBuffer,
}
lgr := &Logr{options: options}
// apply the options
for _, opt := range opts {
if err := opt(lgr); err != nil {
return nil, err
}
}
pkgName := GetLogrPackageName()
if pkgName != "" {
opt := StackFilter(pkgName, pkgName+"/targets", pkgName+"/formatters")
_ = opt(lgr)
}
lgr.in = make(chan *LogRec, lgr.options.maxQueueSize)
lgr.quit = make(chan struct{})
lgr.done = make(chan struct{})
if lgr.options.useSyncMapLevelCache {
lgr.lvlCache = &syncMapLevelCache{}
} else {
lgr.lvlCache = &arrayLevelCache{}
}
lgr.lvlCache.setup()
lgr.bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
lgr.initMetrics(lgr.options.metricsCollector, lgr.options.metricsUpdateFreqMillis)
go lgr.start()
return lgr, nil
}
// AddTarget adds a target to the logger which will receive
// log records for outputting.
func (lgr *Logr) AddTarget(target Target, name string, filter Filter, formatter Formatter, maxQueueSize int) error {
if lgr.IsShutdown() {
return fmt.Errorf("AddTarget called after Logr shut down")
}
lgr.metricsMux.RLock()
metrics := lgr.metrics
lgr.metricsMux.RUnlock()
hostOpts := targetHostOptions{
name: name,
filter: filter,
formatter: formatter,
maxQueueSize: maxQueueSize,
metrics: metrics,
}
host, err := newTargetHost(target, hostOpts)
if err != nil {
return err
}
lgr.tmux.Lock()
defer lgr.tmux.Unlock()
lgr.targetHosts = append(lgr.targetHosts, host)
lgr.ResetLevelCache()
return nil
}
// NewLogger creates a Logger using defaults. A `Logger` is light-weight
// enough to create on-demand, but typically one or more Loggers are
// created and re-used.
func (lgr *Logr) NewLogger() Logger {
logger := Logger{lgr: lgr}
return logger
}
var levelStatusDisabled = LevelStatus{}
// IsLevelEnabled returns true if at least one target has the specified
// level enabled. The result is cached so that subsequent checks are fast.
func (lgr *Logr) IsLevelEnabled(lvl Level) LevelStatus {
// No levels enabled after shutdown
if atomic.LoadInt32(&lgr.shutdown) != 0 {
return levelStatusDisabled
}
// Check cache.
status, ok := lgr.lvlCache.get(lvl.ID)
if ok {
return status
}
status = LevelStatus{}
// Cache miss; check each target.
lgr.tmux.RLock()
defer lgr.tmux.RUnlock()
for _, host := range lgr.targetHosts {
enabled, level := host.IsLevelEnabled(lvl)
if enabled {
status.Enabled = true
if level.Stacktrace || host.formatter.IsStacktraceNeeded() {
status.Stacktrace = true
break // if both level and stacktrace enabled then no sense checking more targets
}
}
}
// Cache and return the result.
if err := lgr.lvlCache.put(lvl.ID, status); err != nil {
lgr.ReportError(err)
return LevelStatus{}
}
return status
}
// HasTargets returns true only if at least one target exists within the lgr.
func (lgr *Logr) HasTargets() bool {
lgr.tmux.RLock()
defer lgr.tmux.RUnlock()
return len(lgr.targetHosts) > 0
}
// TargetInfo provides name and type for a Target.
type TargetInfo struct {
Name string
Type string
}
// TargetInfos enumerates all the targets added to this lgr.
// The resulting slice represents a snapshot at time of calling.
func (lgr *Logr) TargetInfos() []TargetInfo {
infos := make([]TargetInfo, 0)
lgr.tmux.RLock()
defer lgr.tmux.RUnlock()
for _, host := range lgr.targetHosts {
inf := TargetInfo{
Name: host.String(),
Type: fmt.Sprintf("%T", host.target),
}
infos = append(infos, inf)
}
return infos
}
// RemoveTargets safely removes one or more targets based on the filtering method.
// f should return true to delete the target, false to keep it.
// When removing a target, best effort is made to write any queued log records before
// closing, with cxt determining how much time can be spent in total.
// Note, keep the timeout short since this method blocks certain logging operations.
func (lgr *Logr) RemoveTargets(cxt context.Context, f func(ti TargetInfo) bool) error {
errs := merror.New()
hosts := make([]*TargetHost, 0)
lgr.tmux.Lock()
defer lgr.tmux.Unlock()
for _, host := range lgr.targetHosts {
inf := TargetInfo{
Name: host.String(),
Type: fmt.Sprintf("%T", host.target),
}
if f(inf) {
if err := host.Shutdown(cxt); err != nil {
errs.Append(err)
}
} else {
hosts = append(hosts, host)
}
}
lgr.targetHosts = hosts
lgr.ResetLevelCache()
return errs.ErrorOrNil()
}
// ResetLevelCache resets the cached results of `IsLevelEnabled`. This is
// called any time a Target is added or a target's level is changed.
func (lgr *Logr) ResetLevelCache() {
lgr.lvlCache.clear()
}
// SetMetricsCollector sets (or resets) the metrics collector to be used for gathering
// metrics for all targets. Only targets added after this call will use the collector.
//
// To ensure all targets use a collector, use the `SetMetricsCollector` option when
// creating the Logr instead, or configure/reconfigure the Logr after calling this method.
func (lgr *Logr) SetMetricsCollector(collector MetricsCollector, updateFreqMillis int64) {
lgr.initMetrics(collector, updateFreqMillis)
}
// enqueue adds a log record to the logr queue. If the queue is full then
// this function either blocks or the log record is dropped, depending on
// the result of calling `OnQueueFull`.
func (lgr *Logr) enqueue(rec *LogRec) {
select {
case lgr.in <- rec:
default:
if lgr.options.onQueueFull != nil && lgr.options.onQueueFull(rec, cap(lgr.in)) {
return // drop the record
}
select {
case <-time.After(lgr.options.enqueueTimeout):
lgr.ReportError(fmt.Errorf("enqueue timed out for log rec [%v]", rec))
case lgr.in <- rec: // block until success or timeout
}
}
}
// Flush blocks while flushing the logr queue and all target queues, by
// writing existing log records to valid targets.
// Any attempts to add new log records will block until flush is complete.
// `logr.FlushTimeout` determines how long flush can execute before
// timing out. Use `IsTimeoutError` to determine if the returned error is
// due to a timeout.
func (lgr *Logr) Flush() error {
ctx, cancel := context.WithTimeout(context.Background(), lgr.options.flushTimeout)
defer cancel()
return lgr.FlushWithTimeout(ctx)
}
// Flush blocks while flushing the logr queue and all target queues, by
// writing existing log records to valid targets.
// Any attempts to add new log records will block until flush is complete.
// Use `IsTimeoutError` to determine if the returned error is
// due to a timeout.
func (lgr *Logr) FlushWithTimeout(ctx context.Context) error {
if !lgr.HasTargets() {
return nil
}
if lgr.IsShutdown() {
return errors.New("Flush called on shut down Logr")
}
rec := newFlushLogRec(lgr.NewLogger())
lgr.enqueue(rec)
select {
case <-ctx.Done():
return newTimeoutError("logr queue flush timeout")
case <-rec.flush:
}
return nil
}
// IsShutdown returns true if this Logr instance has been shut down.
// No further log records can be enqueued and no targets added after
// shutdown.
func (lgr *Logr) IsShutdown() bool {
return atomic.LoadInt32(&lgr.shutdown) != 0
}
// Shutdown cleanly stops the logging engine after making best efforts
// to flush all targets. Call this function right before application
// exit - logr cannot be restarted once shut down.
// `logr.ShutdownTimeout` determines how long shutdown can execute before
// timing out. Use `IsTimeoutError` to determine if the returned error is
// due to a timeout.
func (lgr *Logr) Shutdown() error {
ctx, cancel := context.WithTimeout(context.Background(), lgr.options.shutdownTimeout)
defer cancel()
return lgr.ShutdownWithTimeout(ctx)
}
// Shutdown cleanly stops the logging engine after making best efforts
// to flush all targets. Call this function right before application
// exit - logr cannot be restarted once shut down.
// Use `IsTimeoutError` to determine if the returned error is due to a
// timeout.
func (lgr *Logr) ShutdownWithTimeout(ctx context.Context) error {
if err := lgr.FlushWithTimeout(ctx); err != nil {
return err
}
if atomic.SwapInt32(&lgr.shutdown, 1) != 0 {
return errors.New("Shutdown called again after shut down")
}
lgr.ResetLevelCache()
lgr.stopMetricsUpdater()
close(lgr.quit)
errs := merror.New()
// Wait for read loop to exit
select {
case <-ctx.Done():
errs.Append(newTimeoutError("logr queue shutdown timeout"))
case <-lgr.done:
}
// logr.in channel should now be drained to targets and no more log records
// can be added.
lgr.tmux.RLock()
defer lgr.tmux.RUnlock()
for _, host := range lgr.targetHosts {
err := host.Shutdown(ctx)
if err != nil {
errs.Append(err)
}
}
return errs.ErrorOrNil()
}
// ReportError is used to notify the host application of any internal logging errors.
// If `OnLoggerError` is not nil, it is called with the error, otherwise the error is
// output to `os.Stderr`.
func (lgr *Logr) ReportError(err interface{}) {
lgr.incErrorCounter()
if lgr.options.onLoggerError == nil {
fmt.Fprintln(os.Stderr, err)
return
}
lgr.options.onLoggerError(fmt.Errorf("%v", err))
}
// BorrowBuffer borrows a buffer from the pool. Release the buffer to reduce garbage collection.
func (lgr *Logr) BorrowBuffer() *bytes.Buffer {
if lgr.options.disableBufferPool {
return &bytes.Buffer{}
}
return lgr.bufferPool.Get().(*bytes.Buffer)
}
// ReleaseBuffer returns a buffer to the pool to reduce garbage collection. The buffer is only
// retained if less than MaxPooledBuffer.
func (lgr *Logr) ReleaseBuffer(buf *bytes.Buffer) {
if !lgr.options.disableBufferPool && buf.Cap() < lgr.options.maxPooledBuffer {
buf.Reset()
lgr.bufferPool.Put(buf)
}
}
// start selects on incoming log records until shutdown record is received.
// Incoming log records are fanned out to all log targets.
func (lgr *Logr) start() {
defer func() {
if r := recover(); r != nil {
lgr.ReportError(r)
go lgr.start()
} else {
close(lgr.done)
}
}()
for {
var rec *LogRec
select {
case rec = <-lgr.in:
if rec.flush != nil {
lgr.flush(rec.flush)
} else {
rec.prep()
lgr.fanout(rec)
}
case <-lgr.quit:
return
}
}
}
// fanout pushes a LogRec to all targets.
func (lgr *Logr) fanout(rec *LogRec) {
var host *TargetHost
defer func() {
if r := recover(); r != nil {
lgr.ReportError(fmt.Errorf("fanout failed for target %s, %v", host.String(), r))
}
}()
var logged bool
lgr.tmux.RLock()
defer lgr.tmux.RUnlock()
for _, host = range lgr.targetHosts {
if enabled, _ := host.IsLevelEnabled(rec.Level()); enabled {
host.Log(rec)
logged = true
}
}
if logged {
lgr.incLoggedCounter()
}
}
// flush drains the queue and notifies when done.
func (lgr *Logr) flush(done chan<- struct{}) {
// first drain the logr queue.
loop:
for {
var rec *LogRec
select {
case rec = <-lgr.in:
if rec.flush == nil {
rec.prep()
lgr.fanout(rec)
}
default:
break loop
}
}
logger := lgr.NewLogger()
// drain all the targets; block until finished.
lgr.tmux.RLock()
defer lgr.tmux.RUnlock()
for _, host := range lgr.targetHosts {
rec := newFlushLogRec(logger)
host.Log(rec)
<-rec.flush
}
done <- struct{}{}
}