summary refs log tree commit diff stats
path: root/lib/pure/logging.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/logging.nim')
-rw-r--r--lib/pure/logging.nim1026
1 files changed, 821 insertions, 205 deletions
diff --git a/lib/pure/logging.nim b/lib/pure/logging.nim
index ca674af4b..c30f68af8 100644
--- a/lib/pure/logging.nim
+++ b/lib/pure/logging.nim
@@ -7,91 +7,311 @@
 #    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.
+## This module implements a simple logger.
 ##
-## Format strings support the following variables which must be prefixed with
-## the dollar operator (``$``):
+## It has been designed to be as simple as possible to avoid bloat.
+## If this library does not fulfill your needs, write your own.
+##
+## Basic usage
+## ===========
+##
+## To get started, first create a logger:
+##
+##   ```Nim
+##   import std/logging
+##
+##   var logger = newConsoleLogger()
+##   ```
+##
+## The logger that was created above logs to the console, but this module
+## also provides loggers that log to files, such as the
+## `FileLogger<#FileLogger>`_. Creating custom loggers is also possible by
+## inheriting from the `Logger<#Logger>`_ type.
+##
+## Once a logger has been created, call its `log proc
+## <#log.e,ConsoleLogger,Level,varargs[string,]>`_ to log a message:
+##
+##   ```Nim
+##   logger.log(lvlInfo, "a log message")
+##   # Output: INFO a log message
+##   ```
+##
+## The ``INFO`` within the output is the result of a format string being
+## prepended to the message, and it will differ depending on the message's
+## level. Format strings are `explained in more detail
+## here<#basic-usage-format-strings>`_.
+##
+## There are six logging levels: debug, info, notice, warn, error, and fatal.
+## They are described in more detail within the `Level enum's documentation
+## <#Level>`_. A message is logged if its level is at or above both the logger's
+## ``levelThreshold`` field and the global log filter. The latter can be changed
+## with the `setLogFilter proc<#setLogFilter,Level>`_.
+##
+## .. warning::
+##   For loggers that log to a console or to files, only error and fatal
+##   messages will cause their output buffers to be flushed immediately by default.
+##   set ``flushThreshold`` when creating the logger to change this.
+##
+## Handlers
+## --------
+##
+## When using multiple loggers, calling the log proc for each logger can
+## become repetitive. Instead of doing that, register each logger that will be
+## used with the `addHandler proc<#addHandler,Logger>`_, which is demonstrated
+## in the following example:
+##
+##   ```Nim
+##   import std/logging
+##
+##   var consoleLog = newConsoleLogger()
+##   var fileLog = newFileLogger("errors.log", levelThreshold=lvlError)
+##   var rollingLog = newRollingFileLogger("rolling.log")
+##
+##   addHandler(consoleLog)
+##   addHandler(fileLog)
+##   addHandler(rollingLog)
+##   ```
+##
+## After doing this, use either the `log template
+## <#log.t,Level,varargs[string,]>`_ or one of the level-specific templates,
+## such as the `error template<#error.t,varargs[string,]>`_, to log messages
+## to all registered handlers at once.
+##
+##   ```Nim
+##   # This example uses the loggers created above
+##   log(lvlError, "an error occurred")
+##   error("an error occurred")  # Equivalent to the above line
+##   info("something normal happened")  # Will not be written to errors.log
+##   ```
+##
+## Note that a message's level is still checked against each handler's
+## ``levelThreshold`` and the global log filter.
+##
+## Format strings
+## --------------
+##
+## Log messages are prefixed with format strings. These strings contain
+## placeholders for variables, such as ``$time``, that are replaced with their
+## corresponding values, such as the current time, before they are prepended to
+## a log message. Characters that are not part of variables are unaffected.
+##
+## The format string used by a logger can be specified by providing the `fmtStr`
+## argument when creating the logger or by setting its `fmtStr` field afterward.
+## If not specified, the `default format string<#defaultFmtStr>`_ is used.
+##
+## The following variables, which must be prefixed with a dollar sign (``$``),
+## are available:
 ##
 ## ============  =======================
-##   Operator     Output
+##   Variable      Output
 ## ============  =======================
 ## $date         Current date
 ## $time         Current time
-## $app          ``os.getAppFilename()``
+## $datetime     $dateT$time
+## $app          `os.getAppFilename()<os.html#getAppFilename>`_
+## $appname      Base name of ``$app``
+## $appdir       Directory name of ``$app``
+## $levelid      First letter of log level
+## $levelname    Log level name
 ## ============  =======================
 ##
+## Note that ``$app``, ``$appname``, and ``$appdir`` are not supported when
+## using the JavaScript backend.
+##
+## The following example illustrates how to use format strings:
+##
+##   ```Nim
+##   import std/logging
 ##
