Logging
In the final part of the square‑root story we augment Heron's iteration with logging functionality. For example, we might be interested in the convergence behavior throughout the iterations, timing information, or storing intermediate values for later analysis. The logging system is designed to provide full flexibility over this behavior, without polluting the core algorithm implementation. Additionally, we strive to pay for what you get: when no logging is configured, there is minimal overhead.
Why separate logging from algorithms?
Decoupling logging from algorithm logic lets us:
- Add diagnostic output without modifying algorithm code.
- Compose multiple logging behaviors (printing, storing, timing) independently.
- Reuse generic logging actions across different algorithms.
- Disable logging globally with zero runtime cost.
- Instrument algorithms with custom events for domain-specific diagnostics.
- Customize logging behavior a posteriori: users can add logging features to existing algorithms without modifying library code.
The logging system aims to achieve these goals by separating the logging logic into two separate parts. These parts can be roughly described as events and actions, where the logging system is responsible for mapping between them. Concretely, we have:
- When do we log? → an
with_algorithmloggerto control how to map events to actions. - What happens when we log? → a
LoggingActionto determine what to do when an event happens.
This separation allows users to compose rich behaviors (printing, collecting statistics, plotting) without modifying algorithm code, and lets algorithm authors emit domain‑specific events.
Using the default logging actions
Continuing from the Stopping Criteria page, we have our Heron's method implementation ready:
using AlgorithmsInterface
using Printf
struct SqrtProblem <: Problem
S::Float64
end
struct HeronAlgorithm <: Algorithm
stopping_criterion
end
mutable struct HeronState <: State
iterate::Float64
iteration::Int
stopping_criterion_state
end
function AlgorithmsInterface.initialize_state(problem::SqrtProblem, algorithm::HeronAlgorithm; kwargs...)
x0 = rand()
stopping_criterion_state = initialize_state(problem, algorithm, algorithm.stopping_criterion)
return HeronState(x0, 0, stopping_criterion_state)
end
function AlgorithmsInterface.initialize_state!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState; kwargs...)
state.iterate = rand()
state.iteration = 0
initialize_state!(problem, algorithm, algorithm.stopping_criterion, state.stopping_criterion_state)
return state
end
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
S = problem.S
x = state.iterate
state.iterate = 0.5 * (x + S / x)
return state
end
function heron_sqrt(x; stopping_criterion = StopAfterIteration(10))
prob = SqrtProblem(x)
alg = HeronAlgorithm(stopping_criterion)
state = solve(prob, alg) # allocates & runs
return state.iterate
endIt is already interesting to note that there are no further modifications necessary to start leveraging the logging system.
Basic iteration printing
Let's start with a very basic example of logging: printing iteration information after each step. We use CallbackAction to wrap a simple function that accesses the state, and prints the iteration as well as the iterate.
using Printf
iter_printer = CallbackAction() do problem, algorithm, state
@printf("Iter %3d: x = %.12f\n", state.iteration, state.iterate)
endTo activate this logger, we wrap the section of code that we want to enable logging for, and map the :PostStep context to our action. This is achieved through the with_algorithmlogger function, which under the hood uses Julia's with function to manipulate a scoped value.
with_algorithmlogger(:PostStep => iter_printer) do
sqrt2 = heron_sqrt(2.0)
endIter 1: x = 4.535815689214
Iter 2: x = 2.488375356631
Iter 3: x = 1.646056310126
Iter 4: x = 1.430540786222
Iter 5: x = 1.414306736312
Iter 6: x = 1.414213565442
Iter 7: x = 1.414213562373
Iter 8: x = 1.414213562373
Iter 9: x = 1.414213562373
Iter 10: x = 1.414213562373Default logging contexts
The default solve! loop emits logging events at several key points during iteration:
| context | event |
|---|---|
| :Start | The solver will start. |
| :PreStep | The solver is about to take a step. |
| :PostStep | The solver has taken a step. |
| :Stop | The solver has finished. |
Any of these events can be hooked into to attach a logging action. For example, we may expand on the previous example as follows:
start_printer = CallbackAction() do problem, algorithm, state
@printf("Start: x = %.12f\n", state.iterate)
end
stop_printer = CallbackAction() do problem, algorithm, state
@printf("Stop %3d: x = %.12f\n", state.iteration, state.iterate)
end
with_algorithmlogger(:Start => start_printer, :PostStep => iter_printer, :Stop => stop_printer) do
sqrt2 = heron_sqrt(2.0)
endStart: x = 0.811567040884
Iter 1: x = 1.637967615684
Iter 2: x = 1.429496488572
Iter 3: x = 1.414295258213
Iter 4: x = 1.414213564733
Iter 5: x = 1.414213562373
Iter 6: x = 1.414213562373
Iter 7: x = 1.414213562373
Iter 8: x = 1.414213562373
Iter 9: x = 1.414213562373
Iter 10: x = 1.414213562373
Stop 10: x = 1.414213562373Furthermore, specific algorithms could emit events for custom contexts too. We will come back to this in the section on the AlgorithmLogger design.
Timing execution
Let's add timing information to see how long each iteration takes:
start_time = Ref{Float64}(0.0)
record_start = CallbackAction() do problem, algorithm, state
start_time[] = time()
end
show_elapsed = CallbackAction() do problem, algorithm, state
dt = time() - start_time[]
@printf(" elapsed = %.3fs\n", dt)
end
with_algorithmlogger(
:Start => record_start,
:PostStep => show_elapsed,
:Stop => CallbackAction() do problem, algorithm, state
total = time() - start_time[]
@printf("Done after %d iterations (total %.3fs)\n", state.iteration, total)
end,
) do
sqrt2 = heron_sqrt(2)
end elapsed = 0.043s
elapsed = 0.047s
elapsed = 0.047s
elapsed = 0.047s
elapsed = 0.047s
elapsed = 0.047s
elapsed = 0.047s
elapsed = 0.047s
elapsed = 0.047s
elapsed = 0.047s
Done after 10 iterations (total 0.083s)Conditional logging
Sometimes we only want to log at specific iterations. IfAction wraps another action behind a predicate:
every_two = IfAction(
(problem, algorithm, state; kwargs...) -> state.iteration % 2 == 0,
iter_printer,
)
with_algorithmlogger(:PostStep => every_two) do
sqrt2 = heron_sqrt(2)
endIter 2: x = 1.423330968192
Iter 4: x = 1.414213562675
Iter 6: x = 1.414213562373
Iter 8: x = 1.414213562373
Iter 10: x = 1.414213562373This prints only on even iterations, reducing output for long-running algorithms.
Storing intermediate values
Instead of just printing, we can capture the entire trajectory for later analysis:
struct CaptureHistory <: LoggingAction
iterates::Vector{Float64}
end
CaptureHistory() = CaptureHistory(Float64[])
function AlgorithmsInterface.handle_message!(
action::CaptureHistory,
problem::SqrtProblem,
algorithm::HeronAlgorithm,
state::HeronState;
kwargs...
)
push!(action.iterates, state.iterate)
return nothing
end
history = CaptureHistory()
with_algorithmlogger(:PostStep => history) do
sqrt2 = heron_sqrt(2)
end
println("Stored ", length(history.iterates), " iterates")
println("First few values: ", history.iterates[1:min(3, end)])Stored 10 iterates
First few values: [7.908502888444397, 4.08069762678112, 2.2854049512031542]You can later analyze convergence rates, plot trajectories, or export data—all without modifying the algorithm.
Combining multiple logging behaviors
We can combine printing, timing, and storage simultaneously:
history2 = CaptureHistory()
with_algorithmlogger(
:Start => record_start,
:PostStep => ActionGroup(iter_printer, history2),
:Stop => CallbackAction() do problem, algorithm, state
@printf("Captured %d iterates in %.3fs\n", length(history2.iterates), time() - start_time[])
end,
) do
sqrt2 = heron_sqrt(2)
endIter 1: x = 3.965665033375
Iter 2: x = 2.234997031740
Iter 3: x = 1.564926403155
Iter 4: x = 1.421470887807
Iter 5: x = 1.414232088525
Iter 6: x = 1.414213562494
Iter 7: x = 1.414213562373
Iter 8: x = 1.414213562373
Iter 9: x = 1.414213562373
Iter 10: x = 1.414213562373
Captured 10 iterates in 0.055sImplementing custom LoggingActions
While CallbackAction is convenient for quick instrumentation, custom types give more control and possibly better performance. Let's implement a more sophisticated example: tracking iteration statistics.
The required interface
To implement a custom LoggingAction, you need:
- A concrete subtype of
LoggingAction. - An implementation of
AlgorithmsInterface.handle_message!that defines the behavior.
The signature of handle_message! is:
function handle_message!(
action::YourAction, problem::Problem, algorithm::Algorithm, state::State; kwargs...
)
# Your logging logic here
return nothing
endThe kwargs... can contain context-specific information, though the default contexts don't currently pass additional data.
Example: Statistics collector
Let's build an action that tracks statistics across iterations:
mutable struct StatsCollector <: LoggingAction
count::Int # aggregate number of evaluations
sum::Float64 # sum of all intermediate values
sum_squares::Float64 # square sum of all intermediate values
end
StatsCollector() = StatsCollector(0, 0.0, 0.0)
function AlgorithmsInterface.handle_message!(
action::StatsCollector, problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState;
kwargs...
)
action.count += 1
action.sum += state.iterate
action.sum_squares += state.iterate^2
return nothing
end
function compute_stats(stats::StatsCollector)
n = stats.count
mean = stats.sum / n
variance = (stats.sum_squares / n) - mean^2
return (mean=mean, variance=variance, count=n)
end
stats = StatsCollector()
with_algorithmlogger(:PostStep => stats) do
sqrt2 = heron_sqrt(2.0; stopping_criterion = StopAfter(Millisecond(50)))
end
result = compute_stats(stats)
println("Collected $(result.count) samples")
println("Mean iterate: $(result.mean)")
println("Variance: $(result.variance)")Collected 41283 samples
Mean iterate: 1.4143364797845428
Variance: 0.0003377447193804173This pattern of collecting data during iteration and post-processing afterward is efficient and keeps the hot loop fast.
The AlgorithmLogger
The AlgorithmsInterface.AlgorithmLogger is the dispatcher that routes logging events to actions. Understanding its design helps when adding custom logging contexts.
How logging events are emitted
Inside the solve! function, logging events are emitted at key points:
function solve!(problem::Problem, algorithm::Algorithm, state::State; kwargs...)
initialize_state!(problem, algorithm, state; kwargs...)
emit_message(problem, algorithm, state, :Start)
while !is_finished!(problem, algorithm, state)
emit_message(problem, algorithm, state, :PreStep)
increment!(state)
step!(problem, algorithm, state)
emit_message(problem, algorithm, state, :PostStep)
end
emit_message(problem, algorithm, state, :Stop)
return state
endThe emit_message function looks up the context (e.g., :PostStep) in the logger's action dictionary and calls handle_message! on the corresponding action.
Global enable/disable
For production runs or benchmarking, you can disable all logging globally:
# By default, logging is enabled:
println("Logging enabled: ", AlgorithmsInterface.get_global_logging_state())
with_algorithmlogger(:PostStep => iter_printer) do
heron_sqrt(2.0)
endLogging enabled: true
Iter 1: x = 1.525520600209
Iter 2: x = 1.418274227522
Iter 3: x = 1.414219375424
Iter 4: x = 1.414213562385
Iter 5: x = 1.414213562373
Iter 6: x = 1.414213562373
Iter 7: x = 1.414213562373
Iter 8: x = 1.414213562373
Iter 9: x = 1.414213562373
Iter 10: x = 1.414213562373# But, logging can also be disabled:
previous_state = AlgorithmsInterface.set_global_logging_state!(false)
# This will not log anything, even with a logger configured
with_algorithmlogger(:PostStep => iter_printer) do
heron_sqrt(2.0)
end
# Restore previous state
AlgorithmsInterface.set_global_logging_state!(previous_state)This works since the default implementation of emit_message first retrieves the current logger through AlgorithmsInterface.algorithm_logger:
emit_message(problem, algorithm, state, context; kwargs...) =
emit_message(algorithm_logger(), problem, algorithm, state, context; kwargs...)When logging is disabled globally, algorithm_logger returns nothing, and emit_message becomes a no-op with minimal overhead.
Error isolation
If a LoggingAction throws an exception, the logging system catches it and reports an error without aborting the algorithm:
buggy_action = CallbackAction() do problem, algorithm, state
if state.iteration == 3
error("Intentional logging error at iteration 3")
end
@printf("Iter %d\n", state.iteration)
end
with_algorithmlogger(:PostStep => buggy_action) do
heron_sqrt(2.0)
println("Algorithm completed despite logging error")
endIter 1
Iter 2
┌ Error: Error during the handling of a logging action
│ action = CallbackAction{Main.var"#41#42"}(Main.var"#41#42"())
│ exception =
│ Intentional logging error at iteration 3
│ Stacktrace:
│ [1] error(s::String)
│ @ Base ./error.jl:35
│ [2] (::Main.var"#41#42")(problem::Main.SqrtProblem, algorithm::Main.HeronAlgorithm, state::Main.HeronState)
│ @ Main ./logging.md:381
│ [3] #handle_message!#20
│ @ ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/logging.jl:60 [inlined]
│ [4] handle_message!(action::CallbackAction{Main.var"#41#42"}, problem::Main.SqrtProblem, algorithm::Main.HeronAlgorithm, state::Main.HeronState)
│ @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/logging.jl:57
│ [5] emit_message(logger::AlgorithmsInterface.AlgorithmLogger, problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...)
│ @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/logging.jl:188
│ [6] emit_message(logger::AlgorithmsInterface.AlgorithmLogger, problem::Problem, algorithm::Algorithm, state::State, context::Symbol)
│ @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/logging.jl:178
│ [7] solve!(problem::Main.SqrtProblem, algorithm::Main.HeronAlgorithm, state::Main.HeronState; kwargs::@Kwargs{})
│ @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/interface/interface.jl:65
│ [8] solve!
│ @ ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/interface/interface.jl:45 [inlined]
│ [9] #solve#1
│ @ ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/interface/interface.jl:34 [inlined]
│ [10] solve
│ @ ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/interface/interface.jl:32 [inlined]
│ [11] #heron_sqrt#3
│ @ ./logging.md:78 [inlined]
│ [12] heron_sqrt
│ @ ./logging.md:75 [inlined]
│ [13] (::Main.var"#43#44")()
│ @ Main ./logging.md:387
│ [14] with(::Main.var"#43#44", ::Pair{Base.ScopedValues.ScopedValue{AlgorithmsInterface.AlgorithmLogger}, AlgorithmsInterface.AlgorithmLogger})
│ @ Base.ScopedValues ./scopedvalues.jl:269
│ [15] with_algorithmlogger(f::Function, args::Pair{Symbol, CallbackAction{Main.var"#41#42"}})
│ @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/logging.jl:126
│ [16] top-level scope
│ @ logging.md:386
│ [17] eval
│ @ ./boot.jl:430 [inlined]
│ [18] #59
│ @ ~/.julia/packages/Documenter/HdXI4/src/expander_pipeline.jl:856 [inlined]
│ [19] cd(f::Documenter.var"#59#61"{Module, Expr}, dir::String)
│ @ Base.Filesystem ./file.jl:112
│ [20] (::Documenter.var"#58#60"{Documenter.Page, Module, Expr})()
│ @ Documenter ~/.julia/packages/Documenter/HdXI4/src/expander_pipeline.jl:855
│ [21] (::IOCapture.var"#5#9"{DataType, Documenter.var"#58#60"{Documenter.Page, Module, Expr}, IOContext{Base.PipeEndpoint}, IOContext{Base.PipeEndpoint}, Base.PipeEndpoint, Base.PipeEndpoint})()
│ @ IOCapture ~/.julia/packages/IOCapture/Y5rEA/src/IOCapture.jl:170
│ [22] with_logstate(f::IOCapture.var"#5#9"{DataType, Documenter.var"#58#60"{Documenter.Page, Module, Expr}, IOContext{Base.PipeEndpoint}, IOContext{Base.PipeEndpoint}, Base.PipeEndpoint, Base.PipeEndpoint}, logstate::Base.CoreLogging.LogState)
│ @ Base.CoreLogging ./logging/logging.jl:524
│ [23] with_logger(f::Function, logger::Base.CoreLogging.ConsoleLogger)
│ @ Base.CoreLogging ./logging/logging.jl:635
│ [24] capture(f::Documenter.var"#58#60"{Documenter.Page, Module, Expr}; rethrow::Type, color::Bool, passthrough::Bool, capture_buffer::IOBuffer, io_context::Vector{Any})
│ @ IOCapture ~/.julia/packages/IOCapture/Y5rEA/src/IOCapture.jl:167
│ [25] runner(::Type{Documenter.Expanders.ExampleBlocks}, node::MarkdownAST.Node{Nothing}, page::Documenter.Page, doc::Documenter.Document)
│ @ Documenter ~/.julia/packages/Documenter/HdXI4/src/expander_pipeline.jl:854
│ [26] dispatch(::Type{Documenter.Expanders.ExpanderPipeline}, ::MarkdownAST.Node{Nothing}, ::Vararg{Any})
│ @ Documenter.Selectors ~/.julia/packages/Documenter/HdXI4/src/utilities/Selectors.jl:170
│ [27] expand(doc::Documenter.Document)
│ @ Documenter ~/.julia/packages/Documenter/HdXI4/src/expander_pipeline.jl:59
│ [28] runner(::Type{Documenter.Builder.ExpandTemplates}, doc::Documenter.Document)
│ @ Documenter ~/.julia/packages/Documenter/HdXI4/src/builder_pipeline.jl:224
│ [29] dispatch(::Type{Documenter.Builder.DocumentPipeline}, x::Documenter.Document)
│ @ Documenter.Selectors ~/.julia/packages/Documenter/HdXI4/src/utilities/Selectors.jl:170
│ [30] #88
│ @ ~/.julia/packages/Documenter/HdXI4/src/makedocs.jl:280 [inlined]
│ [31] withenv(::Documenter.var"#88#90"{Documenter.Document}, ::Pair{String, Nothing}, ::Vararg{Pair{String, Nothing}})
│ @ Base ./env.jl:265
│ [32] #87
│ @ ~/.julia/packages/Documenter/HdXI4/src/makedocs.jl:279 [inlined]
│ [33] cd(f::Documenter.var"#87#89"{Documenter.Document}, dir::String)
│ @ Base.Filesystem ./file.jl:112
│ [34] makedocs(; debug::Bool, format::Documenter.HTMLWriter.HTML, kwargs::@Kwargs{modules::Vector{Module}, authors::String, sitename::String, pages::Vector{Pair{String, String}}, expandfirst::Vector{String}, plugins::Vector{Documenter.Plugin}})
│ @ Documenter ~/.julia/packages/Documenter/HdXI4/src/makedocs.jl:278
│ [35] top-level scope
│ @ ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/docs/make.jl:33
│ [36] include(mod::Module, _path::String)
│ @ Base ./Base.jl:562
│ [37] exec_options(opts::Base.JLOptions)
│ @ Base ./client.jl:323
│ [38] _start()
│ @ Base ./client.jl:531
└ @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/logging.jl:191
Iter 4
Iter 5
Iter 6
Iter 7
Iter 8
Iter 9
Iter 10
Algorithm completed despite logging errorThis robustness ensures that bugs in logging code don't compromise the algorithm's correctness.
Adding custom logging contexts
Algorithms can emit custom logging events for domain-specific scenarios. For example, adaptive algorithms might emit events when step sizes are reduced, or when steps are rejected. Here we will illustrate this by a slight adaptation of our algorithm, which could restart if convergence wasn't reached after 10 iterations.
Emitting custom events
To emit a custom logging event from within your algorithm, call emit_message:
function AlgorithmsInterface.step!(problem::SqrtProblem, algorithm::HeronAlgorithm, state::HeronState)
# Suppose we check for numerical issues
if !isfinite(state.iterate) || mod(state.iteration, 10) == 0
emit_message(problem, algorithm, state, :Restart)
state.iterate = rand() # Reset the iterate an try again
end
# Normal step
S = problem.S
x = state.iterate
state.iterate = 0.5 * (x + S / x)
return state
endNow users can attach actions to the :Restart context:
issue_counter = Ref(0)
issue_action = CallbackAction() do problem, algorithm, state
issue_counter[] += 1
println("⚠️ Numerical issue detected at iteration ", state.iteration)
end
with_algorithmlogger(:Restart => issue_action, :PostStep => iter_printer) do
sqrt2 = heron_sqrt(2.0; stopping_criterion = StopAfterIteration(30))
endIter 1: x = 12.159411854754
Iter 2: x = 6.161946747241
Iter 3: x = 3.243259748531
Iter 4: x = 1.929961638458
Iter 5: x = 1.483125833137
Iter 6: x = 1.415814539497
Iter 7: x = 1.414214467551
Iter 8: x = 1.414213562373
Iter 9: x = 1.414213562373
⚠️ Numerical issue detected at iteration 10
Iter 10: x = 2.355117884090
Iter 11: x = 1.602166137615
Iter 12: x = 1.425238065298
Iter 13: x = 1.414256200746
Iter 14: x = 1.414213563016
Iter 15: x = 1.414213562373
Iter 16: x = 1.414213562373
Iter 17: x = 1.414213562373
Iter 18: x = 1.414213562373
Iter 19: x = 1.414213562373
⚠️ Numerical issue detected at iteration 20
Iter 20: x = 56.540712870374
Iter 21: x = 28.288042805755
Iter 22: x = 14.179372028118
Iter 23: x = 7.160210999088
Iter 24: x = 3.719766188332
Iter 25: x = 2.128717195389
Iter 26: x = 1.534125085308
Iter 27: x = 1.418899872984
Iter 28: x = 1.414221301294
Iter 29: x = 1.414213562394
⚠️ Numerical issue detected at iteration 30
Iter 30: x = 2.030776946916Best practices
Performance considerations
- Logging actions may be fast or slow, since the overhead is only incurred when actually using them.
- Algorithms should be mindful of emitting events in hot loops. These events incur an overhead similar to accessing a
ScopedValue(~10-100 ns), even when no logging action is registered. - For expensive operations (plotting, I/O), it is often better to collect data during iteration and process afterward.
- Use
set_global_logging_state!(false)for production benchmarks.
Guidelines for custom actions
When designing custom logging actions for your algorithms:
- It is good practice to avoid modifying the algorithm state, as this might leave the algorithm in an invalid state to continue running.
- The logging state and global state can be mutated as you see fit, but be mindful of properly initializing and resetting the state if so desired.
- If you need to influence the algorithm, use stopping criteria or modify the algorithm itself.
- For generic and reusable actions, document which properties they access from the
problem, algorithm, statetriplet, and be prepared to handle cases where these aren't present.
Guidelines for custom contexts
When designing custom logging contexts for your algorithms:
- Use descriptive symbol names (
:LineSearchFailed,:StepRejected,:Refined). - Document which contexts your algorithm emits and when.
- Keep context-specific data in
kwargs...if needed (though the default contexts don't use this). - Emit events at meaningful decision points, not in tight inner loops.
Summary
Implementing logging involves three main components:
LoggingAction: Define what happens when a logging event occurs.
- Use
CallbackActionfor quick inline functions. - Implement custom subtypes for reusable, stateful logging.
- Implement
handle_message!(action, problem, algorithm, state; kwargs...).
- Use
AlgorithmLogger: Map contexts (
:Start,:PostStep, etc.) to actions.- Construct with
with_algorithmlogger(:Context => action, ...). - Use
ActionGroupto compose multiple actions at one context.
- Construct with
Custom contexts: Emit domain-specific events from algorithms.
- Call
emit_message(problem, algorithm, state, :YourContext). - Document custom contexts in your algorithm's documentation.
- Use descriptive symbol names.
- Call
The logging system is designed for composability and zero-overhead when disabled, letting you instrument algorithms without compromising performance or code clarity.
Reference API
Auto‑generated documentation for logging infrastructure follows.
AlgorithmsInterface.ActionGroup — TypeActionGroup(actions::LoggingAction...)
ActionGroup(actions::Vector{<:LoggingAction})Concrete LoggingAction that can be used to sequentially perform each of the actions.
AlgorithmsInterface.AlgorithmLogger — TypeAlgorithmLogger(context => action, ...) -> loggerLogging transformer that handles the logic of dispatching logging events to logging actions. This is implemented through logger[context].
See also the scoped value AlgorithmsInterface.algorithm_logger.
AlgorithmsInterface.CallbackAction — TypeCallbackAction(callback)Concrete LoggingAction that handles a logging event through an arbitrary callback function. The callback function must have the following signature:
callback(problem, algorithm, state; kwargs...) = ...Here kwargs... are optional and can be filled out with context-specific information.
AlgorithmsInterface.IfAction — TypeIfAction(predicate, action)Concrete LoggingAction that wraps another action and hides it behind a clause, only emitting logging events whenever the predicate evaluates to true. The predicate must have the signature:
predicate(problem, algorithm, state; kwargs...)::BoolAlgorithmsInterface.LoggingAction — TypeLoggingActionAbstract supertype for defining an action that generates a log record.
Methods
Any concrete subtype should at least implement the following method to handle the logging event:
AlgorithmsInterface.algorithm_logger — Functionalgorithm_logger()::Union{AlgorithmLogger, Nothing}Retrieve the current logger that is responsible for handling logging events. The current logger is determined by a ScopedValue. Whenever nothing is returned, no logging should happen.
See also set_global_logging_state! for globally toggling whether logging should happen.
AlgorithmsInterface.emit_message — Methodemit_message(problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...) -> nothing
emit_message(algorithm_logger, problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...) -> nothingUse the current or the provided algorithm logger to handle the logging event of the given context. The first signature should be favored as it correctly handles accessing the logger and respecting global toggles for enabling and disabling the logging system.
The second signature should be used exclusively in (very) hot loops, where the overhead of AlgorithmsInterface.algorithm_logger() is too large. In this case, you can manually extract the algorithm_logger() once outside of the hot loop.
AlgorithmsInterface.get_global_logging_state — Functionget_global_logging_state()
set_global_logging_state!(state::Bool) -> previous_stateRetrieve or set the value to globally enable or disable the handling of logging events.
AlgorithmsInterface.handle_message! — Methodhandle_message!(action::LoggingAction, problem::Problem, algorithm::Algorithm, state::State; kwargs...)Entry-point for defining an implementation of how to handle a logging event for a given LoggingAction.
AlgorithmsInterface.set_global_logging_state! — Functionget_global_logging_state()
set_global_logging_state!(state::Bool) -> previous_stateRetrieve or set the value to globally enable or disable the handling of logging events.
AlgorithmsInterface.with_algorithmlogger — Methodwith_algorithmlogger(f, (context => action)::Pair{Symbol, LoggingAction}...)
with_algorithmlogger((context => action)::Pair{Symbol, LoggingAction}...) do
# insert arbitrary code here
endRun the given zero-argument function f() while mapping events of given contexts to their respective actions. By default, the following events trigger a logging action with the given context:
| context | event |
|---|---|
| :Start | The solver will start. |
| :PreStep | The solver is about to take a step. |
| :PostStep | The solver has taken a step. |
| :Stop | The solver has finished. |
However, further events and actions can be emitted through the emit_message interface.
See also the scoped value AlgorithmsInterface.algorithm_logger.
Wrap‑up
You have now seen the three pillars of the AlgorithmsInterface:
- Interface: Defining algorithms with
Problem,Algorithm, andState. - Stopping criteria: Controlling when iteration halts with composable conditions.
- Logging: Instrumenting execution with flexible, composable actions.
Together, these patterns encourage modular, testable, and maintainable iterative algorithm design. You can now build algorithms that are easy to configure, monitor, and extend without invasive modifications to core logic.