Pastey Logging tools

--****
-- == Logging tools 
-- 
-- <<LEVELTOC level=2 depth=4>> 
 
namespace logging 
 
include std/datetime.e 
include std/filesys.e 
include std/io.e 
include std/search.e 
include std/text.e 
ifdef EU4_1 then 
include euphoria/debug/debug.e 
end ifdef 
 
--**** 
-- === Log level constants 
-- Description: 
-- See [[:set_log_level]] for how to use these constants. 
 
public enum 
--** do not log any messages 
    LOG_SILENT = 0, 
--** log severe messages only 
    LOG_SEVERE, 
--** log errors and severe messages 
    LOG_ERRORS, 
--** log warnings, errors, and severe 
    LOG_WARNING, 
--** log all messages 
    LOG_VERBOSE, 
$ 
 
--**** 
-- === Log output macros 
-- Description: 
-- See [[:set_log_header]] for how to use these macros. 
 
public constant 
--** displays the current date, see [[:set_date_format]] 
    __DATE__    = "__DATE__", 
--** displays the current time, see [[:set_time_format]] 
    __TIME__    = "__TIME__", 
$ 
 
ifdef EU4_1 then 
public constant 
--** displays the current file name (Euphoria 4.1+ only) 
    __FILE__    = "__FILE__", 
--** displays the current file path (Euphoria 4.1+ only) 
    __PATH__    = "__PATH__", 
--** displays the current line number (Euphoria 4.1+ only) 
    __LINE__    = "__LINE__", 
--** displays the current routine name (Euphoria 4.1+ only) 
    __ROUTINE__ = "__ROUTINE__", 
$ 
end ifdef 
 
public constant 
--** displays the log level of the message 
    __LEVEL__   = "__LEVEL__", 
$ 
 
constant VALID_LOG_LEVELS = { 
    LOG_SILENT, 
    LOG_SEVERE, 
    LOG_ERRORS, 
    LOG_WARNING, 
    LOG_VERBOSE 
} 
 
constant LOG_LEVEL_NAMES = { 
    "LOG_SILENT", 
    "LOG_SEVERE", 
    "LOG_ERRORS", 
    "LOG_WARNING", 
    "LOG_VERBOSE" 
} 
 
integer log_level = LOG_WARNING 
sequence log_mode = "a" 
sequence log_output = {STDOUT} 
sequence log_header = "__DATE__ __TIME__ " 
sequence date_format = "%Y/%m/%d" 
sequence time_format = "%H:%M:%S" 
 
ifdef LOG_VERBOSE then 
log_level = LOG_VERBOSE 
 
elsifdef LOG_WARNING then 
log_level = LOG_WARNING 
 
elsifdef LOG_ERRORS then 
log_level = LOG_ERRORS 
 
elsifdef LOG_SEVERE then 
log_level = LOG_SEVERE 
 
elsifdef LOG_SILENT then 
log_level = LOG_SILENT 
 
end ifdef 
 
function expand_macros( sequence msg, datetime dt, sequence cs, integer level ) 
     
    if match( "__", msg ) then 
         
        sequence dt_date = datetime:format( dt, date_format ) 
        sequence dt_time = datetime:format( dt, time_format ) 
         
        msg = match_replace( __DATE__, msg, dt_date ) 
        msg = match_replace( __TIME__, msg, dt_time ) 
         
ifdef EU4_1 then 
         
        sequence cs_file_name    = filename( cs[CS_FILE_NAME] ) 
        sequence cs_path_name    = canonical_path( cs[CS_FILE_NAME] ) 
        sequence cs_line_no      = sprint( cs[CS_LINE_NO] ) 
        sequence cs_routine_name = cs[CS_ROUTINE_NAME] 
         
        msg = match_replace( __FILE__,    msg, cs_file_name    ) 
        msg = match_replace( __PATH__,    msg, cs_path_name    ) 
        msg = match_replace( __LINE__,    msg, cs_line_no      ) 
        msg = match_replace( __ROUTINE__, msg, cs_routine_name ) 
         
end ifdef 
         
        integer level_id = find( level, VALID_LOG_LEVELS ) 
        sequence level_name = LOG_LEVEL_NAMES[level_id] 
         
        msg = match_replace( __LEVEL__, msg, level_name ) 
         
    end if 
     
    return msg 
end function 
 
--**** 
-- === Logging routines 
 
--** 
-- Gets the current log header. 
-- 
-- Returns: 
--   The value previously set by [[:set_log_header]]. 
-- 
-- See Also: 
--     [[:set_log_header]], [[:write_log]] 
 
public function get_log_header() 
    return log_header 
end function 
 
--** 
-- Gets the current log level. 
-- 
-- Returns: 
--   The log level set by [[:set_log_level]]. 
-- 
-- See Also: 
--     [[:set_log_level]], [[:write_log]] 
 
public function get_log_level() 
    return log_level 
end function 
 
--** 
-- Gets the log output destination. 
-- 
-- Returns: 
--   The log output destination set by [[:set_log_output]]. 
-- 
-- Comments: 
--   This function always returns a sequence of log destinations, 
--   even if [[:set_log_output]] was called with a single file number. 
-- 
-- See Also: 
--   [[:set_log_output]], [[:write_log]] 
 
public function get_log_output() 
    return log_output 
end function 
 
