日志收集并提醒企业微信

功能描述:每天采集 系统操作日志 和 异常日志,系统异常日志发送到企业微信中;

发送的异常信息包括:异常来源(服务器IP)、异常内容、异常时间、异常描述;

代码实现

1. 在application.properties定义logging

logging:
config: classpath:logback-spring.xml

2. 定义logback-spring.xml文件

开发环境 直接跳过即可,生产环境 与 测试环境 需要 解开注释。
注意:一般在“生产环境(用户) 与 测试环境(一般与生产环境的配置相同,用于测试功能是否正确)” 才开启发送至企业微信的接口,开发环境(编码和调试)就不用解开注释。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property name="LOG_CONTEXT_NAME" value="log"/>
    <property name="LOG_HOME"
              value="src/main/resources/static/logs/${LOG_CONTEXT_NAME}"/>
    <!-- 定义日志上下文的名称 -->
    <contextName>${LOG_CONTEXT_NAME}</contextName>

    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!-- 将异常信息发送到企业微信,生产环境解除注释,以及下面的86、87、88行-->

<!--    <appender name="ALARM" class="WechatAlarmAppender的相对路径"/>-->

    <!--1. 输出到控制台-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--info日志统一输出到这里-->
    <appender name="file.info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <Prudent>true</Prudent>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/info/info.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <MaxHistory>30</MaxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56}.%method:%L - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>


<!--    错误日志统一输出到这里-->
    <appender name="file.error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <Prudent>true</Prudent>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名,按天生成-->
            <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/error/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>30</MaxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- 除按日志记录之外,还配置了日志文件不能超过10M(默认),若超过10M,日志文件会以索引0开始, -->
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %method 方法名  %L 行数 %msg:日志消息,%n是换行符,%throwable{full}输出完整日志-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56}.%method:%L - %msg%n%rEx{full}%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!-- 此日志文件只记录error级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!--异常信息发送至企业微信-->

<!--    <root level="INFO">-->
<!--        <appender-ref ref="ALARM"/>-->
<!--    </root>-->


    <!--  日志输出级别 -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="file.error"/>
        <appender-ref ref="file.info"/>
    </root>

</configuration>

3. 自定义LogOperation注解,这里不用修改。

/**
 * Description: 日志注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogOperation {

}

 4. 定义日志处理异常类,并将异常日志发送至企业微信,由于企业微信发送的消息文字数量有字数限制(4096位),企业微信发送的异常信息一般可以定位到异常所在位置以及原因,如果还找不到信息,可以在“resource/static/log/日期/error中查询详情信息”。

/**
 * Description: error日志处理类,将异常信息发送至企业微信。
 */
public abstract class AbstractAlarmAppender extends AppenderBase<LoggingEvent> {

    @Override
    protected void append(LoggingEvent eventObject) {
        try {
            Level level = eventObject.getLevel();
            if (Level.ERROR != level) {
                // 只处理error级别的报错
                return;
            }
            // 获取当前线程绑定的 HttpServletRequest 对象
            HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
            // 获取请求参数信息
            String data = getPostParameters(request);
            //构造消息通知内容
            String message = initMessage(eventObject,data);
            //发送消息通知
            monitor(message);
        } catch (Exception e) {
            addError("日志报警异常,异常原因:{}", e);
        }
    }

