azkaban邮件自定义详解

注:本人只安装过3.9和2.5,文章中也只会说明这两个版本

目录

一、准备工作 

二、邮件配置说明

 三、修改邮件内容

 四、替换

3.9可正常编译,也可在idea中编译

2.5我们只能通过替换jar包内容的方式进行修改

五、2.5定时任务发送邮件

六、定时发送邮件


一、准备工作 

github上下载对应版本的源码,如果不是最新版则需要在README.md文件点击超链接跳转的历史版本下载页面

2.5编译用的是ant,3.9编译用的是gredle(基于Apache Ant和Apache Maven概念的项目自动化构建开源工具)

如果是版本是2.5则还需要再网上下载编译过后的安装包,因为我尝试了很多次都无法再本地编译,解决一个还会出现别的问题,windows和Linux都尝试了下,所以不想再浪费时间

https://codeload.github.com/azkaban/azkaban/zip/2.5.0

部署可参考:Azkaban2.5.0部署安装(含安装包) (icode9.com)

3.9的版本官方文档说的很详细,可以用浏览器的翻译或者有道词典的划词翻译和截图翻译来阅读

Getting Started — Azkaban documentation

二、邮件配置说明

邮件的配置只需要修改前端项目azkaban-web的azkaban.properties就可以

mail.sender=mhy_404@163.com
mail.host=smtp.163.com
mail.user=mhy_404@163.com
mail.password=PIWVMKXTPW......

这4个配置的都是发件人,azkaban会用该邮箱发送邮件,qq邮箱也是可以的,需要将mail.host设置为smtp.qq.com。密码并不是邮箱的密码,而是邮箱开启smtp的时候给的授权码,该授权码只会再开启的时候出现一次,如果忘记那就把smtp功能重新关闭后再开启

 

job.failure.email=
job.success.email=

成功或收件人的邮箱,但该配置azkaban的代码中没有用到,实际配置也没有什么用。我们可以修改源码,来把这两个配置项用起来。

 三、修改邮件内容

邮件的内容是在 DefaultMailCreator中调整

邮箱最终效果

 azkaban2.5的ExecutableFlow的对象中并没有项目名称的属性,我们可以修改源码进行添加

 效果:

 失败代码:

