github.com/viant/toolbox@v0.34.5/file_logger.go (about)

     1  package toolbox
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/signal"
     9  	"strings"
    10  	"sync"
    11  	"sync/atomic"
    12  	"syscall"
    13  	"time"
    14  )
    15  
    16  //FileLoggerConfig represents FileLogger
    17  type FileLoggerConfig struct {
    18  	LogType            string
    19  	FileTemplate       string
    20  	filenameProvider   func(t time.Time) string
    21  	QueueFlashCount    int
    22  	MaxQueueSize       int
    23  	FlushRequencyInMs  int //type backward-forward compatibility
    24  	FlushFrequencyInMs int
    25  	MaxIddleTimeInSec  int
    26  	inited             bool
    27  }
    28  
    29  func (c *FileLoggerConfig) Init() {
    30  	if c.inited {
    31  		return
    32  	}
    33  	defaultProvider := func(t time.Time) string {
    34  		return c.FileTemplate
    35  	}
    36  	c.inited = true
    37  	template := c.FileTemplate
    38  	c.filenameProvider = defaultProvider
    39  	startIndex := strings.Index(template, "[")
    40  	if startIndex == -1 {
    41  		return
    42  	}
    43  	endIndex := strings.Index(template, "]")
    44  	if endIndex == -1 {
    45  		return
    46  	}
    47  	format := template[startIndex+1 : endIndex]
    48  	layout := DateFormatToLayout(format)
    49  	c.filenameProvider = func(t time.Time) string {
    50  		formatted := t.Format(layout)
    51  		return strings.Replace(template, "["+format+"]", formatted, 1)
    52  	}
    53  }
    54  
    55  //Validate valides configuration sttings
    56  func (c *FileLoggerConfig) Validate() error {
    57  	if len(c.LogType) == 0 {
    58  		return errors.New("Log type was empty")
    59  	}
    60  	if c.FlushFrequencyInMs == 0 {
    61  		c.FlushFrequencyInMs = c.FlushRequencyInMs
    62  	}
    63  	if c.FlushFrequencyInMs == 0 {
    64  		return errors.New("FlushFrequencyInMs was 0")
    65  	}
    66  
    67  	if c.MaxQueueSize == 0 {
    68  		return errors.New("MaxQueueSize was 0")
    69  	}
    70  	if len(c.FileTemplate) == 0 {
    71  		return errors.New("FileTemplate was empty")
    72  	}
    73  
    74  	if c.MaxIddleTimeInSec == 0 {
    75  		return errors.New("MaxIddleTimeInSec was 0")
    76  	}
    77  	if c.QueueFlashCount == 0 {
    78  		return errors.New("QueueFlashCount was 0")
    79  	}
    80  	return nil
    81  }
    82  
    83  //LogStream represents individual log stream
    84  type LogStream struct {
    85  	Name             string
    86  	Logger           *FileLogger
    87  	Config           *FileLoggerConfig
    88  	RecordCount      int
    89  	File             *os.File
    90  	LastAddQueueTime time.Time
    91  	LastWriteTime    uint64
    92  	Messages         chan string
    93  	Complete         chan bool
    94  }
    95  
    96  //Log logs message into stream
    97  func (s *LogStream) Log(message *LogMessage) error {
    98  	if message == nil {
    99  		return errors.New("message was nil")
   100  	}
   101  	var textMessage = ""
   102  	var ok bool
   103  	if textMessage, ok = message.Message.(string); ok {
   104  	} else if IsStruct(message.Message) || IsMap(message.Message) || IsSlice(message.Message) {
   105  		var buf = new(bytes.Buffer)
   106  		err := NewJSONEncoderFactory().Create(buf).Encode(message.Message)
   107  		if err != nil {
   108  			return err
   109  		}
   110  		textMessage = strings.Trim(buf.String(), "\n\r")
   111  	} else {
   112  		return fmt.Errorf("unsupported type: %T", message.Message)
   113  	}
   114  	s.Messages <- textMessage
   115  	s.LastAddQueueTime = time.Now()
   116  	return nil
   117  }
   118  
   119  func (s *LogStream) write(message string) error {
   120  	atomic.StoreUint64(&s.LastWriteTime, uint64(time.Now().UnixNano()))
   121  	_, err := s.File.WriteString(message)
   122  	if err != nil {
   123  		return err
   124  	}
   125  	return s.File.Sync()
   126  }
   127  
   128  //Close closes stream.
   129  func (s *LogStream) Close() {
   130  	s.Logger.streamMapMutex.Lock()
   131  	delete(s.Logger.streams, s.Name)
   132  	s.Logger.streamMapMutex.Unlock()
   133  	s.File.Close()
   134  
   135  }
   136  
   137  func (s *LogStream) isFrequencyFlushNeeded() bool {
   138  	elapsedInMs := (int(time.Now().UnixNano()) - int(atomic.LoadUint64(&s.LastWriteTime))) / 1000000
   139  	return elapsedInMs >= s.Config.FlushFrequencyInMs
   140  }
   141  
   142  func (s *LogStream) manageWritesInBatch() {
   143  	messageCount := 0
   144  	var message, messages string
   145  	var timeout = time.Duration(2 * int(s.Config.FlushFrequencyInMs) * int(time.Millisecond))
   146  	for {
   147  		select {
   148  		case done := <-s.Complete:
   149  			if done {
   150  				manageWritesInBatchLoopFlush(s, messageCount, messages)
   151  				s.Close()
   152  				os.Exit(0)
   153  			}
   154  		case <-time.After(timeout):
   155  			if !manageWritesInBatchLoopFlush(s, messageCount, messages) {
   156  				return
   157  			}
   158  			messageCount = 0
   159  			messages = ""
   160  		case message = <-s.Messages:
   161  			messages += message + "\n"
   162  			messageCount++
   163  			s.RecordCount++
   164  
   165  			var hasReachMaxRecrods = messageCount >= s.Config.QueueFlashCount && s.Config.QueueFlashCount > 0
   166  			if hasReachMaxRecrods || s.isFrequencyFlushNeeded() {
   167  				_ = s.write(messages)
   168  				messages = ""
   169  				messageCount = 0
   170  			}
   171  
   172  		}
   173  	}
   174  }
   175  
   176  func manageWritesInBatchLoopFlush(s *LogStream, messageCount int, messages string) bool {
   177  	if messageCount > 0 {
   178  		if s.isFrequencyFlushNeeded() {
   179  			err := s.write(messages)
   180  			if err != nil {
   181  				fmt.Printf("failed to write to log due to %v", err)
   182  			}
   183  			return true
   184  		}
   185  	}
   186  	elapsedInMs := (int(time.Now().UnixNano()) - int(atomic.LoadUint64(&s.LastWriteTime))) / 1000000
   187  	if elapsedInMs > s.Config.MaxIddleTimeInSec*1000 {
   188  		s.Close()
   189  		return false
   190  	}
   191  	return true
   192  }
   193  
   194  //FileLogger represents a file logger
   195  type FileLogger struct {
   196  	config         map[string]*FileLoggerConfig
   197  	streamMapMutex *sync.RWMutex
   198  	streams        map[string]*LogStream
   199  	siginal        chan os.Signal
   200  }
   201  
   202  func (l *FileLogger) getConfig(messageType string) (*FileLoggerConfig, error) {
   203  	config, found := l.config[messageType]
   204  	if !found {
   205  		return nil, errors.New("failed to lookup config for " + messageType)
   206  	}
   207  	config.Init()
   208  	return config, nil
   209  }
   210  
   211  //NewLogStream creat a new LogStream for passed om path and file config
   212  func (l *FileLogger) NewLogStream(path string, config *FileLoggerConfig) (*LogStream, error) {
   213  	osFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	logStream := &LogStream{Name: path, Logger: l, Config: config, File: osFile, Messages: make(chan string, config.MaxQueueSize), Complete: make(chan bool)}
   218  	go func() {
   219  		logStream.manageWritesInBatch()
   220  	}()
   221  	return logStream, nil
   222  }
   223  
   224  func (l *FileLogger) acquireLogStream(messageType string) (*LogStream, error) {
   225  	config, err := l.getConfig(messageType)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	fileName := config.filenameProvider(time.Now())
   230  	l.streamMapMutex.RLock()
   231  	logStream, found := l.streams[fileName]
   232  	l.streamMapMutex.RUnlock()
   233  	if found {
   234  		return logStream, nil
   235  	}
   236  
   237  	logStream, err = l.NewLogStream(fileName, config)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	l.streamMapMutex.Lock()
   242  	l.streams[fileName] = logStream
   243  	l.streamMapMutex.Unlock()
   244  	return logStream, nil
   245  }
   246  
   247  //Log logs message into stream
   248  func (l *FileLogger) Log(message *LogMessage) error {
   249  	logStream, err := l.acquireLogStream(message.MessageType)
   250  	if err != nil {
   251  		return err
   252  	}
   253  	return logStream.Log(message)
   254  }
   255  
   256  //Notify notifies logger
   257  func (l *FileLogger) Notify(siginal os.Signal) {
   258  	l.siginal <- siginal
   259  }
   260  
   261  //NewFileLogger create new file logger
   262  func NewFileLogger(configs ...FileLoggerConfig) (*FileLogger, error) {
   263  	result := &FileLogger{
   264  		config:         make(map[string]*FileLoggerConfig),
   265  		streamMapMutex: &sync.RWMutex{},
   266  		streams:        make(map[string]*LogStream),
   267  	}
   268  
   269  	for i := range configs {
   270  		err := configs[i].Validate()
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  		result.config[configs[i].LogType] = &configs[i]
   275  	}
   276  
   277  	// If there's a signal to quit the program send it to channel
   278  	result.siginal = make(chan os.Signal, 1)
   279  	signal.Notify(result.siginal,
   280  		syscall.SIGINT,
   281  		syscall.SIGTERM)
   282  
   283  	go func() {
   284  
   285  		// Block until receive a quit signal
   286  		_quit := <-result.siginal
   287  		_ = _quit // don't care which type
   288  		for _, stream := range result.streams {
   289  			// No wait flush
   290  			stream.Config.FlushFrequencyInMs = 0
   291  			// Write logs now
   292  			stream.Complete <- true
   293  		}
   294  	}()
   295  
   296  	return result, nil
   297  }