Logging
Overview
Logging is a crucial tool for debugging, monitoring, and understanding the behavior of your applications. ClimaComms extends Julia's built-in logging functionality (provided by Logging.jl
) to work seamlessly in distributed computing environments, particularly with MPI.
Julia's Logging System
Julia's standard library provides a flexible logging system through Logging.jl
. Key concepts include:
- Logger: An object that handles log messages, determining how they are formatted and where they are sent
- Log Level: Indicates the severity/importance of a message
- Log Record: Contains the message and associated metadata (level, module, line number, etc.)
The default logger in Julia (as of v1.11) is Logging.ConsoleLogger()
, which prints messages to the console. You can check your current logger using:
using Logging; current_logger()
ClimaComms Logging Features
ClimaComms builds upon Julia's logging system by providing specialized loggers for distributed computing.
To set any of the loggers below as the global logger, use Logging.global_logger(logger)
:
using ClimaComms, Logging
ctx = ClimaComms.context()
logger = ClimaComms.OnlyRootLogger(ctx)
global_logger(logger)
OnlyRootLogger
OnlyRootLogger(context)
returns a logger that silences non-root processes. If using MPI, this logger is enabled on the first ClimaComms.init
call.
FileLogger
FileLogger(context, log_dir)
writes to stdout
and to a file log_dir/output.log
simultaneously. If using MPI, the FileLogger
separates logs by MPI process into different files, so that process i
will write to the rank_$i.log
file in the $log_dir
directory, where $log_dir
is of your choosing. In this case, $log_dir/output.log
is a symbolic link to the root process logs. In other words, you can always look at $log_dir/output.log
for the output. Logging to stdout
can be disabled by setting the keyword argument log_stdout = false
.
using ClimaComms, Logging
ctx = ClimaComms.context()
logger = ClimaComms.FileLogger(ctx, "logs")
with_logger(logger) do
@warn "Memory usage high" # Written to rank-specific log file
end
This will output the following in both the REPL and logs/rank_1.log
:
┌ Warning: Memory usage high
└ @ Main REPL[6]:2
MPILogger
MPILogger(context)
adds an MPI rank prefix to all log messages:
using ClimaComms, Logging
ctx = ClimaComms.context()
logger = MPILogger(ctx)
with_logger(logger) do
@info "Processing data..." # Output: [P1] Info: Processing data...
end
Log Levels
Julia provides four standard log levels, in order of increasing severity:
Debug
: Detailed information for debuggingInfo
: General information about program executionWarn
: Warnings about potential issuesError
: Error conditions that might still allow the program to continue
See Julia documentation for more detailed information.
You can define custom log levels using LogLevel
:
const Trace = LogLevel(-1000) # Lower number = less severe
@macroexpand @logmsg Trace "Very detailed trace message"
To disable all log messages at log levels equal to or less than a given LogLevel
, use Logging.disable_logging(level)
.
Filtering Log Messages
LoggingExtras.jl provides powerful filtering capabilities through the EarlyFilteredLogger(filter, logger)
, which takes two arguments:
filter(log_args)
is a function which takes inlog_args
and returns a Boolean determining if the message should be logged.log_args
is a NamedTuple with fieldslevel
,_module
,id
andgroup
. Examplefilter
functions are provided below in the Common Use Cases.logger
is any existing logger, such asLogging.ConsoleLogger()
orMPILogger(ctx)
.
Common Use Cases
How do I save log output to stdout and a file simultaneously?
ClimaComms.FileLogger
logs to files and stdout simultaneously. For full customization, LoggingExtras.TeeLogger(loggers)
composes multiple loggers, allowing for multiple loggers at once as shown below.
using Logging, LoggingExtras
io = open("simulation.log", "w")
loggers = (Logging.ConsoleLogger(stdout), Logging.ConsoleLogger(io))
tee_logger = LoggingExtras.TeeLogger(loggers)
with_logger(tee_logger) do
@warn "Log to stdout and file"
end
close(io)
How do I filter out warning messages?
using Logging, LoggingExtras
function no_warnings(log_args)
return log_args.level != Logging.Warn
end
filtered_logger = EarlyFilteredLogger(no_warnings, Logging.current_logger())
with_logger(filtered_logger) do
@warn "Hide this warning"
@info "Display this message"
end
[ Info: Display this message
How do I filter out messages from certain modules?
We can create a custom filter that returns false
if a log message originates from a list of excluded modules.
The same pattern can be reversed to filter messages only coming from certain modules.
using Logging, LoggingExtras
module_filter(excluded_modules) = log_args ->
!(log_args._module in excluded_modules)
ModuleFilteredLogger(excluded) =
EarlyFilteredLogger(module_filter(excluded), Logging.current_logger())
# To test this logger:
module TestModule
using Logging
function log_something()
@info "This message will appear"
end
end
excluded = (Main, Base)
with_logger(ModuleFilteredLogger(excluded)) do
@info "Hide this message"
TestModule.log_something()
end
[ Info: This message will appear