@Override
public boolean createErrorEmail(ExecutableFlow flow, EmailMessage message, String azkabanName, String clientHostname, String clientPortNumber, String... vars) {

	ExecutionOptions option = flow.getExecutionOptions();
	//获取失败后收件人日志
	List<String> emailList = option.getFailureEmails();

	int execId = flow.getExecutionId();

	//获取job内容列表
	Map<String, Object> executableNodeInfo = getExecutableNodeInfo(flow);

	HashMap<String, Object> newNode = new HashMap<>(executableNodeInfo);
	List<Map<String, Object>> nodes = (ArrayList<Map<String, Object>>) newNode.get("nodes");

	int succeededNum = 0;
	List<Map<String, String>> failedList = new ArrayList<>();

	//获取所有job节点
	for (Map<String, Object> map : nodes) {
		String status = map.get("status").toString();

		if ("SUCCEEDED".equals(status)) {
			succeededNum++;
		} else if ("FAILED".equals(status) || "FAILED_FINISHING".equals(status)) {
			Map<String, String> failedMap = new HashMap<>();
			String id = map.get("id").toString();
			//获取后续影响节点
			String outStr = getOut(map);
			failedMap.put("id", id);
			failedMap.put("out", outStr);

			Set<String> impactAll = new HashSet<>();
			//节点失败后后续影响节点分析
			impactAnalysis(nodes, outStr, impactAll);

			String impactAllStr = impactAll.toString();
			failedMap.put("impactAll", impactAllStr.substring(1, impactAllStr.length() - 1));
			failedList.add(failedMap);
		}
	}

	SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	String newDate = simpleDateFormat.format(new Date(flow.getStartTime()));
	String endTime = simpleDateFormat.format(new Date(flow.getEndTime()));
	if (emailList != null && !emailList.isEmpty()) {
		message.addAllToAddress(emailList);
		message.setMimeType("text/html;charset=UTF-8");
		message.setSubject("项目名称:" + flow.getProjectName() + ",execution '" + execId + "' 今日调度作业运行:失败");

		message.println("<h3>作业开始时间:" + newDate + "</h3>");
		message.println("<h3>作业结束时间:" + endTime + "</h3>");
		message.println("<table width=\"400\" style=\"border: 1pt solid rgb(232, 242, 249); border-image: none;\" border=\"1\" cellpadding=\"0\">");
		message.println("<tr style=\"background: rgb(66, 139, 202); padding: 0.75pt;\">" +
				"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">作业状态</td>" +
				"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">记录数</td>" +
				"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">作业数据日期</td>" +
				"</tr>");
		message.println("<tr>" +
				"<td>调度作业总数</td>" +
				"<td>" + nodes.size() + "</td>" +
				"<td>" + LocalDate.now().minusDays(1) + "</td>" +
				"</tr>");
		message.println("<tr>" +
				"<td>当前成功执行</td>" +
				"<td>" + succeededNum + "</td>" +
				"<td>" + LocalDate.now().minusDays(1) + "</td>" +
				"</tr>");
		message.println("<tr style=\"background: crimson; padding: 0.75pt;\">" +
				"<td style=\"color: white;\">当前运行失败</td>" +
				"<td style=\"color: white;\">" + failedList.size() + "</td>" +
				"<td style=\"color: white;\">" + LocalDate.now().minusDays(1) + "</td>" +
				"</tr>");
		message.println("</table>");

		message.println("<h3>当前失败作业明细</h3>");
		message.println("<table width=\"550\" style=\"border: 1pt solid rgb(232, 242, 249); border-image: none;\" border=\"1\" cellpadding=\"0\">");
		message.println("<tr style=\"background: rgb(66, 139, 202); padding: 0.75pt;\">" +
				"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">失败作业名称</td>" +
				"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">后续影响第二级作业</td>" +
				"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">后续影响所有的作业</td>" +
				"</tr>");

		for (Map<String, String> failedMap : failedList) {
			message.println("<tr>" +
					"<td>" + failedMap.get("id") + "</td>" +
					"<td>" + failedMap.get("out") + "</td>" +
					"<td>" + failedMap.get("impactAll") + "</td>" +
					"</tr>");
		}
		message.println("</table>");

		return true;
	}
	return false;
}

/**
 * 获取后续影响节点
 */
private static String getOut(Map<String, Object> map) {
	Object out = map.get("out");
	String outStr = null;
	if (out != null) {
		outStr = out.toString();
		outStr = outStr.substring(1, outStr.length() - 1);
	}
	return outStr;
}
/**
 * 节点失败后,所有后续影响节点分析
 */
private void impactAnalysis(List<Map<String, Object>> nodes, String outStr, Set<String> impactAll) {
	if (outStr != null) {
		String[] sonNodeIds = outStr.split(",");

		Set<String> collect = nodes.stream().filter(node -> {
			String sonId = node.get("id").toString();
			for (String sonNodeId : sonNodeIds) {
				if (sonId.equals(sonNodeId.trim()) && node.get("out") != null) {
					return true;
				}
			}
			return false;
		}).map(DefaultMailCreator::getOut).collect(Collectors.toSet());

		for (String set : collect) {
			impactAnalysis(nodes, set, impactAll);
		}
		impactAll.addAll(collect);
	}
}

/**
 * 获取job内容列表
 */
