Merge: Introduce `Logger`, a simple yet powerful logging system
authorJean Privat <jean@pryen.org>
Wed, 3 Jul 2019 18:50:43 +0000 (14:50 -0400)
committerJean Privat <jean@pryen.org>
Wed, 3 Jul 2019 18:50:43 +0000 (14:50 -0400)
# A simple logger for Nit

## Basic Usage

Create a new `Logger` with a severity level threshold set to `warn_level`:

~~~nit
var logger = new Logger(warn_level)
~~~

Messages with a severity equal or higher than `warn_level` will be displayed:

~~~nit
logger.error "Displays an error."
logger.warn "Displays a warning."
~~~

Messages with a lower severity are silenced:

~~~nit
logger.info "Displays nothing."
~~~

`FileLogger` can be used to output the messages into a file:

~~~nit
var log_file = "my.log"

logger = new FileLogger(warn_level, log_file, append = false)
logger.error("An error")
logger.info("Some info")
logger.close

assert log_file.to_path.read_all == "An error\n"
log_file.to_path.delete
~~~

## Severity levels

Each message is associated with a level that indicate its severity.
Only messages with a severity equal to or higher than the logger `level`
threshold will be displayed.

Severity levels from the most severe to the least severe:

* `unknown_level`: An unknown message that should always be outputted.
* `fatal_level`: An unhandleable error that results in a program crash.
* `error_level`: A handleable error condition.
* `warn_level`: A warning.
* `info_level`: Generic (useful) information about system operation.
* `debug_level`: Low-level information for developpers.

## Formatting messages

You can create custom formatters by implementing the `Formatter` interface.

~~~nit
class MyFormatter
super Formatter

redef fun format(level, message) do
if level < warn_level then return message
return "!!!{message}!!!"
end
end
~~~

See `DefaultFormatter` for a more advanced implementation example.

Each Logger can be given a default formatter used to format the every messages
before outputting them:

~~~nit
var formatter = new MyFormatter
var stderr = new StringWriter
var logger = new Logger(warn_level, stderr, formatter)

logger.warn("This is a warning.")
assert stderr.to_s.trim.split("\n").last == "!!!This is a warning.!!!"
~~~

Optionally, a `Formatter` can be given to replace the `default_formatter`
used by default:

~~~nit
# Create a formatter with no default decorator
logger = new Logger(warn_level, stderr, null)

# Display a message without any formatter
logger.warn("This is a warning.")
assert stderr.to_s.trim.split("\n").last == "This is a warning."

# Display a message with a custom formatter
logger.warn("This is a warning.", formatter)
assert stderr.to_s.trim.split("\n").last == "!!!This is a warning.!!!"
~~~

Pull-Request: #2751
Reviewed-by: Jean Privat <jean@pryen.org>

lib/github/loader.nit
lib/logger/logger.nit [new file with mode: 0644]
lib/logger/package.ini [new file with mode: 0644]
lib/popcorn/README.md
lib/popcorn/pop_logging.nit
lib/popcorn/pop_tracker.nit
src/nitweb.nit

index 4ffa501..4cd6fda 100644 (file)
@@ -128,15 +128,19 @@ class LoaderConfig
        # Verbosity level (the higher the more verbose)
        fun verbose_level: Int do
                var opt = opt_start.value
-               if opt > 0 then return opt
+               if opt > 0 then
+                       return info_level
+               end
                var v = ini["loader.verbose"]
-               if v != null then return v.to_i
-               return 4
+               if v != null and v.to_i > 0 then
+                       return info_level
+               end
+               return warn_level
        end
 
        # Logger used to print things
-       var logger: ConsoleLog is lazy do
-               var logger = new ConsoleLog
+       var logger: PopLogger is lazy do
+               var logger = new PopLogger
                logger.level = verbose_level
                return logger
        end
@@ -412,7 +416,7 @@ class Loader
        end
 
        # Logger shortcut
-       fun log: ConsoleLog do return config.logger
+       fun log: PopLogger do return config.logger
 
        # Display a error and exit
        fun error(msg: String) do
