日志记录

Logging 模块提供了一个将历史和计算进度记录为事件的日志。事件通过在源代码里插入日志语句产生,例如:

  1. @warn "Abandon printf debugging, all ye who enter here!"
  2. Warning: Abandon printf debugging, all ye who enter here!
  3. @ Main REPL[1]:1

The system provides several advantages over peppering your source code with calls to println(). First, it allows you to control the visibility and presentation of messages without editing the source code. For example, in contrast to the @warn above

  1. @debug "The sum of some values $(sum(rand(100)))"

will produce no output by default. Furthermore, it’s very cheap to leave debug statements like this in the source code because the system avoids evaluating the message if it would later be ignored. In this case sum(rand(100)) and the associated string processing will never be executed unless debug logging is enabled.

Second, the logging tools allow you to attach arbitrary data to each event as a set of key—value pairs. This allows you to capture local variables and other program state for later analysis. For example, to attach the local array variable A and the sum of a vector v as the key s you can use

  1. A = ones(Int, 4, 4)
  2. v = ones(100)
  3. @info "Some variables" A s=sum(v)
  4. # output
  5. Info: Some variables
  6. A =
  7. 4×4 Matrix{Int64}:
  8. 1 1 1 1
  9. 1 1 1 1
  10. 1 1 1 1
  11. 1 1 1 1
  12. s = 100.0

所有的日志宏如 @debug, @info, @warn@error 有着共同的特征, 这些共同特征在更通用的宏 @logmsg 的文档里有细致说明。

日志事件结构

Each event generates several pieces of data, some provided by the user and some automatically extracted. Let’s examine the user-defined data first:

  • The log level is a broad category for the message that is used for early filtering. There are several standard levels of type LogLevel; user-defined levels are also possible. Each is distinct in purpose:

    • Logging.Debug (log level -1000) is information intended for the developer of the program. These events are disabled by default.
    • Logging.Info (log level 0) is for general information to the user. Think of it as an alternative to using println directly.
    • Logging.Warn (log level 1000) means something is wrong and action is likely required but that for now the program is still working.
    • Logging.Error (log level 2000) means something is wrong and it is unlikely to be recovered, at least by this part of the code. Often this log-level is unneeded as throwing an exception can convey all the required information.
  • The message is an object describing the event. By convention AbstractStrings passed as messages are assumed to be in markdown format. Other types will be displayed using print(io, obj) or string(obj) for text-based output and possibly show(io,mime,obj) for other multimedia displays used in the installed logger.

  • Optional key—value pairs allow arbitrary data to be attached to each event. Some keys have conventional meaning that can affect the way an event is interpreted (see @logmsg).

The system also generates some standard information for each event:

  • The module in which the logging macro was expanded.
  • The file and line where the logging macro occurs in the source code.
  • A message id that is a unique, fixed identifier for the source code statement where the logging macro appears. This identifier is designed to be fairly stable even if the source code of the file changes, as long as the logging statement itself remains the same.
  • A group for the event, which is set to the base name of the file by default, without extension. This can be used to group messages into categories more finely than the log level (for example, all deprecation warnings have group :depwarn), or into logical groupings across or within modules.

Notice that some useful information such as the event time is not included by default. This is because such information can be expensive to extract and is also dynamically available to the current logger. It’s simple to define a custom logger to augment event data with the time, backtrace, values of global variables and other useful information as required.

Processing log events

As you can see in the examples, logging statements make no mention of where log events go or how they are processed. This is a key design feature that makes the system composable and natural for concurrent use. It does this by separating two different concerns:

  • Creating log events is the concern of the module author who needs to decide where events are triggered and which information to include.
  • Processing of log events — that is, display, filtering, aggregation and recording — is the concern of the application author who needs to bring multiple modules together into a cooperating application.

Loggers

Processing of events is performed by a logger, which is the first piece of user configurable code to see the event. All loggers must be subtypes of AbstractLogger.

When an event is triggered, the appropriate logger is found by looking for a task-local logger with the global logger as fallback. The idea here is that the application code knows how log events should be processed and exists somewhere at the top of the call stack. So we should look up through the call stack to discover the logger — that is, the logger should be dynamically scoped. (This is a point of contrast with logging frameworks where the logger is lexically scoped; provided explicitly by the module author or as a simple global variable. In such a system it’s awkward to control logging while composing functionality from multiple modules.)

The global logger may be set with global_logger, and task-local loggers controlled using with_logger. Newly spawned tasks inherit the logger of the parent task.

There are three logger types provided by the library. ConsoleLogger is the default logger you see when starting the REPL. It displays events in a readable text format and tries to give simple but user friendly control over formatting and filtering. NullLogger is a convenient way to drop all messages where necessary; it is the logging equivalent of the devnull stream. SimpleLogger is a very simplistic text formatting logger, mainly useful for debugging the logging system itself.

Custom loggers should come with overloads for the functions described in the reference section.

Early filtering and message handling