    /**
     * 拼接异常消息体
     */
    public String initMessage(LoggingEvent eventObject, String data) {
        // 获取异常堆栈信息
        IThrowableProxy proxy = eventObject.getThrowableProxy();
        String track = "";
        String trackMessage = "";

        if (proxy != null) {
            Throwable t = ((ThrowableProxy) proxy).getThrowable();
            // 获取完整的异常描述和堆栈信息
            track = t.toString();
            trackMessage = Arrays.toString(t.getStackTrace());
            int trackMessageMaxLength = 4000; // 留出一定长度以防超出
            if (trackMessage.length() > trackMessageMaxLength) {
                trackMessage = trackMessage.substring(0, trackMessageMaxLength);
            }
        }

        // 获取服务器的 IP 地址
        String ip = getLocalIpAddress();

        // 基本消息部分
        String loggerName = String.format("异常来源: %s", eventObject.getLoggerName());
        String formattedMessage = String.format("日志内容: %s", eventObject.getFormattedMessage());
        String exceptionTime = String.format("异常时间: %s", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        String exceptionDesc = String.format("异常描述: %s", track);

        // 构建基本消息部分
        String basicMessage = String.format("服务器IP: %s \n操作数据: %s \n%s \n%s \n%s \n%s",
                ip,
                data,
                loggerName,
                formattedMessage,
                exceptionTime,
                exceptionDesc);

        // 计算详细信息可以容纳的最大长度
        int maxLength = 4000;
        int remainingLength = maxLength - basicMessage.length();

        // 截取详细信息以适应剩余长度
        String detailedInfo = String.format("详细信息:\n%s", trackMessage);
        if (detailedInfo.length() > remainingLength) {
            detailedInfo = detailedInfo.substring(0, remainingLength);
        }

        // 构建最终消息
        String fullMessage = String.format("%s \n%s", basicMessage, detailedInfo);

        // 打印消息长度用于调试
        // System.out.println("Full message length: " + fullMessage.length());

        // 确保消息长度不超过 4096 字符
        return fullMessage.length() > maxLength ? fullMessage.substring(0, maxLength) : fullMessage;
    }



    protected abstract void monitor(String messageText);

    /**
     * 获取服务器IP
     */
    private String getLocalIpAddress() {
        try {
            // 获取所有网络接口(例如以太网、Wi-Fi 等)
            Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();

            // 遍历每个网络接口
            while (networkInterfaces.hasMoreElements()) {
                NetworkInterface networkInterface = networkInterfaces.nextElement();

                // 获取当前网络接口的所有 IP 地址(可以是 IPv4 或 IPv6)
                Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
                while (inetAddresses.hasMoreElements()) {
                    InetAddress inetAddress = inetAddresses.nextElement();

                    // 排除回环地址(127.0.0.1)和链路本地地址(通常用于自动配置本地网络)
                    if (!inetAddress.isLoopbackAddress() && !inetAddress.isLinkLocalAddress() && inetAddress.isSiteLocalAddress()) {
                        // 返回找到的有效局域网 IP 地址
                        return inetAddress.getHostAddress();
                    }
                }
            }

            // 如果遍历完所有网络接口后没有找到合适的 IP 地址,返回提示信息
            return "无法找到有效的IP地址";
        } catch (Exception e) {
            // 捕获异常并返回错误提示
            return "无法获取IP地址";
        }
    }


    /**
     * 获取post请求参数
     */
    private String getPostParameters(HttpServletRequest request) {
        Map<String, String> postParams = new HashMap<>();
        try {
            Enumeration<String> parameterNames = request.getParameterNames();

            while (parameterNames.hasMoreElements()) {
                String paramName = parameterNames.nextElement();
                String paramValue = request.getParameter(paramName);
                postParams.put(paramName, paramValue);
            }
        } catch (Exception e) {
            // 返回一个指示出错的消息,或者根据需要返回空字符串或其他值
            return "参数请求错误" + e.getMessage();
        }
        return postParams.toString();
    }
}

5. 这里需要将WEB_HOOK_URL改成自己所在企业微信群聊的机器人WEB_HOOK_URL,如果没有机器人创建一下即可。

/**
 * Description: 异常日志消息提醒到企业微信机器人
 */
public class WechatAlarmAppender extends AbstractAlarmAppender {

    // 这里替换为你的机器人的 webHookUrl
    private final String WEB_HOOK_URL = 企业微信机器人的WEB_HOOK_URL;

    /**
     * 可以改写 monitor 方法来实现给其他软件发送通知、或者发邮件
     *
     * @param messageText 消息文本
     */
    @Override
    protected void monitor(String messageText) {
        try {
            Map<String, Object> text = new HashMap<>();
            text.put("content", messageText);

            Map<String, Object> body = new HashMap<>();
            // 消息类型 这是是设置 markdown 类型
            body.put("msgtype", "markdown");
            body.put("markdown", text);
            // 调用企业微信接口发送消息
            String response = HttpUtils.sendPost(WEB_HOOK_URL, JSON.toJSONString(body), null);
            if (response != null) {
                System.out.println("消息发送成功,响应: " + response);
            } else {
                System.err.println("消息发送失败,响应为空");
            }
        } catch (Exception e) {
            System.err.println("消息发送失败,发生异常: " + e.getMessage());
        }
    }
}

6. 定义HTTP POST请求类,这里不用修改。

/**
 * Description: 发送 HTTP POST 请求
 */
public class HttpUtils {

    /**
     * @param urlString   请求的 URL
     * @param jsonPayload JSON 格式的请求负载
     * @param charset     字符集,默认为 UTF-8
     * @return 响应内容
     */
    public static String sendPost(String urlString, String jsonPayload, String charset) {
        HttpURLConnection connection = null;
        OutputStream os = null;

        try {
            // 创建 URL 对象
            URL url = new URL(urlString);

            // 打开连接
            connection = (HttpURLConnection) url.openConnection();

            // 设置请求方法为 POST
            connection.setRequestMethod("POST");

            // 设置请求头
            connection.setRequestProperty("Content-Type", "application/json; charset=" + (charset != null ? charset : "UTF-8"));
            connection.setDoOutput(true); // 允许输出数据

            // 将 JSON 负载写入输出流
            os = connection.getOutputStream();
            byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
            os.write(input, 0, input.length);
            os.flush(); // 刷新输出流

            // 获取响应代码
            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) { // 成功
                return readResponse(connection.getInputStream());
            } else {
                System.err.println("POST 请求失败,响应码: " + responseCode);
            }

        } catch (Exception e) {
            System.err.println("发送 POST 请求时发生异常: " + e.getMessage());
        } finally {
            try {
                if (os != null) {
                    os.close();
                }
                if (connection != null) {
                    connection.disconnect();
                }
            } catch (Exception ex) {
                System.err.println("关闭连接时发生异常: " + ex.getMessage());
            }
        }

        return null;
    }

    /**
     * 读取输入流并将其转换为字符串
     *
     * @param inputStream 输入流
     * @return 输入流内容的字符串表示
     */
    private static String readResponse(InputStream inputStream) {
        StringBuilder response = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
        } catch (Exception e) {
            System.err.println("读取响应时发生异常: " + e.getMessage());
        }
        return response.toString();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值