diff --git a/lib/logger/logger.nit b/lib/logger/logger.nit
new file mode 100644 (file)
index 0000000..974ff9c
--- /dev/null
@@ -0,0 +1,402 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A simple logger for Nit
+#
+# ## Basic Usage
+#
+# Create a new `Logger` with a severity level threshold set to `warn_level`:
+#
+# ~~~
+# var logger = new Logger(warn_level)
+# ~~~
+#
+# Messages with a severity equal or higher than `warn_level` will be displayed:
+#
+# ~~~
+# logger.error "Displays an error."
+# logger.warn "Displays a warning."
+# ~~~
+#
+# Messages with a lower severity are silenced:
+#
+# ~~~
+# logger.info "Displays nothing."
+# ~~~
+#
+# `FileLogger` can be used to output the messages into a file:
+#
+# ~~~
+# var log_file = "my.log"
+#
+# logger = new FileLogger(warn_level, log_file, append = false)
+# logger.error("An error")
+# logger.info("Some info")
+# logger.close
+#
+# assert log_file.to_path.read_all == "An error\n"
+# log_file.to_path.delete
+# ~~~
+#
+# ## Severity levels
+#
+# Each message is associated with a level that indicate its severity.
+# Only messages with a severity equal to or higher than the logger `level`
+# threshold will be displayed.
+#
+# Severity levels from the most severe to the least severe:
+#
+# * `unknown_level`: An unknown message that should always be outputted.
+# * `fatal_level`: An unhandleable error that results in a program crash.
+# * `error_level`: A handleable error condition.
+# * `warn_level`: A warning.
+# * `info_level`: Generic (useful) information about system operation.
+# * `debug_level`: Low-level information for developpers.
+#
+# ## Formatting messages
+#
+# You can create custom formatters by implementing the `Formatter` interface.
+#
+# ~~~
+# class MyFormatter
+#      super Formatter
+#
+#      redef fun format(level, message) do
+#              if level < warn_level then return super
+#              return "!!!{message}!!!"
+#      end
+# end
+# ~~~
+#
+# See `DefaultFormatter` for a more advanced implementation example.
+#
+# Each Logger can be given a default formatter used to format the every messages
+# before outputting them:
+#
+# ~~~
+# var formatter = new MyFormatter
+# var stderr = new StringWriter
+# var logger = new Logger(warn_level, stderr, formatter)
+#
+# logger.warn("This is a warning.")
+# assert stderr.to_s.trim.split("\n").last == "!!!This is a warning.!!!"
+# ~~~
+#
+# Optionally, a `Formatter` can be given to replace the `default_formatter`
+# used by default:
+#
+# ~~~
+# # Create a formatter with no default decorator
+# logger = new Logger(warn_level, stderr, null)
+#
+# # Display a message without any formatter
+# logger.warn("This is a warning.")
+# assert stderr.to_s.trim.split("\n").last == "This is a warning."
+#
+# # Display a message with a custom formatter
+# logger.warn("This is a warning.", formatter)
+# assert stderr.to_s.trim.split("\n").last == "!!!This is a warning.!!!"
+# ~~~
+module logger
+
+import console
+
+# A simple logging utility
+#
+# `Logger` provides a simple way to output messages from applications.
+#
+# Each message is associated with a level that indicate its severity.
+# Only messages with a severity equal to or higher than the logger `level`
+# threshold will be displayed.
+#
+# ~~~
+# var logger = new Logger(warn_level)
+# assert logger.unknown("unkown")
+# assert logger.fatal("fatal")
+# assert logger.error("error")
+# assert logger.warn("warn")
+# assert not logger.info("info")
+# assert not logger.debug("debug")
+# ~~~
+class Logger
+
+       # Severity threshold
+       #
+       # Messages with a severity level greater than or equal to `level` will be displayed.
+       # Default is `warn_level`.
+       #
+       # See `unknown_level`, `fatal_level`, error_level``, `warn_level`,
+       # `info_level` and `debug_level`.
+       var level: Int = warn_level is optional, writable
+
+       # Kind of `Writer` used to output messages
+       type OUT: Writer
+
+       # Writer used to output messages
+       #
+       # Default is `stderr`.
+       var out: OUT = stderr is optional
+
+       # Formatter used to format messages before outputting them
+       #
+       # By default no formatter is used.
+       #
+       # See `DefaultFormatter`.
+       var default_formatter: nullable Formatter = null is optional, writable
+
+       # Output a message with `level` severity
+       #
+       # Only output messages with `level` severity greater than of equal to `self.level`.
+       #
+       # ~~~
+       # var stderr = new StringWriter
+       # var logger = new Logger(warn_level, stderr, null)
+       #
+       # # This message will be displayed:
+       # assert logger.warn("This is a warning.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       #
+       # # This message will not:
+       # assert not logger.info("This is some info.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       # ~~~
+       #
+       # Each logger can be given a default formatter used to format the messages
+       # before outputting them:
+       #
+       # ~~~
+       # var formatter = new DefaultFormatter(no_color = true)
+       # logger = new Logger(warn_level, stderr, formatter)
+       # logger.warn("This is a warning.")
+       # assert stderr.to_s.trim.split("\n").last == "Warning: This is a warning."
+       # ~~~
+       #
+       # Optionally, a `Formatter` can be given to replace the `default_formatter`
+       # used by default.
+       #
+       # ~~~
+       # # Create a formatter with no default decorator
+       # logger = new Logger(warn_level, stderr, null)
+       #
+       # # Display a message without any formatter
+       # logger.warn("This is a warning.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       #
+       # # Display a message with a custom formatter
+       # logger.warn("This is a warning.", formatter)
+       # assert stderr.to_s.trim.split("\n").last == "Warning: This is a warning."
+       # ~~~
+       fun add(level: Int, message: Writable, formatter: nullable Formatter): Bool do
+               var format = formatter or else default_formatter
+               if format == null then
+                       return add_raw(level, message)
+               end
+               return add_raw(level, format.format(level, message))
+       end
+
+       # Output a message with `level` severity without formatting it
+       #
+       # Only output messages with `level` severity greater than of equal to `self.level`.
+       #
+       # ~~~
+       # var stderr = new StringWriter
+       # var logger = new Logger(warn_level, stderr, null)
+       #
+       # # This message will be displayed:
+       # assert logger.add_raw(warn_level, "This is a warning.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       #
+       # # This message will not:
+       # assert not logger.add_raw(info_level, "This is some info.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       # ~~~
+       fun add_raw(level: Int, message: Writable): Bool do
+               if level < self.level then return false
+               out.write(message.write_to_string)
+               out.write("\n")
+               return true
+       end
+
+       # Output a message with `unknown_level` severity
+       #
+       # Unkown severity messages are always outputted.
+       fun unknown(message: String, formatter: nullable Formatter): Bool do
+               return add(unknown_level, message, formatter)
+       end
+
+       # Output a message with `fatal_level` severity
+       fun fatal(message: String, formatter: nullable Formatter): Bool do
+               return add(fatal_level, message, formatter)
+       end
+
+       # Output a message with `error_level` severity
+       fun error(message: String, formatter: nullable Formatter): Bool do
+               return add(error_level, message, formatter)
+       end
+
+       # Output a message with `warn_level` severity
+       fun warn(message: String, formatter: nullable Formatter): Bool do
+               return add(warn_level, message, formatter)
+       end
+
+       # Output a message with `info_level` severity
+       fun info(message: String, formatter: nullable Formatter): Bool do
+               return add(info_level, message, formatter)
+       end
+
+       # Output a message with `debug` severity
+       fun debug(message: String, formatter: nullable Formatter): Bool do
+               return add(debug_level, message, formatter)
+       end
+end
+
+# Log messages to a file
+#
+# ~~~
+# var log_file = "my_file.log"
+# var logger = new FileLogger(warn_level, log_file, append = false)
+# logger.error("An error")
+# logger.info("Some info")
+# logger.close
+# assert log_file.to_path.read_all == "An error\n"
+#
+# logger = new FileLogger(warn_level, log_file, append = true)
+# logger.error("Another error")
+# logger.close
+# assert log_file.to_path.read_all == "An error\nAnother error\n"
+#
+# log_file.to_path.delete
+# ~~~
+class FileLogger
+       super Logger
+       autoinit level, file, append, default_formatter
+
+       redef type OUT: FileWriter
+
+       # File where messages will be written
+       var file: String
+
+       # Append messages to `file`
+       #
+       # If `append` is `false`, the `file` will be overwritten.
+       var append: Bool = true is optional
+
+       init do
+               var old = null
+               if append then
+                       old = file.to_path.read_all
+               end
+               out = new FileWriter.open(file)
+               out.set_buffering_mode(0, buffer_mode_line)
+               if old != null then
+                       out.write(old)
+               end
+       end
+
+       # Close the logger and its `file`
+       fun close do out.close
+end
+
+# Format messages before outputing them
+#
+# A `Logger` can use a `Formatter` to format the messages before outputting them.
+#
+# See `DefaultFormatter`.
+interface Formatter
+
+       # Format `message` depending of its severity `level`
+       fun format(level: Int, message: Writable): Writable do return message
+end
+
+# Default `Logger` formatter
+#
+# The default formatter decorates the messages with severity labels and colors.
+class DefaultFormatter
+       super Formatter
+
+       # Do not decorate messages with colors
+       #
+       # ~~~
+       # var formatter = new DefaultFormatter(no_color = true)
+       # assert formatter.format(error_level, "My message.") == "Error: My message."
+       # ~~~
+       var no_color = false is optional, writable
+
+       redef fun format(level, message) do
+               var string = message.write_to_string
+
+               if level == fatal_level then
+                       string = "Fatal: {string}"
+               else if level == error_level then
+                       string = "Error: {string}"
+               else if level == warn_level then
+                       string = "Warning: {string}"
+               else if level == info_level then
+                       string = "Info: {string}"
+               else if level == debug_level then
+                       string = "Debug: {string}"
+               end
+
+               if no_color then return string
+
+               if level == fatal_level then
+                       return string.red
+               else if level == error_level then
+                       return string.red
+               else if level == warn_level then
+                       return string.yellow
+               else if level == info_level then
+                       return string.purple
+               else if level == debug_level then
+                       return string.blue
+               end
+
+               return string
+       end
+end
+
+redef class Sys
+
+       # Unknown severity level
+       #
+       # These messages are always displayed.
+       #
+       # See `Logger`.
+       var unknown_level = 5
+
+       # Fatal severity level
+       #
+       # See `Logger`.
+       var fatal_level = 4
+
+       # Error severity level
+       #
+       # See `Logger`.
+       var error_level = 3
+
+       # Warning severity level
+       #
+       # See `Logger`.
+       var warn_level = 2
+
+       # Info severity level
+       #
+       # See `Logger`.
+       var info_level = 1
+
+       # Debug severity level
+       #
+       # See `Logger`.
+       var debug_level = 0
+end
diff --git a/lib/logger/package.ini b/lib/logger/package.ini
new file mode 100644 (file)
index 0000000..f4ba7bd
--- /dev/null
@@ -0,0 +1,12 @@
+[package]
+name=logger
+tags=logging,lib
+maintainer=Alexandre Terrasa <alexandre@moz-code.org>
+license=Apache-2.0
+desc=A simple logger for Nit
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/logger/
+git=https://github.com/nitlang/nit.git
+git.directory=lib/logger/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
index f7c6346..50a0de6 100644 (file)
@@ -511,7 +511,7 @@ with the `use_before` method.
 Next, we’ll create a middleware handler called “LogHandler” that prints the requested
 uri, the response status and the time it took to Popcorn to process the request.
 
