logging.nim 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2015 Andreas Rumpf, Dominik Picheta
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module implements a simple logger. It has been designed to be as simple
  10. ## as possible to avoid bloat, if this library does not fulfill your needs,
  11. ## write your own.
  12. ##
  13. ## Format strings support the following variables which must be prefixed with
  14. ## the dollar operator (``$``, see example below):
  15. ##
  16. ## ============ =======================
  17. ## Operator Output
  18. ## ============ =======================
  19. ## $date Current date
  20. ## $time Current time
  21. ## $datetime $dateT$time
  22. ## $app ``os.getAppFilename()``
  23. ## $appname base name of $app
  24. ## $appdir directory name of $app
  25. ## $levelid first letter of log level
  26. ## $levelname log level name
  27. ## ============ =======================
  28. ##
  29. ##
  30. ## The following example demonstrates logging to three different handlers
  31. ## simultaneously:
  32. ##
  33. ## .. code-block:: nim
  34. ##
  35. ## var L = newConsoleLogger()
  36. ## var fL = newFileLogger("test.log", fmtStr = verboseFmtStr)
  37. ## var rL = newRollingFileLogger("rolling.log", fmtStr = verboseFmtStr)
  38. ## addHandler(L)
  39. ## addHandler(fL)
  40. ## addHandler(rL)
  41. ## info("920410:52 accepted")
  42. ## warn("4 8 15 16 23 4-- Error")
  43. ## error("922044:16 SYSTEM FAILURE")
  44. ## fatal("SYSTEM FAILURE SYSTEM FAILURE")
  45. ## # Using the aformetioned operator
  46. ## var opL = newConsoleLogger(fmtStr = "$datetime :: ")
  47. ## addHandler(opL)
  48. ## info("Starting web server...")
  49. ## # Will print something like 2018-12-17T19:28:05 :: Starting web server...
  50. ##
  51. ## **Warning:** The global list of handlers is a thread var, this means that
  52. ## the handlers must be re-added in each thread.
  53. ## **Warning:** When logging on disk or console, only error and fatal messages
  54. ## are flushed out immediately. Use flushFile() where needed.
  55. import strutils, times
  56. when not defined(js):
  57. import os
  58. type
  59. Level* = enum ## logging level
  60. lvlAll, ## all levels active
  61. lvlDebug, ## debug level (and any above) active
  62. lvlInfo, ## info level (and any above) active
  63. lvlNotice, ## info notice (and any above) active
  64. lvlWarn, ## warn level (and any above) active
  65. lvlError, ## error level (and any above) active
  66. lvlFatal, ## fatal level (and any above) active
  67. lvlNone ## no levels active
  68. const
  69. LevelNames*: array[Level, string] = [
  70. "DEBUG", "DEBUG", "INFO", "NOTICE", "WARN", "ERROR", "FATAL", "NONE"
  71. ]
  72. defaultFmtStr* = "$levelname " ## default format string
  73. verboseFmtStr* = "$levelid, [$datetime] -- $appname: "
  74. type
  75. Logger* = ref object of RootObj ## abstract logger; the base type of all loggers
  76. levelThreshold*: Level ## only messages of level >= levelThreshold
  77. ## should be processed
  78. fmtStr*: string ## = defaultFmtStr by default, see substituteLog for $date etc.
  79. ConsoleLogger* = ref object of Logger ## logger that writes the messages to the
  80. ## console
  81. useStderr*: bool ## will send logs into Stderr if set
  82. when not defined(js):
  83. type
  84. FileLogger* = ref object of Logger ## logger that writes the messages to a file
  85. file*: File ## the wrapped file.
  86. RollingFileLogger* = ref object of FileLogger ## logger that writes the
  87. ## messages to a file and
  88. ## performs log rotation
  89. maxLines: int # maximum number of lines
  90. curLine : int
  91. baseName: string # initial filename
  92. baseMode: FileMode # initial file mode
  93. logFiles: int # how many log files already created, e.g. basename.1, basename.2...
  94. bufSize: int # size of output buffer (-1: use system defaults, 0: unbuffered, >0: fixed buffer size)
  95. var
  96. level {.threadvar.}: Level ## global log filter
  97. handlers {.threadvar.}: seq[Logger] ## handlers with their own log levels
  98. proc substituteLog*(frmt: string, level: Level, args: varargs[string, `$`]): string =
  99. ## Format a log message using the ``frmt`` format string, ``level`` and varargs.
  100. ## See the module documentation for the format string syntax.
  101. var msgLen = 0
  102. for arg in args:
  103. msgLen += arg.len
  104. result = newStringOfCap(frmt.len + msgLen + 20)
  105. var i = 0
  106. while i < frmt.len:
  107. if frmt[i] != '$':
  108. result.add(frmt[i])
  109. inc(i)
  110. else:
  111. inc(i)
  112. var v = ""
  113. let app = when defined(js): "" else: getAppFilename()
  114. while frmt[i] in IdentChars:
  115. v.add(toLowerAscii(frmt[i]))
  116. inc(i)
  117. case v
  118. of "date": result.add(getDateStr())
  119. of "time": result.add(getClockStr())
  120. of "datetime": result.add(getDateStr() & "T" & getClockStr())
  121. of "app": result.add(app)
  122. of "appdir":
  123. when not defined(js): result.add(app.splitFile.dir)
  124. of "appname":
  125. when not defined(js): result.add(app.splitFile.name)
  126. of "levelid": result.add(LevelNames[level][0])
  127. of "levelname": result.add(LevelNames[level])
  128. else: discard
  129. for arg in args:
  130. result.add(arg)
  131. method log*(logger: Logger, level: Level, args: varargs[string, `$`]) {.
  132. raises: [Exception], gcsafe,
  133. tags: [TimeEffect, WriteIOEffect, ReadIOEffect], base.} =
  134. ## Override this method in custom loggers. Default implementation does
  135. ## nothing.
  136. discard
  137. method log*(logger: ConsoleLogger, level: Level, args: varargs[string, `$`]) =
  138. ## Logs to the console using ``logger`` only.
  139. if level >= logging.level and level >= logger.levelThreshold:
  140. let ln = substituteLog(logger.fmtStr, level, args)
  141. when defined(js):
  142. let cln: cstring = ln
  143. {.emit: "console.log(`cln`);".}
  144. else:
  145. try:
  146. var handle = stdout
  147. if logger.useStderr:
  148. handle = stderr
  149. writeLine(handle, ln)
  150. if level in {lvlError, lvlFatal}: flushFile(handle)
  151. except IOError:
  152. discard
  153. proc newConsoleLogger*(levelThreshold = lvlAll, fmtStr = defaultFmtStr, useStderr=false): ConsoleLogger =
  154. ## Creates a new console logger. This logger logs to the console.
  155. new result
  156. result.fmtStr = fmtStr
  157. result.levelThreshold = levelThreshold
  158. result.useStderr = useStderr
  159. when not defined(js):
  160. method log*(logger: FileLogger, level: Level, args: varargs[string, `$`]) =
  161. ## Logs to a file using ``logger`` only.
  162. if level >= logging.level and level >= logger.levelThreshold:
  163. writeLine(logger.file, substituteLog(logger.fmtStr, level, args))
  164. if level in {lvlError, lvlFatal}: flushFile(logger.file)
  165. proc defaultFilename*(): string =
  166. ## Returns the default filename for a logger.
  167. var (path, name, _) = splitFile(getAppFilename())
  168. result = changeFileExt(path / name, "log")
  169. proc newFileLogger*(file: File,
  170. levelThreshold = lvlAll,
  171. fmtStr = defaultFmtStr): FileLogger =
  172. ## Creates a new file logger. This logger logs to ``file``.
  173. new(result)
  174. result.file = file
  175. result.levelThreshold = levelThreshold
  176. result.fmtStr = fmtStr
  177. proc newFileLogger*(filename = defaultFilename(),
  178. mode: FileMode = fmAppend,
  179. levelThreshold = lvlAll,
  180. fmtStr = defaultFmtStr,
  181. bufSize: int = -1): FileLogger =
  182. ## Creates a new file logger. This logger logs to a file, specified
  183. ## by ``fileName``.
  184. ## Use ``bufSize`` as size of the output buffer when writing the file
  185. ## (-1: use system defaults, 0: unbuffered, >0: fixed buffer size).
  186. let file = open(filename, mode, bufSize = bufSize)
  187. newFileLogger(file, levelThreshold, fmtStr)
  188. # ------
  189. proc countLogLines(logger: RollingFileLogger): int =
  190. result = 0
  191. let fp = open(logger.baseName, fmRead)
  192. for line in fp.lines():
  193. result.inc()
  194. fp.close()
  195. proc countFiles(filename: string): int =
  196. # Example: file.log.1
  197. result = 0
  198. var (dir, name, ext) = splitFile(filename)
  199. if dir == "":
  200. dir = "."
  201. for kind, path in walkDir(dir):
  202. if kind == pcFile:
  203. let llfn = name & ext & ExtSep
  204. if path.extractFilename.startsWith(llfn):
  205. let numS = path.extractFilename[llfn.len .. ^1]
  206. try:
  207. let num = parseInt(numS)
  208. if num > result:
  209. result = num
  210. except ValueError: discard
  211. proc newRollingFileLogger*(filename = defaultFilename(),
  212. mode: FileMode = fmReadWrite,
  213. levelThreshold = lvlAll,
  214. fmtStr = defaultFmtStr,
  215. maxLines = 1000,
  216. bufSize: int = -1): RollingFileLogger =
  217. ## Creates a new rolling file logger. Once a file reaches ``maxLines`` lines
  218. ## a new log file will be started and the old will be renamed.
  219. ## Use ``bufSize`` as size of the output buffer when writing the file
  220. ## (-1: use system defaults, 0: unbuffered, >0: fixed buffer size).
  221. new(result)
  222. result.levelThreshold = levelThreshold
  223. result.fmtStr = fmtStr
  224. result.maxLines = maxLines
  225. result.bufSize = bufSize
  226. result.file = open(filename, mode, bufSize=result.bufSize)
  227. result.curLine = 0
  228. result.baseName = filename
  229. result.baseMode = mode
  230. result.logFiles = countFiles(filename)
  231. if mode == fmAppend:
  232. # We need to get a line count because we will be appending to the file.
  233. result.curLine = countLogLines(result)
  234. proc rotate(logger: RollingFileLogger) =
  235. let (dir, name, ext) = splitFile(logger.baseName)
  236. for i in countdown(logger.logFiles, 0):
  237. let srcSuff = if i != 0: ExtSep & $i else: ""
  238. moveFile(dir / (name & ext & srcSuff),
  239. dir / (name & ext & ExtSep & $(i+1)))
  240. method log*(logger: RollingFileLogger, level: Level, args: varargs[string, `$`]) =
  241. ## Logs to a file using rolling ``logger`` only.
  242. if level >= logging.level and level >= logger.levelThreshold:
  243. if logger.curLine >= logger.maxLines:
  244. logger.file.close()
  245. rotate(logger)
  246. logger.logFiles.inc
  247. logger.curLine = 0
  248. logger.file = open(logger.baseName, logger.baseMode, bufSize = logger.bufSize)
  249. writeLine(logger.file, substituteLog(logger.fmtStr, level, args))
  250. if level in {lvlError, lvlFatal}: flushFile(logger.file)
  251. logger.curLine.inc
  252. # --------
  253. proc logLoop(level: Level, args: varargs[string, `$`]) =
  254. for logger in items(handlers):
  255. if level >= logger.levelThreshold:
  256. log(logger, level, args)
  257. template log*(level: Level, args: varargs[string, `$`]) =
  258. ## Logs a message to all registered handlers at the given level.
  259. bind logLoop
  260. bind `%`
  261. bind logging.level
  262. if level >= logging.level:
  263. logLoop(level, args)
  264. template debug*(args: varargs[string, `$`]) =
  265. ## Logs a debug message to all registered handlers.
  266. ##
  267. ## Messages that are useful to the application developer only and are usually
  268. ## turned off in release.
  269. log(lvlDebug, args)
  270. template info*(args: varargs[string, `$`]) =
  271. ## Logs an info message to all registered handlers.
  272. ##
  273. ## Messages that are generated during the normal operation of an application
  274. ## and are of no particular importance. Useful to aggregate for potential
  275. ## later analysis.
  276. log(lvlInfo, args)
  277. template notice*(args: varargs[string, `$`]) =
  278. ## Logs an notice message to all registered handlers.
  279. ##
  280. ## Semantically very similar to `info`, but meant to be messages you want to
  281. ## be actively notified about (depending on your application).
  282. ## These could be, for example, grouped by hour and mailed out.
  283. log(lvlNotice, args)
  284. template warn*(args: varargs[string, `$`]) =
  285. ## Logs a warning message to all registered handlers.
  286. ##
  287. ## A non-error message that may indicate a potential problem rising or
  288. ## impacted performance.
  289. log(lvlWarn, args)
  290. template error*(args: varargs[string, `$`]) =
  291. ## Logs an error message to all registered handlers.
  292. ##
  293. ## A application-level error condition. For example, some user input generated
  294. ## an exception. The application will continue to run, but functionality or
  295. ## data was impacted, possibly visible to users.
  296. log(lvlError, args)
  297. template fatal*(args: varargs[string, `$`]) =
  298. ## Logs a fatal error message to all registered handlers.
  299. ##
  300. ## A application-level fatal condition. FATAL usually means that the application
  301. ## cannot go on and will exit (but this logging event will not do that for you).
  302. log(lvlFatal, args)
  303. proc addHandler*(handler: Logger) =
  304. ## Adds ``handler`` to the list of handlers.
  305. handlers.add(handler)
  306. proc getHandlers*(): seq[Logger] =
  307. ## Returns a list of all the registered handlers.
  308. return handlers
  309. proc setLogFilter*(lvl: Level) =
  310. ## Sets the global log filter.
  311. level = lvl
  312. proc getLogFilter*(): Level =
  313. ## Gets the global log filter.
  314. return level
  315. # --------------
  316. when not defined(testing) and isMainModule:
  317. var L = newConsoleLogger()
  318. when not defined(js):
  319. var fL = newFileLogger("test.log", fmtStr = verboseFmtStr)
  320. var rL = newRollingFileLogger("rolling.log", fmtStr = verboseFmtStr)
  321. addHandler(fL)
  322. addHandler(rL)
  323. addHandler(L)
  324. for i in 0 .. 25:
  325. info("hello", i)
  326. var nilString: string
  327. info "hello ", nilString