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 }