先看下效果,我主要做了时间,ip,日志消息和 curl 等内容的推送,方便我们系统出现异常可以及时发现及处理。
第一步:邀请自定义机器人入群
进入你的目标群组,打开会话设置,找到群机器人,并点击添加机器人,选择自定义机器人加入群聊
修改机器人基本信息
第二步:配置 webhook
你会获取该机器人的 webhook 地址,格式如下:
第三步:调用webhook发送消息
用任意方式向该 webhook 发起 HTTP POST 请求,即可向这个自定义机器人所在的群聊发送消息。
如请求成功,返回体为:
{
"Extra": null,
"StatusCode": 0,
"StatusMessage": "success"
}
接下来就是 java 代码的编写
这里我的思路是把每条异常的日志加上 id通过 logstash推送到 ES,这样在消息推送到飞书群就可以通过查看详情的按钮请求到完整的异常堆栈,不会显得消息太臃肿。
(1)添加一个 filter,在 filter里面把我们需要的 logId加到每个 request,
@Component
public class MDCFilter implements Filter {
private static final String LOG_ID = "logId";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain chain) throws IOException, ServletException {
MDC.put(LOG_ID, UUID.randomUUID().toString().trim().replaceAll("-", "").substring(0, 16));
try {
// 将HttpServletRequest实例存储在ThreadLocal中
if (servletRequest instanceof HttpServletRequest) {
FlyBookAppender.setHttpServletRequest((HttpServletRequest) servletRequest);
}
chain.doFilter(servletRequest, response);
} finally {
FlyBookAppender.clearHttpServletRequest();
OrderByInterceptor.clearSqlHolder();
MDC.clear();
}
}
@Override
public void destroy() {
}
}
(2)新增一个LogUniqueIdConverter,在ILoggingEvent中让他获取到logId, 这样这个 logId就可以贯穿整个请求,在后面可以为我们使用。
public class LogUniqueIdConverter extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
return event.getMDCPropertyMap().get("logId");
}
}
(3)在 logback-spring.xml中去到这个 logId 并且拼到日志等级为 ERROR 的日志中一起推送到ES
这样带着 logId的错误日志将会被我们推送到 ES
(4)异常消息推送到飞书的具体操作
我的思路是通过继承AppenderBase<ILoggingEvent>对异常日志进行操作,完整代码如下
@Setter
@Component
public class FlyBookAppender extends AppenderBase<ILoggingEvent> {
// 这个是机器人的webhook地址,我放在 yml 了
private String alertUrl;
@Value("${server.ip}")
private String ip;
@Value("${server.port}")
private String port;
private static String addressId;
private static String addressPort;
@PostConstruct
public void init() {
addressId = this.ip;
addressPort = this.port;
}
private static ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>();
public static void setHttpServletRequest(HttpServletRequest request) {
requestHolder.set(request);
}
public static HttpServletRequest getHttpServletRequest() {
return requestHolder.get();
}
public static void clearHttpServletRequest() {
requestHolder.remove();
}
@Override
protected void append(ILoggingEvent event) {
String logId = event.getMDCPropertyMap().get("logId");
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
String message = event.getFormattedMessage();
Throwable throwable = event.getThrowableProxy() != null ? ((ThrowableProxy) event.getThrowableProxy()).getThrowable() : null;
String exceptionType = null;
// 业务异常不推送
if (throwable != null) {
if (throwable instanceof BizException) {
exceptionType = ((BizException) throwable).getCode();
}
}
if ("BIZ_EXCEPTION".equals(exceptionType) || StringUtils.isBlank(logId) || !Level.ERROR.equals(event.getLevel())) {
return;
}
if (event.getLoggerName().contains("aop.LogRecordInterceptor") || (StrUtil.isNotBlank(message) && message.contains("log record execute"))) {
return;
}
String sql = throwable instanceof SQLException ? OrderByInterceptor.getSql() : "/";
HttpServletRequest request = getHttpServletRequest();
String curlCommand = getCurlCommand(request);
String requestId = request != null ? IpUtils.getIP(request) : null;
// 异步推送错误信息
CompletableFuture.runAsync(() -> pushToFlyBook(logId, date, message, throwable, sql, requestId, curlCommand));
}
private void pushToFlyBook(String logId, String date, String message, Throwable throwable, String sql, String requestId, String curlCommand) {
String hostIp = IpUtils.getHostIp();
String imgKey = getFlyBookImgKey();
String ipAddr = AddressUtils.getRealAddressByIP(requestId);
String stack = "无";
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter, true));
stack = stringWriter.toString();
FlyBookCardMessage cardMessage = new FlyBookCardMessage(new FlyBookCardMessageContent(
FlyBookCardMessageConfig.DEFAULT,
new FlyBookCardMessageHeader(
new FlyBookCardMessageHeaderTitle("错误日志告警"),
FlyBookCardMessageHeader.ERROR
),
Lists.newArrayList(
new FlyBookCardMessageImgElement(
new FlyBookCardMessageTextElement("", "plain_text"), imgKey, "img"
),
new FlyBookCardMessageTextElement(
String.format("%s [%s]", date, logId), "markdown"
),
new FlyBookCardMessageTextElement(
String.format("%s", ipAddr), "markdown"
),
new FlyBookCardMessageTextElement(
String.format("%s", hostIp), "markdown"
),
new FlyBookCardMessageTextElement(
"**日志消息:**" + message, "markdown"
),
new FlyBookCardMessageTextElement(
"**sql:**" + sql, "markdown"
),
new FlyBookCardMessageTextElement(
"**异常堆栈:**" + stack.substring(0, 100), "markdown"
),
new FlyBookCardMessageTextElement(
"**curl:**" + curlCommand, "markdown"
),
new FlyBookCardMessageButtonElement(
"action", logId)
)
));
HttpRequest.post(this.alertUrl).body(JSONUtil.parseObj(cardMessage).toString(), "application/json;charset=UTF-8").execute().body();
}
public static String getFlyBookImgKey() {
ILbpAttachmentService lbpAttachmentService = SpringContextUtil.getBean(ILbpAttachmentService.class);
LbpAttachmentVO attachmentVO = lbpAttachmentService.getRandomDataByBizName("flyBookImg");
if (attachmentVO == null) {
return null;
}
return attachmentVO.getBizId();
}
@Data
static abstract class FlyBookBootMessage implements Serializable {
private final String msg_type;
}
@EqualsAndHashCode(callSuper = true)
@Getter
static class FlyBookCardMessage extends FlyBookBootMessage {
private final FlyBookCardMessageContent card;
public FlyBookCardMessage(FlyBookCardMessageContent card) {
super("interactive");
this.card = card;
}
}
@Data
static class FlyBookCardMessageContent implements Serializable {
private final FlyBookCardMessageConfig config;
private final FlyBookCardMessageHeader header;
private final List<FlyBookCardMessageElement> elements;
}
@Data
@AllArgsConstructor
static class FlyBookCardMessageConfig implements Serializable {
public static final FlyBookCardMessageConfig DEFAULT = new FlyBookCardMessageConfig(true, true);
private boolean wideScreenMode;
private boolean enableForward;
}
@Data
static class FlyBookCardMessageHeader implements Serializable {
public static final String ERROR = "red";
public static final String WARNING = "orange";
public static final String SUCCESS = "green";
public static final String PRIMARY = "blue";
public static final String GREY = "grey";
private final FlyBookCardMessageHeaderTitle title;
private final String template;
}
@Data
static class FlyBookCardMessageHeaderTitle implements Serializable {
private final String tag = "plain_text";
private final String content;
}
@Data
static abstract class FlyBookCardMessageElement implements Serializable {
private final String tag;
}
@Getter
@EqualsAndHashCode(callSuper = true)
static class FlyBookCardMessageTextElement extends FlyBookCardMessageElement {
private final String content;
public FlyBookCardMessageTextElement(String content, String tag) {
super(tag);
this.content = content;
}
}
/**
* 图片
*/
@Getter
@EqualsAndHashCode(callSuper = true)
static class FlyBookCardMessageImgElement extends FlyBookCardMessageElement {
private final String img_key;
private final FlyBookCardMessageTextElement alt;
public FlyBookCardMessageImgElement(FlyBookCardMessageTextElement alt, String imgKey, String tag) {
super(tag);
this.img_key = imgKey;
this.alt = alt;
}
}
/**
* 按钮
*/
@Getter
@EqualsAndHashCode(callSuper = true)
static class FlyBookCardMessageButtonElement extends FlyBookCardMessageElement {
private final List<FlyBookCardMessageActionsElement> actions;
public FlyBookCardMessageButtonElement(String tag, String logId) {
super(tag);
String ipAddr = String.format("%s:%s", addressId, addressPort);
if ("xx.xx.xx.xx".equals(addressId) || "xx.xx.xx.xx".equals(addressId)) {
ipAddr = addressId;
}
this.actions = new ArrayList<>();
actions.add(new FlyBookCardMessageActionsElement("button",
new FlyBookCardMessageTextElement("查看详情", "plain_text"),
"primary", String.format("http://%s/xx/xx/xx/getLogFromEs?logId=%s", ipAddr, logId)));
}
}
@Data
static class FlyBookCardMessageActionsElement {
private final String tag;
private final FlyBookCardMessageTextElement text;
private final String type;
private final String url;
public FlyBookCardMessageActionsElement(String tag, FlyBookCardMessageTextElement text, String type, String url) {
this.tag = tag;
this.text = text;
this.type = type;
this.url = url;
}
}
private String getCurlCommand(HttpServletRequest request) {
String requestBody = null;
StringBuilder stringBuilder = new StringBuilder();
String line;
try {
BufferedReader reader = request.getReader();
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
requestBody = stringBuilder.toString();
} catch (Exception e) {
return null;
}
List<String> paramsList = new ArrayList<>();
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
String k = entry.getKey();
paramsList.add(k + "=" + request.getParameterMap().get(k)[0]);
}
String method = request.getMethod() + " ";
String params = String.join("&", paramsList);
String url = request.getRequestURL().toString();
String headers = Collections.list(request.getHeaderNames())
.stream()
.map(name -> "-H '" + name + ": " + request.getHeader(name) + "'" + "\n")
.reduce("", (acc, item) -> acc + " " + item);
String dataRaw = "--data-raw $'" + requestBody + "'";
if (StrUtil.isNotBlank(params)) {
url = url + "?" + params;
}
String curlCommand = "curl -X " + method + "'" + url + "'" + "\n" + headers;
if (StrUtil.isNotBlank(requestBody)) {
curlCommand = curlCommand + dataRaw;
}
return curlCommand;
}
}
(5)实现ApplicationListener重写onApplicationEvent把我们刚刚的FlyBookAppender添加到 Logger 里面
@Component
@Slf4j
@RequiredArgsConstructor
@EnableConfigurationProperties(FlyBookLoggerAppenderConfigProperties.class)
public class FlyBookLoggerAppenderInitializer implements ApplicationListener<ApplicationReadyEvent> {
private final FlyBookLoggerAppenderConfigProperties properties;
/**
* Handle an application event.
*
* @param event the event to respond to
*/
@Override
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
if (!properties.getEnabled()) {
return;
}
// 添加飞书日志Appender
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
FlyBookAppender flyBookAppender = new FlyBookAppender();
flyBookAppender.setName("FLY_BOOK");
flyBookAppender.setAlertUrl(this.properties.getAlertUrl());
flyBookAppender.setContext(loggerContext);
flyBookAppender.start();
Logger logger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
logger.addAppender(flyBookAppender);
}
}
这样就可以实现往飞书群机器人推送我们系统的异常消息