github.com/qlik-oss/gopherciser@v0.18.6/logger/logger.go (about)

     1  package logger
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"sync/atomic"
    11  	"time"
    12  
    13  	"github.com/hashicorp/go-multierror"
    14  	"github.com/pkg/errors"
    15  	"github.com/qlik-oss/gopherciser/helpers"
    16  	"github.com/rs/zerolog"
    17  )
    18  
    19  type (
    20  	// MsgWriter implement to write log entry
    21  	MsgWriter interface {
    22  		// WriteMessage to log
    23  		WriteMessage(msg *LogChanMsg) error
    24  		// Set log level
    25  		Level(lvl LogLevel)
    26  	}
    27  
    28  	// Logger container for writer and close functions
    29  	Logger struct {
    30  		Writer     MsgWriter
    31  		closeFuncs []func() error
    32  	}
    33  
    34  	// LogLevel of logging
    35  	LogLevel int
    36  
    37  	//Message container
    38  	message struct {
    39  		Tick    uint64
    40  		Time    time.Time
    41  		Level   LogLevel
    42  		Message string
    43  	}
    44  
    45  	// LogChanMsg container for row to be logged
    46  	LogChanMsg struct {
    47  		message
    48  		SessionEntry
    49  		ActionEntry
    50  		*ephemeralEntry
    51  	}
    52  
    53  	// LogSettings settings
    54  	LogSettings struct {
    55  		Traffic    bool
    56  		Metrics    bool
    57  		Debug      bool
    58  		Regression bool
    59  	}
    60  
    61  	// Log main struct to keep track of and propagate log entries to loggers. Close finished will be signaled on Closed channel.
    62  	Log struct {
    63  		loggers   []*Logger
    64  		logChan   chan *LogChanMsg
    65  		closeFlag atomic.Value
    66  
    67  		Closed   chan interface{}
    68  		Settings LogSettings
    69  
    70  		regressionLogger RegressionLoggerCloser
    71  	}
    72  )
    73  
    74  // When adding a new level also:
    75  // * Add it to the String function
    76  // * Add it in the StartLogger switch case if not to be logged on info level
    77  const (
    78  	UnknownLevel LogLevel = iota
    79  	ResultLevel
    80  	ErrorLevel
    81  	WarningLevel
    82  	InfoLevel
    83  	MetricsLevel
    84  	TrafficLevel
    85  	DebugLevel
    86  )
    87  
    88  func (l LogLevel) String() string {
    89  	switch l {
    90  	case ResultLevel:
    91  		return "result"
    92  	case ErrorLevel:
    93  		return "error"
    94  	case WarningLevel:
    95  		return "warning"
    96  	case InfoLevel:
    97  		return "info"
    98  	case DebugLevel:
    99  		return "debug"
   100  	case TrafficLevel:
   101  		return "traffic"
   102  	case MetricsLevel:
   103  		return "metric"
   104  	default:
   105  		return "unknown"
   106  	}
   107  }
   108  
   109  // NewLog instance
   110  func NewLog(settings LogSettings) *Log {
   111  	return &Log{
   112  		logChan:  make(chan *LogChanMsg, 2000),
   113  		Settings: settings,
   114  		Closed:   make(chan interface{}),
   115  	}
   116  }
   117  
   118  // NewLogger instance
   119  func NewLogger(w MsgWriter) *Logger {
   120  	return &Logger{
   121  		Writer: w,
   122  	}
   123  }
   124  
   125  // NewLogChanMsg create new LogChanMsg, to be used for testing purposes
   126  func NewEmptyLogChanMsg() *LogChanMsg {
   127  	return &LogChanMsg{message{},
   128  		SessionEntry{},
   129  		ActionEntry{},
   130  		&ephemeralEntry{}}
   131  }
   132  
   133  // NewLogEntry create new LogEntry using current logger
   134  func (log *Log) NewLogEntry() *LogEntry {
   135  	return NewLogEntry(log)
   136  }
   137  
   138  // AddLoggers to be used for logging
   139  func (log *Log) AddLoggers(loggers ...*Logger) {
   140  	if log.loggers == nil {
   141  		log.loggers = []*Logger{}
   142  		log.loggers = append(log.loggers, loggers...)
   143  		return
   144  	}
   145  	log.loggers = append(log.loggers, loggers...)
   146  }
   147  
   148  // SetRegressionLoggerFile to be used for logging regression data to file. The
   149  // file name is chosen, using `backupName`, to match the name of the standard
   150  // log file.
   151  func (log *Log) SetRegressionLoggerFile(fileName string) error {
   152  	fileName = strings.TrimSuffix(backupName(fileName), filepath.Ext(fileName)) + ".regression"
   153  	f, err := NewWriter(fileName)
   154  	if err != nil {
   155  		return errors.WithStack(err)
   156  	}
   157  	log.regressionLogger = NewRegressionLogger(f, HeaderEntry{"ID_FORMAT", "sessionID.actionID.objectID"})
   158  	return nil
   159  }
   160  
   161  // CloseWithTimeout functions with custom timeout
   162  func (log *Log) CloseWithTimeout(timeout time.Duration) error {
   163  	log.closeFlag.Store(true)
   164  
   165  	//wait for all logs to be written or max 5 minutes
   166  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   167  	defer cancel()
   168  	WaitForChanClose(ctx, log.Closed)
   169  
   170  	var mErr *multierror.Error
   171  	if log.loggers != nil {
   172  		for _, v := range log.loggers {
   173  			if err := v.Close(); err != nil {
   174  				mErr = multierror.Append(mErr, err)
   175  			}
   176  		}
   177  		log.loggers = nil
   178  	}
   179  	if log.regressionLogger != nil {
   180  		log.regressionLogger.Close()
   181  	}
   182  
   183  	return errors.WithStack(helpers.FlattenMultiError(mErr))
   184  }
   185  
   186  // Close functions with default timeout of 5 minutes
   187  func (log *Log) Close() error {
   188  	return errors.WithStack(log.CloseWithTimeout(5 * time.Minute))
   189  }
   190  
   191  // StartLogger start async reading on log channel
   192  func (log *Log) StartLogger(ctx context.Context) {
   193  	go log.logListen(ctx)
   194  }
   195  
   196  func (log *Log) logListen(ctx context.Context) {
   197  	doClose := false
   198  	for {
   199  		if flag, ok := log.closeFlag.Load().(bool); ok && flag {
   200  			doClose = true
   201  		}
   202  
   203  		select {
   204  		case msg, ok := <-log.logChan:
   205  			if log.onLogChanMsg(msg, ok) {
   206  				return
   207  			}
   208  		case <-ctx.Done():
   209  			doClose = true
   210  			for {
   211  				select {
   212  				case msg, ok := <-log.logChan:
   213  					if log.onLogChanMsg(msg, ok) {
   214  						return
   215  					}
   216  				case <-time.After(time.Millisecond * 50):
   217  					// logChan is never closed, but this is only executed when the program terminates
   218  
   219  					close(log.Closed)
   220  					return
   221  				}
   222  			}
   223  		case <-time.After(time.Millisecond * 50):
   224  			if doClose {
   225  				close(log.logChan)
   226  			}
   227  		}
   228  	}
   229  }
   230  
   231  func (log *Log) onLogChanMsg(msg *LogChanMsg, ok bool) bool {
   232  	if !ok {
   233  		close(log.Closed) //Notify logger closed
   234  		return true
   235  	}
   236  
   237  	for _, l := range log.loggers {
   238  		if l == nil || l.Writer == nil {
   239  			continue
   240  		}
   241  		if err := l.Writer.WriteMessage(msg); err != nil {
   242  			_, _ = fmt.Fprintf(os.Stderr, "Error writing log: %v\n", err)
   243  		}
   244  	}
   245  	return false
   246  }
   247  
   248  // Write log message, should be done in go routine to not block
   249  func (log *Log) Write(msg *LogChanMsg) {
   250  	if msg == nil {
   251  		return
   252  	}
   253  
   254  	for _, l := range log.loggers {
   255  		if l == nil || l.Writer == nil {
   256  			continue
   257  		}
   258  		if err := l.Writer.WriteMessage(msg); err != nil {
   259  			_, _ = fmt.Fprintf(os.Stderr, "Error writing log: %v\n", err)
   260  		}
   261  	}
   262  }
   263  
   264  // SetMetrics level on logging for all loggers
   265  func (log *Log) SetMetrics() {
   266  	if log == nil {
   267  		return
   268  	}
   269  	for _, l := range log.loggers {
   270  		l.Writer.Level(MetricsLevel)
   271  	}
   272  }
   273  
   274  // SetTraffic level on logging for all loggers
   275  func (log *Log) SetTraffic() {
   276  	if log == nil {
   277  		return
   278  	}
   279  	for _, l := range log.loggers {
   280  		l.Writer.Level(TrafficLevel)
   281  	}
   282  }
   283  
   284  // SetDebug level on logging for all loggers
   285  func (log *Log) SetDebug() {
   286  	if log == nil {
   287  		return
   288  	}
   289  
   290  	for _, l := range log.loggers {
   291  		l.Writer.Level(DebugLevel)
   292  	}
   293  }
   294  
   295  // Close logger
   296  func (logger *Logger) Close() error {
   297  	if logger == nil {
   298  		return nil
   299  	}
   300  	var mErr *multierror.Error
   301  	if logger.closeFuncs != nil {
   302  		for _, v := range logger.closeFuncs {
   303  			if err := v(); err != nil {
   304  				mErr = multierror.Append(mErr, err)
   305  			}
   306  		}
   307  	}
   308  
   309  	return errors.WithStack(helpers.FlattenMultiError(mErr))
   310  }
   311  
   312  // AddCloseFunc add sub logger close function to be called upon logger close
   313  func (logger *Logger) AddCloseFunc(f func() error) {
   314  	if logger == nil {
   315  		return
   316  	}
   317  	if logger.closeFuncs == nil {
   318  		logger.closeFuncs = []func() error{f}
   319  		return
   320  	}
   321  	logger.closeFuncs = append(logger.closeFuncs, f)
   322  }
   323  
   324  // CreateStdoutJSONLogger create logger for JSON on terminal for later adding to loggers list
   325  func CreateStdoutJSONLogger() *Logger {
   326  	zerolog.LevelFieldName = "zerologlevel"
   327  	zlgr := zerolog.New(os.Stdout)
   328  	zlgr = zlgr.Level(zerolog.InfoLevel)
   329  	jsonWriter := NewJSONWriter(&zlgr)
   330  
   331  	return NewLogger(jsonWriter)
   332  }
   333  
   334  // CreateJSONLogger with io.Writer
   335  func CreateJSONLogger(writer io.Writer, closeFunc func() error) *Logger {
   336  	zerolog.LevelFieldName = "zerologlevel"
   337  	zlgr := zerolog.New(writer)
   338  	zlgr = zlgr.Level(zerolog.InfoLevel)
   339  	jsonLogger := NewLogger(NewJSONWriter(&zlgr))
   340  	if closeFunc != nil {
   341  		jsonLogger.AddCloseFunc(closeFunc)
   342  	}
   343  	return jsonLogger
   344  }
   345  
   346  // CreateTSVLogger with io.Writer
   347  func CreateTSVLogger(header []string, writer io.Writer, closeFunc func() error) (*Logger, error) {
   348  	tsvWriter := NewTSVWriter(header, writer)
   349  	tsvLogger := NewLogger(tsvWriter)
   350  	if closeFunc != nil {
   351  		tsvLogger.AddCloseFunc(closeFunc)
   352  	}
   353  	if err := tsvWriter.WriteHeader(); err != nil {
   354  		return nil, errors.Wrap(err, "Failed writing TSV header")
   355  	}
   356  	return tsvLogger, nil
   357  }
   358  
   359  // CreateStdoutLogger create logger for JSON on terminal for later adding to loggers list
   360  func CreateStdoutLogger() *Logger {
   361  	zlgr := zerolog.New(zerolog.ConsoleWriter{
   362  		Out:     os.Stdout,
   363  		NoColor: false,
   364  	})
   365  	zlgr = zlgr.Level(zerolog.InfoLevel)
   366  	jsonWriter := NewJSONWriter(&zlgr)
   367  
   368  	return NewLogger(jsonWriter)
   369  }
   370  
   371  // CreateDummyLogger auto discarding all entries
   372  func CreateDummyLogger() *Logger {
   373  	dummyWriter := NewTSVWriter(nil, io.Discard)
   374  	dummyLogger := NewLogger(dummyWriter)
   375  	return dummyLogger
   376  }
   377  
   378  // WaitForChanClose which ever comes first context cancel or c closed. Returns instantly if channel is nil.
   379  func WaitForChanClose(ctx context.Context, c chan interface{}) {
   380  	if c == nil {
   381  		return
   382  	}
   383  	select {
   384  	case <-ctx.Done():
   385  	case <-c:
   386  	}
   387  }