背景

监控异常日志是一个很重要的事情,如何快速简单的通过一个脚本实现呢?今天我们来一起探索一下吧!我这里部署了一个springboot3+jdk17的web服务,日志格式如下所示:

2024-03-19 11:14:02.628 [http-nio-8080-exec-10] ERROR c.u.f.exception.GlobalExceptionHandler - 
org.springframework.web.servlet.resource.NoResourceFoundException: No static resource .
        at org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(ResourceHttpRequestHandler.java:585)
        at org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter.handle(HttpRequestHandlerAdapter.java:52)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)

我的目的是把这完整的错误日志通过飞书的webhook 进行发送。

实现过程

通过tail -f 监听异常日志的,这里假定日志文件:logs/error.log,这个文件里面的日志都是异常日志,当然你也可以加各种过滤条件, 通过和ai助手交流,写出的第一个脚本如下:

tail -n 0 -f "logs/error.log" | awk '
    BEGIN {
        RS="[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]+ \\[.*\\] "  # 设置记录分隔符为时间戳开头的模式
    }
    {
            print "Record Separator:", RT
            print "Record:", RT $0  # 打印记录时包含记录分隔符
    }'

简单解释下:就是通过 tail -n 0 -f 实时显示新增的内容,然后通过管道 | 使用awk 的 BEGIN 设置分隔符,这样就可以打印出完整的异常日志,但是测试中发现,只能打印上一条日志,最新的由于没有新的分隔符出现,导致无法打印出来,于是开始研究awk有没有后超时机制,长时间无法等到分隔符的时候,打印最后的日志行,遗憾的是我并没有知道合适的超时机制。

思来想去,找到的解决方案就是起一个子线程去监听最后一次收到消息的时间,如果大于规定的时间(比如10s),则打印这些日志,打印后清空缓存的日志,线程之间传递数据,我这边选择使用管道,接着就是调试代码了。

完成日志监控

开始写代码之前,我发现了read可以加超时时间,于是有了下面的脚本代码。

#!/bin/bash

monitor_logs() {
    # 监控日志输入,并处理日志输入
    tail -n 0 -f "logs/error.log" | (
        while true; do
            # 初始化 pre_log
            prev_log=""
            while IFS= read -r -t 10 line; do
                if [[ $line =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+ ]]; then
                    if [[ -n $prev_log ]]; then
                        echo "Record: $prev_log"  # 输出一条日志记录
                        prev_log=""
                    fi
                fi
                prev_log="$prev_log$line"
                
            done
            echo "timeout Record: $prev_log" # 输出一条日志记录
        done
    )
}

monitor_logs

这里发现没有没有换行符,需要再在合并日志行的时候加上$'\n' 就可以了,超时的时候还需要加下判断是否有prev_log,同时需要注意,--follow=name,如果直接使用-f 是以描述符监听文件,因为我们的日志会滚动,索引应该使用 --follow=name 最终代码如下

#!/bin/bash

monitor_logs() {
    # 监控日志输入,并处理日志输入
    tail -n 0 --follow=name "logs/error.log" | (
        while true; do
            # 初始化 pre_log
            prev_log=""
            while IFS= read -r -t 10 line; do
                if [[ $line =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+ ]]; then
                    if [[ -n $prev_log ]]; then
                        # 这里可以进行通过飞书webhook进行通知
                        echo "Record: $prev_log"  # 输出一条日志记录
                        prev_log=""
                    fi
                fi
                prev_log="$prev_log$line"$'\n'
            done
            if [[ -n $prev_log ]]; then
                # 这里可以进行通过飞书webhook进行通知
                echo "timeout Record: $prev_log" 
            fi
        done
    )
}
monitor_logs

换行符正常输出

至此,已经完成了脚本,代码比较简单,希望可以给大家提供一个思路,监控任重道远,AI让我们获取知识的途径又多了一种。

飞书机器人

写这部分主要是因为一点,发文本消息的时候换行符、制表符等会造成发送失败,下面是一个可用的消息发送,主要逻辑是过滤掉 \t ,并替换\n 让消息可读行更好,希望对大家有帮助。

# 发送飞书通知
send_feishu_notification() {
    local message="$1"
    message=$(echo "$message" |  tr -d '\t')
    message="${message//$'\n'/\\\n}"
    # 发送通知到飞书 webhook
    curl -X POST -H "Content-Type: application/json" -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"$message\"}}" "$FEISHU_WEBHOOK_URL"
}

结束!