KETTLE执行监控(三):发出报警和查看日志

前提

  • 我采用的是XXL-JOB+JAVA语言扫库+http调用短信接口
    并不是必须跟我一样,只要满足能扫库+能通知就行

代码

  1. 修改jdbcUrl
  2. 修改driverClassName
  3. 修改userName
  4. 修改password
  5. 修改smsAlertUrl,以及调整发通知的方式
package com.xxl.job.executor.service.jobhandler;

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.toolkit.MapUtils;
import com.baomidou.mybatisplus.toolkit.StringEscape;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.log.XxlJobLogger;
import org.springframework.http.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StopWatch;
import org.springframework.web.client.RestTemplate;

import java.io.Serializable;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

/**
 * @author hjp
 * @since 2022年6月9日 11:37:41
 * kettle执行监控
 */
public class KettleExecuteMonitorJobHandler extends IJobHandler {

    public static final ReturnT<String> SUCCESS = new ReturnT<>(null);
    public static final ReturnT<String> FAIL = new ReturnT<>(ReturnT.FAIL_CODE, null);

    private static final String jdbcUrl = "xxxJdbcUrl";
    private static final String driverClassName = "xxxDriver";
    private static final String userName = "xxxUserName";
    private static final String password = "xxxPassword";
    private DruidDataSource dataSource = null;
    private JdbcTemplate jdbcTemplate = null;

    private int initialSize = 1;
    private int minIdle = 1;
    private int maxActive = 1;
    private int queryTimeout = 3;// seconds
    private int maxWait = 12 * 1000;
    private Long timeBetweenEvictionRunsMillis = 60 * 1000L;
    private Long minEvictableIdleTimeMillis = 5 * 60 * 1000L;
    private Boolean useUnfairLock = true;
    private Boolean poolPreparedStatements = true;
    private Integer maxOpenPreparedStatements = 200;
    private String validationQuery = "select 1 from dual;";
    private Boolean testOnBorrow = false;
    private Boolean testOnReturn = false;
    private Boolean testWhileIdle = true;
    private String filters = "wall";
    private Boolean logAbandoned = true;

    //查找错误的步骤日志
    private static final String stepQuerySql = "select t.TRANSNAME,t.STEPNAME,t.CHANNEL_ID,to_char(t.log_date,'yyyy-MM-dd HH24:mi:ss') as LOGDATE from t_kettle_step_log t  where t.errors >?";
    //查询作业通道日志(包含步骤),用作跳板,二次查询转换日志、作业日志
    private static final String jobChannelQuerySql = "select t.ROOT_CHANNEL_ID,t.PARENT_CHANNEL_ID from t_kettle_job_channel_log t where t.channel_id=?";
    //查询转换日志
    private static final String transformationQuerySql = "select t.LOG_FIELD from t_kettle_transformation_log t where t.channel_id=?";
    //查询作业日志
    private static final String jobQuerySql = "select t.JOBNAME,t.START_JOB_ENTRY,t.EXECUTING_SERVER,t.EXECUTING_USER,t.CLIENT from t_kettle_job_log t where t.channel_id=?";

    private static final String smsAlertUrl = "xxxSmsAlertUrl";
    private final RestTemplate restTemplate = new RestTemplate();
    private final ObjectMapper jackson = new ObjectMapper();
    private StopWatch stopWatch = null;