-## The following example demonstrates logging to three different handlers
-## simultaneously:
+##   var logger = newConsoleLogger(fmtStr="[$time] - $levelname: ")
+##   logger.log(lvlInfo, "this is a message")
+##   # Output: [19:50:13] - INFO: this is a message
+##   ```
 ##
-## .. code-block:: nim
+## Notes when using multiple threads
+## ---------------------------------
 ##
-##    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")
+## There are a few details to keep in mind when using this module within
+## multiple threads:
+## * The global log filter is actually a thread-local variable, so it needs to
+##   be set in each thread that uses this module.
+## * The list of registered handlers is also a thread-local variable. If a
+##   handler will be used in multiple threads, it needs to be registered in
+##   each of those threads.
 ##
-## **Warning:** The global list of handlers is a thread var, this means that
-## the handlers must be re-added in each thread.
+## See also
+## ========
+## * `strutils module<strutils.html>`_ for common string functions
+## * `strformat module<strformat.html>`_ for string interpolation and formatting
+## * `strscans module<strscans.html>`_ for ``scanf`` and ``scanp`` macros, which
+##   offer easier substring extraction than regular expressions
 
-import strutils, os, times
+import std/[strutils, times]
+when not defined(js):
+  import std/os
+
+when defined(nimPreviewSlimSystem):
+  import std/syncio
 
 type
-  Level* = enum  ## logging level
-    lvlAll,       ## all levels active
-    lvlDebug,     ## debug level (and any above) active
-    lvlInfo,      ## info level (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
+  Level* = enum ## \
+    ## Enumeration of logging levels.
+    ##
+    ## Debug messages represent the lowest logging level, and fatal error
+    ## messages represent the highest logging level. ``lvlAll`` can be used
+    ## to enable all messages, while ``lvlNone`` can be used to disable all
+    ## messages.
+    ##
+    ## Typical usage for each logging level, from lowest to highest, is
+    ## described below:
+    ##
+    ## * **Debug** - debugging information helpful only to developers
+    ## * **Info** - anything associated with normal operation and without
+    ##   any particular importance
+    ## * **Notice** - more important information that users should be
+    ##   notified about
+    ## * **Warn** - impending problems that require some attention
+    ## * **Error** - error conditions that the application can recover from
+    ## * **Fatal** - fatal errors that prevent the application from continuing
+    ##
+    ## It is completely up to the application how to utilize each level.
+    ##
+    ## Individual loggers have a ``levelThreshold`` field that filters out
+    ## any messages with a level lower than the threshold. There is also
+    ## a global filter that applies to all log messages, and it can be changed
+    ## using the `setLogFilter proc<#setLogFilter,Level>`_.
+    lvlAll,     ## All levels active
+    lvlDebug,   ## Debug level and above are active
+    lvlInfo,    ## Info level and above are active
+    lvlNotice,  ## Notice level and above are active
+    lvlWarn,    ## Warn level and above are active
+    lvlError,   ## Error level and above are active
+    lvlFatal,   ## Fatal level and above are active
+    lvlNone     ## No levels active; nothing is logged
 
 const
-  LevelNames*: array [Level, string] = [
-    "DEBUG", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE"
-  ]
-
-  defaultFmtStr* = "" ## default string between log level and message per logger
-  verboseFmtStr* = "$date $time "
+  LevelNames*: array[Level, string] = [
+    "DEBUG", "DEBUG", "INFO", "NOTICE", "WARN", "ERROR", "FATAL", "NONE"
+  ] ## Array of strings representing each logging level.
+
+  defaultFmtStr* = "$levelname "                         ## The default format string.
+  verboseFmtStr* = "$levelid, [$datetime] -- $appname: " ## \
+  ## A more verbose format string.
+  ##
+  ## This string can be passed as the ``frmStr`` argument to procs that create
+  ## new loggers, such as the `newConsoleLogger proc<#newConsoleLogger>`_.
+  ##
+  ## If a different format string is preferred, refer to the
+  ## `documentation about format strings<#basic-usage-format-strings>`_
+  ## for more information, including a list of available variables.
+  defaultFlushThreshold = when NimMajor >= 2:
+      when defined(nimV1LogFlushBehavior): lvlError else: lvlAll
+    else:
+      when defined(nimFlushAllLogs): lvlAll else: lvlError
+  ## The threshold above which log messages to file-like loggers
+  ## are automatically flushed.
+  ## 
+  ## By default, only error and fatal messages are logged,
+  ## but defining ``-d:nimFlushAllLogs`` will make all levels be flushed
 
 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
