ComponentLogging.jl is a lightweight, high-performance logging layer for Julia built around explicit component loggers and hierarchical log-level control.
Unlike Julia's standard Logging, which dynamically selects the active logger from the current task with a global fallback, ComponentLogging primarily associates shared logging configuration with a module or software component. This provides stable hierarchical control across tasks and threads while retaining explicit logger passing when execution-specific logging is needed.
julia>] add ComponentLoggingusing ComponentLogging
# Keys = groups; Values = minimum enabled level (integer or LogLevel)
rules = Dict(
:core => 0, # Info+
:io => 1000, # Warn+
(:net, :http) => 2000, # Error+
:__default__ => 0 # fallback for unmatched groups (default to Info)
)
sink = PlainLogger() # any AbstractLogger sink works
clogger = ComponentLogger(rules; sink) # router/filter; does not own IO
@forward_logger clogger
clog(:core, 0, "starting job"; jobid=42) # 0 = Info
clog(:io, 1000, "retrying I/O"; attempt=3) # 1000 = WarnTo inspect the hierarchical rules inside clogger, use display(clogger).
Output:
ComponentLogger
sink: PlainLogger
min: 0
rules: 4
├─ :__default__ 0
├─ :core 0
├─ :io 1000
└─ :net
└─ :http 2000
What the rules mean
- Key: a group (
SymbolorNTuple{N,Symbol}) such as:coreor(:net,:http). - Value: the minimum level enabled for that group. Messages below this level are filtered out.
- Define a catch-all like
:__default__ => 0to control unmatched groups.
The same group hierarchy can be used as a lightweight runtime control plane. With the forwarding helpers created by @forward_logger, set_log_level!(group, true) enables the group's default Info-level check, while set_log_level!(group, false) disables it.
function solve(problem)
clogenabled((:solver, :presolve)) && run_presolve!(problem)
if clogenabled((:solver, :heuristics))
run_heuristics!(problem)
end
clogenabled((:solver, :cache)) && update_cache!(problem)
end
# Change behavior globally for this module/component.
set_log_level!((:solver, :presolve), true)
set_log_level!((:solver, :heuristics), false)
set_log_level!((:solver, :cache), true)
solve(problem)This lets deeply nested behavior be switched at runtime without passing verbose, enable_*, or other configuration flags through every function in the call chain. The switches can control logging, diagnostics, optional computations, algorithmic branches, caching strategies, or any other behavior you choose to place behind clogenabled.
Without forwarding helpers, use the logger-explicit forms instead:
set_log_level!(clogger, (:solver, :heuristics), false)
clogenabled(clogger, (:solver, :heuristics))
# Query the level after hierarchical fallback.
get_log_level(clogger, (:solver, :heuristics))clog
Emits a log record for a group at a given level.
clog(logger, group, level, msg...; kwargs...)Arguments:
logger::AbstractLogger— any logger instance. Typically you pass aComponentLoggerconfigured with per-group rules and a sink (e.g.PlainLogger). Many codebases also define forwarding helpers to avoid threading the logger explicitly (see below).group::Union{Symbol,NTuple{N,Symbol}}— aSymbolor a tuple of symbols, e.g.:coreor(:net, :http).level::Union{Integer,LogLevel}— prefer integers (no need to importLogging). We immediately convert withLogLevel(level).- Mapping (integers first):
-1000 (Debug),0 (Info),1000 (Warn),2000 (Error).
- Mapping (integers first):
Example:
clog(clogger, :core, 0, "starting job"; jobid=42) # 0 = Info
clog(clogger, :io, 1000, "retrying I/O"; attempt=3) # 1000 = Warnclogenabled
Checks whether records at level are enabled for group.
Example:
if clogenabled(clogger, :core, 1000) # guard expensive work
stats = compute_expensive_stats()
clog(clogger, :core, 1000, "stats ready"; stats)
end@clog
Evaluates the block only when enabled and logs its return value.
Use @clog logger group level msg..., or after @forward_logger, the local
form @clog group level msg.... It evaluates message expressions only when
logging is enabled, keeps them inline to avoid closure capture, and captures the
emitting call's module, file, and line automatically. If the final message
expression evaluates to nothing, no log record is emitted.
arr = randn(1000)
sumsq = 0.0
for i in eachindex(arr)
x = arr[i]
sumsq += x^2
@clog :core 1 begin
meanval = mean(arr[1:i])
stdval = std(arr[1:i])
# The return value will be used as the log message
"i=$i, x=$x, mean=$(meanval), std=$(stdval), sumsq=$(sumsq)"
end
end@forward_logger
@forward_logger cloggerThis creates module-local forwarding methods for clog, clogenabled,
set_log_level!, get_log_level, and with_min_level, plus a local
@clog that uses clogger. The logger expression is bound where
@forward_logger is declared, including when the generated macro is imported
by another module.
For example, the generated clog is conceptually equivalent to the following module-local forwarding method:
clog(args...; kwargs...) = ComponentLogging.clog(clogger, args...; kwargs...)
Assuming you set up the forwarding helpers, you can use clog like this:
function compute_vector_sum(n)
clog(:core, 2, "Processing a $n-element vector")
v = randn(n)
s = sum(v)
clog(:core, 0, "Done."; s, v)
return s
end
compute_vector_sum(3);Output:
Processing a 3-element vector
Done.
s = 2.435219412665466
v = [-0.20970686116839346, 1.2387800065077361, 1.4061462673261231]clogenabled checks whether a given component is enabled at a given level. It is intended to drive control‑flow decisions so that certain code runs only when logging is enabled. Returns Bool.
function compute_sumsq()
arr = randn(1000)
sumsq = 0.0
for i in eachindex(arr)
x = arr[i]
sumsq += x^2
if clogenabled(:core, 1)
# Compute mean and standard deviation (intermediates) only when logging is enabled
meanval = mean(arr[1:i])
stdval = std(arr[1:i])
clog(:core, 1, "i=$i, x=$x, mean=$(meanval), std=$(stdval), sumsq=$(sumsq)")
end
end
endBy guarding with clogenabled, intermediate computations are performed only when logs will be emitted, maximizing performance.
with_min_level temporarily sets the logger’s global minimum level and restores it on exit.
For example, to benchmark compute_sumsq() without any logging‑related work:
with_min_level(2000) do
@benchmark compute_sumsq()
endPlainLogger is roughly a Base.CoreLogging.SimpleLogger without the [Info:-style prefixes. Its output looks like print, subject to its configured min_level filter. When wrapped by ComponentLogger, the component rules decide whether a record is enabled.
PlainLogger and ComponentLogger are independent. You can also include("src/PlainLogger.jl") to use PlainLogger on its own.
Example:
using ComponentLogging, Logging
logger = PlainLogger()
with_logger(logger) do
@info "Hello, Julia!"
endOutput:
Hello, Julia!
@ README.md:183PlainLogger uses show with MIME"text/plain" to display 2D and 3D matrices, as it improves matrix readability. For other values, it prints them directly using print.
with_logger(logger) do
@warn rand(1:9, 3, 3)
endOutput:
3×3 Matrix{Int64}:
8 5 6
3 4 9
7 8 5
@ README.md:196ComponentLogging is continuously benchmarked over time. Current and historical performance results are available on the benchmark dashboard.
Memento.jl is a flexible, hierarchical logging framework that brings its own ecosystem of loggers, handlers, formatters, records, and IO backends. Loggers are named (e.g., "Foo.bar"), form a hierarchy with propagation to a root logger, and are configured via config!, setlevel!, and by attaching handlers (file, custom formatters, etc.).
HierarchicalLogging.jl defines a Base.Logging-compatible HierarchicalLogger that associates loggers with hierarchically-related objects (e.g., module → submodule). Each node has a LogLevel that can be set with min_enabled_level!, which also recursively updates children; you can attach different underlying loggers (e.g., ConsoleLogger) to different parts of the tree.
ComponentLogging.jl takes a different approach: it is a lightweight, performance-oriented layer over Base.CoreLogging built around explicit component loggers and hierarchical group rules. It routes accepted records to any AbstractLogger sink, keeps filtered paths extremely cheap, and also exposes the same hierarchy as a lightweight runtime control plane through clogenabled and Boolean level switches.
- Choose Memento.jl for a self-contained logging framework with hierarchical named loggers and a rich handler/formatter system.
- Choose HierarchicalLogging.jl for stdlib-compatible hierarchical control over hierarchically related objects with recursive level management.
- Choose ComponentLogging.jl for explicit hierarchical control, very low filtered-call overhead, direct composition with
AbstractLoggersinks, and a hierarchy that can double as a runtime control plane.