private Map<String, Object> getExecutableNodeInfo(ExecutableNode node) {
	HashMap<String, Object> nodeObj = new HashMap<String, Object>();
	nodeObj.put("id", node.getId());
	nodeObj.put("status", node.getStatus());
	nodeObj.put("startTime", node.getStartTime());
	nodeObj.put("endTime", node.getEndTime());

	//前置节点
	if (node.getInNodes() != null && !node.getInNodes().isEmpty()) {
		nodeObj.put("in", node.getInNodes());
	}
	//后置节点
	if (node.getInNodes() != null && !node.getOutNodes().isEmpty()) {
		nodeObj.put("out", node.getOutNodes());
	}

	if (node instanceof ExecutableFlowBase) {
		ExecutableFlowBase base = (ExecutableFlowBase) node;
		ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();

		for (ExecutableNode subNode : base.getExecutableNodes()) {
			Map<String, Object> subNodeObj = getExecutableNodeInfo(subNode);
			if (!subNodeObj.isEmpty()) {
				nodeList.add(subNodeObj);
			}
		}

		nodeObj.put("nodes", nodeList);
	}

	return nodeObj;
}

通过页面的url查看源码的示例: 

上述代码中:getExecutableNodeInfo方法是根据ExecutorServlet.getExecutableNodeInfo改编

该方法对应历史中Job List的内容

 该方法的请求地址为:https://192.168.189.130:8443/executor?execid=60&ajax=fetchexecflow

通过全文搜索可知该方法的地址在ExecutorServlet中

 

 四、替换

3.9可正常编译,也可在idea中编译

编译后会生成build目录

instarll目录可以启动和关闭项目

如果不想执行命令编译,也可以通过下面方法替换lib中jar的内容

 

2.5我们只能通过替换jar包内容的方式进行修改

1、通过idea对代码进行编译,编译后的代码将会存放在azkaban-2.5.0\dist\classes\azkaban目录中

2、首先把网上下载的编译包中azkaban-2.5.0.jar复制出来。对了,github下载的2.5源码包中并没有引用相关jar包,可以直接用编译包中的jar,但需要注意把azkaban-2.5.0.jar删掉

 3、拷贝azkaban-2.5.0\dist\classes\azkaban中所有修改过代码的classes文件拷贝到azkaban-2.5.0.jar中对应的位置,jar可以用压缩文件打开,直接拖着进来进行替换。

也可以完全直接替换所有的目录

 

 4、停止服务器上的azkaban服务,azkaban-executor和azkaban-web都要停止。

5、将替换过后的jar上传到两个azkaban目录的lib中进行替换

五、2.5定时任务发送邮件

手动的邮件发送可以在页面上直接配置,而2.5的定时任务并没有配置的地方,如果想要定时任务也能发送邮件,那么就需要在上传的时候再最后一个job中添加一下配置

failure.emails --> 任务失败时的邮件提醒设置,以逗号分隔多个邮箱
success.emails --> 任务成功时的邮件提醒设置,以逗号分隔多个邮箱
notify.emails --> 任务无论失败还是成功都邮件提醒设置,以逗号分隔多个邮箱

需要注意三点:

1、必须是最后一个job,否则可能配置的当前job执行完成后就会发送邮件。(没试过,猜测)

2、必须是通过页面上传的job中包含上面配置才可以,上传后通过页面修改无效。(亲测)

3、修改azkaban-executor-2.5.0/projects/中的job也不会生效。

2和3就算修改后重新也不会生效

可以看到填写邮箱的地方并没有邮箱,说明修改无效,并不会发送邮件。

当然修改命令什么的还是会生效的。

如果配置生效,那么打开执行页面后填写邮箱的地方就直接有邮箱,如:

 查看本次执行的job文件内容

先看数据库的id为4,版本为6

进入项目目录,所有执行过后的目录都会有记录,没有执行过则不会有

 

可以看到notify.emails的配置并没有在文件中

第二种方法:

除了上面上传的时候最后一个job配置外,还可以同通过修改源码的方式

通过方法的调用链,我们可以发现是否发送邮件是ExecutorManager.finalizeFlows方法控制的

 

如果任务状态是失败或成功,且没有设置对应的邮箱那么将不会调用邮件发送的方法

解决方法: 我们可以在配置文件中添加收件人的地址,且在代码判断前把收件人加入代码中

上文提到这两个配置并没有被用到,我们可以利用一下,多个邮箱用逗号隔开

job.failure.email=mhy_404@163.com,xxx@163.com
job.success.email=mhy_404@163.com,xxx@163.com

 

