Developer Tutorial: Adding SciMLLogging to Your Package
This tutorial is for Julia package developers who want to integrate SciMLLogging.jl into their packages to provide users with fine-grained verbosity control.
Overview
SciMLLogging.jl provides three main components for package developers:
AbstractVerbositySpecifier{T}
- Base type for creating custom verbosity types@SciMLMessage
- Macro for emitting conditional log messages- Verbosity levels - Predefined log levels (
Silent
,InfoLevel
,WarnLevel
,ErrorLevel
,CustomLevel(n)
)
Step 1: Design Your Verbosity Interface
First, decide what aspects of your package should be controllable by users. For example, a solver might have:
- Initialization messages
- Iteration progress
- Convergence information
- Warning messages
Step 2: Create Your Verbosity Type
Define a struct that inherits from AbstractVerbositySpecifier{T}
:
using SciMLLogging
struct MySolverVerbosity{T} <: AbstractVerbositySpecifier{T}
initialization::MessageLevel
iterations::MessageLevel
convergence::MessageLevel
warnings::MessageLevel
function MySolverVerbosity{T}(;
initialization = InfoLevel(),
iterations = Silent(),
convergence = InfoLevel(),
warnings = WarnLevel()
) where T
new{T}(initialization, iterations, convergence, warnings)
end
end
Key Design Principles:
- The type parameter
T
controls whether any logging is enabled or not:T=true
enables messages,T=false
disables them - Each field represents a category of messages your package can emit
- Provide sensible defaults that work for most users
- Use keyword arguments for flexibility
Step 3: Add Convenience Constructors
Make it easy for users to create verbosity instances:
# Default enabled verbosity
MySolverVerbosity() = MySolverVerbosity{true}()
# Boolean constructor
MySolverVerbosity(enabled::Bool) = enabled ? MySolverVerbosity{true}() : MySolverVerbosity{false}()
# Preset-based constructor (optional)
function MySolverVerbosity(preset::VerbosityPreset)
if preset isa None
MySolverVerbosity{false}()
elseif preset isa All
MySolverVerbosity{true}(
initialization = InfoLevel(),
iterations = InfoLevel(),
convergence = InfoLevel(),
warnings = WarnLevel()
)
elseif preset isa Minimal
MySolverVerbosity{true}(
initialization = Silent(),
iterations = Silent(),
convergence = ErrorLevel(),
warnings = ErrorLevel()
)
else
MySolverVerbosity{true}() # Default
end
end
Step 4: Integrate Messages Into Your Code
Use @SciMLMessage
throughout your package code:
function my_solve(problem, verbose::MySolverVerbosity)
@SciMLMessage("Initializing solver for $(typeof(problem))", verbose, :initialization)
# Setup code here...
for iteration in 1:maxiters
# Solver iteration...
@SciMLMessage(verbose, :iterations) do
"Iteration $iteration: residual = $(compute_residual())"
end
if converged
@SciMLMessage("Converged after $iteration iterations", verbose, :convergence)
return solution
end
if should_warn_about_something()
@SciMLMessage("Convergence is slow, consider adjusting parameters", verbose, :warnings)
end
end
@SciMLMessage("Failed to converge after $maxiters iterations", verbose, :convergence)
return nothing
end
Message Types:
- String messages:
@SciMLMessage("Fixed message", verbose, :category)
- Function messages:
@SciMLMessage(verbose, :category) do; "Dynamic message"; end
Use function messages when:
- Message generation is expensive
- Message includes computed values
- You want lazy evaluation
Step 5: Export Your Verbosity Type
In your main module file:
module MySolver
using SciMLLogging
import SciMLLogging: AbstractVerbositySpecifier
# Your verbosity type definition...
include("verbosity.jl")
# Your solver code...
include("solver.jl")
# Export the verbosity type
export MySolverVerbosity
end
Step 6: Document for Users
Provide clear documentation for your users:
"""
MySolverVerbosity{T}(; kwargs...)
Controls verbosity output from MySolver functions.
# Keyword Arguments
- `initialization = InfoLevel()`: Messages about solver setup
- `iterations = Silent()`: Per-iteration progress messages
- `convergence = InfoLevel()`: Convergence/failure notifications
- `warnings = WarnLevel()`: Warning messages during solving
# Constructors
- `MySolverVerbosity()`: Default enabled verbosity
- `MySolverVerbosity(false)`: Disabled (zero overhead)
- `MySolverVerbosity(All())`: Enable all message categories
- `MySolverVerbosity(Minimal())`: Only errors and convergence
# Example
julia
Default verbosity
verbose = MySolverVerbosity()
Custom verbosity - show everything except iterations
verbose = MySolverVerbosity(iterations = Silent())
Silent mode (no runtime overhead)
verbose = MySolverVerbosity(false)
"""
Step 7: Add Tests
Test your verbosity implementation:
using Test
using MySolver
using Logging
@testset "Verbosity Tests" begin
# Test message emission
verbose = MySolverVerbosity()
@test_logs (:info, r"Initializing solver") begin
my_solve(test_problem, verbose)
end
# Test silent mode produces no output
silent = MySolverVerbosity(false)
@test_logs min_level=Logging.Debug begin
my_solve(test_problem, silent)
end
end
Best Practices
Performance
- Always use the type parameter
T
to control whether logging is enabled or not - Use function-based messages for expensive computations
- Consider message frequency - don't spam users with too many messages
User Experience
- Provide sensible defaults that work for most users
- Use descriptive category names (
:initialization
not:init
) - Group related messages into logical categories
- Document what each category controls
Message Content
- Include relevant context (iteration numbers, values, etc.)
- Use consistent formatting across your package
- Make messages actionable when possible
- Avoid overly technical jargon in user-facing messages
Integration
- Accept verbosity parameters in your main API functions
- Consider making verbosity optional with sensible defaults
- Thread verbosity through your call stack as needed
Advanced: Custom Log Levels
For specialized needs, you can create custom log levels:
struct MySolverVerbosity{T} <: AbstractVerbositySpecifier{T}
debug::MessageLevel
# ... other fields
function MySolverVerbosity{T}(;
debug = CustomLevel(-1000), # Custom level below Info
# ... other defaults
) where T
new{T}(debug, ...)
end
end
Complete Example
Here's a complete minimal example:
module ExampleSolver
using SciMLLogging
import SciMLLogging: AbstractVerbositySpecifier
struct ExampleVerbosity{T} <: AbstractVerbositySpecifier{T}
progress::MessageLevel
ExampleVerbosity{T}(progress = InfoLevel()) where T = new{T}(progress)
end
ExampleVerbosity() = ExampleVerbosity{true}()
ExampleVerbosity(enabled::Bool) = enabled ? ExampleVerbosity{true}() : ExampleVerbosity{false}()
function solve_example(n::Int, verbose::ExampleVerbosity)
result = 0
for i in 1:n
result += i
@SciMLMessage("Step $i: sum = $result", verbose, :progress)
end
return result
end
export ExampleVerbosity, solve_example
end
This example shows the minimal structure needed to integrate SciMLLogging into a package.