JAVA基于Slack实现异常日志报警

2023-11-15

一、功能介绍

在我们日常开发中,如果系统在线上环境上,发生异常,开发人员不能及时知晓来修复,可能会造成重大的损失,因此后端服务中加入异常报警的功能是十分必要的,而开发一个功能全面的异常报警服务,可能会花费较长的周期,今天给大家带来一种基于Slack实现异常日志报警的办法。

实现逻辑:一般情况下,代码中都会对会出现异常的地方,进行处理,最基本的就是打印日志,本文将实现在打印日志时,同时将异常信息发送到Slack频道中,开发或运维人员创建Slack账号,加入频道,便可实时收到异常信息的告警。

二、Slack介绍

Slack 它是一种基于Web的实时通信工具,可作为台式机/笔记本电脑、移动设备的单个应用程序以及Web应用程序使用。基本上,它是您的私人聊天和协作室。对于许多公司而言,它已取代电子邮件/私人论坛/聊天室成为主要的内部基于文本的沟通渠道。可以理解为它是聊天群组 + 大规模工具集成 + 文件整合 + 统一搜索。截至2014年底,Slack 已经整合了电子邮件、短信、Google Drives、Twitter、Trello、Asana、GitHub 等 65 种工具和服务,可以把各种碎片化的企业沟通和协作集中到一起。几个重要的概念:

工作区:相当去工作空间,用户可以加入或者创建不同的工作区,很多时候,工作区的名称和URL将是公司名称。

频道:频道可以区分为不同的团队或者主题,也可以理解成相当于微信,频道中的成员共享频道中的信息。

三、前期准备

slack配置

  1. 创建账号,登录,可以使用app或者用浏览器登录网页版
  2. 创建自己的工作区,还可以邀请其他人加入工作区。
  3. 创建频道,邀请同事加入,此时可以往频道中发信息,加入频道的人都可以看到信息
  4. 工作区添加应用Incoming WebHook,选择频道,保存Webhook URL,后面将通过Webhook实现程序往频道中发消息。

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>
复制代码

四、具体实现

1.实现Slack发送消息

SlackUtil 给Slack发消息工具类

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 ="运维机器人";

    private static int MAX_RETRY =3;


    /**
     * 线程池 抛弃策略DiscardPolicy:这种策略,会默默的把新来的这个任务给丢弃;不会得到通知
      */
    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异步发消息,保证不能影响到主功能
      * @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发消息失败,会最多尝试发三次,三次都失败,会打印异常信息
      * @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发起请求通过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);
    }
}
复制代码

发送成功,可以在频道中看到信息

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 发生异常!\n环境 :{0}\n信息 :{1}\n详情 :{2}";

    private static String WARNING_ALARM_FORMAT = "WARNING 发生告警!\n环境 :{0}\n信息 :{1}";

    private static String SEVERE_ALARM_FORMAT = "SEVERE 发生告警!\n环境 :{0}\n信息 :{1}";

    private static String LOG_ALARM_FORMAT = "LOG 发生告警!\n环境 :{0}\n信息 :{1}";

    private static String USER_BEHAVIOR_FORMAT = "CUSTOMER \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;
   }
}
复制代码

测试日志类

定义日志类时发生改变,调用出的代码无需更改,以较小的代价,集成异常报警功能

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);
        }
    }
}
复制代码

测试结果,频道中出现打印的异常信息,方便开发运维人员定位

