【问题标题】:Simple logging levels in BashBash 中的简单日志记录级别
【发布时间】:2018-06-13 16:10:36
【问题描述】:

问题

我正在努力调整我多年来编写并在许多脚本中重复使用的日志记录功能,以尊重日志记录级别。

简而言之,我想使用单个全局变量仅打印与所需日志记录级别详细程度匹配的日志。

目前

我当前的代码如下所示:

#################################################################################
# SCRIPT LOGGING CONFIGURATION
#
# The following is used by the script to output log data. Depending upon the log
# level indicated, more or less data may be output, with a "lower" level
# providing more detail, and the "higher" level providing less verbose output.
#################################################################################
DATETIME="`date +%Y-%m-%d` `date +%T%z`" # Date format at beginning of log entries to match RFC
DATE_FOR_FILENAME=`date +%Y%m%d`
#
SCRIPT_LOG_DIR="/var/log/company/${APP_NAME}/"
SCRIPT_LOGFILE="${SCRIPT_LOG_DIR}-APPNAME-${DATE_FOR_FILENAME}.log"
#
# Logging Level configuration works as follows:
# DEBUG - Provides all logging output
# INFO  - Provides all but debug messages
# WARN  - Provides all but debug and info
# ERROR - Provides all but debug, info and warn
#
# SEVERE and CRITICAL are also supported levels as extremes of ERROR
#
SCRIPT_LOGGING_LEVEL="DEBUG"
#################################################################################
#          ##      END OF GLOBAL VARIABLE CONFIGURATION      ##
#################################################################################
# LOGGING
#
# Calls to the logThis() function will determine if an appropriate log file
# exists. If it does, then it will use it, if not, a call to openLog() is made,
# if the log file is created successfully, then it is used.
#
# All log output is comprised of
# [+] An RFC 3339 standard date/time stamp
# [+] The declared level of the log output
# [+] The runtime process ID (PID) of the script
# [+] The log message
#################################################################################
function openLog {
    echo -e "${DATETIME} : PID $$ : INFO : New log file (${logFile}) created." >> "${SCRIPT_LOGFILE}"

    if ! [[ "$?" -eq 0 ]]
    then
        echo "${DATETIME} - ERROR : UNABLE TO OPEN LOG FILE - EXITING SCRIPT."
        exit 1
    fi
}

function logThis() {
    DATETIME=$(date --rfc-3339=seconds)
    if [[ -z "${1}" || -z "${2}" ]]
    then
        echo "${DATETIME} - ERROR : LOGGING REQUIRES A DESTINATION FILE, A MESSAGE AND A PRIORITY, IN THAT ORDER."
        echo "${DATETIME} - ERROR : INPUTS WERE: ${1} and ${2}."
        exit 1
    fi

    LOG_MESSAGE="${1}"
    LOG_PRIORITY="${2}"

    # Determine if logging level is supported and desired
    #
    # This seems more complex than may be necessary
    if [[ ${LOG_PRIORITY} -eq "DEBUG" ]] && [[ ${SCRIPT_LOGGING_LEVEL} -eq "DEBUG" ]]
    then
        LOG_PRIORITY_SUPPORTED=true
    elif [[ ${LOG_PRIORITY} -eq "INFO" ]] && [[ ${SCRIPT_LOGGING_LEVEL} -eq "DEBUG"||"INFO" ]]
    then
        LOG_PRIORITY_SUPPORTED=true
    elif [[ ${LOG_PRIORITY} -eq "WARN" ]] && [[ ${SCRIPT_LOGGING_LEVEL} -eq "DEBUG"||"INFO"||"WARN" ]]
    then
        LOG_PRIORITY_SUPPORTED=true
    elif [[ ${LOG_PRIORITY} -eq "ERROR"||"SEVERE"||"CRITICAL" ]] && [[ ${SCRIPT_LOGGING_LEVEL} -eq "DEBUG"||"INFO"||"WARN"||"ERROR"||"SEVERE"||"CRITICAL" ]]
    then
        LOG_PRIORITY_SUPPORTED=true
    else
        echo -e "CRITICAL: Declared log priority is not supported."
        exit 1
    fi

    # If logging level NOT supported, dump it
    if ! [ ${LOG_PRIORITY_SUPPORTED} ]
    then
        echo "priority unsupported"
        break
    fi

    # No log file, create it.
    if ! [[ -f ${SCRIPT_LOGFILE} ]]
    then
        echo -e "INFO : No log file located, creating new log file (${SCRIPT_LOGFILE})."
        echo "${DATETIME} : PID $$ :INFO : No log file located, creating new log file (${SCRIPT_LOGFILE})." >> "${SCRIPT_LOGFILE}"
        openLog
    fi

    # Write log details to file
    echo -e "${LOG_PRIORITY} : ${LOG_MESSAGE}"
    echo -e "${DATETIME} : PID $$ : ${LOG_PRIORITY} : ${LOG_MESSAGE}" >> "${SCRIPT_LOGFILE}"

    # Reset log level support flag
    LOG_PRIORITY_SUPPORTED=false
}

