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_algorithmlogger to control how to map events to actions.
  • What happens when we log? → a LoggingAction to 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
end

It 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)
end

To 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)
end
Iter   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.414213562373

Default logging contexts

The default solve! loop emits logging events at several key points during iteration:

contextevent
:StartThe solver will start.
:PreStepThe solver is about to take a step.
:PostStepThe solver has taken a step.
:StopThe 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)
end
Start: 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.414213562373

Furthermore, 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)
end
Iter   2: x = 1.423330968192
Iter   4: x = 1.414213562675
Iter   6: x = 1.414213562373
Iter   8: x = 1.414213562373
Iter  10: x = 1.414213562373

This 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)
end
Iter   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.055s

Implementing 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:

  1. A concrete subtype of LoggingAction.
  2. 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
end

The 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.0003377447193804173

This 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
end

The 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)
end
Logging 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")
end
Iter 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 error

This 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
end

Now 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))
end
Iter   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.030776946916

Best 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, state triplet, 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:

  1. LoggingAction: Define what happens when a logging event occurs.

    • Use CallbackAction for quick inline functions.
    • Implement custom subtypes for reusable, stateful logging.
    • Implement handle_message!(action, problem, algorithm, state; kwargs...).
  2. AlgorithmLogger: Map contexts (:Start, :PostStep, etc.) to actions.

    • Construct with with_algorithmlogger(:Context => action, ...).
    • Use ActionGroup to compose multiple actions at one context.
  3. 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.

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.CallbackActionType
CallbackAction(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.

source
AlgorithmsInterface.IfActionType
IfAction(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...)::Bool
source
AlgorithmsInterface.algorithm_loggerFunction
algorithm_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.

source
AlgorithmsInterface.emit_messageMethod
emit_message(problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...) -> nothing
emit_message(algorithm_logger, problem::Problem, algorithm::Algorithm, state::State, context::Symbol; kwargs...) -> nothing

Use 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.

source
AlgorithmsInterface.with_algorithmloggerMethod
with_algorithmlogger(f, (context => action)::Pair{Symbol, LoggingAction}...)
with_algorithmlogger((context => action)::Pair{Symbol, LoggingAction}...) do
    # insert arbitrary code here
end

Run 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:

contextevent
:StartThe solver will start.
:PreStepThe solver is about to take a step.
:PostStepThe solver has taken a step.
:StopThe solver has finished.

However, further events and actions can be emitted through the emit_message interface.

See also the scoped value AlgorithmsInterface.algorithm_logger.

source

Wrap‑up

You have now seen the three pillars of the AlgorithmsInterface:

  • Interface: Defining algorithms with Problem, Algorithm, and State.
  • 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.