日志框架:标准化日志输出 | Logging Framework: Standardized Log Output

章节概述

本章讲解如何在 Shell 脚本中构建标准化的日志框架,涵盖日志级别定义、日志函数封装、时间戳格式化、彩色输出、日志文件 rotation 以及 syslog 集成。标准化的日志是生产环境脚本不可或缺的基础设施。

核心理念:好的日志是脚本的黑匣子。当生产环境出现问题时,标准化的日志能帮你快速定位根因,而不是对着空空如也的终端发愁。


第1节:日志级别定义

日志级别规范

# 日志级别定义
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
readonly LOG_LEVEL_FATAL=4
 
# 默认级别
LOG_LEVEL="${LOG_LEVEL_INFO}"
 
# 级别名称映射
get_level_name() {
  case "${1}" in
    0) echo "DEBUG" ;;
    1) echo "INFO" ;;
    2) echo "WARN" ;;
    3) echo "ERROR" ;;
    4) echo "FATAL" ;;
    *) echo "UNKNOWN" ;;
  esac
}

日志级别对照表

级别数值颜色用途输出目标
DEBUG0灰色调试信息stdout/stderr
INFO1绿色一般信息stdout
WARN2黄色警告信息stderr
ERROR3红色错误信息stderr
FATAL4红色加粗致命错误stderr

级别过滤

# 仅输出指定级别及以上的日志
should_log() {
  local level="${1}"
  [ "${level}" -ge "${LOG_LEVEL}" ]
}
 
# 使用示例
if should_log "${LOG_LEVEL_DEBUG}"; then
  echo "Debug message"
fi

第2节:日志函数封装

完整的日志函数

#!/usr/bin/env bash
 
# ============================================================
# 日志框架
# ============================================================
 
# 颜色定义
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[0;33m'
readonly BLUE='\033[0;34m'
readonly MAGENTA='\033[0;35m'
readonly CYAN='\033[0;36m'
readonly GRAY='\033[0;90m'
readonly BOLD='\033[1m'
readonly NC='\033[0m'  # No Color
 
# 日志级别
LOG_LEVEL="${LOG_LEVEL:-1}"
LOG_FILE="${LOG_FILE:-}"
LOG_FORMAT="${LOG_FORMAT:-timestamp}"
 
# 内部日志函数
_log() {
  local level="${1}"
  local color="${2}"
  local message="${3}"
  local timestamp
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')
 
  # 格式化日志行
  local log_line=""
  case "${LOG_FORMAT}" in
    timestamp)
      log_line="${timestamp} [${level}] ${message}"
      ;;
    simple)
      log_line="[${level}] ${message}"
      ;;
    json)
      log_line="{\"time\":\"${timestamp}\",\"level\":\"${level}\",\"msg\":\"${message}\"}"
      ;;
  esac
 
  # 输出到终端(带颜色)
  echo -e "${color}${log_line}${NC}" >&2
 
  # 输出到日志文件(无颜色)
  if [ -n "${LOG_FILE}" ]; then
    echo "${log_line}" >> "${LOG_FILE}"
  fi
}
 
# 公开的日志函数
log_debug() { _log "DEBUG" "${GRAY}" "$*"; }
log_info()  { _log "INFO"  "${GREEN}" "$*"; }
log_warn()  { _log "WARN"  "${YELLOW}" "$*"; }
log_error() { _log "ERROR" "${RED}" "$*"; }
log_fatal() { _log "FATAL" "${RED}${BOLD}" "$*"; exit 1; }

使用示例

# 基本使用
log_info "开始处理文件"
log_debug "文件路径: ${filepath}"
log_warn "磁盘空间不足"
log_error "无法连接数据库"
log_fatal "配置文件缺失"
 
# 设置日志级别
LOG_LEVEL="${LOG_LEVEL_DEBUG}"    # 显示所有日志
LOG_LEVEL="${LOG_LEVEL_ERROR}"    # 仅显示错误
 
# 输出到文件
LOG_FILE="/var/log/myscript.log"
log_info "此消息同时输出到文件和终端"

第3节:时间戳格式化

时间戳格式选项

# 获取当前时间戳
get_timestamp() {
  local format="${1:-full}"
  case "${format}" in
    full)     date '+%Y-%m-%d %H:%M:%S' ;;
    date)     date '+%Y-%m-%d' ;;
    time)     date '+%H:%M:%S' ;;
    iso)      date '+%Y-%m-%dT%H:%M:%S%z' ;;
    unix)     date '+%s' ;;
    compact)  date '+%Y%m%d%H%M%S' ;;
    ms)       date '+%Y-%m-%d %H:%M:%S.%3N' ;;
  esac
}
 
# 使用示例
log_with_timestamp() {
  local timestamp
  timestamp=$(get_timestamp "full")
  echo "${timestamp} $*"
}

时间戳格式对照

格式命令输出示例
fulldate '+%Y-%m-%d %H:%M:%S'2026-08-22 14:30:00
isodate '+%Y-%m-%dT%H:%M:%S%z'2026-08-22T14:30:00+0800
unixdate '+%s'1787539800
compactdate '+%Y%m%d%H%M%S'20260822143000
msdate '+%Y-%m-%d %H:%M:%S.%3N'2026-08-22 14:30:00.123

第4节:颜色输出与终端适配

颜色输出函数

# 颜色输出(自动检测终端支持)
supports_color() {
  if [ -t 1 ] && [ -n "${TERM:-}" ] && [ "${TERM}" != "dumb" ]; then
    return 0
  fi
  return 1
}
 
# 带颜色的日志(自动禁用非终端输出)
_color() {
  local color="${1}"
  shift
  if supports_color; then
    echo -e "${color}$*${NC}"
  else
    echo "$*"
  fi
}
 
# 彩色输出函数
red()    { _color "${RED}" "$*"; }
green()  { _color "${GREEN}" "$*"; }
yellow() { _color "${YELLOW}" "$*"; }
blue()   { _color "${BLUE}" "$*"; }
cyan()   { _color "${CYAN}" "$*"; }
bold()   { _color "${BOLD}" "$*"; }
gray()   { _color "${GRAY}" "$*"; }

ANSI 颜色码参考

颜色代码亮色代码
红色\033[0;31m\033[1;31m
绿色\033[0;32m\033[1;32m
黄色\033[0;33m\033[1;33m
蓝色\033[0;34m\033[1;34m
紫色\033[0;35m\033[1;35m
青色\033[0;36m\033[1;36m
灰色\033[0;90m\033[1;90m
重置\033[0m
加粗\033[1m

进度条与动画

# 简单进度条
show_progress() {
  local current="${1}"
  local total="${2}"
  local width=50
  local percent=$(( current * 100 / total ))
  local filled=$(( current * width / total ))
  local empty=$(( width - filled ))
 
  printf "\r["
  printf "%${filled}s" | tr ' ' '█'
  printf "%${empty}s" | tr ' ' '░'
  printf "] %3d%%" "${percent}"
}
 
# 使用示例
for i in $(seq 1 100); do
  show_progress "${i}" 100
  sleep 0.05
done
echo ""

第5节:日志文件 Rotation

自动日志 rotation

#!/usr/bin/env bash
 
# ============================================================
# 日志 Rotation 框架
# ============================================================
 
# 配置
LOG_DIR="/var/log/myscript"
LOG_FILE="${LOG_DIR}/app.log"
MAX_SIZE="10M"       # 最大文件大小
MAX_FILES=5          # 保留的旧日志文件数量
COMPRESS=true        # 是否压缩旧日志
 
# 确保日志目录存在
init_logging() {
  mkdir -p "${LOG_DIR}"
  touch "${LOG_FILE}"
}
 
# 检查并执行 rotation
rotate_log() {
  local file="${1}"
  local max_size="${2}"
  local max_files="${3}"
 
  # 检查文件大小
  if [ ! -f "${file}" ]; then
    return 0
  fi
 
  local file_size
  file_size=$(stat -f%z "${file}" 2>/dev/null || stat -c%s "${file}" 2>/dev/null || echo 0)
 
  # 将大小转换为字节
  local size_bytes
  size_bytes=$(human_to_bytes "${max_size}")
 
  if [ "${file_size}" -lt "${size_bytes}" ]; then
    return 0
  fi
 
  # 执行 rotation
  for i in $(seq $(( max_files - 1 )) -1 1); do
    local prev=$((i - 1))
    if [ -f "${file}.${prev}.gz" ]; then
      mv "${file}.${prev}.gz" "${file}.${i}.gz"
    elif [ -f "${file}.${prev}" ]; then
      mv "${file}.${prev}" "${file}.${i}"
    fi
  done
 
  # 当前文件变成 .1
  mv "${file}" "${file}.1"
 
  # 压缩旧文件
  if [ "${COMPRESS}" = true ]; then
    gzip "${file}.1" 2>/dev/null || true
  fi
 
  # 创建新文件
  touch "${file}"
}
 
# 人类可读大小转字节
human_to_bytes() {
  local size="${1}"
  local number="${size%[KMGkmg]}"
  local unit="${size: -1}"
 
  case "${unit}" in
    K|k) echo $(( number * 1024 )) ;;
    M|m) echo $(( number * 1024 * 1024 )) ;;
    G|g) echo $(( number * 1024 * 1024 * 1024 )) ;;
    *)   echo "${number}" ;;
  esac
}

Logrotate 配置(系统级)

# /etc/logrotate.d/myscript
/var/log/myscript/*.log {
    daily            # 每天 rotation
    missingok        # 文件不存在时不报错
    rotate 14        # 保留 14 天
    compress         # 压缩旧文件
    delaycompress    # 延迟一天压缩
    notifempty       # 空文件不 rotation
    create 0644 root root  # 创建新文件的权限
    sharedscripts    # 多文件共享脚本
    postrotate
        # rotation 后发送信号给应用
        systemctl reload myscript 2>/dev/null || true
    endscript
}

第6节:syslog 集成

使用 logger 命令

# 基本 syslog 输出
logger "Application started"
logger -t "myscript" "Processing file ${filename}"
logger -p "user.warning" "Low disk space"
logger -p "user.err" "Connection failed"
 
# 结构化日志
log_to_syslog() {
  local level="${1}"
  local facility="${2:-user}"
  local message="${3}"
 
  logger -t "$(basename "$0")" -p "${facility}.${level}" "${message}"
}
 
# 使用示例
log_to_syslog "info" "Script started"
log_to_syslog "warning" "Disk space low"
log_to_syslog "err" "Database connection failed"

syslog 级别映射

Shell 级别syslog 级别syslog 优先级
DEBUGdebuguser.debug
INFOinfouser.info
WARNwarninguser.warning
ERRORerruser.err
FATALcrituser.crit

rsyslog 配置

# /etc/rsyslog.d/myscript.conf
# 将 myscript 的日志写入独立文件
if $programname == 'myscript' then /var/log/myscript.log
& stop

本节帮助你构建生产级的日志系统,让脚本运行状态一目了然、问题可追溯。