当使用函数时变成这样使用:

logThis "This is my log message" "DEBUG"

logThis "This is my log message" "ERROR"

logThis "This is my log message" "INFO"

尝试

您可以在上面的代码中看到,我已经尝试(无论多么复杂)在传入的消息上使用案例选择来过滤消息。

这不起作用。无论为LOG_PRIORITY 提供的值如何,所有消息都会通过。

即使它不是受支持的值。 例如,以下仍然允许处理日志消息:

SCRIPT_LOGGING_LEVEL="FARCE"

或者即使我像这样为给定消息设置值:

logThis "This is my log message" "FARCE"

您的帮助

我不打算完全重构我拥有的功能。我有太多的脚本在使用所涉及的函数,如果我改变标准化,也需要对这些脚本进行返工。

我不一定需要有人像他们所说的那样“为我做这项工作”,但考虑到我的限制,朝一个有效的方向轻推就足够了。 我很高兴在以后的编辑中发布最终实现

明白

我认识到现在有更新更好的方法来处理 BASH 脚本中的日志记录功能,但是这些功能在如此多的脚本中的流行意味着对正在使用的功能的简单更新将产生非常广泛的影响。

===

最终解决方案

为了结束这个问题的循环,最终的解决方案包括一些最初不在范围内的更改,但为了满足一些更好的做法,我做了以下操作:

  1. 将所有变量名称转换为大小写混合,而不是为系统和环境变量保留的所有大写字母。一些评论者(@PesaThe 和 @CharlesDuffy)注意到了这一点。
  2. 我的原始帖子指出我最初使用了案例选择,但显示了 ifelif 语句的集合。我之前确实尝试过选择案例,但出于沮丧,我转向了维护繁重且难以辨认的 if + elif 选项。
  3. 代码更改允许删除(如 @PesaThe 建议的那样)在函数末尾重置的不干净变量。

解决方案详情

此解决方案符合我的要求,因为它需要对现有脚本代码的更改最少,并允许调用函数的现有方法工作。

在接受解决方案时,针对我的帖子推荐了三个选项。这三个选项都很有帮助而且很重要,但我最终选择的那个只需要三行代码即可实现。

如上所述,我确实做了一些不在范围内但不影响本文提供的代码之外的功能的更改。

另外一点:我确实在我的目标环境中验证了这些功能,并且在编辑时它们在 Ubuntu 16.04 上可以正常工作。

最终代码

#################################################################################
# SCRIPT LOGGING CONFIGURATION
#
# The following is used by the script to output log data. Depending upon the log
# level indicated, more or less data may be output, with a "lower" level
# providing more detail, and the "higher" level providing less verbose output.
#################################################################################
dateTime="`date +%Y-%m-%d` `date +%T%z`" # Date format at beginning of log entries to match RFC
dateForFileName=`date +%Y%m%d`
#
scriptLogDir="/var/log/company/${appName}/"
scriptLogPath="${scriptLogDir}${appName}-${dateForFileName}.log"
#
# Logging Level configuration works as follows:
# DEBUG - Provides all logging output
# INFO  - Provides all but debug messages
# WARN  - Provides all but debug and info
# ERROR - Provides all but debug, info and warn
#
# SEVERE and CRITICAL are also supported levels as extremes of ERROR
#
scriptLoggingLevel="DEBUG"
#################################################################################
#          ##      END OF GLOBAL VARIABLE CONFIGURATION      ##
#################################################################################
# LOGGING
#
# Calls to the logThis() function will determine if an appropriate log file
# exists. If it does, then it will use it, if not, a call to openLog() is made,
# if the log file is created successfully, then it is used.
#
# All log output is comprised of
# [+] An RFC 3339 standard date/time stamp
# [+] The declared level of the log output
# [+] The runtime process ID (PID) of the script
# [+] The log message
#################################################################################
function openLog {
    echo -e "${dateTime} : PID $$ : INFO : New log file (${scriptLogPath}) created." >> "${scriptLogPath}"

    if ! [[ "$?" -eq 0 ]]
    then
        echo "${dateTime} - ERROR : UNABLE TO OPEN LOG FILE - EXITING SCRIPT."
        exit 1
    fi
}

