MM-27744 disable Zap for unit tests. Zap has no concept of shutdown or close. Zap is only shutdown when the app exits. Not a problem for console logging, but when creating a new Zap logger that outputs to files on every unit test, that leaves no easy way to clean up until process exit. Depending on what else is running this can exhaust all file handles and cause unit tests to fail. Zap is now disabled unit tests and uses Logr instead, regardless of config settings. `make test-server` peak file handle usage dropped from ~5K to less than 100.
665 строки
18 KiB
Go
665 строки
18 KiB
Go
package logr
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/wiggin77/cfg"
|
|
"github.com/wiggin77/merror"
|
|
)
|
|
|
|
// Logr maintains a list of log targets and accepts incoming
|
|
// log records.
|
|
type Logr struct {
|
|
tmux sync.RWMutex // target mutex
|
|
targets []Target
|
|
|
|
mux sync.RWMutex
|
|
maxQueueSizeActual int
|
|
in chan *LogRec
|
|
done chan struct{}
|
|
once sync.Once
|
|
shutdown bool
|
|
lvlCache levelCache
|
|
|
|
metricsInitOnce sync.Once
|
|
metricsCloseOnce sync.Once
|
|
metricsDone chan struct{}
|
|
metrics MetricsCollector
|
|
queueSizeGauge Gauge
|
|
loggedCounter Counter
|
|
errorCounter Counter
|
|
|
|
bufferPool sync.Pool
|
|
|
|
// MaxQueueSize is the maximum number of log records that can be queued.
|
|
// If exceeded, `OnQueueFull` is called which determines if the log
|
|
// record will be dropped or block until add is successful.
|
|
// If this is modified, it must be done before `Configure` or
|
|
// `AddTarget`. Defaults to DefaultMaxQueueSize.
|
|
MaxQueueSize int
|
|
|
|
// OnLoggerError, when not nil, is called any time an internal
|
|
// logging error occurs. For example, this can happen when a
|
|
// target cannot connect to its data sink.
|
|
OnLoggerError func(error)
|
|
|
|
// OnQueueFull, when not nil, is called on an attempt to add
|
|
// a log record to a full Logr queue.
|
|
// `MaxQueueSize` can be used to modify the maximum queue size.
|
|
// This function should return quickly, with a bool indicating whether
|
|
// the log record should be dropped (true) or block until the log record
|
|
// is successfully added (false). If nil then blocking (false) is assumed.
|
|
OnQueueFull func(rec *LogRec, maxQueueSize int) bool
|
|
|
|
// OnTargetQueueFull, when not nil, is called on an attempt to add
|
|
// a log record to a full target queue provided the target supports reporting
|
|
// this condition.
|
|
// This function should return quickly, with a bool indicating whether
|
|
// the log record should be dropped (true) or block until the log record
|
|
// is successfully added (false). If nil then blocking (false) is assumed.
|
|
OnTargetQueueFull func(target Target, rec *LogRec, maxQueueSize int) bool
|
|
|
|
// OnExit, when not nil, is called when a FatalXXX style log API is called.
|
|
// When nil, then the default behavior is to cleanly shut down this Logr and
|
|
// call `os.Exit(code)`.
|
|
OnExit func(code int)
|
|
|
|
// OnPanic, when not nil, is called when a PanicXXX style log API is called.
|
|
// When nil, then the default behavior is to cleanly shut down this Logr and
|
|
// call `panic(err)`.
|
|
OnPanic func(err interface{})
|
|
|
|
// EnqueueTimeout is the amount of time a log record can take to be queued.
|
|
// This only applies to blocking enqueue which happen after `logr.OnQueueFull`
|
|
// is called and returns false.
|
|
EnqueueTimeout time.Duration
|
|
|
|
// ShutdownTimeout is the amount of time `logr.Shutdown` can execute before
|
|
// timing out.
|
|
ShutdownTimeout time.Duration
|
|
|
|
// FlushTimeout is the amount of time `logr.Flush` can execute before
|
|
// timing out.
|
|
FlushTimeout time.Duration
|
|
|
|
// UseSyncMapLevelCache can be set to true before the first target is added
|
|
// when high concurrency (e.g. >32 cores) is expected. This may improve
|
|
// performance with large numbers of cores - benchmark for your use case.
|
|
UseSyncMapLevelCache bool
|
|
|
|
// MaxPooledFormatBuffer determines the maximum size of a buffer that can be
|
|
// pooled. To reduce allocations, the buffers needed during formatting (etc)
|
|
// are pooled. A very large log item will grow a buffer that could stay in
|
|
// memory indefinitely. This settings lets you control how big a pooled buffer
|
|
// can be - anything larger will be garbage collected after use.
|
|
// Defaults to 1MB.
|
|
MaxPooledBuffer int
|
|
|
|
// DisableBufferPool when true disables the buffer pool. See MaxPooledBuffer.
|
|
DisableBufferPool bool
|
|
|
|
// MetricsUpdateFreqMillis determines how often polled metrics are updated
|
|
// when metrics are enabled.
|
|
MetricsUpdateFreqMillis int64
|
|
}
|
|
|
|
// Configure adds/removes targets via the supplied `Config`.
|
|
func (logr *Logr) Configure(config *cfg.Config) error {
|
|
// TODO
|
|
return fmt.Errorf("not implemented yet")
|
|
}
|
|
|
|
func (logr *Logr) ensureInit() {
|
|
logr.once.Do(func() {
|
|
defer func() {
|
|
go logr.start()
|
|
}()
|
|
|
|
logr.mux.Lock()
|
|
defer logr.mux.Unlock()
|
|
|
|
logr.maxQueueSizeActual = logr.MaxQueueSize
|
|
if logr.maxQueueSizeActual == 0 {
|
|
logr.maxQueueSizeActual = DefaultMaxQueueSize
|
|
}
|
|
|
|
if logr.maxQueueSizeActual < 0 {
|
|
logr.maxQueueSizeActual = 0
|
|
}
|
|
|
|
logr.in = make(chan *LogRec, logr.maxQueueSizeActual)
|
|
logr.done = make(chan struct{})
|
|
|
|
if logr.UseSyncMapLevelCache {
|
|
logr.lvlCache = &syncMapLevelCache{}
|
|
} else {
|
|
logr.lvlCache = &arrayLevelCache{}
|
|
}
|
|
|
|
if logr.MaxPooledBuffer == 0 {
|
|
logr.MaxPooledBuffer = DefaultMaxPooledBuffer
|
|
}
|
|
logr.bufferPool = sync.Pool{
|
|
New: func() interface{} {
|
|
return new(bytes.Buffer)
|
|
},
|
|
}
|
|
|
|
logr.lvlCache.setup()
|
|
})
|
|
}
|
|
|
|
// AddTarget adds one or more targets to the logger which will receive
|
|
// log records for outputting.
|
|
func (logr *Logr) AddTarget(targets ...Target) error {
|
|
if logr.IsShutdown() {
|
|
return fmt.Errorf("AddTarget called after Logr shut down")
|
|
}
|
|
|
|
logr.ensureInit()
|
|
metrics := logr.getMetricsCollector()
|
|
defer logr.ResetLevelCache() // call this after tmux is released
|
|
|
|
logr.tmux.Lock()
|
|
defer logr.tmux.Unlock()
|
|
|
|
errs := merror.New()
|
|
for _, t := range targets {
|
|
if t == nil {
|
|
continue
|
|
}
|
|
|
|
logr.targets = append(logr.targets, t)
|
|
if metrics != nil {
|
|
if tm, ok := t.(TargetWithMetrics); ok {
|
|
if err := tm.EnableMetrics(metrics, logr.MetricsUpdateFreqMillis); err != nil {
|
|
errs.Append(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return errs.ErrorOrNil()
|
|
}
|
|
|
|
// 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 (logr *Logr) NewLogger() Logger {
|
|
logger := Logger{logr: logr}
|
|
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 (logr *Logr) IsLevelEnabled(lvl Level) LevelStatus {
|
|
status, ok := logr.isLevelEnabledFromCache(lvl)
|
|
if ok {
|
|
return status
|
|
}
|
|
|
|
// Check each target.
|
|
logr.tmux.RLock()
|
|
for _, t := range logr.targets {
|
|
e, s := t.IsLevelEnabled(lvl)
|
|
if e {
|
|
status.Enabled = true
|
|
if s {
|
|
status.Stacktrace = true
|
|
break // if both enabled then no sense checking more targets
|
|
}
|
|
}
|
|
}
|
|
logr.tmux.RUnlock()
|
|
|
|
// Cache and return the result.
|
|
if err := logr.updateLevelCache(lvl.ID, status); err != nil {
|
|
logr.ReportError(err)
|
|
return LevelStatus{}
|
|
}
|
|
return status
|
|
}
|
|
|
|
func (logr *Logr) isLevelEnabledFromCache(lvl Level) (LevelStatus, bool) {
|
|
logr.mux.RLock()
|
|
defer logr.mux.RUnlock()
|
|
|
|
// Don't accept new log records after shutdown.
|
|
if logr.shutdown {
|
|
return levelStatusDisabled, true
|
|
}
|
|
|
|
// Check cache. lvlCache may still be nil if no targets added.
|
|
if logr.lvlCache == nil {
|
|
return levelStatusDisabled, true
|
|
}
|
|
status, ok := logr.lvlCache.get(lvl.ID)
|
|
if ok {
|
|
return status, true
|
|
}
|
|
return LevelStatus{}, false
|
|
}
|
|
|
|
func (logr *Logr) updateLevelCache(id LevelID, status LevelStatus) error {
|
|
logr.mux.RLock()
|
|
defer logr.mux.RUnlock()
|
|
if logr.lvlCache != nil {
|
|
return logr.lvlCache.put(id, status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HasTargets returns true only if at least one target exists within the Logr.
|
|
func (logr *Logr) HasTargets() bool {
|
|
logr.tmux.RLock()
|
|
defer logr.tmux.RUnlock()
|
|
return len(logr.targets) > 0
|
|
}
|
|
|
|
// TargetInfo provides name and type for a Target.
|
|
type TargetInfo struct {
|
|
Name string
|
|
Type string
|
|
}
|
|
|
|
// TargetInfos enumerates all the targets added to this Logr.
|
|
// The resulting slice represents a snapshot at time of calling.
|
|
func (logr *Logr) TargetInfos() []TargetInfo {
|
|
logr.tmux.RLock()
|
|
defer logr.tmux.RUnlock()
|
|
|
|
infos := make([]TargetInfo, 0)
|
|
|
|
for _, t := range logr.targets {
|
|
inf := TargetInfo{
|
|
Name: fmt.Sprintf("%v", t),
|
|
Type: fmt.Sprintf("%T", t),
|
|
}
|
|
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 (logr *Logr) RemoveTargets(cxt context.Context, f func(ti TargetInfo) bool) error {
|
|
var removed bool
|
|
defer func() {
|
|
if removed {
|
|
// call this after tmux is released since
|
|
// it will lock mux and we don't want to
|
|
// introduce possible deadlock.
|
|
logr.ResetLevelCache()
|
|
}
|
|
}()
|
|
|
|
errs := merror.New()
|
|
|
|
logr.tmux.Lock()
|
|
defer logr.tmux.Unlock()
|
|
|
|
cp := make([]Target, 0)
|
|
|
|
for _, t := range logr.targets {
|
|
inf := TargetInfo{
|
|
Name: fmt.Sprintf("%v", t),
|
|
Type: fmt.Sprintf("%T", t),
|
|
}
|
|
if f(inf) {
|
|
if err := t.Shutdown(cxt); err != nil {
|
|
errs.Append(err)
|
|
}
|
|
removed = true
|
|
} else {
|
|
cp = append(cp, t)
|
|
}
|
|
}
|
|
logr.targets = cp
|
|
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 (logr *Logr) ResetLevelCache() {
|
|
// Write lock so that new cache entries cannot be stored while we
|
|
// clear the cache.
|
|
logr.mux.Lock()
|
|
defer logr.mux.Unlock()
|
|
logr.resetLevelCache()
|
|
}
|
|
|
|
// resetLevelCache empties the level cache without locking.
|
|
// mux.Lock must be held before calling this function.
|
|
func (logr *Logr) resetLevelCache() {
|
|
// lvlCache may still be nil if no targets added.
|
|
if logr.lvlCache != nil {
|
|
logr.lvlCache.clear()
|
|
}
|
|
}
|
|
|
|
// 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 (logr *Logr) enqueue(rec *LogRec) {
|
|
if logr.in == nil {
|
|
logr.ReportError(fmt.Errorf("AddTarget or Configure must be called before enqueue"))
|
|
}
|
|
|
|
select {
|
|
case logr.in <- rec:
|
|
default:
|
|
if logr.OnQueueFull != nil && logr.OnQueueFull(rec, logr.maxQueueSizeActual) {
|
|
return // drop the record
|
|
}
|
|
select {
|
|
case <-time.After(logr.enqueueTimeout()):
|
|
logr.ReportError(fmt.Errorf("enqueue timed out for log rec [%v]", rec))
|
|
case logr.in <- rec: // block until success or timeout
|
|
}
|
|
}
|
|
}
|
|
|
|
// exit is called by one of the FatalXXX style APIS. If `logr.OnExit` is not nil
|
|
// then that method is called, otherwise the default behavior is to shut down this
|
|
// Logr cleanly then call `os.Exit(code)`.
|
|
func (logr *Logr) exit(code int) {
|
|
if logr.OnExit != nil {
|
|
logr.OnExit(code)
|
|
return
|
|
}
|
|
|
|
if err := logr.Shutdown(); err != nil {
|
|
logr.ReportError(err)
|
|
}
|
|
os.Exit(code)
|
|
}
|
|
|
|
// panic is called by one of the PanicXXX style APIS. If `logr.OnPanic` is not nil
|
|
// then that method is called, otherwise the default behavior is to shut down this
|
|
// Logr cleanly then call `panic(err)`.
|
|
func (logr *Logr) panic(err interface{}) {
|
|
if logr.OnPanic != nil {
|
|
logr.OnPanic(err)
|
|
return
|
|
}
|
|
|
|
if err := logr.Shutdown(); err != nil {
|
|
logr.ReportError(err)
|
|
}
|
|
panic(err)
|
|
}
|
|
|
|
// 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 (logr *Logr) Flush() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), logr.flushTimeout())
|
|
defer cancel()
|
|
return logr.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 (logr *Logr) FlushWithTimeout(ctx context.Context) error {
|
|
if !logr.HasTargets() {
|
|
return nil
|
|
}
|
|
|
|
if logr.IsShutdown() {
|
|
return errors.New("Flush called on shut down Logr")
|
|
}
|
|
|
|
rec := newFlushLogRec(logr.NewLogger())
|
|
logr.enqueue(rec)
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return newTimeoutError("logr queue shutdown 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 (logr *Logr) IsShutdown() bool {
|
|
logr.mux.Lock()
|
|
defer logr.mux.Unlock()
|
|
return logr.shutdown
|
|
}
|
|
|
|
// 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 (logr *Logr) Shutdown() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), logr.shutdownTimeout())
|
|
defer cancel()
|
|
return logr.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 (logr *Logr) ShutdownWithTimeout(ctx context.Context) error {
|
|
logr.mux.Lock()
|
|
if logr.shutdown {
|
|
logr.mux.Unlock()
|
|
return errors.New("Shutdown called again after shut down")
|
|
}
|
|
logr.shutdown = true
|
|
logr.resetLevelCache()
|
|
logr.mux.Unlock()
|
|
|
|
logr.metricsCloseOnce.Do(func() {
|
|
if logr.metricsDone != nil {
|
|
close(logr.metricsDone)
|
|
}
|
|
})
|
|
|
|
errs := merror.New()
|
|
|
|
// close the incoming channel and wait for read loop to exit.
|
|
if logr.in != nil {
|
|
close(logr.in)
|
|
select {
|
|
case <-ctx.Done():
|
|
errs.Append(newTimeoutError("logr queue shutdown timeout"))
|
|
case <-logr.done:
|
|
}
|
|
}
|
|
|
|
// logr.in channel should now be drained to targets and no more log records
|
|
// can be added.
|
|
logr.tmux.RLock()
|
|
defer logr.tmux.RUnlock()
|
|
for _, t := range logr.targets {
|
|
err := t.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 (logr *Logr) ReportError(err interface{}) {
|
|
logr.incErrorCounter()
|
|
|
|
if logr.OnLoggerError == nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return
|
|
}
|
|
logr.OnLoggerError(fmt.Errorf("%v", err))
|
|
}
|
|
|
|
// BorrowBuffer borrows a buffer from the pool. Release the buffer to reduce garbage collection.
|
|
func (logr *Logr) BorrowBuffer() *bytes.Buffer {
|
|
if logr.DisableBufferPool {
|
|
return &bytes.Buffer{}
|
|
}
|
|
return logr.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 (logr *Logr) ReleaseBuffer(buf *bytes.Buffer) {
|
|
if !logr.DisableBufferPool && buf.Cap() < logr.MaxPooledBuffer {
|
|
buf.Reset()
|
|
logr.bufferPool.Put(buf)
|
|
}
|
|
}
|
|
|
|
// enqueueTimeout returns amount of time a log record can take to be queued.
|
|
// This only applies to blocking enqueue which happen after `logr.OnQueueFull` is called
|
|
// and returns false.
|
|
func (logr *Logr) enqueueTimeout() time.Duration {
|
|
if logr.EnqueueTimeout == 0 {
|
|
return DefaultEnqueueTimeout
|
|
}
|
|
return logr.EnqueueTimeout
|
|
}
|
|
|
|
// shutdownTimeout returns the timeout duration for `logr.Shutdown`.
|
|
func (logr *Logr) shutdownTimeout() time.Duration {
|
|
if logr.ShutdownTimeout == 0 {
|
|
return DefaultShutdownTimeout
|
|
}
|
|
return logr.ShutdownTimeout
|
|
}
|
|
|
|
// flushTimeout returns the timeout duration for `logr.Flush`.
|
|
func (logr *Logr) flushTimeout() time.Duration {
|
|
if logr.FlushTimeout == 0 {
|
|
return DefaultFlushTimeout
|
|
}
|
|
return logr.FlushTimeout
|
|
}
|
|
|
|
// start selects on incoming log records until done channel signals.
|
|
// Incoming log records are fanned out to all log targets.
|
|
func (logr *Logr) start() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logr.ReportError(r)
|
|
go logr.start()
|
|
}
|
|
}()
|
|
|
|
for rec := range logr.in {
|
|
if rec.flush != nil {
|
|
logr.flush(rec.flush)
|
|
} else {
|
|
rec.prep()
|
|
logr.fanout(rec)
|
|
}
|
|
}
|
|
close(logr.done)
|
|
}
|
|
|
|
// startMetricsUpdater updates the metrics for any polled values every `MetricsUpdateFreqSecs` seconds until
|
|
// logr is closed.
|
|
func (logr *Logr) startMetricsUpdater() {
|
|
for {
|
|
updateFreq := logr.getMetricsUpdateFreqMillis()
|
|
if updateFreq == 0 {
|
|
updateFreq = DefMetricsUpdateFreqMillis
|
|
}
|
|
if updateFreq < 250 {
|
|
updateFreq = 250 // don't peg the CPU
|
|
}
|
|
|
|
select {
|
|
case <-logr.metricsDone:
|
|
return
|
|
case <-time.After(time.Duration(updateFreq) * time.Millisecond):
|
|
logr.setQueueSizeGauge(float64(len(logr.in)))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (logr *Logr) getMetricsUpdateFreqMillis() int64 {
|
|
logr.mux.RLock()
|
|
defer logr.mux.RUnlock()
|
|
return logr.MetricsUpdateFreqMillis
|
|
}
|
|
|
|
// fanout pushes a LogRec to all targets.
|
|
func (logr *Logr) fanout(rec *LogRec) {
|
|
var target Target
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logr.ReportError(fmt.Errorf("fanout failed for target %s, %v", target, r))
|
|
}
|
|
}()
|
|
|
|
var logged bool
|
|
defer func() {
|
|
if logged {
|
|
logr.incLoggedCounter() // call this after tmux is released
|
|
}
|
|
}()
|
|
|
|
logr.tmux.RLock()
|
|
defer logr.tmux.RUnlock()
|
|
for _, target = range logr.targets {
|
|
if enabled, _ := target.IsLevelEnabled(rec.Level()); enabled {
|
|
target.Log(rec)
|
|
logged = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// flush drains the queue and notifies when done.
|
|
func (logr *Logr) flush(done chan<- struct{}) {
|
|
// first drain the logr queue.
|
|
loop:
|
|
for {
|
|
var rec *LogRec
|
|
select {
|
|
case rec = <-logr.in:
|
|
if rec.flush == nil {
|
|
rec.prep()
|
|
logr.fanout(rec)
|
|
}
|
|
default:
|
|
break loop
|
|
}
|
|
}
|
|
|
|
logger := logr.NewLogger()
|
|
|
|
// drain all the targets; block until finished.
|
|
logr.tmux.RLock()
|
|
defer logr.tmux.RUnlock()
|
|
for _, target := range logr.targets {
|
|
rec := newFlushLogRec(logger)
|
|
target.Log(rec)
|
|
<-rec.flush
|
|
}
|
|
done <- struct{}{}
|
|
}
|