五、优化扩展想法

  1. 可以不仅仅实现打印异常日志,也可以打印用户的一些关键行为,如充值等,频道可以设置多个,发送不同主题的消息
  2. 可以优化线程池
  3. 如果开发人员不能及时查看slack,也可以集成电子邮件,Slack中可以添加mailclark应用(单独收费),经过配置后,发动频道中的信息,可以自动邮件发送给任意邮箱,接受者无需创建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(nameSpecific, 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;
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JAVA基于Slack实现异常日志报警 的相关文章

随机推荐

  • Qt之QGraphicsView实战篇

    作者 billy 版权声明 著作权归作者所有 商业转载请联系作者获得授权 非商业转载请注明出处 前言 前面的章节介绍了 Graphics View 绘图架构 终于到实战了 真的是千呼万唤始出来 这一章节就用 Graphics View 绘图
  • 第二章:Matplotlib之艺术画笔见乾坤

    第二章 艺术画笔见乾坤 一 概述 1 matplotlib的三层api matplotlib的原理或者说基础逻辑是 用Artist对象在画布 canvas 上绘制 Render 图形 就和人作画的步骤类似 准备一块画布或画纸 准备好颜料 画
  • Tomcat重启单个服务

    一个tomcat中有多个服务 每次为了重启某个服务 需要重新启动Tomcat 会影响其他服务的正常运行 可通过以下方式进行设置 进入Tomcat服务管理页面 对单个服务进行管理 1 首先我们启动tomcat 访问主页 可以看到右上位置有三个
  • MYSQL--基础--10--慢查询日志

    MYSQL 基础 10 慢查询日志 1 是什么 是MySQL提供的一种日志记录 支持将日志记录写入文件 将SQL查询时间超过 大于 设置阈值的语句 记录到慢查询日志中 2 查看语句 2 1 慢日志是否开启和日志文件位置 show varia
  • 【yolo】yolov5-seg实例分割标签

    文章目录 前言 yolo标签 yolov5 seg标签 转换代码 前言 不同于以往的yolo数据 用的是目标框的label 再加上语义标签 yolov5 seg中用的标签只有一个语义标签 没有目标框的标签 具体对比如下 yolo标签 1 目
  • java 获取月份 几周_java – 我想在特定的月份获得几周的时间

    Calendar类的WEEK OF YEAR属性可能对您有用 参考 Calendar class 创建一个新的日期 将是一个月的第一天 得到这一天的一周的这个星期 让你说起点价值 创建一个新的日期 这将是给定月份的最后一天 获得今年的一周
  • FastCFS核心组件FastStore架构及特点

    FastCFS核心组件FastStore架构及特点 本篇文章转载于 FastCFS 作者 余庆 大佬的 FastDFS分享与交流 公众号 上一篇文章介绍了 FastCFS 服务端两大核心组件 FastDIR 和 FastStore 其中 F
  • python文本数据处理_Python文本数据分析与处理

    Python文本数据分析与处理 新闻摘要 分词 使用jieba分词 注意lcut只接受字符串 过滤停用词 TF IDF得到摘要信息或者使用LDA主题模型 TF IDF有两种 jieba analyse extract tags conten
  • Linux中su、su -和sudo的区别

    su 切换到root用户 但是并没有转到root用户家目录下 即没有改变用户的环境 su 切换到root用户 并转到root用户的家目录下 即改变到了root用户的环境 这个涉及到不同用户下的环境变量的配置 sudo 通过sudo 我们能把
  • 【C++】迭代器

    目录 1 迭代器 1 1 迭代器的操作 1 2 迭代器范围 1 3 使用左闭合范围蕴含的编程假定 2 begin和end成员 3 容器操作可能使迭代器失效 3 1 迭代器失效 3 2 编写改变容器的循环程序 3 3 不要保存end返回的迭代
  • (1)分类算法

    分类算法原理 一 KNN K 近邻 1 定义 如果待推测点 空心点 在中间的某个位置 则计算出与其最邻近的4个样本点 K 4 而此时这4个样本点包含了3个类别 1红 1蓝 2绿 针对这样的情况 knn算法通常采用投票法来进行类别推测 即找出
  • 安装VMware Tools方法(对于 18.04LTS版本)

    大家都知道 每一种虚拟机 如VMware Parallels Desktop等都有一个Tools用来让linux系统和windows系统可以共用剪贴板等工具 使得在windows上复制了的东西 在linux ubuntu 上可以访问 对于V
  • MQ知识梳理

    常见MQ有哪几种 分别适用什么场景 常见的有ActiveMQ RabbitMQ RocketMQ Kafka ActiveMQ比较成熟 功能多 但也比较老 各方面都不突出 目前已很少使用 RabbitMQ性能高 功能多 吞度量万级 有开源活
  • C#中的接口(interface)

    接口的命名规范 I 名词 接口与抽象类的区别 接口是由抽象类演变而来的 抽象类是未完全实现逻辑的类 其内部可以有抽象成员 也可以有非抽象成员 且子类在覆写抽象成员时 需要修饰符override 而接口是完全未实现逻辑的 其内部只允许存在抽象
  • java报错:com.alibaba.druid.pool.DruidDataSource.info {dataSource-1} inited

    JDBC使用Druid连接池连接数据库的时候 遇到报错 com alibaba druid pool DruidDataSource info dataSource 1 inited 具体报错信息如下 从网页上报错信息 可以看到是获取驱动名
  • 彻底理解vue底层运用的核心函数Object.defineProperty

    一个函数诞生一个框架 vue就是得益于javaScrit的原生函数Object defineProperty而诞生的 那么Object defineProperty到底是什么 它的用法又是怎样的呢 很简单 它就是用来为对象定义属性的 从字面
  • 51单片机串口通信数码管显示

    外部晶振 11 0592MHZ 主控芯片 STC89C52 程序功能 串口工作方式1 8位UART 比特率9600 接收串口数据 数码管以十 进制格式显示 并且把接收到的数据加1后通过串口发出
  • 【Idea】创建包自动分层

    Idea 创建包自动分层 创建Maven 项目时 新建包使得Tomcat查找访问路径时更准确 但是有时包会不分层 如图1 然后我们使用图3的方法取消勾选 使得新建包时自动分层 如图2
  • 华为机试--简单题(一)

    HJ14 字符串排序 知识点 字符串 排序 描述 给定 n 个字符串 请对 n 个字符串按照字典序排列 数据范围 1 n 1000 字符串长度满足1 len 100 输入描述 输入第一行为一个正整数n 1 n 1000 下面n行为n个字符串
  • JAVA基于Slack实现异常日志报警

    一 功能介绍 在我们日常开发中 如果系统在线上环境上 发生异常 开发人员不能及时知晓来修复 可能会造成重大的损失 因此后端服务中加入异常报警的功能是十分必要的 而开发一个功能全面的异常报警服务 可能会花费较长的周期 今天给大家带来一种基于S