--** 
-- Sets the log header, which is prepended to each line output by [[:write_log]]. 
-- 
-- Parameters: 
--   # ##header## : a sequence in ##printf()## format 
--   # ##data## : //(optional)// values passed to the format in ##header## 
-- 
-- Comments: 
--   The log header can contain one or more macros that are expanded with each call to [[:write_log]]. 
-- 
--   * ##~__DATE__## : current date 
--   * ##~__TIME__## : current time 
--   * ##~__FILE__## : current file name* 
--   * ##~__PATH__## : current file path* 
--   * ##~__LINE__## : current line number* 
--   * ##~__ROUTINE__## : current routine name* 
--   * ##~__LEVEL__## : the log level of the message\\ 
--\\ 
-- 
--   ~*these macros are only available with Euphoria 4.1 and up 
-- 
--   The default log header is ##"~__DATE~__ ~__TIME~__ "## 
-- 
-- See Also: 
--     [[:get_log_header]], [[:set_date_format]], [[:set_time_format]], [[:write_log]] 
 
public procedure set_log_header( sequence header, object data = {} ) 
    log_header = sprintf( header, data ) 
end procedure 
 
--** 
-- Sets the log level, which determines the output of future [[:write_log]] calls. 
-- 
-- Parameters: 
--   # ##level## : one of the available [[:Log level constants]] 
-- 
-- Comments: 
--   The default log level is ##LOG_WARNING##. 
-- 
--   Log levels can be set via the command line using ##-D##, e.g. ##eui -D LOG_WARNING myapp.ex## 
-- 
-- See Also: 
--     [[:get_log_level]], [[:write_log]] 
 
public procedure set_log_level( integer level ) 
    if find( level, VALID_LOG_LEVELS ) then 
        log_level = level 
    end if 
end procedure 
 
--** 
-- Sets the log output destination. 
-- 
-- Parameters: 
--   # ##output## : an object, either: 
--   ** an **atom**, which is an open file number, or ##STDOUT##/##STDERR## 
--   ** a **sequence**, which is a //list// of file numbers or file names (not a single file name) 
--   # ##mode## : //(optional)// the mode used for ##open()## with file names 
-- 
-- Comment: 
--   The default ##mode## for ##open()## is "a". 
-- 
-- See Also: 
--   [[:get_log_output]], [[:write_log]] 
 
public procedure set_log_output( object output, sequence mode = "a" ) 
     
    if atom( output ) then 
        output = {output} 
    end if 
     
    log_mode = mode 
    log_output = output 
     
end procedure 
 
--** 
-- Sets the format used by the ~__DATE~__ macro. 
-- 
-- Parameters: 
--   # ##format## : the format string to use 
-- 
-- Comments: 
--   This is the same format used by ##datetime:format()##. 
-- 
-- See Also: 
--   [[:set_time_format]] 
 
public procedure set_date_format( sequence format ) 
    date_format = format 
end procedure 
 
--** 
-- Sets the format used by the ~__TIME~__ macro. 
-- 
-- Parameters: 
--   # ##format## : the format string to use 
-- 
-- Comments: 
--   This is the same format used by ##datetime:format()##. 
-- 
-- See Also: 
--   [[:set_date_format]] 
 
public procedure set_time_format( sequence format ) 
    time_format = format 
end procedure 
 
--** 
-- Writes a message to the current log outputs. 
-- 
-- Parameters: 
--   # ##level## : the logging level of this message, see [[:Log level constants]] 
--   # ##message## : //(optional)// the message to write out, using ##printf()## format 
--   # ##data## : //(optional)// the data passed to ##printf()## when writing ##message## 
-- 
-- Comments: 
--   The ##message## parameter may contain [[:Log header macros]], 
--   which will be expanded automatically during output. 
-- 
-- See Also: 
--   [[:Log header macros]], [[:Log level constants]] 
 
public procedure write_log( integer level, sequence message = "", object data = {} ) 
     
    if log_level >= level then 
         
        -- get the required macro values 
        datetime dt = datetime:now() 
ifdef EU4_1 then 
        sequence cs = call_stack() 
        cs = cs[$-1] -- the stack item before this one 
elsedef 
        cs = {} -- call_stack() not supported 
end ifdef 
         
        -- expand macros in the header and message 
        -- and combine them into a new message 
        message = expand_macros( log_header, dt, cs, level ) 
            & expand_macros( message, dt, cs, level ) 
         
        -- verify that the message ends with a new line 
        if not search:ends( EOL, message ) then 
            message &= EOL 
        end if 
         
        -- send the message to the provided outputs 
        for i = 1 to length( log_output ) do 
             
            object fn = log_output[i] 
            integer close_fn = 0 
             
            if sequence( fn ) then 
                -- open the file temporarily 
                fn = open( fn, log_mode ) 
                close_fn = 1 
            end if 
             
            printf( fn, message, data ) 
             
            if close_fn then 
                close( fn ) 
            end if 
             
        end for 
         
    end if 
     
end procedure

1. Comment by ghaberek May 18, 2015

There is a bug on line 359. The variable cs is not declared as a sequence in non-4.1 versions.

/* 355 */ ifdef EU4_1 then 
/* 356 */        sequence cs = call_stack() 
/* 357 */         cs = cs[$-1] -- the stack item before this one 
/* 358 */ elsedef 
/* 359 */         sequence cs = {} -- call_stack() not supported    (!! add 'sequence' to this line !!) 
/* 360 */ end ifdef 

-Greg