最近看到一个很nice的脚本,写的很牛逼,功能也比较强大,已被Oschina收录 考虑后续能不能稍加修改,工作中使用起来
获取方式: git clone https://github.com/alex8866/lpp
主要功能如下
-
脚本合适用来监视大型日志文件(根据扫描频率将日志增量的部分进行分析)
-
将监视的错误信息发送到指定邮件列表
-
设置监视频率
-
将错误信息发送到指定tty
-
自动指定脚本从何时开始监视,何时结束监视
-
指定错误发生时脚本所有执行的操作( -c ./command )
-
配色功能
-
日志分析以及生成日志报告(这个还真没有发现)
-
指定所要监控的报错信息(可根据自己需求修改awk.example文件)
我也只是大体上看懂,有些细节也没有很深入的看,mark 一下
脚本中的一些实现方法值得学习和借鉴,为了能更好的理解,挑选几处重要的地方做一下注释
脚本主框架的介绍
- 初始化变量
# load global variables
# 初始化设置全局变量
global_variables
# 设置颜色方案
def_colors
- 解读options
输入 –foo=bar –> 输出 –foo bar
输入 -hjkl –> 输出 -h -j -k -l
### read cli options
# separate groups of short options. replace --foo=bar with --foo bar
# 解析option
- 解析args,这里涉及到global var的赋值, set –是根据分隔符IFS,把ARGS数组的值依次赋给$1,$2,$3…
# set the separated options as input options.
set -- "${ARGS[@]}"
# 解析args
- 脚本pending住,直到有监视日志产生
#logview will pending until the monitor file exist or be created
while true; do
for F in ${MONFILES[*]}
do
[ -f "$F" ] && FLAG=1
done
[ "$FLAG" == "1" ] && break
sleep 5
done
- 核心内容,定时扫描文件,监控是否有日志匹配,有error则输出
while true
do
# 脚本超时判断,如果设置了超时时间
[ -n "$timeout_end" ] &&
[ "$total_timeout_end" -ge "$timeout_end" ] &&
EXIT
# 脚本start_time end_time的判断,有别于timeout
[ -n "$end_time" ] &&
[ $(checktime "$end_time") -eq 0 ] &&
EXIT
# 这个没用到
entry_count=$((0+1))
# 如果设置了error日志的行数最大值,超过最大行数进行回滚
if [ -f "$errorfile" -a -n "$maxrecord" ];then
size=$(wc -l $errorfile | awk '{print $1}')
if [[ $size -gt $maxrecord ]];then
file_record $size $maxrecord
fi
fi
# 初始化,将此时日志行数作为BASE基数,赋值给BASE$i
# 意味着该脚本是不能监控日志里面之前出现的error信息
for ((i=0;i<FILENUM;i++))
do
if [ "$(eval echo '$COUNT'$i)" = "" ];then
[ -f "${MONFILES[i]}" ] &&
eval BASE$i=$(wc -l ${MONFILES[i]} 2>/dev/null| awk '{print $1}') ||
eval BASE$i=0
fi
done
# 休眠时间,scan 间隔
sleep $delay
# 将休眠$delay秒后,文件行数的增量作为检测内容
for ((i=0;i<FILENUM;i++))
do
[ -f "${MONFILES[i]}" ] &&
eval COUNT$i=$(wc -l ${MONFILES[i]} | awk '{print $1}') ||
eval COUNT$i=0
#eval declare -i comp$i=0
comp=$(($(eval echo '$COUNT'$i) - $(eval echo '$BASE'$i)))
if [ $comp -gt 0 ];then
LINES=$(eval expr '$COUNT'$i - '$BASE'$i)
eval BASE$i='$COUNT'$i
IFS=$'\n'
# 与awk.example里面检索内容匹配,满足则处理(deal MSG),并告知终端(tellb)
for MSGS in $(tail -$LINES ${MONFILES[i]}| eval "$GrepAwk")
do
[ $DEBUG -eq 0 ] && echo "DEBUG: \"error\" message is: [$MSGS]"
[ -n "$MSGS" ] && {
deal "$MSGS" "${MONFILES[i]}"
tellb
}
done
fi
done
# 累计脚本执行耗时时间
[ -n "$timeout_end" ] && total_timeout_end=$((total_timeout_end + delay))
done
一些函数的细节实现
- 打印终端及退出函数
# Print message to standerr
die()
{
echo "$@" >&2
exit 1
}
#Exit function
EXIT()
{
exit 0
}
# Just ignore this function (Unused)
Exit()
{
exitcode="$1"
shift
for arg
do
echo >&2 "logview: $1"
shift
done
exit ${exitcode}
}
- 时间单位转换函数,统一转换为秒单位
# Convert 5h/5m/5s to 18000s/300s/5s
# conv2seconds 5h
conv2seconds()
{
TIME="$(echo $1 | tr '[A-Z]' '[a-z]')"
TAG="$(echo $TIME | tr -d '[0-9]')"
NUM="$(echo $TIME | tr -d '[a-z]')"
case "$TAG" in
h )
SECOND=$((NUM * 3600))
;;
m )
SECOND=$((NUM * 60))
;;
s | "" )
SECOND=$NUM
;;
* )
SECOND=unknow
;;
esac
[ "$DEBUG" -eq 0 ] && echo "DEBUG: --timeout = $SECOND"
echo $SECOND
}
- 传参转入数组样例
# Get file names from COMMAND LINE arguments
getfilenames()
{
for f in "$@"; do
#[[ -f $f ]] || die "$0: $f No such file found."
SOURCE="files"
FILES[${#FILES[*]}]="$f"
done
}
- 文件大小超出阈值(最大行数),保留最新N行内容
function file_record()
{
local str="'1,$(($1-$2))d'"
eval sed $str $errorfile > $tmpfile
cp -f $tmpfile $errorfile
rm -f $tmpfile
}
- 终端输出信息
#Send "error" messages to terminal
tellb()
{
[ "$notice" == "no" ] && return 0
[ "$notice" == "one" ] && {
warncat "$MSGINC" | write "$(whoami)" "$TTY" &>/dev/null
}
[ "$notice" == "all" ] && {
warncat "$MSGINC" | wall $USERNAME
}
}
脚本全文如下
#!/bin/bash
########################################################################
# Copyright (c) Alex, 2013; All Rights Reserved #
# #
# LICENSE #
# GNU GPL, see the license file for details #
# #
# CONTACT #
# lkong@redhat.com #
# #
# For more information, please read README & man logview & logview -h #
########################################################################
version="logview 0.0
Copyright (C) 2013, Free Software Foundation, Inc.
This is free software. You may redistribute copies of it under the terms of
the GNU General Public License <http://www.gnu.org/licenses/gpl.html>.
There is NO WARRANTY, to the extent permitted by law."
# Global variables
# Config file. Any settings "key=value" written there will override the
# global_variables defaults
global_variables()
{
# Can use this variable anywhere & anytime, so be careful to use this variable.
tmpfile=$(mktemp /tmp/logview.XXXXXX)
# Set the default values
# 0-Open debug mode, 1--Close debug mode
DEBUG=1
# "error" message container
MSGINC=""
MSGS=""
# Write message to your terminal
notice=no
# Set the scan frequency
delay=3
# Set mail the "error" messages or not
mail=no
# Set the mail frequency, when mail_time not inculde a value, logview will send
# you a mail when "error" occured.
mail_time=""
#Set the tty to current tty
TTY=$(tty)
#Set default colors
back_color=""
font_color=""
font=""
#logview will exit when there is no "error" message beyond total_end_time
total_timeout_end=0
total_timeout_start=0
declare -a MONFILES
}
#Define color variables
def_colors()
{
#Feature
normal='\033[0m';bold='\033[1m';dim='\033[2m';under='\033[4m';
italic='\033[3m';noitalic='\033[23m';blink='\033[5m';
reverse='\033[7m';conceal='\033[8m';nobold='\033[22m';
nounder='\033[24m';noblink='\033[25m';
#Front color
black='\033[30m';red='\033[31m';green='\033[32m';yellow='\033[33m';
blue='\033[34m';magenta='\033[35m';cyan='\033[36m';white='\033[37m';
#background color
bblack='\033[40m';bred='\033[41m';
bgreen='\033[42m';byellow='\033[43m';
bblue='\033[44m';bmagenta='\033[45m';
bcyan='\033[46m';bwhite='\033[47m';
}
#Print colors you can ues
print_colors()
{
echo
echo -e ${bmagenta}--back-color:${normal}
echo "bblack; bgreen; bblue; bcyan; bred; byellow; bmagenta; bwhite"
echo
echo -e ${red}--font-color:${normal}
echo "black; red; green; yellow; blue; magenta; cyan; white"
echo
echo -e ${bold}--font:${normal}
echo "normal; italic; reverse; nounder; bold; noitalic; conceal; noblink;
dim; blink; nobold; under"
echo
}
# Print message to standerr
die()
{
echo "$@" >&2
exit 1
}
#Exit function
EXIT()
{
exit 0
}
# Just ignore this function (Unused)
Exit()
{
exitcode="$1"
shift
for arg
do
echo >&2 "logview: $1"
shift
done
exit ${exitcode}
}
# Check the argument associate with a option
requiredarg()
{
[ -z "$2" -o "$(echo $2 | awk '$0~/^-/{print 1}')" == "1" ] &&
die "$0: option $1 requires an argument"
((args++))
}
# Get file names from COMMAND LINE arguments
getfilenames()
{
for f in "$@"; do
#[[ -f $f ]] || die "$0: $f No such file found."
SOURCE="files"
FILES[${#FILES[*]}]="$f"
done
}
# Convert 5h/5m/5s to 18000s/300s/5s
# conv2seconds 5h
conv2seconds()
{
TIME="$(echo $1 | tr '[A-Z]' '[a-z]')"
TAG="$(echo $TIME | tr -d '[0-9]')"
NUM="$(echo $TIME | tr -d '[a-z]')"
case "$TAG" in
h )
SECOND=$((NUM * 3600))
;;
m )
SECOND=$((NUM * 60))
;;
s | "" )
SECOND=$NUM
;;
* )
SECOND=unknow
;;
esac
[ "$DEBUG" -eq 0 ] && echo "DEBUG: --timeout = $SECOND"
echo $SECOND
}
# Check the current time is start time or end time
# Before time return 1, start time return 2, match end time return 3
# Time format date +'%Y%m%d %T'
# checktime starttime endtime
# Current Unused
checktime()
{
# START_S=$(date +%s -d "$1")
END_S=$(date +%s -d "$1")
CURRENT_S=$(date +%s)
# [ "$DEBUG" -eq 0 ] && echo "START_S=$START_S END_S=$END_S CURRENT_S=$CURRENT_S"
[ "$DEBUG" -eq 0 ] && echo "DEBUG: END_S=$END_S CURRENT_S=$CURRENT_S"
#[ "$START_S" -ge "$END_S" ] && die "$0: --start-time is larger than --end-time"
# [ "$CURRENT_S" -lt "$START_S" ] && {
# [ "$DEBUG" -eq 0 ] && echo "DEBUG: Current time is BEFORE"
# return 1
# }
# [ "$CURRENT_S" -ge "$START_S" -a "$CURRENT_S" -lt "$END_S" ] && {
# [ "$DEBUG" -eq 0 ] && echo "DEBUG: Current time is START"
# return 2
# }
[ "$CURRENT_S" -ge "$END_S" ] && {
[ "$DEBUG" -eq 0 ] && echo "DEBUG: Current time is END"
#return 3
echo 0
} ||
{
[ "$DEBUG" -eq 0 ] && echo "DEBUG: Current time is NOT END"
echo 1
}
}
#Generate a report from input file and awk file
print()
{
:
}
# Print usage infomation
Usage()
{
cat << EOF
Usage: $0 [ OPTIONS ] FILE1 -f FILE2 [ OUTPUT ] ...
Monitor log files and email error messages.
FILE1 awk file you used to filter message. You can provide this
file to specify your own filter string.You can use '-a' option
to get a example awk file (awk.example).
FILE2 The file you need to monitor.
You can add -f FILE3 to monitor more than one files.
OUTPUT This is a optional option, logview will output the "error"
message to this file or the standard output if you don't
provide a file.
OPTIONS:
-h, --help usage information.
-m, --mail-list mail list, eg: ll@gmail.com,ff@gmail.com.
--max-record Set the max record number for [OUTPUT] file.(default unlimited)
--max-record=unlimited or --max-record=5000
-f The file you need to monitor.
-s, --scan-time Set the frequency you monitor the log file(default 3s)
-n, --notice Write message to your terminal.
--notice=no: disable logview send "error" messages to your terminal
If you don't provide a OUTPUT file, --notice will set to "no". Also
this is the default value.
--notice=one: only send the "error" messages to your current tty.
--notice=all: send the "error" to all your tty.
--mail-time Set the email frequency, 5h/5m/5s/5
default: mail you when "error" messages occured.
-v, --version Print the version message
-a Get the a example awk file: awk.example, you can build your own
awk file.
--timeout-start Set the timeout ...(5h/5m/5s/5)
--timeout-end Set the end timeout, (5h/5m/5s/5)
--end-time Give a accurate time to stop logview script, eg: "20131101 14:49:03"
--debug be *very* verbose (implies -v)
-c, --command-message When the "error" occured, this script will execut the command you
specify, the command should be a executable file.
--back-color background color, yellow & red & blue & ...
--font-color font color, red & greep & cyan & ...
--font font type, bold & italic & conceal & ...
-p, --print Print background & font color you can use.
-r, --report Make a simplay report --future feature
--format Generate a PDF/excel or txt format file --future feature
--format=pdf, --format=excel, --format=txt
With no OUTPUT, or when OUTPUT is -, output "error" messages to standout.
Reprot bugs to <lkong@redhat.com>.
EOF
}
#When the log file record number reach to the max number, this function will trancate it to
function file_record()
{
local str="'1,$(($1-$2))d'"
eval sed $str $errorfile > $tmpfile
cp -f $tmpfile $errorfile
rm -f $tmpfile
}
#Send the "error" message to your terminal in the following format
function warncat()
{
echo $(date +'%Y%m%d %T')"|" $MSGINC
}
#This function is used to deal the "error" message.
function deal()
{
# Execute a command when there is a "error" message
[ -n "$command_message" ] && "$command_message"
monfile="$(basename $2)'|'"
[ $FILENUM -eq 1 ] && monfile=""
#Set total_timeout_ent=0
total_timeout_end=0
: << EOF
[ -n "$errorfile" ] && echo "$(date +'%T') $monfile $1" >> $errorfile || {
eval echo -n "$(date +'%T')|$monfile"
eval echo -ne "$back_color"
eval echo -ne "$font_color""$font"'$1'
echo -e "${normal}"
}
EOF
[ -n "$errorfile" ] && {
echo "$(date +'%T')|$monfile""$1" >> $errorfile
echo "$(date +'%T')'|'$monfile"'$1' >> /tmp/logview.txt
} || {
eval echo -n "$(date +'%T')'|'$monfile"
echo "$(date +'%T')'|'$monfile"'$1' >> /tmp/logview.txt
eval echo -ne "$back_color"
eval echo -ne "$font_color""$font"'$1'
echo -e "${normal}"
}
MSGINC="$1"
[ -z "$maillist" ] && return 0
#send email to you
cur_time="$(date)"
[ -n "${mail_time}" ] && {
[ -s /tmp/logview.time ] && {
last_time="$(</tmp/logview.time)"
[ $(($(date +%s -d "$cur_time") - $(date +%s -d "$last_time"))) -ge $mail_time ] &&
{
#echo "$1" |
cat /tmp/logview.txt |
mail -s "$logfile have \"error\" messages, PLEASE CHECK IT!" $maillist
date > /tmp/logview.time
> /tmp/logview.txt
} || {
return 0
}
} || {
echo "$1" |
mail -s "$logfile have \"error\" messages, PLEASE CHECK IT!" $maillist
date > /tmp/logview.time
> /tmp/logview.txt
}
} || {
echo "$1" |
mail -s "$logfile have \"error\" messages, PLEASE CHECK IT!" $maillist
}
}
#Send "error" messages to terminal
tellb()
{
[ "$notice" == "no" ] && return 0
[ "$notice" == "one" ] && {
warncat "$MSGINC" | write "$(whoami)" "$TTY" &>/dev/null
}
[ "$notice" == "all" ] && {
warncat "$MSGINC" | wall $USERNAME
}
}
getawkfile()
{
cat << EOF > awk.example
#By default, this awk file will filter error and failed messages.
#You can modify or add your own filter error string by
# #If MATCH "error" or "failed", logview will filter the error message to you.
# if (match(lower, /error|failed/))
# {
# print \$0
# }
#
# #This is what you add:
# if (match(lower, /no such/))
# {
# print \$0
# }
# if (!match(lower, /\/bin\/mv:/))
# {
# print \$0
# }
#
{
lower=tolower(\$0);
if (match(lower, /error|failed/))
{
print \$0
}
}
EOF
}
# load global variables
global_variables
def_colors
### read cli options
# separate groups of short options. replace --foo=bar with --foo bar
while [[ -n $1 ]]; do
case "$1" in
-- )
for arg in "$@"; do
ARGS[${#ARGS[*]}]="$arg"
done
break
;;
--debug )
set -v
DEBUG=0
;;
--*=?* )
ARGS[${#ARGS[*]}]="${1%%=*}"
ARGS[${#ARGS[*]}]="${1#*=}"
;;
--* )
#die "$0: option $1 requires a value"
ARGS[${#ARGS[*]}]="$1"
;;
-* )
for shortarg in $(sed -e 's|.| -&|g' <<< "${1#-}"); do
ARGS[${#ARGS[*]}]="$shortarg"
done
;;
* )
ARGS[${#ARGS[*]}]="$1"
esac
shift
done
# set the separated options as input options.
set -- "${ARGS[@]}"
[ "$DEBUG" -eq 0 ] && echo "DEBUG: ARGS[@]: ${ARGS[@]}"
while [[ -n $1 ]]; do
((args=1))
case "$1" in
-- )
shift && getfilenames "$@" && break
;;
-h | --help )
Usage
exit 0
;;
-a )
getawkfile
exit 0
;;
-m | --mail-list )
requiredarg "$@"
maillist="$2"
;;
--max-record )
requiredarg "$@"
maxrecord=$2
;;
-s | --scan-time )
requiredarg "$@"
delay=$(conv2seconds "$2")
[ "$delay" == "unknow" ] && die "$0: Unavailable time format."
;;
-n | --notice )
requiredarg "$@"
notice=$2
;;
--mail-time )
requiredarg "$@"
mail_time=$(conv2seconds "$2")
[ "$mail_time" == "unknow" ] && die "$0: Unavailable time format."
;;
--start-time )
requiredarg "$@"
start_time=$2
;;
--end-time )
requiredarg "$@"
end_time=$2
;;
-r | --report )
requiredarg "$@"
reprot=$2
;;
--format )
requiredarg "$@"
format=$2
;;
--parse )
requiredarg "$@"
parse="$2"
;;
--timeout-start )
requiredarg "$@"
timeout_start=$(conv2seconds "$2")
[ "$timeout_start" == "unknow" ] && die "$0: Unavailable time format."
;;
--timeout-end )
requiredarg "$@"
timeout_end=$(conv2seconds "$2")
[ "$timeout_end" == "unknow" ] && die "$0: Unavailable time format."
;;
--back-color )
requiredarg "$@"
back_color=\${b$2}
;;
--font-color )
requiredarg "$@"
font_color=\${$2}
;;
--font )
requiredarg "$@"
font=\${$2}
;;
-f )
requiredarg "$@"
[ "$2" == "" ] && die "$0: no input file for '-f' option."
MONFILES[${#MONFILES[*]}]="$2"
;;
-c | --command-message )
requiredarg "$@"
command_message="$2"
;;
-p | --print )
print_colors
exit 0
;;
-v | --version )
echo "$version"
exit 0
;;
-* )
die "$0: unrecognized option '$1'"
;;
*)
getfilenames "$1"
;;
esac
shift $args
done
[ ${#MONFILES[*]} -le 0 ] && die "No input file. Nothing need to be monitored. Aborting.
Try logview -h to get help."
awkfile=${FILES[0]}
[ ! -r "$awkfile" ] && die "No awk file or file can not read. Aborting."
#monfile=${FILES[1]}
errorfile=${FILES[1]}
[ -z "$errorfile" ] && notice=no
#Why set LOGCHK like this? It is for future use. Using this variable, the script can have some COOL feature
LOGCHK="$monfile:ind:excstr:error"
logfile=$(echo $LOGCHK | cut -d: -f1)
#The following three variable are also for future use.
notify=$(echo $LOGCHK | cut -d: -f4)
suffix=$(echo $logfile | sed -e 's/\//_/g')
suffix=$(echo $suffix | sed -e 's/\./_/g')
[ $DEBUG -eq 0 ] && echo "DEBUG: MONFILES is: ${MONFILES[*]}"
#logview will pending until the monitor file exist or be created
while true
do
FLAG=""
[ -n "$timeout_start" ] && [ "$total_timeout_start" -ge "$timeout_start" ] && EXIT
for F in ${MONFILES[*]}
do
[ -f "$F" ] && FLAG=1
done
[ "$FLAG" == "1" ] && break
sleep 5
[ -n "$timeout_start" ] && total_timeout_start=$((total_timeout_start+5))
done
GrepAwk="awk -f $awkfile"
FILENUM=${#MONFILES[*]}
#If $tell == on, logview will allow write to your terminal automatic.
[ "$notice" == "one" ] && mesg y
while true
do
[ -n "$timeout_end" ] &&
[ "$total_timeout_end" -ge "$timeout_end" ] &&
EXIT
[ -n "$end_time" ] &&
[ $(checktime "$end_time") -eq 0 ] &&
EXIT
entry_count=$((0+1))
if [ -f "$errorfile" -a -n "$maxrecord" ];then
size=$(wc -l $errorfile | awk '{print $1}')
if [[ $size -gt $maxrecord ]];then
file_record $size $maxrecord
fi
fi
for ((i=0;i<FILENUM;i++))
do
if [ "$(eval echo '$COUNT'$i)" = "" ];then
[ -f "${MONFILES[i]}" ] &&
eval BASE$i=$(wc -l ${MONFILES[i]} 2>/dev/null| awk '{print $1}') ||
eval BASE$i=0
fi
done
sleep $delay
for ((i=0;i<FILENUM;i++))
do
[ -f "${MONFILES[i]}" ] &&
eval COUNT$i=$(wc -l ${MONFILES[i]} | awk '{print $1}') ||
eval COUNT$i=0
#eval declare -i comp$i=0
comp=$(($(eval echo '$COUNT'$i) - $(eval echo '$BASE'$i)))
if [ $comp -gt 0 ];then
LINES=$(eval expr '$COUNT'$i - '$BASE'$i)
eval BASE$i='$COUNT'$i
IFS=$'\n'
for MSGS in $(tail -$LINES ${MONFILES[i]}| eval "$GrepAwk")
do
[ $DEBUG -eq 0 ] && echo "DEBUG: \"error\" message is: [$MSGS]"
[ -n "$MSGS" ] && {
deal "$MSGS" "${MONFILES[i]}"
tellb
}
done
fi
done
#else
# [ $DEBUG -eq 0 ] && echo "DEBUG: No change in size of $logfile"
#fi
[ -n "$timeout_end" ] && total_timeout_end=$((total_timeout_end + delay))
done
EXIT