-
-  FileLogger* = ref object of Logger ## logger that writes the messages to a file
-    f: 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...
-
-{.deprecated: [TLevel: Level, PLogger: Logger, PConsoleLogger: ConsoleLogger,
-    PFileLogger: FileLogger, PRollingFileLogger: RollingFileLogger].}
-
-proc substituteLog(frmt: string): string =
-  ## converts $date to the current date
-  ## converts $time to the current time
-  ## converts $app to getAppFilename()
-  ## converts
-  result = newStringOfCap(frmt.len + 20)
+  Logger* = ref object of RootObj
+    ## The abstract base type of all loggers.
+    ##
+    ## Custom loggers should inherit from this type. They should also provide
+    ## their own implementation of the
+    ## `log method<#log.e,Logger,Level,varargs[string,]>`_.
+    ##
+    ## See also:
+    ## * `ConsoleLogger<#ConsoleLogger>`_
+    ## * `FileLogger<#FileLogger>`_
+    ## * `RollingFileLogger<#RollingFileLogger>`_
+    levelThreshold*: Level ## Only messages that are at or above this
+                           ## threshold will be logged
+    fmtStr*: string ## Format string to prepend to each log message;
+                    ## defaultFmtStr is the default
+
+  ConsoleLogger* = ref object of Logger
+    ## A logger that writes log messages to the console.
+    ##
+    ## Create a new ``ConsoleLogger`` with the `newConsoleLogger proc
+    ## <#newConsoleLogger>`_.
+    ##
+    ## See also:
+    ## * `FileLogger<#FileLogger>`_
+    ## * `RollingFileLogger<#RollingFileLogger>`_
+    useStderr*: bool ## If true, writes to stderr; otherwise, writes to stdout
+    flushThreshold*: Level ## Only messages that are at or above this
+                           ## threshold will be flushed immediately
+
+when not defined(js):
+  type
+    FileLogger* = ref object of Logger
+      ## A logger that writes log messages to a file.
+      ##
+      ## Create a new ``FileLogger`` with the `newFileLogger proc
+      ## <#newFileLogger,File>`_.
+      ##
+      ## **Note:** This logger is not available for the JavaScript backend.
+      ##
+      ## See also:
+      ## * `ConsoleLogger<#ConsoleLogger>`_
+      ## * `RollingFileLogger<#RollingFileLogger>`_
+      file*: File ## The wrapped file
+      flushThreshold*: Level ## Only messages that are at or above this
+                           ## threshold will be flushed immediately
+
+    RollingFileLogger* = ref object of FileLogger
+      ## A logger that writes log messages to a file while performing log
+      ## rotation.
+      ##
+      ## Create a new ``RollingFileLogger`` with the `newRollingFileLogger proc
+      ## <#newRollingFileLogger,FileMode,Positive,int>`_.
+      ##
+      ## **Note:** This logger is not available for the JavaScript backend.
+      ##
+      ## See also:
+      ## * `ConsoleLogger<#ConsoleLogger>`_
+      ## * `FileLogger<#FileLogger>`_
+      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 =
+  ## Formats a log message at the specified level with the given format string.
+  ##
+  ## The `format variables<#basic-usage-format-strings>`_ present within
+  ## ``frmt`` will be replaced with the corresponding values before being
+  ## prepended to ``args`` and returned.
+  ##
+  ## Unless you are implementing a custom logger, there is little need to call
+  ## this directly. Use either a logger's log method or one of the logging
+  ## templates.
+  ##
+  ## See also:
+  ## * `log method<#log.e,ConsoleLogger,Level,varargs[string,]>`_
+  ##   for the ConsoleLogger
+  ## * `log method<#log.e,FileLogger,Level,varargs[string,]>`_
+  ##   for the FileLogger
+  ## * `log method<#log.e,RollingFileLogger,Level,varargs[string,]>`_
+  ##   for the RollingFileLogger
+  ## * `log template<#log.t,Level,varargs[string,]>`_
+  runnableExamples:
+    doAssert substituteLog(defaultFmtStr, lvlInfo, "a message") == "INFO a message"
+    doAssert substituteLog("$levelid - ", lvlError, "an error") == "E - an error"
+    doAssert substituteLog("$levelid", lvlDebug, "error") == "Derror"
+  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] != '$':
@@ -100,192 +320,588 @@ proc substituteLog(frmt: string): string =
     else:
       inc(i)
       var v = ""
-      var app = getAppFilename()
-      while frmt[i] in IdentChars:
-        v.add(toLower(frmt[i]))
+      let app = when defined(js): "" else: getAppFilename()
+      while i < frmt.len and frmt[i] in IdentChars:
+        v.add(toLowerAscii(frmt[i]))
         inc(i)
       case v
       of "date": result.add(getDateStr())
       of "time": result.add(getClockStr())
