JAVA基于Slack實(shí)現(xiàn)異常日志報警詳解
一、功能介紹
在我們?nèi)粘i_發(fā)中,如果系統(tǒng)在線上環(huán)境上,發(fā)生異常,開發(fā)人員不能及時知曉來修復(fù),可能會造成重大的損失,因此后端服務(wù)中加入異常報警的功能是十分必要的,而開發(fā)一個功能全面的異常報警服務(wù),可能會花費(fèi)較長的周期,今天給大家?guī)硪环N基于Slack實(shí)現(xiàn)異常日志報警的辦法。
實(shí)現(xiàn)邏輯:一般情況下,代碼中都會對會出現(xiàn)異常的地方,進(jìn)行處理,最基本的就是打印日志,本文將實(shí)現(xiàn)在打印日志時,同時將異常信息發(fā)送到Slack頻道中,開發(fā)或運(yùn)維人員創(chuàng)建Slack賬號,加入頻道,便可實(shí)時收到異常信息的告警。
二、Slack介紹
Slack 它是一種基于Web的實(shí)時通信工具,可作為臺式機(jī)/筆記本電腦、移動設(shè)備的單個應(yīng)用程序以及Web應(yīng)用程序使用?;旧?,它是您的私人聊天和協(xié)作室。對于許多公司而言,它已取代電子郵件/私人論壇/聊天室成為主要的內(nèi)部基于文本的溝通渠道。
可以理解為它是聊天群組 + 大規(guī)模工具集成 + 文件整合 + 統(tǒng)一搜索。截至2014年底,Slack 已經(jīng)整合了電子郵件、短信、Google Drives、Twitter、Trello、Asana、GitHub 等 65 種工具和服務(wù),可以把各種碎片化的企業(yè)溝通和協(xié)作集中到一起。幾個重要的概念:
工作區(qū):相當(dāng)去工作空間,用戶可以加入或者創(chuàng)建不同的工作區(qū),很多時候,工作區(qū)的名稱和URL將是公司名稱。
頻道:頻道可以區(qū)分為不同的團(tuán)隊(duì)或者主題,也可以理解成相當(dāng)于微信,頻道中的成員共享頻道中的信息。
三、前期準(zhǔn)備
slack配置
- 創(chuàng)建賬號,登錄,可以使用app或者用瀏覽器登錄網(wǎng)頁版
- 創(chuàng)建自己的工作區(qū),還可以邀請其他人加入工作區(qū)。
- 創(chuàng)建頻道,邀請同事加入,此時可以往頻道中發(fā)信息,加入頻道的人都可以看到信息
工作區(qū)添加應(yīng)用Incoming WebHook,選擇頻道,保存Webhook URL,后面將通過Webhook實(shí)現(xiàn)程序往頻道中發(fā)消息。