function logThis() {
    dateTime=$(date --rfc-3339=seconds)

    if [[ -z "${1}" || -z "${2}" ]]
    then
        echo "${dateTime} - ERROR : LOGGING REQUIRES A DESTINATION FILE, A MESSAGE AND A PRIORITY, IN THAT ORDER."
        echo "${dateTime} - ERROR : INPUTS WERE: ${1} and ${2}."
        exit 1
    fi

    logMessage="${1}"
    logMessagePriority="${2}"

    declare -A logPriorities=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [SEVERE]=4 [CRITICAL]=5)
    [[ ${logPriorities[$logMessagePriority]} ]] || return 1
    (( ${logPriorities[$logMessagePriority]} < ${logPriorities[$scriptLoggingLevel]} )) && return 2


    # No log file, create it.
    if ! [[ -f ${scriptLogPath} ]]
    then
        echo -e "INFO : No log file located, creating new log file (${scriptLogPath})."
        echo "${dateTime} : PID $$ :INFO : No log file located, creating new log file (${scriptLogPath})." >> "${scriptLogPath}"
        openLog
    fi

    # Write log details to file
    echo -e "${logMessagePriority} : ${logMessage}"
    echo -e "${dateTime} : PID $$ : ${logMessagePriority} : ${logMessage}" >> "${scriptLogPath}"
}

【问题讨论】:

  • 顺便说一句,全大写名称用于对 shell 和操作系统有意义的变量,而具有至少一个小写字符的名称保证应用程序使用安全。请参阅相关的 POSIX 规范 @pubs.opengroup.org/onlinepubs/9699919799/basedefs/…,第四段。
  • 另外,echo -e 通常最好避免使用。请参阅the POSIX spec for echo,尤其是应用程序用法和基本原理部分:支持-e 明显违反标准(不允许使用-n 以外的选项,这会使行为未定义)和printf明确推荐用于新开发。
  • @CharlesDuffy 我将实现您的第一个表示法(很好),但不是第二个。 logThis() 函数的其他部分的重写不在我定义的工作范围内,目前使用的平台支持 -e 参数。虽然不像标准机构所期望的那样跨 POSIX 平台的可移植性,但对于目标环境来说,可移植性已经足够了。
  • 请注意,即使是 bash 也不总是支持echo -e;它是否可用取决于xpg_echoposix 运行时标志的状态(以及它们是否默认打开取决于编译时标志)。
  • @CharlesDuffy 明白了。谢谢。

标签: linux bash logging


【解决方案1】:

我相信你写错了。

首先,您应该使用== 进行字符串比较,而不是-eq(我怀疑这存在于[[ ]],我认为它只存在于[ ]

其次,||没有做你认为应该做的事。它可能非常复杂,因为您必须编写

elif [ ${LOG_PRIORITY} = "ERROR" -o ${LOG_PRIORITY} = "SEVERE" -o ${LOG_PRIORITY} = "CRITICAL" ] && [ ${SCRIPT_LOGGING_LEVEL} = "DEBUG" -o ${SCRIPT_LOGGING_LEVEL} = "INFO" -o ${SCRIPT_LOGGING_LEVEL} = "WARN" -o ${SCRIPT_LOGGING_LEVEL} = "ERROR" -o ${SCRIPT_LOGGING_LEVEL} = "SEVERE" -o ${SCRIPT_LOGGING_LEVEL} = "CRITICAL" ]

不过,您可以使用case

case "${LOG_PRIORITY}" in
    "ERROR"|"CRITICAL"|"SEVERE")
        # Do stuff
        ;;
    *)
        echo "Error"
        exit 1 ;;
