!> @file logger.f90 !> @brief Singleton logger with configurable verbosity and dual output targets. !! !! Provides five log levels (SILENT, ERROR, WARN, INFO, DEBUG) and routes !! messages to stdout, an optional log file, or both. The module is safe to !! use before `log_init` is called: it defaults to INFO level with no file. !! !! Level constants (use these in namelist `verbosity` field): !! LOGLVL_SILENT = 0 — suppress all output !! LOGLVL_ERROR = 1 — errors only !! LOGLVL_WARN = 2 — warnings and errors !! LOGLVL_INFO = 3 — normal operational messages (default) !! LOGLVL_DEBUG = 4 — developer diagnostics !! !! Typical usage in the driver: !! @code !! call log_init(cfg % verbosity, cfg % log_file) !! call log_info('config: loaded "input.nml"') !! call log_warn('reconstruction: char_proj ignored on FDS path') !! call log_finalize() !! @endcode module logger use iso_fortran_env, only: output_unit implicit none private !> Log level constants. Higher value = more verbose. !! Note: names use LOGLVL_ prefix to avoid case-insensitive clash with !! the log_error / log_warn / log_info / log_debug subroutines. integer, parameter, public :: LOGLVL_SILENT = 0 !< Suppress all output integer, parameter, public :: LOGLVL_ERROR = 1 !< Fatal / unrecoverable errors integer, parameter, public :: LOGLVL_WARN = 2 !< Non-fatal warnings integer, parameter, public :: LOGLVL_INFO = 3 !< Normal operational messages integer, parameter, public :: LOGLVL_DEBUG = 4 !< Developer diagnostics ! Singleton state — safe to call log_* before log_init (defaults to INFO, no file). integer :: log_verbosity = LOGLVL_INFO integer :: log_file_unit = -1 logical :: log_file_open = .false. public :: log_init, log_finalize public :: log_error, log_warn, log_info, log_debug contains !> Initialise the logger: set verbosity level and optionally open a log file. !! !! @param[in] verbosity Threshold level (LOGLVL_SILENT … LOGLVL_DEBUG). !! @param[in] log_file Path for the log file. Empty string disables file logging. subroutine log_init(verbosity, log_file) use mpi_runtime, only: my_rank, mpi_runtime_is_initialised integer, intent(in) :: verbosity character(len=*), intent(in) :: log_file integer :: stat logical :: this_rank_logs character(len=8) :: rank_env integer :: env_status ! Close any already-open log file before re-initialising so that a second ! call to log_init (e.g. in tests that reconfigure the logger) does not leak ! the previous unit or leave two files open simultaneously. if (log_file_open) call log_finalize() log_verbosity = verbosity ! Gate file-open to the same ranks that emit() writes: rank 0 always logs; ! a non-root rank logs only when LOG_ALL_RANKS is set to a non-empty value. ! This prevents non-root ranks from truncating the log file after rank 0 has ! already written to it (status='replace' on every rank was the bug). this_rank_logs = .true. if (mpi_runtime_is_initialised()) then if (my_rank() /= 0) then call get_environment_variable('LOG_ALL_RANKS', rank_env, status=env_status) this_rank_logs = (env_status == 0 .and. trim(rank_env) /= '') end if end if if (this_rank_logs .and. len_trim(log_file) > 0) then open (newunit=log_file_unit, file=trim(log_file), status='replace', & action='write', iostat=stat) if (stat /= 0) then write (output_unit, '(A,A,A)') '[WARN] logger: could not open log file "', & trim(log_file), '" — file logging disabled' log_file_open = .false. else log_file_open = .true. end if end if end subroutine log_init !> Close the log file and reset file-logging state. subroutine log_finalize() integer :: stat if (log_file_open) then close (log_file_unit, iostat=stat) log_file_open = .false. log_file_unit = -1 end if end subroutine log_finalize !> Emit an ERROR-level message. subroutine log_error(msg) character(len=*), intent(in) :: msg call emit('[ERROR] ', msg, LOGLVL_ERROR) end subroutine log_error !> Emit a WARN-level message. subroutine log_warn(msg) character(len=*), intent(in) :: msg call emit('[WARN] ', msg, LOGLVL_WARN) end subroutine log_warn !> Emit an INFO-level message. subroutine log_info(msg) character(len=*), intent(in) :: msg call emit('[INFO] ', msg, LOGLVL_INFO) end subroutine log_info !> Emit a DEBUG-level message. subroutine log_debug(msg) character(len=*), intent(in) :: msg call emit('[DEBUG] ', msg, LOGLVL_DEBUG) end subroutine log_debug ! --------------------------------------------------------------------------- ! Private helper — performs the actual write to stdout and/or the log file. ! --------------------------------------------------------------------------- subroutine emit(tag, msg, threshold) use mpi_runtime, only: my_rank, mpi_runtime_is_initialised character(len=*), intent(in) :: tag, msg integer, intent(in) :: threshold character(len=8) :: rank_env integer :: env_status integer :: stat if (mpi_runtime_is_initialised()) then if (my_rank() /= 0) then call get_environment_variable('LOG_ALL_RANKS', rank_env, status=env_status) if (env_status /= 0) return if (trim(rank_env) == '') return end if end if if (log_verbosity < threshold) return write (output_unit, '(A,A)', iostat=stat) tag, trim(msg) if (log_file_open) & write (log_file_unit, '(A,A)', iostat=stat) tag, trim(msg) end subroutine emit end module logger