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 }