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?with_algorithmlogger controls how events are mapped to actions.
  • What happens when we log? → a LoggingAction determines 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)
    return solve(prob, alg)  # allocates & runs
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 = 3.217529539440
Iter   2: x = 1.919562227130
Iter   3: x = 1.480733227472
Iter   4: x = 1.415707709247
Iter   5: x = 1.414214350839
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

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.825509492983
Iter   1: x = 1.624127854252
Iter   2: x = 1.427778999915
Iter   3: x = 1.414278005503
Iter   4: x = 1.414213563841
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.050s
  elapsed = 0.053s
  elapsed = 0.053s
  elapsed = 0.053s
  elapsed = 0.053s
  elapsed = 0.053s
  elapsed = 0.053s
  elapsed = 0.053s
  elapsed = 0.053s
  elapsed = 0.053s
Done after 10 iterations (total 0.117s)

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.465590916738
Iter   4: x = 1.414213848910
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: [1.8047801337870326, 1.4564741801210181, 1.4148266730745451]

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.120840612235
Iter   2: x = 1.880846795083
Iter   3: x = 1.472098812368
Iter   4: x = 1.415351632093
Iter   5: x = 1.414214019928
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
Captured 10 iterates in 0.039s

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 58066 samples
Mean iterate: 1.4144981168113608
Variance: 0.002099876461622685

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 finalize_state!(problem, algorithm, 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.663991468545
Iter   2: x = 1.432960353925
Iter   3: x = 1.414336190397
Iter   4: x = 1.414213567689
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"#59#60"}(Main.var"#59#60"())
│   exception =
│    Intentional logging error at iteration 3
│    Stacktrace:
│      [1] error(s::String)
│        @ Base ./error.jl:44
│      [2] (::Main.var"#59#60")(problem::Main.SqrtProblem, algorithm::Main.HeronAlgorithm, state::Main.HeronState)
│        @ Main ./logging.md:380
│      [3] #handle_message!#17
│        @ ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/logging.jl:60 [inlined]
│      [4] handle_message!(action::CallbackAction{Main.var"#59#60"}, 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_loop!(problem::Main.SqrtProblem, algorithm::Main.HeronAlgorithm, state::Main.HeronState)
│        @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/interface/interface.jl:102
│      [8] solve(problem::Main.SqrtProblem, algorithm::Main.HeronAlgorithm; kwargs::@Kwargs{})
│        @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/interface/interface.jl:51
│      [9] solve
│        @ ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/interface/interface.jl:41 [inlined]
│     [10] #heron_sqrt#3
│        @ ./logging.md:78 [inlined]
│     [11] heron_sqrt
│        @ ./logging.md:75 [inlined]
│     [12] #62
│        @ ./logging.md:386 [inlined]
│     [13] with(::Main.var"#62#63", ::Pair{Base.ScopedValues.ScopedValue{AlgorithmsInterface.AlgorithmLogger}, AlgorithmsInterface.AlgorithmLogger})
│        @ Base.ScopedValues ./scopedvalues.jl:269
│     [14] with_algorithmlogger(f::Function, args::Pair{Symbol, CallbackAction{Main.var"#59#60"}})
│        @ AlgorithmsInterface ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/src/logging.jl:126
│     [15] top-level scope
│        @ logging.md:385
│     [16] eval(m::Module, e::Any)
│        @ Core ./boot.jl:489
│     [17] #61
│        @ ~/.julia/packages/Documenter/AXNMp/src/expander_pipeline.jl:879 [inlined]
│     [18] cd(f::Documenter.var"#61#62"{Module, Expr}, dir::String)
│        @ Base.Filesystem ./file.jl:112
│     [19] (::Documenter.var"#59#60"{Documenter.Page, Module, Expr})()
│        @ Documenter ~/.julia/packages/Documenter/AXNMp/src/expander_pipeline.jl:878
│     [20] (::IOCapture.var"#12#13"{Type{InterruptException}, Documenter.var"#59#60"{Documenter.Page, Module, Expr}, IOContext{Base.PipeEndpoint}, IOContext{Base.PipeEndpoint}, Base.PipeEndpoint, Base.PipeEndpoint})()
│        @ IOCapture ~/.julia/packages/IOCapture/MR051/src/IOCapture.jl:170
│     [21] with_logstate(f::IOCapture.var"#12#13"{Type{InterruptException}, Documenter.var"#59#60"{Documenter.Page, Module, Expr}, IOContext{Base.PipeEndpoint}, IOContext{Base.PipeEndpoint}, Base.PipeEndpoint, Base.PipeEndpoint}, logstate::Base.CoreLogging.LogState)
│        @ Base.CoreLogging ./logging/logging.jl:542
│     [22] with_logger(f::Function, logger::Base.CoreLogging.ConsoleLogger)
│        @ Base.CoreLogging ./logging/logging.jl:653
│     [23] capture(f::Documenter.var"#59#60"{Documenter.Page, Module, Expr}; rethrow::Type, color::Bool, passthrough::Bool, capture_buffer::IOBuffer, io_context::Vector{Any})
│        @ IOCapture ~/.julia/packages/IOCapture/MR051/src/IOCapture.jl:167
│     [24] runner(::Type{Documenter.Expanders.ExampleBlocks}, node::MarkdownAST.Node{Nothing}, page::Documenter.Page, doc::Documenter.Document)
│        @ Documenter ~/.julia/packages/Documenter/AXNMp/src/expander_pipeline.jl:877
│     [25] dispatch(::Type{Documenter.Expanders.ExpanderPipeline}, ::MarkdownAST.Node{Nothing}, ::Vararg{Any})
│        @ Documenter.Selectors ~/.julia/packages/Documenter/AXNMp/src/utilities/Selectors.jl:170
│     [26] expand(doc::Documenter.Document)
│        @ Documenter ~/.julia/packages/Documenter/AXNMp/src/expander_pipeline.jl:60
│     [27] runner(::Type{Documenter.Builder.ExpandTemplates}, doc::Documenter.Document)
│        @ Documenter ~/.julia/packages/Documenter/AXNMp/src/builder_pipeline.jl:224
│     [28] dispatch(::Type{Documenter.Builder.DocumentPipeline}, x::Documenter.Document)
│        @ Documenter.Selectors ~/.julia/packages/Documenter/AXNMp/src/utilities/Selectors.jl:170
│     [29] #95
│        @ ~/.julia/packages/Documenter/AXNMp/src/makedocs.jl:283 [inlined]
│     [30] withenv(::Documenter.var"#95#96"{Documenter.Document}, ::Pair{String, Nothing}, ::Vararg{Pair{String, Nothing}})
│        @ Base ./env.jl:265
│     [31] #93
│        @ ~/.julia/packages/Documenter/AXNMp/src/makedocs.jl:282 [inlined]
│     [32] cd(f::Documenter.var"#93#94"{Documenter.Document}, dir::String)
│        @ Base.Filesystem ./file.jl:112
│     [33] makedocs(; debug::Bool, format::Documenter.HTMLWriter.HTML, kwargs::@Kwargs{modules::Vector{Module}, authors::String, sitename::String, pages::Vector{Pair{String, Any}}, expandfirst::Vector{String}, plugins::Vector{Documenter.Plugin}})
│        @ Documenter ~/.julia/packages/Documenter/AXNMp/src/makedocs.jl:281
│     [34] top-level scope
│        @ ~/work/AlgorithmsInterface.jl/AlgorithmsInterface.jl/docs/make.jl:79
│     [35] include(mod::Module, _path::String)
│        @ Base ./Base.jl:306
│     [36] exec_options(opts::Base.JLOptions)
│        @ Base ./client.jl:317
│     [37] _start()
│        @ Base ./client.jl:550
└ @ 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 and 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 = 1.639506965653
Iter   2: x = 1.429692946915
Iter   3: x = 1.414297360557
Iter   4: x = 1.414213564856
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
⚠️  Numerical issue detected at iteration 10
Iter  10: x = 1.941649147533
Iter  11: x = 1.485850679935
Iter  12: x = 1.415940477696
Iter  13: x = 1.414214615467
Iter  14: x = 1.414213562373
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 = 1.663876083662
Iter  21: x = 1.432944336602
Iter  22: x = 1.414335982307
Iter  23: x = 1.414213567671
Iter  24: x = 1.414213562373
Iter  25: x = 1.414213562373
Iter  26: x = 1.414213562373
Iter  27: x = 1.414213562373
Iter  28: x = 1.414213562373
Iter  29: x = 1.414213562373
⚠️  Numerical issue detected at iteration 30
Iter  30: x = 2.918942717993

Best practices

Performance considerations

  • Logging actions can be as fast or slow as needed; the overhead is only incurred when they are actually used.
  • 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.