pom.xml
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
四、具體實(shí)現(xiàn)
1.實(shí)現(xiàn)Slack發(fā)送消息
SlackUtil 給Slack發(fā)消息工具類
package com.yy.operation;
import com.yy.common.CommonThreadFactory;
import com.yy.common.ConnUtil;
import org.apache.commons.lang.StringUtils;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author :Max
* @date :Created in 2022/8/26 下午12:54
* @description:
*/
public class SlackUtil {
private static final Logger logger = Logger.getLogger(SlackUtil.class.getCanonicalName());
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final String SEND_USER_NAME ="運(yùn)維機(jī)器人";
private static int MAX_RETRY =3;
/**
* 線程池 拋棄策略DiscardPolicy:這種策略,會默默的把新來的這個任務(wù)給丟棄;不會得到通知
*/
private static ExecutorService executor = new ThreadPoolExecutor(10,30,60,TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(200),new CommonThreadFactory("Slack"), new ThreadPoolExecutor.DiscardPolicy());
private static String MSG_FORMAT ="payload='{'"channel": "{0}", "username": "{1}", "text": "{2}", "icon_emoji": ":ghost:"'}'" ;
/**
* 保存的Webhook URL ,需要初始化
*/
private static String WEBHOOK_URL ;
private static boolean SLACK_ABLE;
public static void setSlackConfig(String webhookUrl){
WEBHOOK_URL = webhookUrl;
SLACK_ABLE = true;
}
/**
* slack異步發(fā)消息,保證不能影響到主功能
* @param channel
* @param msg
*/
public static void send(final String channel, final String msg){
if(!SLACK_ABLE){
return;
}
if(StringUtils.isBlank(msg)){
return;
}
executor.submit(new Runnable() {
@Override
public void run() {
try {
SlackUtil.send(channel,sdf.format(System.currentTimeMillis())+" "+msg,MAX_RETRY);
} catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
}
}
});
}
/**
* 如果slask發(fā)消息失敗,會最多嘗試發(fā)三次,三次都失敗,會打印異常信息
* @param channel
* @param msg
* @param retry
* @throws Exception
*/
public static void send(String channel, String msg, int retry) throws Exception {
if(msg.indexOf(""")>=0 ||msg.indexOf("{")>=0 ||msg.indexOf("}")>=0){
msg =msg.replace(""","'").replace("{","[").replace("}","]");
}
String payload = MessageFormat.format(MSG_FORMAT, channel,SEND_USER_NAME,msg);
String result = ConnUtil.getContentByPostWithUrlencode(WEBHOOK_URL,payload);
logger.info("result:"+result);
if(StringUtils.isEmpty(result) ||!result.startsWith("ok")){
--retry;
if(retry>0){
try {
TimeUnit.SECONDS.sleep(retry*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
send(channel,msg,retry);
}else{
throw new Exception("Fail to send slack:"+result+"\nmsg:"+msg);
}
}
}
}
向 webhook發(fā)起請求通過Urlencode
package com.yy.common;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author :Max
* @date :Created in 2022/8/26 下午1:44
* @description:
*/
public class ConnUtil {
private static final Logger logger = Logger.getLogger(ConnUtil.class.getCanonicalName());
public static String getContentByPostWithUrlencode(String url,String msg){
StringEntity entity = new StringEntity(msg, "UTF-8");
entity.setContentEncoding("UTF-8");
entity.setContentType(" application/x-www-form-urlencoded");
HttpClient httpClient = HttpClientBuilder.create().build();
HttpPost request = new HttpPost(url);
request.setEntity(entity);
HttpResponse response = null;
try {
response = httpClient.execute(request);
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null) {
InputStream instream = responseEntity.getContent();
BufferedReader reader = new BufferedReader(new InputStreamReader(instream));
StringBuffer contents = new StringBuffer();
String line = null;
while ((line = reader.readLine()) != null) {
contents.append(line);
contents.append("\n");
}
return contents.toString();
}
} catch (Exception ex) {
logger.log(Level.SEVERE, ex.getMessage(), ex);
}
return null;
}
}
SlackUtil測試
package com.yy.test;
import com.yy.common.SlackChannelEnum;
import com.yy.operation.SlackUtil;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
/**
* @author :Max
* @date :Created in 2022/8/28 下午2:37
* @description:
*/
public class SlackTest {
static {
SlackUtil.setSlackConfig("https://hooks.slack.com/services/*******");
}
@Test
public void test(){
SlackUtil.send(SlackChannelEnum.EXCEPTION.channel,"test ~");
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Assert.assertTrue(true);
}
}
發(fā)送成功,可以在頻道中看到信息