-      of "app":  result.add(app)
-      of "appdir": result.add(app.splitFile.dir)
-      of "appname": result.add(app.splitFile.name)
+      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,
-            frmt: string, args: varargs[string, `$`]) {.
-            raises: [Exception],
-            tags: [TimeEffect, WriteIOEffect, ReadIOEffect].} =
-  ## Override this method in custom loggers. Default implementation does
+method log*(logger: Logger, level: Level, args: varargs[string, `$`]) {.
+            raises: [Exception], gcsafe,
+            tags: [RootEffect], base.} =
+  ## Override this method in custom loggers. The default implementation does
   ## nothing.
+  ##
+  ## See also:
+  ## * `log method<#log.e,ConsoleLogger,Level,varargs[string,]>`_
+  ##   for the ConsoleLogger
+  ## * `log method<#log.e,FileLogger,Level,varargs[string,]>`_
+  ##   for the FileLogger
+  ## * `log method<#log.e,RollingFileLogger,Level,varargs[string,]>`_
+  ##   for the RollingFileLogger
+  ## * `log template<#log.t,Level,varargs[string,]>`_
   discard
 
-method log*(logger: ConsoleLogger, level: Level,
-            frmt: string, args: varargs[string, `$`]) =
-  ## Logs to the console using ``logger`` only.
-  if level >= logger.levelThreshold:
-    writeln(stdout, LevelNames[level], " ", substituteLog(logger.fmtStr),
-            frmt % args)
-
-method log*(logger: FileLogger, level: Level,
-            frmt: string, args: varargs[string, `$`]) =
-  ## Logs to a file using ``logger`` only.
-  if level >= logger.levelThreshold:
-    writeln(logger.f, LevelNames[level], " ",
-            substituteLog(logger.fmtStr), frmt % args)
-
-proc defaultFilename*(): string =
-  ## Returns the default filename for a logger.
-  var (path, name, ext) = splitFile(getAppFilename())
-  result = changeFileExt(path / name, "log")
-
-proc newConsoleLogger*(levelThreshold = lvlAll, fmtStr = defaultFmtStr): ConsoleLogger =
-  ## Creates a new console logger. This logger logs to the console.
+method log*(logger: ConsoleLogger, level: Level, args: varargs[string, `$`]) =
+  ## Logs to the console with the given `ConsoleLogger<#ConsoleLogger>`_ only.
+  ##
+  ## This method ignores the list of registered handlers.
+  ##
+  ## Whether the message is logged depends on both the ConsoleLogger's
+  ## ``levelThreshold`` field and the global log filter set using the
+  ## `setLogFilter proc<#setLogFilter,Level>`_.
+  ##
+  ## **Note:** Only error and fatal messages will cause the output buffer
+  ## to be flushed immediately by default. Set ``flushThreshold`` when creating
+  ## the logger to change this.
+  ##
+  ## See also:
+  ## * `log method<#log.e,FileLogger,Level,varargs[string,]>`_
+  ##   for the FileLogger
+  ## * `log method<#log.e,RollingFileLogger,Level,varargs[string,]>`_
+  ##   for the RollingFileLogger
+  ## * `log template<#log.t,Level,varargs[string,]>`_
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var consoleLog = newConsoleLogger()
+  ##   consoleLog.log(lvlInfo, "this is a message")
+  ##   consoleLog.log(lvlError, "error code is: ", 404)
+  ##   ```
+  if level >= logging.level and level >= logger.levelThreshold:
+    let ln = substituteLog(logger.fmtStr, level, args)
+    when defined(js):
+      let cln = ln.cstring
+      case level
+      of lvlDebug: {.emit: "console.debug(`cln`);".}
+      of lvlInfo:  {.emit: "console.info(`cln`);".}
+      of lvlWarn:  {.emit: "console.warn(`cln`);".}
+      of lvlError: {.emit: "console.error(`cln`);".}
+      else:        {.emit: "console.log(`cln`);".}
+    else:
+      try:
+        var handle = stdout
+        if logger.useStderr:
+          handle = stderr
+        writeLine(handle, ln)
+        if level >= logger.flushThreshold: flushFile(handle)
+      except IOError:
+        discard
+
+proc newConsoleLogger*(levelThreshold = lvlAll, fmtStr = defaultFmtStr,
+    useStderr = false, flushThreshold = defaultFlushThreshold): ConsoleLogger =
+  ## Creates a new `ConsoleLogger<#ConsoleLogger>`_.
+  ##
+  ## By default, log messages are written to ``stdout``. If ``useStderr`` is
+  ## true, they are written to ``stderr`` instead.
+  ##
+  ## For the JavaScript backend, log messages are written to the console,
+  ## and ``useStderr`` is ignored.
+  ##
+  ## See also:
+  ## * `newFileLogger proc<#newFileLogger,File>`_ that uses a file handle
+  ## * `newFileLogger proc<#newFileLogger,FileMode,int>`_
+  ##   that accepts a filename
+  ## * `newRollingFileLogger proc<#newRollingFileLogger,FileMode,Positive,int>`_
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var normalLog = newConsoleLogger()
+  ##   var formatLog = newConsoleLogger(fmtStr=verboseFmtStr)
+  ##   var errorLog = newConsoleLogger(levelThreshold=lvlError, useStderr=true)
+  ##   ```
   new result
   result.fmtStr = fmtStr
   result.levelThreshold = levelThreshold
