五 日志记录和动态函数

五 日志记录和动态函数

【动机】

linux shell 脚本常见的调试手法如下:

  1. 将命令直接粘贴到控制台
  2. echo / printf 数值
  3. 断点调试, 例如 vscode 配合使用 Bash Debug 等调试插件

而日志记录常见用于生产环境下, 记录程序执行正常或者出错的情况, 同时, 当上述三种调试手段不适用, 或者实现难度很大时, 日志记录就可以在调试时发挥一定作用.

【意图】

将不同性质的日志内容分类存放同一目录下的不同的文件中, 如果是出错分成几个级别, 例如 warn error critical 等, 正常日志分 info success 等, 要求运行时不一定创建日志(依赖于目录是否存在), 调试时总是创建日志

【实现】

在 log.sh 中, 写入 log 函数如下

:<<COMMENT
    记录日志, 用指定的日志级别名做文件名保存在 log/<level>.log
    $1: content, default to  "content forgotten."
    $2: level, default to info
    用法: log <content> <log_level>
    注意: 
        1. 以下两个条件均满足, 则日志记录被忽略
            1) 当前非调试态;
            2) 日志目录不存在
        2. ROOT_DIR_PATH 是入口脚本所在目录(绝对路径), 一般在入口脚本中计算, 和当前目录 . 不一定相同. 
            日志目录之父母录, 优先选择前者.
COMMENT
log(){   
    local dir=./log
    [ "$ROOT_DIR_PATH" ] && dir=$ROOT_DIR_PATH/log
    [ "$__DEBUG__" -a ! -d "$dir" ] && mkdir -p $dir

    if [ -d "$dir" ]; then
        local content=${1:-"content forgotten."}
        local time=$(date +'%Y-%m-%d %H:%M:%S')
        local level=$(available_range "$2" info $(echo ${__AVAI_OP__[@]}))
        echo -e "[$time] [process id:$$] [${FUNCNAME[2]}] $content" >> $dir/${level}.log
    fi
}

其中, ROOT_DIR_PATH 一般在入口脚本定义, 如果没有则以当前目录为准; __DEBUG__ 可以在任何文件中定义, 但一般为了同一管理, 也在入口文件中, 不存在或值为空, 则表示当前为非调试态.函数根据这两个全局只读变量, 决定当前是否记录日志, 以及日志文件存放于何处. 另外, __AVAI_OP__ 的定义, 位于 console.sh 中.

log 函数使用并不是很方便, 以下是指定日志 level 的函数, 这是采用纯静态的方法定义:


:<<COMMENT
    记录一般信息日志到 /log/info.log 文件
    $1: content
    用法: log_info <content>
COMMENT
log_info(){
    log "$*" info
}

log_error(){
    log "$*" error
}

log_warn(){
    log "$*" warn
}

log_success(){
    log "$*" success
}

log_critical(){
    log "$*" critical
}

当然, 还可以定义 log_tip / log_menu / log_caption 等等, 这依赖于 __AVAI_OP__ 的定义.

【测试】

新建 test.sh 文件, 输入以下代码:

    # 根据实际文件位置修改路径
    # source ./data.sh    
    # source ./log.sh
    way1(){
        local parts="info error warn success critical"  fun
        for part in $parts; do
            fun=log_$part
            $fun "this log content is by $fun function"
        done
    }
    way1
    unset -f way1

当 __DEBUG__ 为空, 日志记录操作被忽略; 否则, 将在当前目录(或者入口脚本根目录)下, 产生以下的 log 目录结构:

每个文件新添内容大致如下:

[2025-03-16 15:30:02] [process id:17807] [way1] this log content is by log_info function

【必须三思的扩展】

那么是否可以取消 log 函数中的 level 级别的可用性检测, 继而实现动态生成 log_xxx 形式的函数呢? 答案是可以的.

新建 logx 函数, 与 log 函数基本相同, 但是去除了 level 级别的可用性检测, 仍然放在 log.sh 文件中

# $1: content, default to  "content forgotten."
# $2: level, default to info
logx(){
    local dir=./log
    [ "$ROOT_DIR_PATH" ] && dir=$ROOT_DIR_PATH/log
    [ "$__DEBUG__" -a ! -d "$dir" ] && mkdir -p $dir

    if [ -d "$dir" ]; then
        local content=${1:-"content forgotten."}
        local time=$(date +'%Y-%m-%d %H:%M:%S')
        echo -e "[$time] [process id:$$] [${FUNCNAME[2]}] $content" >> $dir/$2.log
    fi    
}

要动态生成日志记录函数, 需要重写系统内置函数 command_not_found_handle. 考虑到该函数的系统级别, 故将它放在 system.sh , 而不是前面的 log.sh, 这样也方便将来扩展(如果需要一些其他类型的动态函数).

:<<COMMENT
    当调用 logx_{XXX} 形式的函数时, 自动剥离出 level, 然后调用 logx 函数, 
    而其他函数, 则执行系统默认处理
COMMENT
command_not_found_handle() {
    if [[ "$1" =~ ^logx_ ]]; then   
        local level=${1#*logx_} 
        shift
        logx "$@" $level
    else
         bash -c "command $1"  # 或者其他方式调用默认处理器
         return 127
    fi
}

【测试】

新建 test.sh 文件, 写入以下代码:

    # 根据实际文件位置修改路径
    # source ./system.sh
    # source ./log.sh
    way2(){
        logx_A "custom leve A log"
        logx_B "B log content"
    }
    way2
    unset -f way2

运行结果显示, log 目录下生成文件 A.log, B.log, 并写入了相应内容. 图略

【看上去很优雅】

但是, 必须注意, 这样扩展的日志记录函数在代码简洁的同时, 也为调用端提供了生成无穷 level 日志的可能, 其结果就是日志文件过多, 反过来会不容易管理. 所以, 实践中, 这样的动态函数还是慎用!

谢谢观看.