2.重寫打印日志類
常見異常打日志處理
public class LoggerTest {
private static final Logger logger = Logger.getLogger(LoggerTest.class.getCanonicalName());
@Test
public void test() {
try {
int i = 1 / 0;
} catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
}
}
}
重寫封裝打印日志的方法
package com.yy.operation;
import com.yy.common.SlackChannelEnum;
import org.apache.commons.lang.StringUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.text.MessageFormat;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
/**
* @author Max
* @date :Created in 2022/8/4 下午5:14
* @description:
*/
public class CommonLogger {
private Logger logger;
private CommonLogger(String className) {
logger = Logger.getLogger(className);
}
private static String SERVER;
private static String EXCEPTION_ALARM_FORMAT = "EXCEPTION 發(fā)生異常!\n環(huán)境 :{0}\n信息 :{1}\n詳情 :{2}";
private static String WARNING_ALARM_FORMAT = "WARNING 發(fā)生告警!\n環(huán)境 :{0}\n信息 :{1}";
private static String SEVERE_ALARM_FORMAT = "SEVERE 發(fā)生告警!\n環(huán)境 :{0}\n信息 :{1}";
private static String LOG_ALARM_FORMAT = "LOG 發(fā)生告警!\n環(huán)境 :{0}\n信息 :{1}";
private static String USER_BEHAVIOR_FORMAT = "CUSTOMER \n環(huán)境 :{0}\n信息 :{1}";
static {
try{
InetAddress ip4 = Inet4Address.getLocalHost();
SERVER = ip4.getHostAddress();
}catch (Exception e){
SERVER ="undefined server";
}
}
public static CommonLogger getLogger(String name) {
return new CommonLogger(name);
}
/**
* Print exception information, send slack
*
* @param level
* @param msg
* @param e
*/
public void log(Level level, String msg, Throwable e) {
if(StringUtils.isBlank(msg)){
return;
}
msg =dolog(level,msg, e);
msg = MessageFormat.format(EXCEPTION_ALARM_FORMAT, SERVER, formatMsg(msg), getErrmessage(e));
SlackUtil.send(SlackChannelEnum.EXCEPTION.channel, msg);
}
/**
* Print user behavior information, send slack
*
* @param msg
*/
public void userBehaviorInfo(String msg) {
if(StringUtils.isBlank(msg)){
return;
}
msg =dolog(Level.INFO,msg);
msg = MessageFormat.format(USER_BEHAVIOR_FORMAT, SERVER, formatMsg(msg));
SlackUtil.send(SlackChannelEnum.EXCEPTION.channel, msg);
}
public String formatMsg(String msg){
StringBuilder source =new StringBuilder(logger.getName());
msg=transferMsgSource(source,msg);
return source.toString()+" "+msg;
}
/**
* Print warning severe information, send slack
*
* @param msg
*/
public void severe(String msg) {
if(StringUtils.isBlank(msg)){
return;
}
msg = dolog(Level.SEVERE,msg);
msg = MessageFormat.format(SEVERE_ALARM_FORMAT, SERVER, formatMsg(msg));
SlackUtil.send(SlackChannelEnum.EXCEPTION.channel, msg);
}
/**
* Print warning severe information, send slack
*
* @param msg
*/
public void warning(String msg) {
if(StringUtils.isBlank(msg)){
return;
}
msg = dolog(Level.WARNING,msg);
msg = MessageFormat.format(WARNING_ALARM_FORMAT, SERVER, formatMsg(msg));
SlackUtil.send(SlackChannelEnum.EXCEPTION.channel, msg);
}
/**
* Print warning log information, send slack
*
* @param msg
*/
public void log(Level severe, String msg) {
if(StringUtils.isBlank(msg)){
return;
}
msg =dolog(severe,msg);
msg = MessageFormat.format(LOG_ALARM_FORMAT, SERVER, formatMsg(msg));
SlackUtil.send(SlackChannelEnum.EXCEPTION.channel, msg);
}
public static String getErrmessage(Throwable t) {
return getThrowable(t);
}
public void info(String msg) {
dolog(Level.INFO,msg);
}
public void fine(String msg) {
logger.fine(msg);
}
public void setLevel(Level level) {
logger.setLevel(level);
}
public String dolog(Level level, String msg) {
return dolog(level,msg,null);
}
/**
*
* @param level
* @param msg
* @param thrown
* @return msg="["+currentThread.getName()+"] "+a.getMethodName()+" "+msg;
*/
public String dolog(Level level, String msg, Throwable thrown) {
LogRecord lr = new LogRecord(level, msg);
lr.setLevel(level);
if(thrown!=null){
lr.setThrown(thrown);
}
Thread currentThread = Thread.currentThread();
StackTraceElement[] temp=currentThread.getStackTrace();
StackTraceElement a=(StackTraceElement)temp[3];
lr.setThreadID((int) currentThread.getId());
lr.setSourceClassName(logger.getName());
lr.setSourceMethodName(a.getMethodName());
lr.setLoggerName(logger.getName());
logger.log(lr);
return "["+currentThread.getName()+"] "+a.getMethodName()+" "+msg;
}
public static String getThrowable(Throwable e) {
String throwable = "";
if (e != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println();
e.printStackTrace(pw);
pw.close();
throwable = sw.toString();
}
return throwable;
}
public static String transferMsgSource(StringBuilder source,String msg){
if(msg.indexOf(" ")>0){
String threadName = msg.substring(0,msg.indexOf(" "))+ " ";
msg=msg.substring(threadName.length());
source.insert(0,threadName);
if(msg.indexOf(" ")>0) {
String method = msg.substring(0, msg.indexOf(" "));
source.append( "." + method);
msg = msg.substring(method.length()+1);
}
}
return msg;
}
}
package com.yy.operation;
import java.text.MessageFormat;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LoggerUtil {
private static Logger curLogger = Logger.getLogger(LoggerUtil.class.getCanonicalName());
private static ConcurrentHashMap<String, CommonLogger> loggers = new ConcurrentHashMap<String, CommonLogger>();
public static CommonLogger getLogger(Class<?> clazz) {
String className = clazz.getCanonicalName();
CommonLogger logger = loggers.get(className);
if (logger == null) {
logger = CommonLogger.getLogger(className);
curLogger.fine(MessageFormat.format("Register logger for {0}", className));
loggers.put(className, logger);
}
return logger;
}
}
測試日志類
定義日志類時發(fā)生改變,調(diào)用出的代碼無需更改,以較小的代價,集成異常報警功能
public class LoggerTest {
private static final Logger logger = Logger.getLogger(LoggerTest.class.getCanonicalName());
@Test
public void test() {
try {
int i = 1 / 0;
} catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
}
}
}
測試結(jié)果,頻道中出現(xiàn)打印的異常信息,方便開發(fā)運(yùn)維人員定位