-
-proc newFileLogger*(filename = defaultFilename(),
-                    mode: FileMode = fmAppend,
-                    levelThreshold = lvlAll,
-                    fmtStr = defaultFmtStr): FileLogger =
-  ## Creates a new file logger. This logger logs to a file.
-  new(result)
-  result.levelThreshold = levelThreshold
-  result.f = open(filename, mode)
-  result.fmtStr = fmtStr
-
-# ------
-
-proc countLogLines(logger: RollingFileLogger): int =
-  result = 0
-  for line in logger.f.lines():
-    result.inc()
-
-proc countFiles(filename: string): int =
-  # Example: file.log.1
-  result = 0
-  let (dir, name, ext) = splitFile(filename)
-  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): 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.
-  new(result)
-  result.levelThreshold = levelThreshold
-  result.fmtStr = fmtStr
-  result.maxLines = maxLines
-  result.f = open(filename, mode)
-  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,
-            frmt: string, args: varargs[string, `$`]) =
-  ## Logs to a file using rolling ``logger`` only.
-  if level >= logger.levelThreshold:
-    if logger.curLine >= logger.maxLines:
-      logger.f.close()
-      rotate(logger)
-      logger.logFiles.inc
-      logger.curLine = 0
-      logger.f = open(logger.baseName, logger.baseMode)
-
-    writeln(logger.f, LevelNames[level], " ",substituteLog(logger.fmtStr), frmt % args)
-    logger.curLine.inc
+  result.flushThreshold = flushThreshold
+  result.useStderr = useStderr
+
+when not defined(js):
+  method log*(logger: FileLogger, level: Level, args: varargs[string, `$`]) =
+    ## Logs a message at the specified level using the given
+    ## `FileLogger<#FileLogger>`_ only.
+    ##
+    ## This method ignores the list of registered handlers.
+    ##
+    ## Whether the message is logged depends on both the FileLogger's
+    ## ``levelThreshold`` field and the global log filter set using the
+    ## `setLogFilter proc<#setLogFilter,Level>`_.
+    ##
+    ## **Notes:**
+    ## * Only error and fatal messages will cause the output buffer
+    ##   to be flushed immediately by default. Set ``flushThreshold`` when creating
+    ##   the logger to change this.
+    ## * This method is not available for the JavaScript backend.
+    ##
+    ## See also:
+    ## * `log method<#log.e,ConsoleLogger,Level,varargs[string,]>`_
+    ##   for the ConsoleLogger
+    ## * `log method<#log.e,RollingFileLogger,Level,varargs[string,]>`_
+    ##   for the RollingFileLogger
+    ## * `log template<#log.t,Level,varargs[string,]>`_
+    ##
+    ## **Examples:**
+    ##
+    ##   ```Nim
+    ##   var fileLog = newFileLogger("messages.log")
+    ##   fileLog.log(lvlInfo, "this is a message")
+    ##   fileLog.log(lvlError, "error code is: ", 404)
+    ##   ```
+    if level >= logging.level and level >= logger.levelThreshold:
+      writeLine(logger.file, substituteLog(logger.fmtStr, level, args))
+      if level >= logger.flushThreshold: flushFile(logger.file)
+
+  proc defaultFilename*(): string =
+    ## Returns the filename that is used by default when naming log files.
+    ##
+    ## **Note:** This proc is not available for the JavaScript backend.
+    var (path, name, _) = splitFile(getAppFilename())
+    result = changeFileExt(path / name, "log")
+
+  proc newFileLogger*(file: File,
+                      levelThreshold = lvlAll,
+                      fmtStr = defaultFmtStr,
+                      flushThreshold = defaultFlushThreshold): FileLogger =
+    ## Creates a new `FileLogger<#FileLogger>`_ that uses the given file handle.
+    ##
+    ## **Note:** This proc is not available for the JavaScript backend.
+    ##
+    ## See also:
+    ## * `newConsoleLogger proc<#newConsoleLogger>`_
+    ## * `newFileLogger proc<#newFileLogger,FileMode,int>`_
+    ##   that accepts a filename
+    ## * `newRollingFileLogger proc<#newRollingFileLogger,FileMode,Positive,int>`_
+    ##
+    ## **Examples:**
+    ##
+    ##   ```Nim
+    ##   var messages = open("messages.log", fmWrite)
+    ##   var formatted = open("formatted.log", fmWrite)
+    ##   var errors = open("errors.log", fmWrite)
+    ##
+    ##   var normalLog = newFileLogger(messages)
+    ##   var formatLog = newFileLogger(formatted, fmtStr=verboseFmtStr)
+    ##   var errorLog = newFileLogger(errors, levelThreshold=lvlError)
+    ##   ```
+    new(result)
+    result.file = file
+    result.levelThreshold = levelThreshold
+    result.flushThreshold = flushThreshold
+    result.fmtStr = fmtStr
+
+  proc newFileLogger*(filename = defaultFilename(),
+                      mode: FileMode = fmAppend,
+                      levelThreshold = lvlAll,
+                      fmtStr = defaultFmtStr,
+                      bufSize: int = -1,
+                      flushThreshold = defaultFlushThreshold): FileLogger =
+    ## Creates a new `FileLogger<#FileLogger>`_ that logs to a file with the
+    ## given filename.
+    ##
+    ## ``bufSize`` controls the size of the output buffer that is used when
+    ## writing to the log file. The following values can be provided:
+    ## * ``-1`` - use system defaults
+    ## * ``0`` - unbuffered
+    ## * ``> 0`` - fixed buffer size
+    ##
+    ## **Note:** This proc is not available for the JavaScript backend.
+    ##
+    ## See also:
+    ## * `newConsoleLogger proc<#newConsoleLogger>`_
+    ## * `newFileLogger proc<#newFileLogger,File>`_ that uses a file handle
+    ## * `newRollingFileLogger proc<#newRollingFileLogger,FileMode,Positive,int>`_
+    ##
+    ## **Examples:**
+    ##
+    ##   ```Nim
+    ##   var normalLog = newFileLogger("messages.log")
+    ##   var formatLog = newFileLogger("formatted.log", fmtStr=verboseFmtStr)
+    ##   var errorLog = newFileLogger("errors.log", levelThreshold=lvlError)
+    ##   ```
+    let file = open(filename, mode, bufSize = bufSize)
+    newFileLogger(file, levelThreshold, fmtStr, flushThreshold)
+
+  # ------
+
+  proc countLogLines(logger: RollingFileLogger): int =
+    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: Positive = 1000,
+                            bufSize: int = -1,
+                            flushThreshold = defaultFlushThreshold): RollingFileLogger =
+    ## Creates a new `RollingFileLogger<#RollingFileLogger>`_.
+    ##
+    ## Once the current log file being written to contains ``maxLines`` lines,
+    ## a new log file will be created, and the old log file will be renamed.
+    ##
+    ## ``bufSize`` controls the size of the output buffer that is used when
+    ## writing to the log file. The following values can be provided:
+    ## * ``-1`` - use system defaults
+    ## * ``0`` - unbuffered
+    ## * ``> 0`` - fixed buffer size
+    ##
+    ## **Note:** This proc is not available in the JavaScript backend.
+    ##
+    ## See also:
+    ## * `newConsoleLogger proc<#newConsoleLogger>`_
+    ## * `newFileLogger proc<#newFileLogger,File>`_ that uses a file handle
+    ## * `newFileLogger proc<#newFileLogger,FileMode,int>`_
+    ##   that accepts a filename
+    ##
+    ## **Examples:**
+    ##
+    ##   ```Nim
+    ##   var normalLog = newRollingFileLogger("messages.log")
+    ##   var formatLog = newRollingFileLogger("formatted.log", fmtStr=verboseFmtStr)
+    ##   var shortLog = newRollingFileLogger("short.log", maxLines=200)
+    ##   var errorLog = newRollingFileLogger("errors.log", levelThreshold=lvlError)
+    ##   ```
+    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.flushThreshold = flushThreshold
+
+    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 a message at the specified level using the given
+    ## `RollingFileLogger<#RollingFileLogger>`_ only.
+    ##
+    ## This method ignores the list of registered handlers.
+    ##
+    ## Whether the message is logged depends on both the RollingFileLogger's
+    ## ``levelThreshold`` field and the global log filter set using the
+    ## `setLogFilter proc<#setLogFilter,Level>`_.
+    ##
+    ## **Notes:**
+    ## * Only error and fatal messages will cause the output buffer
+    ##   to be flushed immediately by default. Set ``flushThreshold`` when creating
+    ##   the logger to change this.
+    ## * This method is not available for the JavaScript backend.
+    ##
+    ## See also:
+    ## * `log method<#log.e,ConsoleLogger,Level,varargs[string,]>`_
+    ##   for the ConsoleLogger
+    ## * `log method<#log.e,FileLogger,Level,varargs[string,]>`_
+    ##   for the FileLogger
+    ## * `log template<#log.t,Level,varargs[string,]>`_
+    ##
+    ## **Examples:**
+    ##
+    ##   ```Nim
+    ##   var rollingLog = newRollingFileLogger("messages.log")
+    ##   rollingLog.log(lvlInfo, "this is a message")
+    ##   rollingLog.log(lvlError, "error code is: ", 404)
+    ##   ```
+    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 >= logger.flushThreshold: flushFile(logger.file)
+      logger.curLine.inc
 
 # --------
 
