Logback-异常日志企业微信机器人通知

在生产环境中,异常日志往往涉及系统故障、接口调用失败或关键业务流程异常,必须及时被相关人员关注并处理。将异常日志实时推送到企业微信,可以确保运维团队和开发人员第一时间收到警报,避免因日志沉淀在服务器而错过关键问题。相比于传统的邮件或短信告警,企业微信具备更高的即时性,并支持群聊讨论,提高协作效率。此外,企业微信的 Webhook 机制使得日志推送简单灵活,能够根据日志级别、异常类型等条件筛选重要日志推送,减少无效告警,提高告警的精准度和响应速度。

如果需要告警钉钉调整机器人接口即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
public class WecomLogAlertAppender extends AppenderBase<ILoggingEvent> {
/**
* 抛弃策略的线程池
*/
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(2, 8, 60_000, TimeUnit.MILLISECONDS
, new LinkedBlockingQueue<>(1000), (Runnable r) -> {
Thread thread = new Thread(r);
thread.setName("WeChatAppender-" + thread.getId());
return thread;
}, new ThreadPoolExecutor.AbortPolicy());
/**
* 初始化OkHttp客户端
*/
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
.connectTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
private static Environment environment;
private final ObjectMapper mapper = new ObjectMapper();
private final String webhookUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=";

/**
* 获取需要推送的日志级别
*/
private static Set<Level> getLogLevels() {
String levelConfig = Optional.ofNullable(getEnvironment()).map(env -> env.getProperty("whisper.log.levels")).orElse("ERROR");
return Arrays.stream(levelConfig.split(","))
.map(String::trim)
.map(String::toUpperCase)
.map(Level::toLevel)
.collect(Collectors.toSet());
}

/**
* 双重检查锁定模式获取environment
*
* @return
*/
private static Environment getEnvironment() {
if (environment == null) {
synchronized (WecomLogAlertAppender.class) {
if (environment == null) {
environment = SpringContextUtil.getBean(Environment.class);
}
}
}
return environment;
}

/**
* 获取企业微信机器人key
*
* @return
*/
private static String getRobotKey() {
if (Objects.isNull(getEnvironment())) {
return null;
}
return getEnvironment().getProperty("whisper.robotKey");
}

/**
* 获取是否启用播报企业微信
*
* @return
*/
private static boolean getEnable() {
if (Objects.isNull(getEnvironment())) {
return Boolean.FALSE;
}
try {
return Boolean.parseBoolean(getEnvironment().getProperty("whisper.enable"));
} catch (Exception e) {
return Boolean.FALSE;
}
}

/**
* 获取企业微信机器人key
*
* @return
*/
private static String getProfiles() {
if (Objects.isNull(getEnvironment())) {
return null;
}
return getEnvironment().getProperty("spring.profiles.active");
}

@Override
protected void append(ILoggingEvent event) {
if (!getEnable()) {
return;
}
if (StringUtils.isBlank(getRobotKey())) {
return;
}
// 仅发送配置的日志级别
if (!getLogLevels().contains(event.getLevel())) {
return;
}
THREAD_POOL_EXECUTOR.execute(() -> {
try {
String json = mapper.writeValueAsString(createMessage(event));
RequestBody body = RequestBody.create(json, MediaType.get("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(webhookUrl + getRobotKey())
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
addError("Failed to send log message to WeChat:", new Exception(response.body().string()));
}
}
} catch (JsonProcessingException e) {
addError("Failed to convert log message to JSON", e);
} catch (Exception e) {
addError("Failed to send log message to WeChat", e);
}
});
}

private Map<String, Object> createMessage(ILoggingEvent event) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String exceptionMessage = getExceptionMessage(event);

// 根据日志级别设置颜色
String color;
if (event.getLevel() == Level.ERROR) {
color = "#F24956"; // 红色
} else if (event.getLevel() == Level.WARN) {
color = "#F2AA27"; // 黄色
} else if (event.getLevel() == Level.INFO) {
color = "#000000"; // 黑色
} else { // DEBUG 及其他
color = "#A0A0A0"; // 浅灰色
}

String content = String.format("[%s] <font color=\"%s\">**%s**</font> %s\n> %s \n%s",
getProfiles(), color, event.getLevel(), sdf.format(event.getTimeStamp()),
event.getFormattedMessage(),
exceptionMessage.substring(0, Math.min(exceptionMessage.length(), 1000)));

Map<String, Object> message = new HashMap<>();
message.put("msgtype", "markdown");
Map<String, String> markdown = new HashMap<>();
markdown.put("content", content.substring(0, Math.min(content.length(), 4095)));
message.put("markdown", markdown);

return message;
}

private String getExceptionMessage(ILoggingEvent event) {
IThrowableProxy throwableProxy = event.getThrowableProxy();
if (throwableProxy != null) {
StringBuilder sb = new StringBuilder();
sb.append(throwableProxy.getClassName()).append(": ").append(throwableProxy.getMessage()).append("\n");
for (StackTraceElementProxy element : throwableProxy.getStackTraceElementProxyArray()) {
sb.append(element.toString()).append("\n");
}
return sb.toString();
}
return "";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Component
public class SpringContextUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext;

/**
* 通过bean名称获取bean
*
* @param beanName bean的名称
* @param <T> bean的类型
* @return 获取到的bean
*/
public static <T> T getBean(String beanName, Class<T> beanType) {
return applicationContext.getBean(beanName, beanType);
}

/**
* 通过bean名称获取bean
*
* @param beanName bean的名称
* @return 获取到的bean
*/
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}

/**
* 通过bean类型获取bean
*
* @param beanType bean的类型
* @param <T> bean的类型
* @return 获取到的bean
*/
public static <T> T getBean(Class<T> beanType) {
return applicationContext.getBean(beanType);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextUtil.applicationContext = applicationContext;
}
}

编程式配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class LogbackConfigurer implements ApplicationListener<ContextRefreshedEvent> {

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
WecomLogAlertAppender wecomAppender = new WecomLogAlertAppender();
wecomAppender.start();
Logger rootLogger = context.getLogger("ROOT");
rootLogger.addAppender(wecomAppender);
}

}

声明式配置

logback.xml

1
2
3
4
5
<appender name="WECOM" class="com.****.WecomLogAlertAppender"></appender>

<root level="INFO">
<appender-ref ref="WECOM"/>
</root>

简单使用

application.properites

1
2
3
4
#企业微信日志告警配置
whisper.robotKey=13xx18b-xxxx-0ecaxxxee36
whisper.enable=true
whisper.log.levels=WARN,ERROR

Logback-异常日志企业微信机器人通知
https://cason.work/2025/02/11/Logback-异常日志企业微信机器人通知/
作者
Cason Mo
发布于
2025年2月11日
许可协议