1// SPDX-License-Identifier: ISC 2// Copyright (c) 2014-2020 Bitmark Inc. 3// Use of this source code is governed by an ISC 4// license that can be found in the LICENSE file. 5 6package logger 7 8import ( 9 "encoding/json" 10 "errors" 11 "fmt" 12 "os" 13 "path" 14 "strings" 15 "sync" 16 "time" 17 18 "github.com/cihub/seelog" 19 20 "github.com/bitmark-inc/logger/level" 21) 22 23// initial configuration for the logger 24// 25// example of use (with structure tags for config file parsing) 26// type AppConfiguration struct { 27// … 28// Logging logger.Configuration `libucl:"logging" hcl:"logging" json:"logging"` 29// … 30// } 31// 32// err := logger.Initialise(conf.Logging) 33// if nil != err { // if failed then display message and exit 34// exitwithstatus.Message("logger error: %s", err) 35// } 36// defer logger.Finalise() 37// 38// example of ucl/hcl configuration section 39// logging { 40// directory = "/var/lib/app/log" 41// file = "app.log" 42// size = 1048576 43// count = 50 44// #console = true # to duplicate messages to console (default false) 45// levels { 46// DEFAULT = "info" 47// system = "error" 48// main = "warn" 49// } 50// } 51type Configuration struct { 52 Directory string `libucl:"directory" hcl:"directory" json:"directory"` 53 File string `libucl:"file" hcl:"file" json:"file"` 54 Size int `libucl:"size" hcl:"size" json:"size"` 55 Count int `libucl:"count" hcl:"count" json:"count"` 56 Levels map[string]string `libucl:"levels" hcl:"levels" json:"levels"` 57 Console bool `libucl:"console" hcl:"console" json:"console"` 58} 59 60// some restrictions on sizes 61const ( 62 minimumSize = 20000 63 minimumCount = 10 64 65 // The log messages will be "<tag><tagSuffix><message>" 66 tagSuffix = ": " 67 68 // the tagname reserved to set the default level for unknown tags 69 DefaultTag = "DEFAULT" 70 71 // the initial level for unknown tags 72 DefaultLevel = "error" 73) 74 75// Holds a set of default values for future NewChannel calls 76var levelMap = map[string]string{DefaultTag: DefaultLevel} 77 78// The logging channel structure 79// example of use 80// 81// var log *logger.L 82// log := logger.New("sometag") 83// 84// log.Debugf("value: %d", value) 85type L struct { 86 sync.Mutex 87 tag string 88 formatPrefix string 89 textPrefix string 90 level string 91 levelNumber int 92 log seelog.LoggerInterface 93} 94 95// LogLevels - log levels 96type LogLevels struct { 97 Levels []Level `json:"levels"` 98} 99 100// Level - log level info 101type Level struct { 102 Tag string `json:"tag"` 103 LogLevel string `json:"category"` 104} 105 106type outputTarget int 107 108const ( 109 stdOut outputTarget = iota + 1 110 fileOut 111) 112 113// loggers - holds all logger info 114type loggers struct { 115 sync.Mutex 116 globalLog *L 117 initialised bool 118 data []*L 119 output outputTarget 120} 121 122var globalData loggers 123 124// default set output to standard out 125func init() { 126 globalData.output = stdOut 127 stdLogger := defaultLogger() 128 129 _ = seelog.ReplaceLogger(stdLogger) 130 // ensure that the global critical/panic functions always written 131 globalData.globalLog = New("PANIC") 132 globalData.globalLog.level = level.Critical 133 globalData.globalLog.levelNumber = level.ValidLevels[level.Critical] 134} 135 136func defaultLogger() seelog.LoggerInterface { 137 config := fmt.Sprintf(` 138 <seelog type="adaptive" 139 mininterval="2000000" 140 maxinterval="100000000" 141 critmsgcount="500" 142 minlevel="trace"> 143 <outputs formatid="all"> 144 <console /> 145 </outputs> 146 <formats> 147 <format id="all" format="%%Date %%Time [%%LEVEL] %%Msg%%n" /> 148 </formats> 149 </seelog>`) 150 151 logger, _ := seelog.LoggerFromConfigAsString(config) 152 return logger 153} 154 155// Set up the logging system 156func Initialise(configuration Configuration) error { 157 if globalData.initialised { 158 return errors.New("logger is already initialised") 159 } 160 161 globalData.output = fileOut 162 163 if "" == configuration.Directory { 164 return errors.New("Directory cannot be empty") 165 } 166 167 if "" == configuration.File { 168 return errors.New("File cannot be empty") 169 } 170 171 d, f := path.Split(configuration.File) 172 if "" != d && f != configuration.File { 173 return fmt.Errorf("File: %q cannot be a path name", configuration.File) 174 } 175 176 if configuration.Size < minimumSize { 177 return fmt.Errorf("Size: %d cannot be less than: %d", configuration.Size, minimumSize) 178 } 179 180 if configuration.Count < minimumCount { 181 return fmt.Errorf("Count: %d cannot be less than: %d", configuration.Count, minimumCount) 182 } 183 184 info, err := os.Lstat(configuration.Directory) 185 if nil != err { 186 return err 187 } 188 if !info.IsDir() { 189 return fmt.Errorf("Directory: %q does not exist", configuration.Directory) 190 } 191 //permission := info.Mode().Perm() 192 193 lockFile := path.Join(configuration.Directory, "log.test") 194 os.Remove(lockFile) 195 fd, err := os.Create(lockFile) 196 if nil != err { 197 return err 198 } 199 defer fd.Close() 200 defer os.Remove(lockFile) 201 n, err := fd.Write([]byte("0123456789")) 202 if nil != err { 203 return err 204 } 205 if 10 != n { 206 return errors.New("unable to write to logging files") 207 } 208 209 for tag, l := range configuration.Levels { 210 // make sure that levelMap only contains correct data 211 // by ignoring invalid levels 212 if _, ok := level.ValidLevels[l]; ok { 213 levelMap[tag] = l 214 } 215 } 216 217 optionalConsole := "" 218 if configuration.Console { 219 optionalConsole = "<console />" 220 } 221 222 filepath := path.Join(configuration.Directory, configuration.File) 223 config := fmt.Sprintf(` 224 <seelog type="adaptive" 225 mininterval="2000000" 226 maxinterval="100000000" 227 critmsgcount="500" 228 minlevel="trace"> 229 <outputs formatid="all"> 230 <rollingfile type="size" filename="%s" maxsize="%d" maxrolls="%d" /> 231 %s 232 </outputs> 233 <formats> 234 <format id="all" format="%%Date %%Time [%%LEVEL] %%Msg%%n" /> 235 </formats> 236 </seelog>`, filepath, configuration.Size, configuration.Count, optionalConsole) 237 238 logger, err := seelog.LoggerFromConfigAsString(config) 239 if err != nil { 240 return err 241 } 242 err = seelog.ReplaceLogger(logger) 243 if nil == err { 244 seelog.Current.Warn("LOGGER: ===== Logging system started =====") 245 globalData.initialised = true 246 247 // ensure that the global critical/panic functions always write to the log file 248 globalData.globalLog = New("PANIC") 249 globalData.globalLog.level = level.Critical 250 globalData.globalLog.levelNumber = level.ValidLevels[level.Critical] 251 } 252 return err 253} 254 255// flush all channels and let log message goes to standard out 256func Finalise() { 257 _ = seelog.Current.Warn("LOGGER: ===== Logging system stopped =====") 258 seelog.Flush() 259 260 // if log message goes to file, make it back to standard output 261 if globalData.output == fileOut { 262 globalData.initialised = false 263 globalData.output = stdOut 264 _ = seelog.ReplaceLogger(defaultLogger()) 265 266 globalData.globalLog = New("PANIC") 267 globalData.globalLog.level = level.Critical 268 globalData.globalLog.levelNumber = level.ValidLevels[level.Critical] 269 } 270 271 globalData.data = globalData.data[:0] 272} 273 274// flush all channels 275func Flush() { 276 seelog.Flush() 277} 278 279// Open a new logging channel with a specified tag 280func New(tag string) *L { 281 // if log message goes to file, then Initialise should be called 282 if globalData.output == fileOut && !globalData.initialised { 283 panic("logger.New Initialise was not called") 284 } 285 286 // map % -> %% to be printf safe 287 s := strings.Split(tag, "%") 288 j := strings.Join(s, "%%") 289 290 // determine the level 291 l, ok := levelMap[tag] 292 if !ok { 293 l, ok = levelMap[DefaultTag] 294 } 295 if !ok { 296 l = DefaultLevel 297 } 298 299 // create a logger channel 300 ptr := &L{ 301 tag: tag, // for referencing default level 302 formatPrefix: j + tagSuffix, 303 textPrefix: tag + tagSuffix, 304 level: l, 305 levelNumber: level.ValidLevels[l], // level is validated so get a non-zero value 306 log: seelog.Current, 307 } 308 309 globalData.Lock() 310 globalData.data = append(globalData.data, ptr) 311 defer globalData.Unlock() 312 313 return ptr 314} 315 316// flush messages 317func (l *L) Flush() { 318 Flush() 319} 320 321// global logging message 322func Critical(message string) { 323 globalData.globalLog.Critical(message) 324} 325 326// global logging formatted message 327func Criticalf(format string, arguments ...interface{}) { 328 globalData.globalLog.Criticalf(format, arguments...) 329} 330 331// global logging message + panic 332func Panic(message string) { 333 globalData.globalLog.Critical(message) 334 Flush() 335 time.Sleep(100 * time.Millisecond) // to allow logging outputTarget 336 panic(message) 337} 338 339// global logging formatted message + panic 340func Panicf(format string, arguments ...interface{}) { 341 globalData.globalLog.Criticalf(format, arguments...) 342 Flush() 343 time.Sleep(100 * time.Millisecond) // to allow logging outputTarget 344 panic(fmt.Sprintf(format, arguments...)) 345} 346 347// conditional panic 348func PanicIfError(message string, err error) { 349 if nil == err { 350 return 351 } 352 Panicf("%s failed with error: %v", message, err) 353} 354 355// ListLevels - return log level info in json format 356// it's not lock protected, because it should be low frequency to list log 357// levels 358func ListLevels() ([]byte, error) { 359 levels := make([]Level, 0) 360 361 for _, l := range globalData.data { 362 levels = append(levels, Level{ 363 Tag: l.tag, 364 LogLevel: l.level, 365 }) 366 } 367 368 ll := LogLevels{Levels: levels} 369 bs, err := json.Marshal(ll) 370 if nil != err { 371 return []byte{}, err 372 } 373 374 return bs, nil 375} 376 377// UpdateTagLogLevel - update log level for specific tag 378func UpdateTagLogLevel(tag, newLevel string) error { 379 globalData.Lock() 380 defer globalData.Unlock() 381 382 for i, l := range globalData.data { 383 if l.tag == tag { 384 if num, ok := level.ValidLevels[newLevel]; !ok { 385 return fmt.Errorf("level %s invalid", newLevel) 386 } else { 387 globalData.data[i].levelNumber = num 388 globalData.data[i].level = newLevel 389 return nil 390 } 391 } 392 } 393 394 return fmt.Errorf("tag %s not found", tag) 395} 396 397func validLogger(l *L) bool { 398 if globalData.output == stdOut { 399 return true 400 } 401 402 if !globalData.initialised || nil == l { 403 return false 404 } 405 406 return true 407} 408