五、優(yōu)化擴(kuò)展想法
- 可以不僅僅實(shí)現(xiàn)打印異常日志,也可以打印用戶的一些關(guān)鍵行為,如充值等,頻道可以設(shè)置多個,發(fā)送不同主題的消息
- 可以優(yōu)化線程池
- 如果開發(fā)人員不能及時查看slack,也可以集成電子郵件,Slack中可以添加mailclark應(yīng)用(單獨(dú)收費(fèi)),經(jīng)過配置后,發(fā)動頻道中的信息,可以自動郵件發(fā)送給任意郵箱,接受者無需創(chuàng)建slack賬號。具體配置可參考鏈接。
其他代碼
package com.yy.common;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author :Max
* @date :Created in 2022/8/26 下午1:51
* @description:
*/
public class CommonThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String threadNamePrefix;
private final String nameSpecific;
private final boolean isDaemon;
public CommonThreadFactory(String nameSpecific) {
this(nameSpecifihttps://juejin.cn/post/7136858841756467230#heading-4c, false);
}
public CommonThreadFactory(String nameSpecific, boolean isDaemon) {
SecurityManager s = System.getSecurityManager();
this.group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
this.threadNamePrefix = "eg-pool-" + poolNumber.getAndIncrement() + "-thread";
this.nameSpecific = nameSpecific;
this.isDaemon = isDaemon;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, String.format("%s-%d-%s",
this.threadNamePrefix, threadNumber.getAndIncrement(), this.nameSpecific), 0);
t.setDaemon(isDaemon);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
public enum SlackChannelEnum {
EXCEPTION("#test-example");
public String channel;
SlackChannelEnum(String channel) {
this.channel = channel;
}
}以上就是JAVA基于Slack實(shí)現(xiàn)異常日志報警詳解的詳細(xì)內(nèi)容,更多關(guān)于JAVA Slack異常日志報警的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java實(shí)現(xiàn)水仙花數(shù)的計(jì)算
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)水仙花數(shù)的計(jì)算,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-08-08
簡單談?wù)凷truts動態(tài)表單(DynamicForm)
下面小編就為大家?guī)硪黄唵握務(wù)凷truts動態(tài)表單(DynamicForm)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
java后臺實(shí)現(xiàn)js關(guān)閉本頁面,父頁面指定跳轉(zhuǎn)或刷新操作
這篇文章主要介紹了java后臺實(shí)現(xiàn)js關(guān)閉本頁面,父頁面指定跳轉(zhuǎn)或刷新操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
Java中的System.arraycopy()淺復(fù)制方法詳解
這篇文章主要介紹了Java中的System.arraycopy()淺復(fù)制方法詳解,Java數(shù)組的復(fù)制操作可以分為深度復(fù)制和淺度復(fù)制,簡單來說深度復(fù)制,可以將對象的值和對象的內(nèi)容復(fù)制;淺復(fù)制是指對對象引用的復(fù)制,需要的朋友可以參考下2023-11-11
詳解application.properties和application.yml文件的區(qū)別
這篇文章主要介紹了詳解application.properties和application.yml文件的區(qū)別,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01