-This example gives a simplified version of the `RequestClock` and `ConsoleLog` middlewares.
+This example gives a simplified version of the `RequestClock` and `PopLogger` middlewares.
 
 ~~~
 import popcorn
@@ -584,7 +584,7 @@ Starting with version 0.1, Popcorn provide a set of built-in middleware that can
 be used to develop your app faster.
 
 * `RequestClock`: initializes requests clock.
-* `ConsoleLog`: displays resquest and response status in console (can be used with `RequestClock`).
+* `PopLogger`: displays resquest and response status in console (can be used with `RequestClock`).
 * `SessionInit`: initializes requests session (see the `Sessions` section).
 * `StaticServer`: serves static files (see the `Serving static files with Popcorn` section).
 * `Router`: a mountable mini-app (see the `Mountable routers` section).
index ba3418f..6f0d154 100644 (file)
@@ -17,7 +17,7 @@
 module pop_logging
 
 import pop_handlers
-import console
+import logger
 import realtime
 
 # Initialize a clock for the resquest.
@@ -30,73 +30,71 @@ class RequestClock
 end
 
 # Display log info about request processing.
-class ConsoleLog
+class PopLogger
+       super Logger
        super Handler
 
-       # Logger level
-       #
-       # * `0`: silent
-       # * `1`: errors
-       # * `2`: warnings
-       # * `3`: info
-       # * `4`: debug
-       #
-       # Request status are always logged, whatever the logger level is.
-       var level = 4 is writable
-
        # Do we want colors in the console output?