When an event occurs, a few steps of early filtering occur to avoid generating messages that will be discarded:

  1. The message log level is checked against a global minimum level (set via disable_logging). This is a crude but extremely cheap global setting.
  2. The current logger state is looked up and the message level checked against the logger’s cached minimum level, as found by calling Logging.min_enabled_level. This behavior can be overridden via environment variables (more on this later).
  3. The Logging.shouldlog function is called with the current logger, taking some minimal information (level, module, group, id) which can be computed statically. Most usefully, shouldlog is passed an event id which can be used to discard events early based on a cached predicate.

If all these checks pass, the message and key—value pairs are evaluated in full and passed to the current logger via the Logging.handle_message function. handle_message() may perform additional filtering as required and display the event to the screen, save it to a file, etc.

Exceptions that occur while generating the log event are captured and logged by default. This prevents individual broken events from crashing the application, which is helpful when enabling little-used debug events in a production system. This behavior can be customized per logger type by extending Logging.catch_exceptions.

Testing log events

Log events are a side effect of running normal code, but you might find yourself wanting to test particular informational messages and warnings. The Test module provides a @test_logs macro that can be used to pattern match against the log event stream.

Environment variables

Message filtering can be influenced through the JULIA_DEBUG environment variable, and serves as an easy way to enable debug logging for a file or module. For example, loading julia with JULIA_DEBUG=loading will activate @debug log messages in loading.jl:

  1. $ JULIA_DEBUG=loading julia -e 'using OhMyREPL'
  2. Debug: Rejecting cache file /home/user/.julia/compiled/v0.7/OhMyREPL.ji due to it containing an invalid cache header
  3. @ Base loading.jl:1328
  4. [ Info: Recompiling stale cache file /home/user/.julia/compiled/v0.7/OhMyREPL.ji for module OhMyREPL
  5. Debug: Rejecting cache file /home/user/.julia/compiled/v0.7/Tokenize.ji due to it containing an invalid cache header
  6. @ Base loading.jl:1328
  7. ...

Similarly, the environment variable can be used to enable debug logging of modules, such as Pkg, or module roots (see Base.moduleroot). To enable all debug logging, use the special value all.

To turn debug logging on from the REPL, set ENV["JULIA_DEBUG"] to the name of the module of interest. Functions defined in the REPL belong to module Main; logging for them can be enabled like this:

  1. julia> foo() = @debug "foo"
  2. foo (generic function with 1 method)
  3. julia> foo()
  4. julia> ENV["JULIA_DEBUG"] = Main
  5. Main
  6. julia> foo()
  7. Debug: foo
  8. @ Main REPL[1]:1

Examples

Example: Writing log events to a file

Sometimes it can be useful to write log events to a file. Here is an example of how to use a task-local and global logger to write information to a text file:

  1. # Load the logging module
  2. julia> using Logging
  3. # Open a textfile for writing
  4. julia> io = open("log.txt", "w+")
  5. IOStream(<file log.txt>)
  6. # Create a simple logger
  7. julia> logger = SimpleLogger(io)
  8. SimpleLogger(IOStream(<file log.txt>), Info, Dict{Any,Int64}())
  9. # Log a task-specific message
  10. julia> with_logger(logger) do
  11. @info("a context specific log message")
  12. end
  13. # Write all buffered messages to the file
  14. julia> flush(io)
  15. # Set the global logger to logger
  16. julia> global_logger(logger)
  17. SimpleLogger(IOStream(<file log.txt>), Info, Dict{Any,Int64}())
  18. # This message will now also be written to the file
  19. julia> @info("a global log message")
  20. # Close the file
  21. julia> close(io)

Example: Enable debug-level messages

Here is an example of creating a ConsoleLogger that lets through any messages with log level higher than, or equal, to Logging.Debug.

  1. julia> using Logging
  2. # Create a ConsoleLogger that prints any log messages with level >= Debug to stderr
  3. julia> debuglogger = ConsoleLogger(stderr, Logging.Debug)
  4. # Enable debuglogger for a task
  5. julia> with_logger(debuglogger) do
  6. @debug "a context specific log message"
  7. end
  8. # Set the global logger
  9. julia> global_logger(debuglogger)

Reference

Logging module

  1. Logging.Logging

Creating events

  1. Logging.@logmsg
  2. Logging.LogLevel
  3. Logging.Debug
  4. Logging.Info
  5. Logging.Warn
  6. Logging.Error

Processing events with AbstractLogger

Event processing is controlled by overriding functions associated with AbstractLogger:

Methods to implement Brief description
Logging.handle_message Handle a log event
Logging.shouldlog Early filtering of events
Logging.min_enabled_level Lower bound for log level of accepted events
Optional methods Default definition Brief description
Logging.catch_exceptions true Catch exceptions during event evaluation
  1. Logging.AbstractLogger
  2. Logging.handle_message
  3. Logging.shouldlog
  4. Logging.min_enabled_level
  5. Logging.catch_exceptions
  6. Logging.disable_logging

Using Loggers

Logger installation and inspection:

  1. Logging.global_logger
  2. Logging.with_logger
  3. Logging.current_logger

Loggers that are supplied with the system:

  1. Logging.NullLogger
  2. Logging.ConsoleLogger
  3. Logging.SimpleLogger