[ANN] ComponentLogging.jl — Nanosecond-scale hierarchical logging router

ComponentLogging.jl gives you fine-grained, per-group control over logs with function-first APIs (clog / clogenabled / clogf) and a plain, IO-agnostic design that keeps hot paths cheap. It is maintained under the JuliaLogging organization.

Why ComponentLogging

Many compute-heavy functions need different levels of verbosity, and printing intermediates can slow hot loops unless you can cheaply decide when to log and when to skip work.

ComponentLogging builds on Julia’s CoreLogging to route/filter by “component” (symbol or tuple of symbols), so you can enable exactly the areas you care about and let everything else be silent.

Nanosecond-scale routing: the router itself is nanosecond-level; end-to-end cost is dominated by your chosen AbstractLogger (formatting/IO) and by message construction (e.g., string interpolation or recomputation). clogenabled(...) lets you defer construction until logging is actually enabled, preserving hot-path performance.

Usage at a glance

using ComponentLogging

rules = Dict(
  :core         => 0,      # Info+
  :io           => 1000,   # Warn+
  (:net, :http) => 2000,   # Error+
  :__default__  => 0,
)
sink    = PlainLogger()                 # any AbstractLogger sink works
clogger = ComponentLogger(rules; sink)  # router / filter

# App-wide forwarding helpers
clog(args...; kwargs...) = ComponentLogging.clog(clogger, args...; kwargs...)
clogenabled(args...)     = ComponentLogging.clogenabled(clogger, args...)
clogf(f, args...)        = ComponentLogging.clogf(f, clogger, args...)

# Use it
clog(:core, 0, "starting job"; jobid=12)

if clogenabled(:core, 1000)
    stats = compute_expensive_stats()
    clog(:core, 1000, "stats ready"; stats)
end

clogf(:core, 1000) do
    stats = compute_expensive_stats()
    "lazy message built only when enabled"
end

This setup lets you control output granularity with a small set of rules and keep expensive work behind guards or lazy blocks.

Deeper docs, options, and benchmarks live in the README.md.

PlainLogger in a nutshell

If you prefer output that looks like println, PlainLogger is a tiny sink that writes messages without the usual [Info: prefixes, and it uses MIME"text/plain" to pretty-print 2D/3D matrices for readability You can use it standalone or as the sink behind ComponentLogger.

A note on levels

You can pass integers instead of LogLevel, which keeps call sites simple while remaining compatible with the stdlib interface For example, 0 corresponds to Info, and you can set group thresholds via integers in the rules table.

Temporarily silence everything

Wrap work in with_min_level to raise the global minimum temporarily—for instance when benchmarking a function without logging overhead.

with_min_level(2000) do
    @benchmark compute_expensive_stats()
end
7 Likes

Very interesting! Is there a pattern to write Julia-native objects to loggers (e.g. a struct) that can then be manipulated later on? Or is then intention to write text-like printed information to a logger?

1 Like

The latter. ComponentLogger is a thin, upper-layer router that only does component grouping and level checks. It holds a sink::AbstractLogger which actually processes the event.

PlainLogger is a text-only sink; anything that implements show will be printed as human-readable text.

If you want to persist native objects or structured events, provide your own structured AbstractLogger and set it as ComponentLogger.sink (or tee to both your structured sink and PlainLogger).

Have you seen any of the build-your-own structured logger for native objects? I’m interested in examining a robust implementation (the use case I’m working on would be logging from various threads).

Sorry, but I don’t know what a structured logger is. I made this package mainly for text printing. :slightly_smiling_face: