123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- #
- #
- # Nim's Runtime Library
- # (c) Copyright 2015 Andreas Rumpf, Dominik Picheta
- #
- # See the file "copying.txt", included in this
- # distribution, for details about the copyright.
- #
- ## This module implements a simple logger. It has been designed to be as simple
- ## as possible to avoid bloat, if this library does not fulfill your needs,
- ## write your own.
- ##
- ## Format strings support the following variables which must be prefixed with
- ## the dollar operator (``$``, see example below):
- ##
- ## ============ =======================
- ## Operator Output
- ## ============ =======================
- ## $date Current date
- ## $time Current time
- ## $datetime $dateT$time
- ## $app ``os.getAppFilename()``
- ## $appname base name of $app
- ## $appdir directory name of $app
- ## $levelid first letter of log level
- ## $levelname log level name
- ## ============ =======================
- ##
- ##
- ## The following example demonstrates logging to three different handlers
- ## simultaneously:
- ##
- ## .. code-block:: nim
- ##
- ## var L = newConsoleLogger()
- ## var fL = newFileLogger("test.log", fmtStr = verboseFmtStr)
- ## var rL = newRollingFileLogger("rolling.log", fmtStr = verboseFmtStr)
- ## addHandler(L)
- ## addHandler(fL)
- ## addHandler(rL)
- ## info("920410:52 accepted")
- ## warn("4 8 15 16 23 4-- Error")
- ## error("922044:16 SYSTEM FAILURE")
- ## fatal("SYSTEM FAILURE SYSTEM FAILURE")
- ## # Using the aformetioned operator
- ## var opL = newConsoleLogger(fmtStr = "$datetime :: ")
- ## addHandler(opL)
- ## info("Starting web server...")
- ## # Will print something like 2018-12-17T19:28:05 :: Starting web server...
- ##
- ## **Warning:** The global list of handlers is a thread var, this means that
- ## the handlers must be re-added in each thread.
- ## **Warning:** When logging on disk or console, only error and fatal messages
- ## are flushed out immediately. Use flushFile() where needed.
- import strutils, times
- when not defined(js):
- import os
- type
- Level* = enum ## logging level
- lvlAll, ## all levels active
- lvlDebug, ## debug level (and any above) active
- lvlInfo, ## info level (and any above) active
- lvlNotice, ## info notice (and any above) active
- lvlWarn, ## warn level (and any above) active
- lvlError, ## error level (and any above) active
- lvlFatal, ## fatal level (and any above) active
- lvlNone ## no levels active
- const
- LevelNames*: array[Level, string] = [
- "DEBUG", "DEBUG", "INFO", "NOTICE", "WARN", "ERROR", "FATAL", "NONE"
- ]
- defaultFmtStr* = "$levelname " ## default format string
- verboseFmtStr* = "$levelid, [$datetime] -- $appname: "
- type
- Logger* = ref object of RootObj ## abstract logger; the base type of all loggers
- levelThreshold*: Level ## only messages of level >= levelThreshold
- ## should be processed
- fmtStr*: string ## = defaultFmtStr by default, see substituteLog for $date etc.
- ConsoleLogger* = ref object of Logger ## logger that writes the messages to the
- ## console
- useStderr*: bool ## will send logs into Stderr if set
- when not defined(js):
- type
- FileLogger* = ref object of Logger ## logger that writes the messages to a file
- file*: File ## the wrapped file.
- RollingFileLogger* = ref object of FileLogger ## logger that writes the
- ## messages to a file and
- ## performs log rotation
- maxLines: int # maximum number of lines
- curLine : int
- baseName: string # initial filename
- baseMode: FileMode # initial file mode
- logFiles: int # how many log files already created, e.g. basename.1, basename.2...
- bufSize: int # size of output buffer (-1: use system defaults, 0: unbuffered, >0: fixed buffer size)
- var
- level {.threadvar.}: Level ## global log filter
- handlers {.threadvar.}: seq[Logger] ## handlers with their own log levels
- proc substituteLog*(frmt: string, level: Level, args: varargs[string, `$`]): string =
- ## Format a log message using the ``frmt`` format string, ``level`` and varargs.
- ## See the module documentation for the format string syntax.
- var msgLen = 0
- for arg in args:
- msgLen += arg.len
- result = newStringOfCap(frmt.len + msgLen + 20)
- var i = 0
- while i < frmt.len:
- if frmt[i] != '$':
- result.add(frmt[i])
- inc(i)
- else:
- inc(i)
- var v = ""
- let app = when defined(js): "" else: getAppFilename()
- while frmt[i] in IdentChars:
- v.add(toLowerAscii(frmt[i]))
- inc(i)
- case v
- of "date": result.add(getDateStr())
- of "time": result.add(getClockStr())
- of "datetime": result.add(getDateStr() & "T" & getClockStr())
- of "app": result.add(app)
- of "appdir":
- when not defined(js): result.add(app.splitFile.dir)
- of "appname":
- when not defined(js): result.add(app.splitFile.name)
- of "levelid": result.add(LevelNames[level][0])
- of "levelname": result.add(LevelNames[level])
- else: discard
- for arg in args:
- result.add(arg)
- method log*(logger: Logger, level: Level, args: varargs[string, `$`]) {.
- raises: [Exception], gcsafe,
- tags: [TimeEffect, WriteIOEffect, ReadIOEffect], base.} =
- ## Override this method in custom loggers. Default implementation does
- ## nothing.
- discard
- method log*(logger: ConsoleLogger, level: Level, args: varargs[string, `$`]) =
- ## Logs to the console using ``logger`` only.
- if level >= logging.level and level >= logger.levelThreshold:
- let ln = substituteLog(logger.fmtStr, level, args)
- when defined(js):
- let cln: cstring = ln
- {.emit: "console.log(`cln`);".}
- else:
- try:
- var handle = stdout
- if logger.useStderr:
- handle = stderr
- writeLine(handle, ln)
- if level in {lvlError, lvlFatal}: flushFile(handle)
- except IOError:
- discard
- proc newConsoleLogger*(levelThreshold = lvlAll, fmtStr = defaultFmtStr, useStderr=false): ConsoleLogger =
- ## Creates a new console logger. This logger logs to the console.
- new result
- result.fmtStr = fmtStr
- result.levelThreshold = levelThreshold
- result.useStderr = useStderr
- when not defined(js):
- method log*(logger: FileLogger, level: Level, args: varargs[string, `$`]) =
- ## Logs to a file using ``logger`` only.
- if level >= logging.level and level >= logger.levelThreshold:
- writeLine(logger.file, substituteLog(logger.fmtStr, level, args))
- if level in {lvlError, lvlFatal}: flushFile(logger.file)
- proc defaultFilename*(): string =
- ## Returns the default filename for a logger.
- var (path, name, _) = splitFile(getAppFilename())
- result = changeFileExt(path / name, "log")
- proc newFileLogger*(file: File,
- levelThreshold = lvlAll,
- fmtStr = defaultFmtStr): FileLogger =
- ## Creates a new file logger. This logger logs to ``file``.
- new(result)
- result.file = file
- result.levelThreshold = levelThreshold
- result.fmtStr = fmtStr
- proc newFileLogger*(filename = defaultFilename(),
- mode: FileMode = fmAppend,
- levelThreshold = lvlAll,
- fmtStr = defaultFmtStr,
- bufSize: int = -1): FileLogger =
- ## Creates a new file logger. This logger logs to a file, specified
- ## by ``fileName``.
- ## Use ``bufSize`` as size of the output buffer when writing the file
- ## (-1: use system defaults, 0: unbuffered, >0: fixed buffer size).
- let file = open(filename, mode, bufSize = bufSize)
- newFileLogger(file, levelThreshold, fmtStr)
- # ------
- proc countLogLines(logger: RollingFileLogger): int =
- result = 0
- let fp = open(logger.baseName, fmRead)
- for line in fp.lines():
- result.inc()
- fp.close()
- proc countFiles(filename: string): int =
- # Example: file.log.1
- result = 0
- var (dir, name, ext) = splitFile(filename)
- if dir == "":
- dir = "."
- for kind, path in walkDir(dir):
- if kind == pcFile:
- let llfn = name & ext & ExtSep
- if path.extractFilename.startsWith(llfn):
- let numS = path.extractFilename[llfn.len .. ^1]
- try:
- let num = parseInt(numS)
- if num > result:
- result = num
- except ValueError: discard
- proc newRollingFileLogger*(filename = defaultFilename(),
- mode: FileMode = fmReadWrite,
- levelThreshold = lvlAll,
- fmtStr = defaultFmtStr,
- maxLines = 1000,
- bufSize: int = -1): RollingFileLogger =
- ## Creates a new rolling file logger. Once a file reaches ``maxLines`` lines
- ## a new log file will be started and the old will be renamed.
- ## Use ``bufSize`` as size of the output buffer when writing the file
- ## (-1: use system defaults, 0: unbuffered, >0: fixed buffer size).
- new(result)
- result.levelThreshold = levelThreshold
- result.fmtStr = fmtStr
- result.maxLines = maxLines
- result.bufSize = bufSize
- result.file = open(filename, mode, bufSize=result.bufSize)
- result.curLine = 0
- result.baseName = filename
- result.baseMode = mode
- result.logFiles = countFiles(filename)
- if mode == fmAppend:
- # We need to get a line count because we will be appending to the file.
- result.curLine = countLogLines(result)
- proc rotate(logger: RollingFileLogger) =
- let (dir, name, ext) = splitFile(logger.baseName)
- for i in countdown(logger.logFiles, 0):
- let srcSuff = if i != 0: ExtSep & $i else: ""
- moveFile(dir / (name & ext & srcSuff),
- dir / (name & ext & ExtSep & $(i+1)))
- method log*(logger: RollingFileLogger, level: Level, args: varargs[string, `$`]) =
- ## Logs to a file using rolling ``logger`` only.
- if level >= logging.level and level >= logger.levelThreshold:
- if logger.curLine >= logger.maxLines:
- logger.file.close()
- rotate(logger)
- logger.logFiles.inc
- logger.curLine = 0
- logger.file = open(logger.baseName, logger.baseMode, bufSize = logger.bufSize)
- writeLine(logger.file, substituteLog(logger.fmtStr, level, args))
- if level in {lvlError, lvlFatal}: flushFile(logger.file)
- logger.curLine.inc
- # --------
- proc logLoop(level: Level, args: varargs[string, `$`]) =
- for logger in items(handlers):
- if level >= logger.levelThreshold:
- log(logger, level, args)
- template log*(level: Level, args: varargs[string, `$`]) =
- ## Logs a message to all registered handlers at the given level.
- bind logLoop
- bind `%`
- bind logging.level
- if level >= logging.level:
- logLoop(level, args)
- template debug*(args: varargs[string, `$`]) =
- ## Logs a debug message to all registered handlers.
- ##
- ## Messages that are useful to the application developer only and are usually
- ## turned off in release.
- log(lvlDebug, args)
- template info*(args: varargs[string, `$`]) =
- ## Logs an info message to all registered handlers.
- ##
- ## Messages that are generated during the normal operation of an application
- ## and are of no particular importance. Useful to aggregate for potential
- ## later analysis.
- log(lvlInfo, args)
- template notice*(args: varargs[string, `$`]) =
- ## Logs an notice message to all registered handlers.
- ##
- ## Semantically very similar to `info`, but meant to be messages you want to
- ## be actively notified about (depending on your application).
- ## These could be, for example, grouped by hour and mailed out.
- log(lvlNotice, args)
- template warn*(args: varargs[string, `$`]) =
- ## Logs a warning message to all registered handlers.
- ##
- ## A non-error message that may indicate a potential problem rising or
- ## impacted performance.
- log(lvlWarn, args)
- template error*(args: varargs[string, `$`]) =
- ## Logs an error message to all registered handlers.
- ##
- ## A application-level error condition. For example, some user input generated
- ## an exception. The application will continue to run, but functionality or
- ## data was impacted, possibly visible to users.
- log(lvlError, args)
- template fatal*(args: varargs[string, `$`]) =
- ## Logs a fatal error message to all registered handlers.
- ##
- ## A application-level fatal condition. FATAL usually means that the application
- ## cannot go on and will exit (but this logging event will not do that for you).
- log(lvlFatal, args)
- proc addHandler*(handler: Logger) =
- ## Adds ``handler`` to the list of handlers.
- handlers.add(handler)
- proc getHandlers*(): seq[Logger] =
- ## Returns a list of all the registered handlers.
- return handlers
- proc setLogFilter*(lvl: Level) =
- ## Sets the global log filter.
- level = lvl
- proc getLogFilter*(): Level =
- ## Gets the global log filter.
- return level
- # --------------
- when not defined(testing) and isMainModule:
- var L = newConsoleLogger()
- when not defined(js):
- var fL = newFileLogger("test.log", fmtStr = verboseFmtStr)
- var rL = newRollingFileLogger("rolling.log", fmtStr = verboseFmtStr)
- addHandler(fL)
- addHandler(rL)
- addHandler(L)
- for i in 0 .. 25:
- info("hello", i)
- var nilString: string
- info "hello ", nilString
|