-       var no_colors = false
+       var no_color = false is optional
+
+       redef var default_formatter = new PopFormatter(no_color) is optional
 
        redef fun all(req, res) do
                var clock = req.clock
                if clock != null then
-                       log "{req.method} {req.url} {status(res)} ({clock.total}s)"
+                       add_raw(info_level, "{req.method} {req.url} {status(res)} ({clock.total}s)")
                else
-                       log "{req.method} {req.url} {status(res)}"
+                       add_raw(info_level, "{req.method} {req.url} {status(res)}")
                end
        end
 
        # Colorize the request status.
        private fun status(res: HttpResponse): String do
-               if no_colors then return res.status_code.to_s
+               if no_color then return res.status_code.to_s
                return res.color_status
        end
+end
 
-       # Display a `message` with `level`.
-       #
-       # Message will only be displayed if `level <= self.level`.
-       # Colors will be used depending on `colors`.
-       #
-       # Use `0` for no coloration.
-       private fun display(level: Int, message: String) do
-               if level > self.level then return
-               if no_colors then
-                       print message
-                       return
+class PopFormatter
+       super Formatter
+
+       # Do not decorate messages with colors
+       var no_color = false is optional, writable
+
+       redef fun format(level, message) do
+               var string = message.write_to_string
+
+               if level == fatal_level then
+                       string = "[FATAL] {string}"
+               else if level == error_level then
+                       string = "[ERROR] {string}"
+               else if level == warn_level then
+                       string = "[WARN] {string}"
+               else if level == info_level then
+                       string = "[INFO] {string}"
+               else if level == debug_level then
+                       string = "[DEBUG] {string}"
                end