-var level {.threadvar.}: Level   ## global log filter
-var handlers {.threadvar.}: seq[Logger] ## handlers with their own log levels
-
-proc logLoop(level: Level, frmt: string, args: varargs[string, `$`]) =
+proc logLoop(level: Level, args: varargs[string, `$`]) =
   for logger in items(handlers):
     if level >= logger.levelThreshold:
-      log(logger, level, frmt, args)
-
-template log*(level: Level, frmt: string, args: varargs[string, `$`]) =
-  ## Logs a message to all registered handlers at the given level.
+      log(logger, level, args)
+
+template log*(level: Level, args: varargs[string, `$`]) =
+  ## Logs a message at the specified level to all registered handlers.
+  ##
+  ## Whether the message is logged depends on both the FileLogger's
+  ## `levelThreshold` field and the global log filter set using the
+  ## `setLogFilter proc<#setLogFilter,Level>`_.
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var logger = newConsoleLogger()
+  ##   addHandler(logger)
+  ##
+  ##   log(lvlInfo, "This is an example.")
+  ##   ```
+  ##
+  ## See also:
+  ## * `debug template<#debug.t,varargs[string,]>`_
+  ## * `info template<#info.t,varargs[string,]>`_
+  ## * `notice template<#notice.t,varargs[string,]>`_
+  ## * `warn template<#warn.t,varargs[string,]>`_
+  ## * `error template<#error.t,varargs[string,]>`_
+  ## * `fatal template<#fatal.t,varargs[string,]>`_
   bind logLoop
   bind `%`
   bind logging.level
 
   if level >= logging.level:
