logging.nim 13 KB

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