//添加邮箱
List<String> successEmails = options.getSuccessEmails();
if (successEmails.isEmpty() && !"".equals(successEmail)) {
	String[] split = successEmail.split(",");
	successEmails.addAll(Arrays.asList(split));
}
List<String> failureEmails = options.getFailureEmails();
if (failureEmails.isEmpty() && !"".equals(failureEmail)) {
	String[] split = failureEmail.split(",");
	failureEmails.addAll(Arrays.asList(split));
}

 源码修改后可以安装步骤4的编译操作

注:此方法修改后将会影响到所有项目

六、定时发送邮件

很多时候我们往往需要某一时刻发送邮件,来查看当前正在运行的任务或者当前定时任务的运行汇总情况。这时候我们可以使用java自带的定时任务来实现,或者使用 while+sleep 方案,简单的说,就是定义一个线程,然后 while 循环,通过 sleep 延迟时间来达到周期性调度任务。

 

package azkaban.executor.mail;

import azkaban.alert.Alerter;
import azkaban.executor.ExecutableFlow;
import azkaban.project.ProjectManager;
import azkaban.utils.Props;
import azkaban.webapp.AzkabanWebServer;
import org.apache.log4j.Logger;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;

/**
 * 今日azkaban定时任务信息汇总
 */
public class InfoScheduleThread implements Runnable {

    private static final Logger logger = Logger.getLogger(InfoScheduleThread.class);

    private final Props azkabanSettings;
    private final AzkabanWebServer application;

    public InfoScheduleThread(Props azkabanSettings, AzkabanWebServer application) {
        this.azkabanSettings = azkabanSettings;
        this.application = application;
    }

    @Override
    public void run() {
        //定时时间  14:00
        String scheduleTime = azkabanSettings.getString("schedule.mail.info.time");
        //收件人
        String recipients = azkabanSettings.getString("job.failure.email");
        if (scheduleTime == null || recipients == null) {
            return;
        }
        List<String> emailList = Arrays.asList(recipients.split(","));

        logger.info("汇总任务每日定时邮件发送时间为:" + scheduleTime);

        String[] split = scheduleTime.split(":");
        String scheduleHour = split[0];
        String scheduleMinute = split[1];

        Alerter mailAlerter = application.getMailAlerter();

        while (true) {
            try {

                LocalDateTime now = LocalDateTime.now();
                //如果不设置withNano,程序会在1秒内执行多次
                LocalDateTime executeTime = now.withHour(Integer.parseInt(scheduleHour))
                        .withMinute(Integer.parseInt(scheduleMinute)).withSecond(0).withNano(0);


                //如果当前时间超过执行时间,则需要睡眠到明天的执行时间
                if (now.compareTo(executeTime) > 0) {
                    executeTime = executeTime.plusDays(1);
                }
                logger.info("汇总任务下次定时邮件发送时间:" + executeTime);

                //线程休眠时间
                long millis = now.until(executeTime, ChronoUnit.MILLIS);
                Thread.sleep(millis);

                logger.info("汇总任务开始执行");

                //设置今天0点
                Calendar calendar = Calendar.getInstance();
                calendar.set(Calendar.HOUR_OF_DAY, 0);
                calendar.set(Calendar.MINUTE, 0);
                calendar.set(Calendar.SECOND, 0);
                calendar.set(Calendar.MILLISECOND, 0);
                long todayZero = calendar.getTimeInMillis();

                //获取今天执行的任务
                List<ExecutableFlow> history = application.getExecutorManager().getExecutableFlows(
                        null, null, null, 0, todayZero, -1, 0, 16);

                //设置任务的项目名
                ProjectManager projectManager = application.getProjectManager();
                for (ExecutableFlow executableFlow : history) {
                    String projectName = projectManager.getProject(executableFlow.getProjectId()).getName();
                    executableFlow.setProjectNam(projectName);
                }
                mailAlerter.alertOnInfo(history, emailList);

            } catch (Exception e) {
                e.printStackTrace();
                logger.error(e);
            }
        }

    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值