-    logLoop(level, frmt, args)
+    logLoop(level, args)
 
-template debug*(frmt: string, args: varargs[string, `$`]) =
+template debug*(args: varargs[string, `$`]) =
   ## Logs a debug message to all registered handlers.
-  log(lvlDebug, frmt, args)
-
-template info*(frmt: string, args: varargs[string, `$`]) =
+  ##
+  ## Debug messages are typically useful to the application developer only,
+  ## and they are usually disabled in release builds, although this template
+  ## does not make that distinction.
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var logger = newConsoleLogger()
+  ##   addHandler(logger)
+  ##
+  ##   debug("myProc called with arguments: foo, 5")
+  ##   ```
+  ##
+  ## See also:
+  ## * `log template<#log.t,Level,varargs[string,]>`_
+  ## * `info template<#info.t,varargs[string,]>`_
+  ## * `notice template<#notice.t,varargs[string,]>`_
+  log(lvlDebug, args)
+
+template info*(args: varargs[string, `$`]) =
   ## Logs an info message to all registered handlers.
-  log(lvlInfo, frmt, args)
-
-template warn*(frmt: string, args: varargs[string, `$`]) =
+  ##
+  ## Info messages are typically generated during the normal operation
+  ## of an application and are of no particular importance. It can be useful to
+  ## aggregate these messages for later analysis.
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var logger = newConsoleLogger()
+  ##   addHandler(logger)
+  ##
+  ##   info("Application started successfully.")
+  ##   ```
+  ##
+  ## See also:
+  ## * `log template<#log.t,Level,varargs[string,]>`_
+  ## * `debug template<#debug.t,varargs[string,]>`_
+  ## * `notice template<#notice.t,varargs[string,]>`_
+  log(lvlInfo, args)
+
+template notice*(args: varargs[string, `$`]) =
+  ## Logs an notice to all registered handlers.
+  ##
+  ## Notices are semantically very similar to info messages, but they are meant
+  ## to be messages that the user should be actively notified about, depending
+  ## on the application.
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var logger = newConsoleLogger()
+  ##   addHandler(logger)
+  ##
+  ##   notice("An important operation has completed.")
+  ##   ```
+  ##
+  ## See also:
+  ## * `log template<#log.t,Level,varargs[string,]>`_
+  ## * `debug template<#debug.t,varargs[string,]>`_
+  ## * `info template<#info.t,varargs[string,]>`_
+  log(lvlNotice, args)
+
+template warn*(args: varargs[string, `$`]) =
   ## Logs a warning message to all registered handlers.
-  log(lvlWarn, frmt, args)
-
-template error*(frmt: string, args: varargs[string, `$`]) =
+  ##
+  ## A warning is a non-error message that may indicate impending problems or
+  ## degraded performance.
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var logger = newConsoleLogger()
+  ##   addHandler(logger)
+  ##
+  ##   warn("The previous operation took too long to process.")
+  ##   ```
+  ##
+  ## See also:
+  ## * `log template<#log.t,Level,varargs[string,]>`_
+  ## * `error template<#error.t,varargs[string,]>`_
+  ## * `fatal template<#fatal.t,varargs[string,]>`_
+  log(lvlWarn, args)
+
+template error*(args: varargs[string, `$`]) =
   ## Logs an error message to all registered handlers.
