功能描述:每天采集 系统操作日志 和 异常日志,系统异常日志发送到企业微信中;
发送的异常信息包括:异常来源(服务器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();
}
}