esac

这可以简化您的条件。

如果您能忍受稍微(只是稍微!)更大的修改,我会建议对您的日志记录级别进行数字化(将它们转换为数字日志级别),然后您将能够使用算术比较,这非常好且易读。

【讨论】:

  • ==[ ] 中的不可移植扩展,如果使用 ash 或 dash 运行,将导致代码中断——test 的唯一 POSIX 指定的字符串比较运算符是单个 @ 987654333@。见pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html
  • Charles,虽然我通常会为可移植性鼓掌,但问题标题和标签都表示bash,所以我不确定这是不是这里的问题。
  • @paxdiablo,这当然不是错误,但也不是一种教学实践,因为它没有比便携式替代方案增加任何价值;虽然在此特定代码中这不是问题,但如果在其他地方采用,这将是一个完全不必要的问题。 (在 [[ ]] 中,就像在您的代码中一样,== 更具防御性,因为该上下文是一个显式不可移植的构造,实际上增加了价值)。
【解决方案2】:

解决此问题的一种方法是创建所有级别的关联数组。每个级别都分配有一个数字,然后将这些数字进行比较以决定您是否应该登录。假设您想添加另一个日志记录级别。你的那些if 声明会失控:

#!/usr/bin/env bash

declare -A levels=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
script_logging_level="INFO"

logThis() {
    local log_message=$1
    local log_priority=$2

    #check if level exists
    [[ ${levels[$log_priority]} ]] || return 1

    #check if level is enough
    (( ${levels[$log_priority]} < ${levels[$script_logging_level]} )) && return 2

    #log here
    echo "${log_priority} : ${log_message}"
}

logThis "This will log" "WARN"
logThis "This will not log" "DEBUG"
logThis "This will not log" "OUCH"

LOG_PRIORITY_SUPPORTED=false 是一个例子,为什么你应该在函数中使用local 变量。此外,您不应使用大写变量,因为它们可能与环境或内部 shell 变量发生冲突。

【讨论】:

  • 您的解决方案是我选择的解决方案。它实际上需要声明一个数组,验证数组中存在的选项的行,以及如果级别与设置不匹配则跳出函数的行。我还对您和@CharlesDuffy 推荐了其他更改。
  • @ChrisGiddings 很高兴你解决了。但是不要忘记在函数中将变量设为本地 :)
  • 这里是 bash 日志的综合实现:github.com/codeforester/base/blob/master/lib/stdlib.sh
【解决方案3】:

您的主要问题是使用如下表达式:

[[ ${LOG_PRIORITY} -eq "DEBUG" ]]

正如bash 文档指出的那样(我的重点):

arg1 OP arg2

OP-eq-ne-lt-le-gt-ge 之一。如果arg1 分别等于、不等于、小于、小于或等于、大于或大于或等于arg2,则这些算术 二元运算符返回真。 Arg1arg2 可以是正整数或负整数。

因此,尝试使用-eq 来比较字符串是行不通的:

pax@paxbox1> [[ a -eq b ]] && echo equal
equal

在该示例中,ab 都被视为零。

为了比较字符串,您应该使用== 而不是-eq

pax@paxbox1> [[ a == b ]] && echo equal
pax@paxbox1> [[ a == a ]] || echo unequal
unequal
pax@paxbox1> [[ a == a ]] && echo equal
equal

但是,鉴于您希望将这些日志记录级别用作 minimim 日志记录阈值,我会尽快将它们转换为整数值,以便您可以使用数字比较来最小化代码大小,例如:

SCRIPT_LOGGING_LEVEL=1 # errors and warnings only
:
case ${LOG_PRIORITY} in
    ERROR) NUM_PRIO=0;;
    WARN)  NUM_PRIO=1;;
    INFO)  NUM_PRIO=2;;
    DEBUG) NUM_PRIO=3;;
    *)     return;;
esac
[[ ${NUM_PRIO} -le ${SCRIPT_LOGGING_LEVEL} ]] && logTheMessage

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-05-06
    • 1970-01-01
    • 1970-01-01
    • 2023-03-23
    • 1970-01-01
    • 1970-01-01
    • 2016-11-27
    • 2014-12-03
    相关资源
    最近更新 更多