-  log(lvlError, frmt, args)
-
-template fatal*(frmt: string, args: varargs[string, `$`]) =
+  ##
+  ## Error messages are for application-level error conditions, such as when
+  ## some user input generated an exception. Typically, the application will
+  ## continue to run, but with degraded functionality or loss of data, and
+  ## these effects might be visible to users.
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var logger = newConsoleLogger()
+  ##   addHandler(logger)
+  ##
+  ##   error("An exception occurred while processing the form.")
+  ##   ```
+  ##
+  ## See also:
+  ## * `log template<#log.t,Level,varargs[string,]>`_
+  ## * `warn template<#warn.t,varargs[string,]>`_
+  ## * `fatal template<#fatal.t,varargs[string,]>`_
+  log(lvlError, args)
+
+template fatal*(args: varargs[string, `$`]) =
   ## Logs a fatal error message to all registered handlers.
-  log(lvlFatal, frmt, args)
+  ##
+  ## Fatal error messages usually indicate that the application cannot continue
+  ## to run and will exit due to a fatal condition. This template only logs the
+  ## message, and it is the application's responsibility to exit properly.
+  ##
+  ## **Examples:**
+  ##
+  ##   ```Nim
+  ##   var logger = newConsoleLogger()
+  ##   addHandler(logger)
+  ##
+  ##   fatal("Can't open database -- exiting.")
+  ##   ```
+  ##
+  ## See also:
+  ## * `log template<#log.t,Level,varargs[string,]>`_
+  ## * `warn template<#warn.t,varargs[string,]>`_
+  ## * `error template<#error.t,varargs[string,]>`_
+  log(lvlFatal, args)
 
 proc addHandler*(handler: Logger) =
-  ## Adds ``handler`` to the list of handlers.
-  if handlers.isNil: handlers = @[]
+  ## Adds a logger to the list of registered handlers.
+  ##
+  ## .. warning:: The list of handlers is a thread-local variable. If the given
+  ##   handler will be used in multiple threads, this proc should be called in
+  ##   each of those threads.
+  ##
+  ## See also:
+  ## * `removeHandler proc`_
+  ## * `getHandlers proc<#getHandlers>`_
+  runnableExamples:
+    var logger = newConsoleLogger()
+    addHandler(logger)
+    doAssert logger in getHandlers()
   handlers.add(handler)
 
+proc removeHandler*(handler: Logger) =
+  ## Removes a logger from the list of registered handlers.
+  ##
+  ## Note that for n times a logger is registered, n calls to this proc
+  ## are required to remove that logger.
+  for i, hnd in handlers:
+    if hnd == handler:
+      handlers.delete(i)
+      return
+
 proc getHandlers*(): seq[Logger] =
   ## Returns a list of all the registered handlers.
+  ##
+  ## See also:
+  ## * `addHandler proc<#addHandler,Logger>`_
   return handlers
 
 proc setLogFilter*(lvl: Level) =
   ## Sets the global log filter.
+  ##
+  ## Messages below the provided level will not be logged regardless of an
+  ## individual logger's ``levelThreshold``. By default, all messages are
+  ## logged.
+  ##
+  ## .. warning:: The global log filter is a thread-local variable. If logging
+  ##   is being performed in multiple threads, this proc should be called in each
+  ##   thread unless it is intended that different threads should log at different
+  ##   logging levels.
+  ##
+  ## See also:
+  ## * `getLogFilter proc<#getLogFilter>`_
+  runnableExamples:
+    setLogFilter(lvlError)
+    doAssert getLogFilter() == lvlError
   level = lvl
 
 proc getLogFilter*(): Level =
   ## Gets the global log filter.
+  ##
+  ## See also:
+  ## * `setLogFilter proc<#setLogFilter,Level>`_
   return level
 
 # --------------
 
-when isMainModule:
+when not defined(testing) and isMainModule:
   var L = newConsoleLogger()
-  var fL = newFileLogger("test.log", fmtStr = verboseFmtStr)
-  var rL = newRollingFileLogger("rolling.log", fmtStr = verboseFmtStr)
+  when not defined(js):
+    var fL = newFileLogger("test.log", fmtStr = verboseFmtStr)
+    var rL = newRollingFileLogger("rolling.log", fmtStr = verboseFmtStr)
+    addHandler(fL)
+    addHandler(rL)
   addHandler(L)
-  addHandler(fL)
-  addHandler(rL)
   for i in 0 .. 25:
-    info("hello" & $i, [])
-
+    info("hello", i)
 
+  var nilString: string
+  info "hello ", nilString