    @Override
    public ReturnT<String> execute(String param) throws Exception {

        List<NoticeEntity> errorLogList = null;

        try {
            stopWatch = new StopWatch(String.format("%n----------------开始扫描错误日志"));

            dataSource = buildDbSource();
            jdbcTemplate = new JdbcTemplate(dataSource);
            //1、获取存在错误的步骤日志
            List<Map<String, Object>> errorStepList =
                    jdbcTemplate.queryForList(stepQuerySql, 0);

            if (CollectionUtils.isEmpty(errorStepList)) {
                stopWatch.start("无报错的作业或转换");
                stopWatch.stop();
                return SUCCESS;
            }

            errorLogList = Lists.newArrayList();

            for (Map<String, Object> errorStep : errorStepList) {

                NoticeEntity noticeEntity = NoticeEntity.builder();

                String stepChannelId = (String) errorStep.get("CHANNEL_ID");
                Map<String, Object> jobChannelLogMap = jdbcTemplate.queryForMap(jobChannelQuerySql, stepChannelId);

                //判断是不是通过作业or转换执行;转换执行的话,是无法获取到详细日志,只能知道是哪个转换报错
                boolean isJobExecute = false;//是否通过作业执行
                if (MapUtils.isNotEmpty(jobChannelLogMap)) {
                    isJobExecute = true;

                    /*
                     * 转换日志信息
                     */
                    String transformationChannelId = (String) jobChannelLogMap.get("PARENT_CHANNEL_ID");
                    Map<String, Object> transformationLogMap = jdbcTemplate.queryForMap(transformationQuerySql, transformationChannelId);

                    /*
                     * 作业日志信息
                     */
                    String jobChannelId = (String) jobChannelLogMap.get("ROOT_CHANNEL_ID");
                    Map<String, Object> jobLogMap = jdbcTemplate.queryForMap(jobQuerySql, jobChannelId);

                    noticeEntity
                            .通过作业或转换执行("作业")
                            .所属作业((String) jobLogMap.get("JOBNAME"))
                            .所属转换((String) errorStep.get("TRANSNAME"))
                            .所属步骤((String) errorStep.get("STEPNAME"))
                            .作业面板的起始步骤((String) jobLogMap.get("START_JOB_ENTRY"))
                            .所属步骤的日志记录时间((String) errorStep.get("LOGDATE"))
                            .日志详细内容(transformationLogMap.get("LOG_FIELD"))
                            .主机名((String) jobLogMap.get("EXECUTING_SERVER"))
                            .用户((String) jobLogMap.get("EXECUTING_USER"))
                            .客户端方式((String) jobLogMap.get("CLIENT"))
                            .作业的ID(jobChannelId)
                            .转换的ID(transformationChannelId);
                } else {
                    noticeEntity
                            .通过作业或转换执行("转换")
                            .所属转换((String) errorStep.get("TRANSNAME"))
                            .所属步骤((String) errorStep.get("STEPNAME"))
                            .所属步骤的日志记录时间((String) errorStep.get("LOGDATE"));
                }

                stopWatch.start(String.format("%n----------------异常转换步骤日志:%s", noticeEntity.build()));
                stopWatch.stop();

                /*
                 * 发送告警信息
                 */
                //因为短信长度有限制、过长的异常信息影响视觉效果、http json对格式严格,因此将日志详细信息去除
                noticeEntity.日志详细内容(null);
                String alertMessageBody = String.format("{\"msg\":\"【kettle报错】%s\"}"
                        , StringEscape.escapeString(noticeEntity.toString()));

                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                HttpEntity<String> httpEntity = new HttpEntity<>(alertMessageBody, headers);

                ResponseEntity<String> responseEntity = restTemplate.exchange(smsAlertUrl,
                        HttpMethod.POST, httpEntity, String.class);

            }

            return SUCCESS;
        } catch (Exception e) {
            e.printStackTrace();
            ReturnT<String> returnT = FAIL;
            returnT.setMsg("异常==" + e);
            return returnT;
        } finally {
            if (dataSource != null) {
                dataSource.close();
            }

            if (stopWatch.isRunning()) {
                stopWatch.stop();
            }
            XxlJobLogger.log(stopWatch.prettyPrint());
        }
    }


    private DruidDataSource buildDbSource() throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setUrl(jdbcUrl);
        druidDataSource.setUsername(userName);
        druidDataSource.setPassword(password);

        druidDataSource.setInitialSize(initialSize);
        druidDataSource.setMinIdle(minIdle);
        druidDataSource.setMaxActive(maxActive);
        druidDataSource.setQueryTimeout(queryTimeout);// 请求超时时间
        druidDataSource.setMaxWait(maxWait);// 连接最大等待时间

        druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); // 检测空闲连接时间间隔
        druidDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); // 线程最小生存时间


        druidDataSource.setUseUnfairLock(useUnfairLock); // 使用非公平锁
        druidDataSource.setPoolPreparedStatements(poolPreparedStatements);
        druidDataSource.setMaxOpenPreparedStatements(maxOpenPreparedStatements);// 缓存preparedStatement

        druidDataSource.setValidationQuery(validationQuery);
        druidDataSource.setTestOnBorrow(testOnBorrow);
        druidDataSource.setTestOnReturn(testOnReturn);
        druidDataSource.setTestWhileIdle(testWhileIdle);

        druidDataSource.setFilters(filters); // sql注入防御插件
        druidDataSource.setLogAbandoned(logAbandoned);// 将当前关闭动作记录到日志

        return druidDataSource;
    }
  
    static class NoticeEntity implements Serializable {
        private String 通过作业或转换执行;
        private String 所属作业;
        private String 所属转换;
        private String 所属步骤;
        private String 作业面板的起始步骤;
        private String 所属步骤的日志记录时间;
        private Object 日志详细内容;
        private String 主机名;
        private String 用户;
        private String 客户端方式;
        private String 作业的ID;
        private String 转换的ID;

        public static NoticeEntity builder() {
            return new NoticeEntity();
        }

        public NoticeEntity build() {
            return this;
        }

        public String get通过作业或转换执行() {
            return 通过作业或转换执行;
        }

        public NoticeEntity 通过作业或转换执行(String 通过作业或转换执行) {
            this.通过作业或转换执行 = 通过作业或转换执行;
            return this;
        }

        public String get所属作业() {
            return 所属作业;
        }

        public NoticeEntity 所属作业(String 所属作业) {
            this.所属作业 = 所属作业;
            return this;
        }

        public String get所属转换() {
            return 所属转换;
        }

        public NoticeEntity 所属转换(String 所属转换) {
            this.所属转换 = 所属转换;
            return this;
        }

        public String get所属步骤() {
            return 所属步骤;
        }

        public NoticeEntity 所属步骤(String 所属步骤) {
            this.所属步骤 = 所属步骤;
            return this;
        }

        public String get作业面板的起始步骤() {
            return 作业面板的起始步骤;
        }

        public NoticeEntity 作业面板的起始步骤(String 作业面板的起始步骤) {
            this.作业面板的起始步骤 = 作业面板的起始步骤;
            return this;
        }

        public String get所属步骤的日志记录时间() {
            return 所属步骤的日志记录时间;
        }

        public NoticeEntity 所属步骤的日志记录时间(String 所属步骤的日志记录时间) {
            this.所属步骤的日志记录时间 = 所属步骤的日志记录时间;
            return this;
        }

        public Object get日志详细内容() {
            return 日志详细内容;
        }

        public NoticeEntity 日志详细内容(Object 日志详细内容) {
            this.日志详细内容 = 日志详细内容;
            return this;
        }

        public String get主机名() {
            return 主机名;
        }

        public NoticeEntity 主机名(String 主机名) {
            this.主机名 = 主机名;
            return this;
        }

        public String get用户() {
            return 用户;
        }

        public NoticeEntity 用户(String 用户) {
            this.用户 = 用户;
            return this;
        }

        public String get客户端方式() {
            return 客户端方式;
        }

        public NoticeEntity 客户端方式(String 客户端方式) {
            this.客户端方式 = 客户端方式;
            return this;
        }

        public String get作业的ID() {
            return 作业的ID;
        }

        public NoticeEntity 作业的ID(String 作业的ID) {
            this.作业的ID = 作业的ID;
            return this;
        }

        public String get转换的ID() {
            return 转换的ID;
        }

        public NoticeEntity 转换的ID(String 转换的ID) {
            this.转换的ID = 转换的ID;
            return this;
        }

        @Override
        public String toString() {
            return "" +
                    "通过作业或转换执行=" + 通过作业或转换执行 +
                    ", 所属作业=" + 所属作业 +
                    ", 所属转换=" + 所属转换 +
                    ", 所属步骤=" + 所属步骤 +
                    ", 作业面板的起始步骤=" + 作业面板的起始步骤 +
                    ", 所属步骤的日志记录时间=" + 所属步骤的日志记录时间 +
                    ", 日志详细内容=" + 日志详细内容 +
                    ", 主机名=" + 主机名 +
                    ", 用户=" + 用户 +
                    ", 客户端方式=" + 客户端方式 +
                    ", 作业的ID=" + 作业的ID +
                    ", 转换的ID=" + 转换的ID;
        }
    }

}

配置定时任务

  • 采用XXL-JOB的GLUE(Java)方式
    在这里插入图片描述
    在这里插入图片描述
  • 编辑代码
    在这里插入图片描述

报警内容

在这里插入图片描述
因为日志内容包含特殊字符,http无法接收,短信对内容长度也有限制,因此详细内容要去到XXL-JOB的调度日志查看
在这里插入图片描述

日志表查询和关联关系

select t.*,rowid from t_kettle_job_log t;--作业日志
select t.*,rowid from t_kettle_job_channel_log t;--作业日志通道
select t.*,rowid from t_kettle_transformation_log t;--转换日志
select t.*,rowid from t_kettle_step_log t;--转换步骤日志
  • 关联关系
  1. 查询是否有异常的步骤
select t.TRANSNAME,t.STEPNAME,t.CHANNEL_ID,to_char(t.log_date,'yyyy-MM-dd HH24:mi:ss') as LOGDATE from t_kettle_step_log t  where t.errors >0;
  1. 跳板:查询出转换日志id和作业日志id

拿到CHANNEL_ID字段的值,带入查询

select t.ROOT_CHANNEL_ID,t.PARENT_CHANNEL_ID from t_kettle_job_channel_log t where t.channel_id=?;
  1. 查询详细报错日志

拿到PARENT_CHANNEL_ID字段的值,导入查询

select t.LOG_FIELD from t_kettle_transformation_log t where t.channel_id=?;
  1. 查询全局性信息

比如日志、所属主机、用户、客户端方式
拿到ROOT_CHANNEL_ID字段的值,导入查询

select t.JOBNAME,t.START_JOB_ENTRY,t.EXECUTING_SERVER,t.EXECUTING_USER,t.CLIENT from t_kettle_job_log t where t.channel_id=?;
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值