十年山茶花
一,为什么设计一个模板
最近有朋友留言说想在shell脚本中,有一个类似pyhton--logging模块类似的功能,所以设计如下一个log模板
大概步骤
- 初始化全局配置
- 设置默认日志目录
/var/log/<script_name> - 配置日志轮转参数(最大文件大小 1024KB,保留 5 个归档)
- 定义 ANSI 颜色码和日志级别常量(DEBUG 到 EMERG)
- 主函数
main() 启动 - 日志系统初始化
init_logger() - 用户自定义配置(可选)
- 通过
set_log_level() 设置最低记录级别(如 "DEBUG") - 通过
set_log_context() 添加上下文前缀(如 [user:123]) - 通过
set_log_filter() 设置正则过滤条件 - 通过
enable_performance_logging() 启用性能计时
- 记录日志(通过快捷函数)
- 若启用性能监控,附加执行耗时(需配合
start_timer)
- 调用
log_info(), log_debug(), log_error() 等
- 日志轮转机制
- 将
.4.gz → .5.gz … .1.gz → .2.gz
- 每次写入日志后自动调用
rotate_logs() - 检查当前日志文件大小是否超过
MAX_LOG_SIZE_KB
- 性能监控(可选)
- 使用
log_perf "command" 包装外部命令 - 成功记录为
INFO,失败记录为 ERROR,均含耗时(毫秒)
- 脚本正常结束
- 若调用
log_emerg(),会立即 exit 1
二,脚本代码(精简注释版)
#!/bin/bash# ==============================================# 日志记录系统 for Shell 脚本# 功能:# - 多级别彩色日志输出# - 日志文件记录与轮转# - 日志过滤功能# - 性能监控# - 上下文信息记录# ==============================================# 基础配置readonly SCRIPT_NAME=$(basename "$0")readonly LOG_DIR="/var/log/${SCRIPT_NAME%.sh}" # 默认日志目录readonly MAX_LOG_SIZE_KB=20 # 单个日志文件最大大小(KB)readonly MAX_LOG_FILES=5 # 保留的旧日志文件数量# 颜色定义readonly RESET='\033[0m'readonly BOLD='\033[1m'readonly DIM='\033[2m'readonly UNDERLINE='\033[4m'readonly BLINK='\033[5m'readonly INVERT='\033[7m'readonly HIDDEN='\033[8m'readonly BLACK='\033[0;30m'readonly RED='\033[0;31m'readonly GREEN='\033[0;32m'readonly YELLOW='\033[0;33m'readonly BLUE='\033[0;34m'readonly PURPLE='\033[0;35m'readonly CYAN='\033[0;36m'readonly WHITE='\033[0;37m'# 日志级别配置readonly LOG_LEVELS=("DEBUG" "INFO" "NOTICE" "WARN" "ERROR" "CRIT" "ALERT" "EMERG")readonly LOG_LEVEL_DEBUG=0readonly LOG_LEVEL_INFO=1readonly LOG_LEVEL_NOTICE=2readonly LOG_LEVEL_WARN=3readonly LOG_LEVEL_ERROR=4readonly LOG_LEVEL_CRIT=5readonly LOG_LEVEL_ALERT=6readonly LOG_LEVEL_EMERG=7# 默认日志级别 (INFO)LOG_LEVEL=${LOG_LEVEL_INFO}# 日志文件配置LOG_FILE="${LOG_DIR}/${SCRIPT_NAME%.sh}.log"LOG_FILTER="" # 日志过滤正则表达式LOG_CONTEXT="" # 日志上下文信息LOG_PERFORMANCE=false # 是否记录性能数据# 初始化日志系统init_logger() { # 创建日志目录 mkdir -p "${LOG_DIR}" 2>/dev/null chmod 755 "${LOG_DIR}" 2>/dev/null # 检查日志目录是否可写 if [[ ! -w "${LOG_DIR}" ]]; then echo -e "${RED}ERROR: Log directory ${LOG_DIR} is not writable${RESET}" >&2 exit 1 fi # 执行日志轮转 rotate_logs}# 设置日志级别set_log_level() { local level=$1 case ${level^^} in DEBUG) LOG_LEVEL=$LOG_LEVEL_DEBUG ;; INFO) LOG_LEVEL=$LOG_LEVEL_INFO ;; NOTICE) LOG_LEVEL=$LOG_LEVEL_NOTICE ;; WARN) LOG_LEVEL=$LOG_LEVEL_WARN ;; ERROR) LOG_LEVEL=$LOG_LEVEL_ERROR ;; CRIT) LOG_LEVEL=$LOG_LEVEL_CRIT ;; ALERT) LOG_LEVEL=$LOG_LEVEL_ALERT ;; EMERG) LOG_LEVEL=$LOG_LEVEL_EMERG ;; *) echo -e "${YELLOW}WARN: Invalid log level '$level', using INFO${RESET}" >&2 LOG_LEVEL=$LOG_LEVEL_INFO ;; esac}# 设置日志过滤set_log_filter() { LOG_FILTER=$1}# 设置日志上下文set_log_context() { LOG_CONTEXT=$1}# 启用性能监控enable_performance_logging() { LOG_PERFORMANCE=true}#mkdir -p "$(dirname "$LOG_FILE")"#touch "$LOG_FILE"# 日志轮转# 日志轮转(修复版)rotate_logs() { # 确保日志目录存在且可写 if [[ ! -d "$LOG_DIR" || ! -w "$LOG_DIR" ]]; then echo -e "${RED}ERROR: Log directory $LOG_DIR is not accessible${RESET}" >&2 return 1 fi # 确保日志文件存在 if [[ ! -f "$LOG_FILE" ]]; then touch "$LOG_FILE" || { echo -e "${RED}ERROR: Cannot create log file $LOG_FILE${RESET}" >&2 return 1 } return 0 fi # 更可靠的文件大小检查方法 local file_size_kb=$(wc -c < "$LOG_FILE" 2>/dev/null) file_size_kb=$((file_size_kb / 1024)) # 如果文件小于最大大小,直接返回 if (( file_size_kb <= MAX_LOG_SIZE_KB )); then return 0 fi # 记录轮转信息(直接写入文件,避免循环) local rotate_msg="[$(date "+%Y-%m-%d %H:%M:%S")] [NOTICE] [${SCRIPT_NAME}] Rotating logs (size ${file_size_kb}KB > ${MAX_LOG_SIZE_KB}KB)" echo "$rotate_msg" >> "$LOG_FILE" # 清理最旧日志(先检查是否存在) local oldest_log="${LOG_FILE}.${MAX_LOG_FILES}.gz" if [[ -f "$oldest_log" ]]; then if ! rm -f "$oldest_log"; then echo -e "${RED}ERROR: Cannot remove oldest log file $oldest_log${RESET}" >&2 return 1 fi fi # 轮转现有日志(从旧到新) for ((i=MAX_LOG_FILES-1; i>=1; i--)); do local src_log="${LOG_FILE}.${i}.gz" local dst_log="${LOG_FILE}.$((i+1)).gz" if [[ -f "$src_log" ]]; then if ! mv -f "$src_log" "$dst_log"; then echo -e "${RED}ERROR: Cannot rotate log $src_log to $dst_log${RESET}" >&2 return 1 fi fi done # 移动当前日志并压缩 local log_archive="${LOG_FILE}.1" if ! mv -f "$LOG_FILE" "$log_archive"; then echo -e "${RED}ERROR: Cannot move current log to $log_archive${RESET}" >&2 return 1 fi if ! gzip "$log_archive"; then echo -e "${RED}ERROR: Cannot compress log archive $log_archive${RESET}" >&2 # 尝试恢复原始日志文件 mv -f "$log_archive" "$LOG_FILE" return 1 fi # 创建新的空日志文件 touch "$LOG_FILE" || { echo -e "${RED}ERROR: Cannot create new log file $LOG_FILE${RESET}" >&2 return 1 } # 确保新日志文件有正确的权限 chmod 644 "$LOG_FILE" 2>/dev/null return 0}# 核心日志函数log() { local level=$1 local message=$2 local timestamp=$(date "+%Y-%m-%d %H:%M:%S.%3N") # 毫秒级时间戳 local level_index=-1 local color=$RESET local pid=$$ local thread="" # Shell 通常没有线程ID,但可以记录子shell # 检查是否在子shell中 if [[ "$BASHPID" != "$pid" ]]; then thread="[$BASHPID]" fi # 获取日志级别索引 for i in "${!LOG_LEVELS[@]}"; do if [[ "${LOG_LEVELS[$i]}" == "${level^^}" ]]; then level_index=$i break fi done # 如果请求的日志级别低于设置的级别,则不记录 if (( level_index < LOG_LEVEL )); then return fi # 设置颜色 case ${level^^} in DEBUG) color=$CYAN ;; INFO) color=$GREEN ;; NOTICE) color=$BLUE ;; WARN) color=$YELLOW ;; ERROR) color=$RED ;; CRIT) color=$PURPLE ;; ALERT) color=$PURPLE; message="${BLINK}${message}${RESET}" ;; EMERG) color=$PURPLE; message="${BOLD}${INVERT}${message}${RESET}" ;; *) color=$RESET ;; esac # 性能监控数据 local perf_data="" if [[ "$LOG_PERFORMANCE" == true ]]; then local end_time=$(date +%s%3N) # 毫秒级时间戳 # 假设我们记录了开始时间在 _LOG_START_TIME 变量中 # 实际应用中,可以在函数开始时调用 start_timer if [[ -n "${_LOG_START_TIME}" ]]; then local duration=$((end_time - _LOG_START_TIME)) perf_data=" [${duration}ms]" fi fi # 格式化日志消息 local log_line="[$timestamp] [${level^^}] [${SCRIPT_NAME}${thread}]${LOG_CONTEXT}${perf_data}$message" # 应用日志过滤 if [[ -n "$LOG_FILTER" ]]; then if [[ ! "$message" =~ $LOG_FILTER ]]; then return fi fi # 输出到控制台 (带颜色) echo -e "${color}${log_line}${RESET}" >&2 # 输出到日志文件 (如果设置了) if [[ -n "$LOG_FILE" ]]; then echo "$log_line" >> "$LOG_FILE" # 触发日志轮转检查 rotate_logs fi}# 计时器开始start_timer() { _LOG_START_TIME=$(date +%s%3N)}# 各种级别的日志快捷方式log_debug() { log "DEBUG" "$1"; }log_info() { log "INFO" "$1"; }log_notice() { log "NOTICE" "$1"; }log_warn() { log "WARN" "$1"; }log_error() { log "ERROR" "$1"; }log_crit() { log "CRIT" "$1"; }log_alert() { log "ALERT" "$1"; }log_emerg() { log "EMERG" "$1"; exit 1; }# 带性能监控的日志函数log_perf() { start_timer local result=$($1 2>&1) local status=$? local duration=$(( $(date +%s%3N) - _LOG_START_TIME )) if (( status == 0 )); then log_info "PERF: $1 completed in ${duration}ms" else log_error "PERF: $1 failed in ${duration}ms: $result" fi}# 示例用法main() { # 初始化日志系统 init_logger # 设置日志级别 (可选,默认为INFO) set_log_level "DEBUG" # 设置日志过滤 (只记录包含"important"的日志) # set_log_filter "important" # 设置日志上下文 (如当前处理的用户/ID) set_log_context "[user:1234]" # 启用性能监控 enable_performance_logging log_info "脚本开始执行" log_debug "这是一条调试信息" log_info "处理中..." if [[ ! -f "/etc/passwd" ]]; then log_warn "/etc/passwd 文件不存在,但这可能没问题" else log_debug "/etc/passwd 文件存在" fi # 示例性能监控 log_perf "sleep 0.5" # 模拟错误 if false; then log_error "某个操作失败了" fi # 模拟致命错误 # log_emerg "发生致命错误,脚本将退出" # 带上下文的日志 set_log_context "[user:5678]" log_notice "切换到用户5678的处理" log_info "脚本执行完成"}# 执行主函数main "$@"
三,如何在其它脚本中引用
1、把loglog.sh 放到脚本同一层目录,或者固定目录,每次使用source一下就好
# 引入日志系统(假设 loglog.sh 和当前脚本在同一目录)source "$(dirname "$0")/loglog.sh" || { echo "ERROR: 无法加载日志系统 loglog.sh" >&2 exit 1
2、完整测试脚本内容,测试主体是ls 遍历当前目录,
测试脚本说明:
日志函数覆盖:
init_logger:初始化日志系统
set_log_level:设置日志级别为 DEBUG
set_log_context:添加上下文信息
enable_performance_logging:启用性能监控
log_perf:监控 ls -l 命令的执行时间
log_debug/info/warn/error:记录不同级别的日志
log_notice:记录文件总数
特色功能演示:
性能监控(log_perf)
条件错误处理(log_error + 条件判断)
目录检查(log_warn 跳过目录)
上下文信息([操作:文件列表])
预期输出:
控制台会显示彩色日志(DEBUG=青色,INFO=绿色等)
日志文件会记录到 /var/log/test_loglog/test_loglog.log
当日志超过 1MB 时会自动轮转(rotate_logs 功能)
#!/bin/bash# 引入日志系统(假设 loglog.sh 和当前脚本在同一目录)source "$(dirname "$0")/loglog.sh" || { echo "ERROR: 无法加载日志系统 loglog.sh" >&2 exit 1}# 主测试函数test_ls_with_logging() { # 初始化日志系统(init_logger) init_logger # 设置日志级别为 DEBUG 以显示所有日志(set_log_level) set_log_level "DEBUG" # 设置日志上下文(set_log_context) set_log_context "[操作:文件列表]" # 启用性能监控(enable_performance_logging) enable_performance_logging # 记录脚本开始(log_info) log_info "===== 开始测试: 列出当前目录文件 =====" # 模拟一个性能监控操作(log_perf) log_perf "ls -l" # 实际会执行并记录耗时 # 记录调试信息:准备列出文件(log_debug) log_debug "准备扫描目录: $(pwd)" # 检查目录是否可读(log_warn + 条件判断) if [[ ! -r "." ]]; then log_error "当前目录不可读!" return 1 fi # 记录普通信息:开始扫描(log_info) log_info "扫描目录内容..." # 使用数组存储文件列表(演示循环中的日志) local files=(*) local file_count=${#files[@]} # 记录通知级日志:文件数量(log_notice) log_notice "找到 ${file_count} 个文件/目录" # 遍历文件并记录日志 for file in "${files[@]}"; do # 记录调试信息:处理每个文件(log_debug) log_debug "处理文件: $file" # 如果是目录则记录警告(log_warn) if [[ -d "$file" ]]; then log_warn "跳过目录: $file" continue fi # 记录文件大小(演示带性能数据的日志) local size=$(du -h "$file" | cut -f1) log_info "文件: $file (大小: $size)" done # 模拟一个错误场景(log_error) if [[ ! -f "nonexistent_file" ]]; then log_error "预期外的文件缺失: nonexistent_file" fi # 记录脚本结束(log_info) log_info "===== 测试完成 ====="}# 执行测试test_ls_with_logging "$@"
四,验证
[root@master1 test2]# ./testloglog.sh [2026-01-24 18:51:08.683] [INFO] [testloglog.sh][user:1234] 脚本开始执行[2026-01-24 18:51:08.691] [DEBUG] [testloglog.sh][user:1234] 这是一条调试信息[2026-01-24 18:51:08.700] [INFO] [testloglog.sh][user:1234] 处理中...[2026-01-24 18:51:08.708] [DEBUG] [testloglog.sh][user:1234] /etc/passwd 文件存在[2026-01-24 18:51:09.222] [INFO] [testloglog.sh][user:1234] [509ms] PERF: sleep 0.5 completed in 504ms[2026-01-24 18:51:09.231] [NOTICE] [testloglog.sh][user:5678] [519ms] 切换到用户5678的处理[2026-01-24 18:51:09.240] [INFO] [testloglog.sh][user:5678] [527ms] 脚本执行完成[2026-01-24 18:51:09.254] [INFO] [testloglog.sh][操作:文件列表] [541ms] ===== 开始测试: 列出当前目录文件 =====[2026-01-24 18:51:09.268] [INFO] [testloglog.sh][操作:文件列表] [9ms] PERF: ls -l completed in 6ms[2026-01-24 18:51:09.275] [DEBUG] [testloglog.sh][操作:文件列表] [16ms] 准备扫描目录: /root/test2[2026-01-24 18:51:09.282] [INFO] [testloglog.sh][操作:文件列表] [22ms] 扫描目录内容...[2026-01-24 18:51:09.288] [NOTICE] [testloglog.sh][操作:文件列表] [29ms] 找到 9 个文件/目录[2026-01-24 18:51:09.294] [DEBUG] [testloglog.sh][操作:文件列表] [35ms] 处理文件: 2025-07-28[2026-01-24 18:51:09.300] [WARN] [testloglog.sh][操作:文件列表] [41ms] 跳过目录: 2025-07-28[2026-01-24 18:51:09.306] [DEBUG] [testloglog.sh][操作:文件列表] [46ms] 处理文件: BBBBBB.txt[2026-01-24 18:51:09.314] [INFO] [testloglog.sh][操作:文件列表] [55ms] 文件: BBBBBB.txt (大小: 0)[2026-01-24 18:51:09.320] [DEBUG] [testloglog.sh][操作:文件列表] [60ms] 处理文件: clean_log.log[2026-01-24 18:51:09.328] [INFO] [testloglog.sh][操作:文件列表] [68ms] 文件: clean_log.log (大小: 4.0K)[2026-01-24 18:51:09.333] [DEBUG] [testloglog.sh][操作:文件列表] [73ms] 处理文件: clwan_dir.sh[2026-01-24 18:51:09.340] [INFO] [testloglog.sh][操作:文件列表] [81ms] 文件: clwan_dir.sh (大小: 4.0K)[2026-01-24 18:51:09.346] [DEBUG] [testloglog.sh][操作:文件列表] [86ms] 处理文件: file.txt.newlink[2026-01-24 18:51:09.353] [INFO] [testloglog.sh][操作:文件列表] [93ms] 文件: file.txt.newlink (大小: 0)[2026-01-24 18:51:09.358] [DEBUG] [testloglog.sh][操作:文件列表] [99ms] 处理文件: loglog.sh[2026-01-24 18:51:09.366] [INFO] [testloglog.sh][操作:文件列表] [106ms] 文件: loglog.sh (大小: 8.0K)[2026-01-24 18:51:09.371] [DEBUG] [testloglog.sh][操作:文件列表] [111ms] 处理文件: tesh.sh[2026-01-24 18:51:09.378] [INFO] [testloglog.sh][操作:文件列表] [118ms] 文件: tesh.sh (大小: 4.0K)[2026-01-24 18:51:09.383] [DEBUG] [testloglog.sh][操作:文件列表] [124ms] 处理文件: testloglog.sh[2026-01-24 18:51:09.391] [INFO] [testloglog.sh][操作:文件列表] [131ms] 文件: testloglog.sh (大小: 4.0K)[2026-01-24 18:51:09.396] [DEBUG] [testloglog.sh][操作:文件列表] [136ms] 处理文件: test_logs[2026-01-24 18:51:09.401] [WARN] [testloglog.sh][操作:文件列表] [142ms] 跳过目录: test_logs[2026-01-24 18:51:09.406] [ERROR] [testloglog.sh][操作:文件列表] [146ms] 预期外的文件缺失: nonexistent_file[2026-01-24 18:51:09.411] [INFO] [testloglog.sh][操作:文件列表] [151ms] ===== 测试完成 =====
- 检查
/var/log/test_loglog/ 目录是否自动创建
[root@master1 test2]# tail -f /var/log/testloglog/testloglog.log [2026-01-24 18:51:09.358] [DEBUG] [testloglog.sh][操作:文件列表] [99ms] 处理文件: loglog.sh[2026-01-24 18:51:09.366] [INFO] [testloglog.sh][操作:文件列表] [106ms] 文件: loglog.sh (大小: 8.0K)[2026-01-24 18:51:09.371] [DEBUG] [testloglog.sh][操作:文件列表] [111ms] 处理文件: tesh.sh[2026-01-24 18:51:09.378] [INFO] [testloglog.sh][操作:文件列表] [118ms] 文件: tesh.sh (大小: 4.0K)[2026-01-24 18:51:09.383] [DEBUG] [testloglog.sh][操作:文件列表] [124ms] 处理文件: testloglog.sh[2026-01-24 18:51:09.391] [INFO] [testloglog.sh][操作:文件列表] [131ms] 文件: testloglog.sh (大小: 4.0K)[2026-01-24 18:51:09.396] [DEBUG] [testloglog.sh][操作:文件列表] [136ms] 处理文件: test_logs[2026-01-24 18:51:09.401] [WARN] [testloglog.sh][操作:文件列表] [142ms] 跳过目录: test_logs[2026-01-24 18:51:09.406] [ERROR] [testloglog.sh][操作:文件列表] [146ms] 预期外的文件缺失: nonexistent_file[2026-01-24 18:51:09.411] [INFO] [testloglog.sh][操作:文件列表] [151ms] ===== 测试完成 =====
3.测试日志轮转:为了测试我修改了多大日志开始轮转为20kb, 生成环境记得改成自定义大小readonly MAX_LOG_SIZE_KB=20 # 单个日志文件最大大小(KB)
# 快速填充日志文件for i in {1..10000}; do ./test_loglog.sh; donels -l /var/log/test_loglog/ # 应看到 .1.gz 等轮转文件
[root@master1 test2]# ls -lth /var/log/testloglog/总用量 28K-rw-r--r-- 1 root root 1.2K 1月 24 20:15 testloglog.log-rw-r--r-- 1 root root 1.8K 1月 24 20:15 testloglog.log.1.gz-rw-r--r-- 1 root root 1.8K 1月 24 20:14 testloglog.log.2.gz-rw-r--r-- 1 root root 1.8K 1月 24 20:14 testloglog.log.3.gz-rw-r--r-- 1 root root 1.8K 1月 24 20:13 testloglog.log.4.gz-rw-r--r-- 1 root root 5.9K 1月 24 20:13 testloglog.log.5.gz
五,总结
以上,就是shell脚本log日志模板的全部内容
最后的最后(Last but not least),欢迎交流:
关注公众号留言,或者在下方直接留言: