将 stderr 和 stdout 记录到日志文件并处理 bash 脚本中的错误

Logging stderr and stdout to log file and handling errors in bash script

解决方案:

编辑 我发现我的原文 post 不够精确,对此感到抱歉!所以现在我将包含我的 bash 脚本的代码示例,其中包含改进的问题:

我的四个问题:

  1. 如何将 stdout 发送到 file 并将 stderr 发送到 STEP 2
  2. mysql 命令中的 log() 函数
  3. 如何将 stdoutstderr 发送到 gzip STEP 中的 log() 函数2 和 rsync 命令步骤 3?
  4. 如何读取上面问题1和问题2中log()函数中的stdoutstderr
  5. 我怎样才能在 onexit() 函数中捕获所有错误?

一般:我希望 stdout 和 stderr 转到 log() 函数,以便可以根据特定格式将消息放入特定日志文件,但在某些情况下,像 mysqldump 命令一样,stdout 需要转到文件。此外,发生错误并发送到 stderr,我想在日志语句完成后在 onexit() 函数中结束。

#!/bin/bash -Eu
# -E: ERR trap is inherited by shell functions.
# -u: Treat unset variables as an error when substituting.

# Example script for handling bash errors.  Exit on error.  Trap exit.
# This script is supposed to run in a subshell.
# See also: http://fvue.nl/wiki/Bash:_Error_handling

#  Trap non-normal exit signals: 2/INT, 3/QUIT, 15/TERM,
trap onexit 1 2 3 15 ERR

# ****** VARIABLES STARTS HERE ***********
BACKUP_ROOT_DIR=/var/app/backup
BACKUP_DIR_DATE=${BACKUP_ROOT_DIR}/`date "+%Y-%m-%d"`
EMAIL_FROM="foo@bar.com"
EMAIL_RECIPIENTS="bar@foo.com"
...
# ****** VARIABLES STARTS HERE ***********

# ****** FUNCTIONS STARTS HERE ***********
# Function that checks if all folders exists and create new ones as required
function checkFolders()
{
    if [ ! -d "${BACKUP_ROOT_DIR}" ] ; then
        log "ERROR" "Backup directory doesn't exist"
        exit
    else
        log "INFO" "All folders exists"
    fi

    if [ ! -d "${BACKUP_DIR_DATE}" ] ; then
        mkdir ${BACKUP_DIR_DATE} -v
        log "INFO" "Created new backup directory"
    else
        log "WARN" "Backup directory already exists"
    fi
}

# Function executed when exiting the script, either because of an error or successfully run
function onexit() {
    local exit_status=${1:-$?}

    # Send email notification with the status
    echo "Backup finished at `date` with status ${exit_status_text} | mail -s "${exit_status_text} - backup" -S from="${EMAIL_FROM}" ${EMAIL_RECIPIENTS}"
    log "INFO" "Email notification sent with execution status ${exit_status_text}"
   
    # Print script duration to the console
    ELAPSED_TIME=$((${SECONDS} - ${START_TIME}))
    log "INFO" "Backup finished" "startDate=\"${START_DATE}\", endDate=\"`date`\", duration=\"$((${ELAPSED_TIME}/60)) min $((${ELAPSED_TIME}%60)) sec\""
    
    exit ${exit_status}
}

# Logs to custom log file according to preferred log format for Splunk
# Input:
#   1. severity (INFO,WARN,DEBUG,ERROR)
#   2. the message
#   3. additional fields
#
function log() {
    local print_msg="`date +"%FT%T.%N%Z"` severity=\"\",message=\"\",transactionID=\"${TRANS_ID}\",source=\"${SCRIPT_NAME}\",environment=\"${ENV}\",application=\"${APP}\""

    # check if additional fields set in the 3. parameter
    if [ $# -eq 3 ] ; then
        print_msg="${print_msg}, "
    fi

    echo ${print_msg} >> ${LOG_FILE}
}
# ****** FUNCTIONS ENDS HERE ***********

# ****** SCRIPT STARTS HERE ***********
log "INFO" "Backup of ${APP} in ${ENV} starting"

# STEP 1 - validate
log "INFO" "1/3 Checking folder paths"
checkFolders

# STEP 2 - mysql dump
log "INFO" "2/3 Dumping ${APP} database"
mysqldump --single-transaction ${DB_NAME} > ${BACKUP_DIR_DATE}/${SQL_BACKUP_FILE}
gzip -f ${BACKUP_DIR_DATE}/${SQL_BACKUP_FILE}
log "INFO" "Mysql dump finished."

# STEP 3 - transfer
# Files are only transferred if all commands has been running successfully. Transfer is done with use of rsync
log "INFO" "3/3 Transferring backup file"
rsync -r -av ${BACKUP_ROOT_DIR}/ ${BACKUP_TRANSFER_USER}@${BACKUP_TRANSFER_DEST}

# ****** SCRIPT ENDS HERE ***********
onexit

谢谢!

假设您的日志函数如下所示(它只是 echo 第一个参数):

log() { echo ""; }

要将 mysqldump 的标准输出保存到某个文件并为标准错误中的每一行调用 log() 函数,请执行以下操作:

mysqldump 2>&1 >/your/sql_dump_file.dat | while IFS= read -r line; do log "$line"; done

如果你想使用xargs,你可以这样做。但是,您每次都会开始一个新的 shell。

export -f log
mysqldump 2>&1 >/your/sql_dump_file.dat | xargs -L1 bash -i -c 'log $@' _

试试这个:

mylogger() { printf "Log: %s\n" "$(</dev/stdin)"; }

mysqldump ... 2>&1 >dumpfilename.sql | mylogger

两者 and 都提供了将 stderr 重定向到 shell 函数的可行解决方案。

它们都暗示 您的函数接受 stdin 输入而不是期望输入 作为参数是有意义的.

解释他们使用的习语:

mysqldump ... 2>&1 > sdtout-file | log-func-that-receives-stderr-via-stdin
  • 2>&1 ... 将 stderr 重定向到 original stdout
    • 从那时起,任何 stderr 输入都被重定向到 stdout
  • > sdtout-file 然后 将原始 stdout stdout 重定向到文件 stdout-out-file
    • 从那时起,任何标准输出输入都将重定向到该文件。

由于 > stdout-file2>&1 之后 ,最终结果是:

  • stdout 输出被重定向到文件 stdout-file
  • stderr 输出发送到[原始] stdout

因此,log-func-that-receives-stderr-via-stdin 仅通过管道接收前一个命令的 stderr 输入,通过 its 标准输入.

类似地,您的原始方法 - command 2> >(logFunction) - 在 原则 中工作,但需要您的 log() 函数从 stdin 而不是期望 arguments:

下面说明原理:

ls / nosuchfile 2> >(sed s'/^/Log: /') > stdout-file
  • ls / nosuchfile 产生 stdout 和 stderr 输出。
  • stdout 输出被重定向到文件 stdout-file.
  • 2> >(...) 使用 [output] process substitution 将 stderr 输出重定向到包含在 >(...) 中的命令 - 该命令通过 接收输入它的标准输入
    • sed s'/^/Log: /' 从其标准输入中读取并将字符串 Log: 添加到每个输入行。

因此,您的 log() 函数应该被重写以处理 stdin:

  • either:通过将输入隐式传递给另一个标准输入处理实用程序,例如 sedawk(如上)。
  • :通过使用while read ...循环来处理shell循环中的每个输入行:
log() {
  # `read` reads from stdin by default
  while IFS= read -r line; do
    printf 'STDERR line: %s\n' "$line"
  done
}

mysqldump ... 2> >(log) > stdout-file