ComponentLogging provides hierarchical control over log levels and messages. It is designed to replace ad‑hoc print/println
calls and verbose flags inside functions, and to strengthen control flow in Julia programs.
Hierarchical logging is critical for building reliable software, especially in compute‑intensive systems. Many computational functions need to emit messages at different detail levels, and there can be lots of such functions. This calls for fine‑grained, per‑function control over logging.
Another challenge is performance: printing intermediate values can be expensive and slow down hot paths. Ideally, those intermediates should be computed and printed only when logging is actually enabled. This requires the ability to alter control flow based on logging decisions.
Julia’s CoreLogging
module provides a solid foundation, and this package builds on it. At its core is the ComponentLogger
, which uses a Dict
keyed by NTuple{N,Symbol}
to control output hierarchically across a module. You can choose the LogLevel
per feature, type, module, or function name to achieve global control with precision.
ComponentLogger
only routes and filters messages. It does not own IO streams. You provide an AbstractLogger
sink such as ConsoleLogger
or the included PlainLogger
. The sink determines where and how messages are written.
- High performance; negligible overhead when logging is disabled. See Benchmarking.
- Suited for controlling module‑wide output granularity using one (or a few) loggers.
- Enables control‑flow changes based on hierarchical log levels to eliminate unnecessary computations from hot paths.
julia>] add ComponentLogging
This package exposes three small, function-first APIs for logging. You use them everywhere; the router (ComponentLogger
) and its rules just decide what gets through.
clog(logger, group, level, msg...; kwargs...)
clogenabled(logger, group, level)::Bool
clogf(f::Function, logger, group, level)
Arguments:
logger::AbstractLogger
— any logger instance. Typically you pass aComponentLogger
configured 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}}
— aSymbol
or a tuple of symbols, e.g.:core
or(: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)
. - General rule:
n → LogLevel(n)
. - Passing
LogLevel
values (e.g.Info
) is also supported and equivalent.
- Mapping (integers first):
Why logger-first? Performance & type-stability. No
global_logger()
:clog
/clogenabled
/clogf
take anAbstractLogger
as the first parameter, which also keeps behavior predictable under concurrency.
clog
— emit a log record for a group at a given level
clog(logger, :core, 0, "starting job"; jobid=42) # 0 = Info
clog(logger, :io, 1, "retrying I/O"; attempt=3) # 1 = LogLevel(1)
clogenabled
— check if logs at level
would pass for group
if clogenabled(logger, :core, 1000) # guard expensive work
stats = compute_expensive_stats()
clog(logger, :core, 1000, "stats ready"; stats)
end
clogf
— evaluate the block only when enabled and log its return value
clogf(logger, :core, 1000) do
val = compute_expensive_stats()
"result = $val"
end
using 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
Output:
ComponentLogger
sink: PlainLogger
min: -1000
rules: 4
├─ :__default__ 0
├─ :core 0
├─ :io 1000
└─ (:net,:http) 2000
What the rules mean
- Key: a group (
Symbol
orNTuple{N,Symbol}
) such as:core
or(:net,:http)
. - Value: the minimum level enabled for that group. Messages below this level are filtered out.
- Define a catch-all like
:__default__ => 0
to control unmatched groups.
Forwarding helpers (recommended) — ergonomic short paths used throughout your codebase
clog(args...; kwargs...) = ComponentLogging.clog(clogger, args...; kwargs...)
clogenabled(args...) = ComponentLogging.clogenabled(clogger, args...)
clogf(f, args...) = ComponentLogging.clogf(f, clogger, args...)
set_log_level(g, lvl) = ComponentLogging.set_log_level!(clogger, g, lvl)
with_min_level(f, lvl) = ComponentLogging.with_min_level(f, clogger, lvl)
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
end
By guarding with clogenabled
, intermediate computations are performed only when logs will be emitted, maximizing performance.
clogf
is similar to clogenabled
, except it logs the return value of the do
-block. When disabled, the block is skipped entirely.
function compute_sumsq()
arr = randn(1000)
sumsq = 0.0
for i in eachindex(arr)
x = arr[i]
sumsq += x^2
clogf(:core, 1) do
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
end
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()
end
- Routing vs. formatting:
ComponentLogger
only routes/filters; the sink (PlainLogger
or anyAbstractLogger
) controls formatting/IO. - Grouping: groups are
Symbol
or tuples ofSymbol
(supports hierarchical/prefix matching if enabled). Be explicit about your matching policy in docs if you customize it. - The function API is the primary entry point. Macro helpers are also provided for convenience. See the Documentation.
PlainLogger
is roughly a Base.CoreLogging.SimpleLogger
without the [Info:
‑style prefixes. Its output looks like print
/println
. It writes messages directly to the console, without additional formatting or filtering beyond color.
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!"
end
Output:
Hello, Julia!
@ README.md:183
PlainLogger
uses show
with MIME"text/plain"
to display 2D and 3D matrices, as it improves matrix readability. For other types, it prints them directly using print
or printstyled
.
with_logger(logger) do
@warn rand(1:9, 3, 3)
end
Output:
3×3 Matrix{Int64}:
8 5 6
3 4 9
7 8 5
@ README.md:196
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 to 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 is a thin, high‑performance layer over the stdlib Base.CoreLogging
interface. It focuses on:
- Performance first: fully type‑stable, no global logger, explicit logger argument for optimal inlining; when disabled, checks are branch‑predictable and near zero‑overhead; when enabled, messages can be built lazily via
clogf
/clogenabled
so expensive work is skipped unless needed. - Simple composition: routes to any
AbstractLogger
sink (ConsoleLogger
, custom sinks, orLoggingExtras
combinators) and defers formatting/IO to the sink. - Explicit component routing: hierarchical group keys (
NTuple{N,Symbol}
) give precise control over noisy areas without imposing a separate handler/formatter stack.
- Choose Memento.jl if you want a self-contained logging framework with built-in handlers/formatters and hierarchical named loggers.
- Choose HierarchicalLogging.jl if you want stdlib-compatible hierarchical control keyed to modules/keys with recursive level management.
- Choose ComponentLogging.jl if you want a high‑performance, type‑stable, component (group) router atop stdlib
Base.CoreLogging
, with lazy message evaluation and minimal overhead when disabled; formatting/IO remains in the sink (ConsoleLogger
, custom sinks,LoggingExtras
, etc.).
We benchmark two paths under identical thresholds:
- filtered (
min=Error
, log atInfo
) — hot path in production; - enabled (
min=Info
, log atInfo
).
All three systems log the same short string to a null sink (no I/O). Keys test four depths: default
, :opti
, (:a,:b)
, (:a,…,:h)
.
In these benchmarks, ComponentLogging (CL) checks group-level thresholds and, if allowed, calls the sink’s handle_message
directly, bypassing the stdlib @info
macro path. HierarchicalLogging (HL) is exercised via the stdlib macros (macro expansion + metadata before reaching the logger). Memento is exercised via its own API and internal handler pipeline. With all three routed to a null/devnull sink, the results mainly measure macro/dispatch/routing overhead, not I/O. The test script is in "benchmark/bench_CL_HL_Me.jl".
system | path | key | time (ns) | allocs | memory (B) |
---|---|---|---|---|---|
CL | enabled | default/str | 9 | 0 | 0 |
CL | enabled | opti/str | 9 | 0 | 0 |
CL | enabled | tuple2/str | 15 | 0 | 0 |
CL | enabled | tuple8/str | 144 | 0 | 0 |
CL | filtered | default | 2 | 0 | 0 |
CL | filtered | opti | 2 | 0 | 0 |
CL | filtered | tuple2 | 2 | 0 | 0 |
CL | filtered | tuple8 | 2 | 0 | 0 |
HL | enabled | default/str | 2189 | 47 | 1984 |
HL | enabled | opti/str | 2178 | 47 | 1984 |
HL | enabled | tuple2/str | 2478 | 51 | 2176 |
HL | enabled | tuple8/str | 4857 | 77 | 5728 |
HL | filtered | default | 2178 | 47 | 1984 |
HL | filtered | opti | 2178 | 47 | 1984 |
HL | filtered | tuple2 | 2467 | 51 | 2176 |
HL | filtered | tuple8 | 4814 | 77 | 5728 |
Memento | enabled | a.b..8/str | 2656 | 76 | 4096 |
Memento | enabled | a.b/str | 1050 | 31 | 1408 |
Memento | enabled | opti/str | 770 | 24 | 1072 |
Memento | enabled | root/str | 622 | 22 | 800 |
Memento | filtered | a.b | 1040 | 31 | 1408 |
Memento | filtered | a.b..8 | 2700 | 76 | 4096 |
Memento | filtered | opti | 770 | 24 | 1072 |
Memento | filtered | root | 621 | 22 | 800 |
Note: Julia v1.10.10, BenchmarkTools v1.6.0, ComponentLogging v0.1.0, HierarchicalLogging v1.0.2, Memento v1.4.1; Windows x86_64, JULIA_NUM_THREADS=1, -O2.
Stdlib Logging (task-local) treats loggers as task-local: messages emitted via @info
/@warn
/... go to the current task’s logger (set with with_logger
/global_logger
). New tasks inherit the parent’s logger upon creation, so concurrent tasks can run with different loggers, levels, and sinks simultaneously.
ComponentLogging (module/group-routed), by design, exposes a module/group-routed policy: the same module or group (e.g., :core
or (:db, :read)
) is routed through the same rules and sink by default—ideal for component-wide noise control and predictable behavior across tasks.
Scope of intent: ComponentLogging
is not aimed at per-task logger isolation by default. Its primary goal is stable, component-level policies with very low overhead on filtered paths.