-               if level == 0 then print message
-               if level == 1 then print message.red
-               if level == 2 then print message.yellow
-               if level == 3 then print message.blue
-               if level == 4 then print message.gray
-       end
-
-       # Display a message wathever the `level`
-       fun log(message: String) do display(0, message)
 
-       # Display a red error `message`.
-       fun error(message: String) do display(1, "[ERROR] {message}")
-
-       # Display a yellow warning `message`.
-       fun warning(message: String) do display(2, "[WARN] {message}")
-
-       # Display a blue info `message`.
-       fun info(message: String) do display(3, "[INFO] {message}")
+               if no_color then return string
+
+               if level == fatal_level then
+                       return string.red
+               else if level == error_level then
+                       return string.red
+               else if level == warn_level then
+                       return string.yellow
+               else if level == info_level then
+                       return string.blue
+               else if level == debug_level then
+                       return string.gray
+               end
 
-       # Display a gray debug `message`.
-       fun debug(message: String) do display(4, "[DEBUG] {message}")
+               return string
+       end
 end
 
+
 redef class HttpRequest
        # Time that request was received by the Popcorn app.
        var clock: nullable Clock = null
index 9ecf5b7..f43b100 100644 (file)
@@ -46,7 +46,6 @@ module pop_tracker
 
 import popcorn
 import popcorn::pop_config
-import popcorn::pop_logging
 import popcorn::pop_json
 import popcorn::pop_repos
 
@@ -91,7 +90,6 @@ end
 
 # Saves logs into a MongoDB collection
 class PopTracker
-       super ConsoleLog
        super TrackerHandler
 
        redef fun all(req, res) do
index 9e9d54f..d3c8c63 100644 (file)
@@ -99,7 +99,7 @@ private class NitwebPhase
                app.use("/oauth", new GithubOAuthCallBack(config.github_client_id, config.github_client_secret))
                app.use("/logout", new GithubLogout)
                app.use("/*", new StaticHandler(toolcontext.share_dir / "nitweb", "index.html"))
-               app.use_after("/*", new ConsoleLog)
+               app.use_after("/*", new PopLogger(info_level))
 
                app.listen(config.app_host, config.app_port)
        end