DolphinScheduler源码分析之任务日志

任务日志打印在调度系统中算是一个比较重要的功能,下面就简要分析一下其打印的逻辑和前端页面查询的流程。

AbstractTask

所有的任务都会继承AbstractTask,这个抽象类有一个比较重要的字段就是logger,其实也就是一个org.slf4j.Logger对象。

也就是说所有的任务都是通过slf4j打印日志的。那这个logger是如何创建的呢?

Logger taskLogger = LoggerFactory.getLogger(LoggerUtils.buildTaskId(LoggerUtils.TASK_LOGGER_INFO_PREFIX,
taskInstance.getProcessDefine().getId(),
taskInstance.getProcessInstance().getId(),
taskInstance.getId()));
public static String buildTaskId(String affix,
int processDefId,
int processInstId,
int taskId){
// - [taskAppId=TASK_79_4084_15210]
return String.format(" - [taskAppId=%s-%s-%s-%s]",affix,
processDefId,
processInstId,
taskId);
}

非常简单,就是通过LoggerFactory.getLogger获取的,名字是由流程定义ID、流程实例ID、任务ID拼接成的。前端查询日志时,taskAppId其实就是logger的名称。通过下图可以很直观的看到,当前任务的流程定义ID是1,流程实例ID是2,任务ID是2 

其实分析到这里,并没有证明最终的进程把日志通过logger写到文件,至少目前没有看到相关的代码。为了更加直观的证明,我们选择Shell类型的任务来分析打印日志的方式。因为它最终创建了一个shell子进程,如果要通过logger字段打印日志,一定会有相关的代码。

ShellCommandExecutor

Shell类型的任务是通过ShellCommandExecutor去执行具体的shell脚本的。

/**
* constructor
* @param logHandler log handler
* @param taskDir task dir
* @param taskAppId task app id
* @param taskInstId task instance id
* @param tenantCode tenant code
* @param envFile env file
* @param startTime start time
* @param timeout timeout
* @param logger logger
*/
public ShellCommandExecutor(Consumer<List<String>> logHandler,
String taskDir,
String taskAppId,
int taskInstId,
String tenantCode,
String envFile,
Date startTime,
int timeout,
Logger logger)

上面是ShellCommandExecutor的构造函数,通过注释以及参数命名大概可以猜到,logHandler是最终打印日志的地方。下面从其赋值以及如何使用分析日志究竟是不是logger打印的。

this.shellCommandExecutor = new ShellCommandExecutor(this::logHandle, taskProps.getTaskDir(),
taskProps.getTaskAppId(),
taskProps.getTaskInstId(),
taskProps.getTenantCode(),
taskProps.getEnvFile(),
taskProps.getTaskStartTime(),
taskProps.getTaskTimeout(),
logger);

ShellCommandExecutor创建的时候,logHandler是通过ShellTask的logHandle方法赋值的。

/**
* log handle
* @param logs log list
*/
public void logHandle(List<String> logs) {
// note that the "new line" is added here to facilitate log parsing
logger.info(" -> {}", String.join("\n\t", logs));
}

上面是logHandle的方法定义,很明显就是通过logger打印日志的。

那logHandler是什么时候使用的呢?

AbstractCommandExecutor

ShellCommandExecutor继承了AbstractCommandExecutor,在AbstractCommandExecutor.run中调用了一个非常重要的方法:parseProcessOutput

private void parseProcessOutput(Process process) {
String threadLoggerInfoName = String.format(LoggerUtils.TASK_LOGGER_THREAD_NAME + "-%s", taskAppId);
ExecutorService parseProcessOutputExecutorService = ThreadUtils.newDaemonSingleThreadExecutor(threadLoggerInfoName);
parseProcessOutputExecutorService.submit(new Runnable(){
@Override
public void run() {
BufferedReader inReader = null; try {
inReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line; long lastFlushTime = System.currentTimeMillis(); while ((line = inReader.readLine()) != null) {
logBuffer.add(line);
lastFlushTime = flush(lastFlushTime);
}
} catch (Exception e) {
logger.error(e.getMessage(),e);
} finally {
clear();
close(inReader);
}
}
});
parseProcessOutputExecutorService.shutdown();
}

parseProcessOutput这个方法就是把Process的标准输入输出打印到了logBuffer中,然后根据条件flush。

private long flush(long lastFlushTime) {
long now = System.currentTimeMillis(); /**
* when log buffer siz or flush time reach condition , then flush
*/
if (logBuffer.size() >= Constants.defaultLogRowsNum || now - lastFlushTime > Constants.defaultLogFlushInterval) {
lastFlushTime = now;
/** log handle */
logHandler.accept(logBuffer); logBuffer.clear();
}
return lastFlushTime;
}

flush就是根据条件(大小、时间)把logBuffer中的内容,通过logHandler打印,其实就是通过logger打印到文件。

分析到这个地方,我们才真正清楚,任务其实就是通过slf4j打印到文件。那么问题又来了,前端是如何查询日志文件的呢?日志文件的路径前端是如何找到的呢?

logback.xml

既然我们知道了是slf4j在打印日志,那么配置文件在哪里呢?

在dolphinscheduler-server模块的resources目录下,有两个logback.xml文件:worker_logback.xml、master_logback.xml。任务打印日志的配置应该是worker_logback.xml,在哪里指定的呢?

dolphinscheduler-daemon.sh文件中有一个关于日志的配置。

-Dlogging.config=classpath:master_logback.xml

上面是worker_logback.xml,可以看到有两个appender,其中TASKLOGFILE是我们关注的对象。它有一个比较关键的filter,根据logback中filter的概念来猜测,这应该就是用来区分workerlogfile这个appender的。也就是说两个appender,会通过filter分别筛选出各自的日志进行打印。

/**
* Accept or reject based on thread name
* @param event event
* @return FilterReply
*/
@Override
public FilterReply decide(ILoggingEvent event) {
if (event.getThreadName().startsWith(LoggerUtils.TASK_LOGGER_THREAD_NAME) || event.getLevel().isGreaterOrEqual(level)) {
return FilterReply.ACCEPT;
}
return FilterReply.DENY;
}

这个filter根据日志级别和线程名过滤,符合条件的才能打印到当前appender。其实也就是只打印任务线程的日志。

当然了,还配置了Discriminator,它限定了logger的名称符合前面的定义。

/**
* logger name should be like:
* Task Logger name should be like: Task-{processDefinitionId}-{processInstanceId}-{taskInstanceId}
*/
@Override
public String getDiscriminatingValue(ILoggingEvent event) {
String loggerName = event.getLoggerName()
.split(Constants.EQUAL_SIGN)[1];
String prefix = LoggerUtils.TASK_LOGGER_INFO_PREFIX + "-";
if (loggerName.startsWith(prefix)) {
return loggerName.substring(prefix.length(),
loggerName.length() - 1).replace("-","/");
} else {
return "unknown_task";
}
}

LoggerController

前面的分析我们知道,任务的日志其实就是打印到本地日志文件中,那么前端查询的时候估计就是直接读取日志文件然后返回。

但有一个很现实的问题,任务是随机分布在各个worker的,如何读取日志文件呢?

LoggerController.queryLog就是用来查询日志的,它调用了LoggerService.queryLog

public Result queryLog(int taskInstId, int skipLineNum, int limit) {

    TaskInstance taskInstance = processDao.findTaskInstanceById(taskInstId);

    if (taskInstance == null){
return new Result(Status.TASK_INSTANCE_NOT_FOUND.getCode(), Status.TASK_INSTANCE_NOT_FOUND.getMsg());
} String host = taskInstance.getHost();
if(StringUtils.isEmpty(host)){
return new Result(Status.TASK_INSTANCE_NOT_FOUND.getCode(), Status.TASK_INSTANCE_NOT_FOUND.getMsg());
} Result result = new Result(Status.SUCCESS.getCode(), Status.SUCCESS.getMsg()); logger.info("log host : {} , logPath : {} , logServer port : {}",host,taskInstance.getLogPath(),Constants.RPC_PORT); LogClient logClient = new LogClient(host, Constants.RPC_PORT);
String log = logClient.rollViewLog(taskInstance.getLogPath(),skipLineNum,limit);
result.setData(log);
logger.info(log); return result;
}

LoggerService.queryLog的逻辑其实就是通过任务实例ID,查询到了任务所在节点以及日志路径,通过LogClient读取日志。当然了,读取的时候,有限定跳过的行数以及需要读取的行数。

LogClient.rollViewLog其实就是一次rpc调用,它连接到对应host的50051端口,读取日志。

LoggerServer

LoggerServer其实就是一个socket服务,它监听Constants.RPC_PORT(50051)端口的连接,交给LogViewServiceGrpcImpl处理对应的rpc请求。

/**
* server start
* @throws IOException io exception
*/
public void start() throws IOException {
/* The port on which the server should run */
int port = Constants.RPC_PORT;
server = ServerBuilder.forPort(port)
.addService(new LogViewServiceGrpcImpl())
.build()
.start();
logger.info("server started, listening on port : {}" , port);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
// Use stderr here since the logger may have been reset by its JVM shutdown hook.
logger.info("shutting down gRPC server since JVM is shutting down");
LoggerServer.this.stop();
logger.info("server shut down");
}
});
}

rollViewLog的实现如下,其实也比较简单,就是调用readFile读取日志文件,然后返回。

public void rollViewLog(LogParameter request, StreamObserver<RetStrInfo> responseObserver) {

    logger.info("log parameter path : {} ,skip line : {}, limit : {}",
request.getPath(),
request.getSkipLineNum(),
request.getLimit());
List<String> list = readFile(request.getPath(), request.getSkipLineNum(), request.getLimit());
StringBuilder sb = new StringBuilder();
boolean errorLineFlag = false;
for (String line : list){
sb.append(line + "\r\n");
}
RetStrInfo retInfoBuild = RetStrInfo.newBuilder().setMsg(sb.toString()).build();
responseObserver.onNext(retInfoBuild);
responseObserver.onCompleted();
}

总结

 

上面是一个简单的流程图,是worker写入日志的流程。

 

这是一个前端读取日志的路程,读取日志的请求按照箭头方向传递,最终由LoggerServer读取本地日志返回给远程的ApiServer,ApiServer返回给前端。

DolphinScheduler源码分析之任务日志的更多相关文章

  1. DolphinScheduler源码分析

    DolphinScheduler源码分析 本博客是基于1.2.0版本进行分析,与最新版本的实现有一些出入,还请读者辩证的看待本源码分析.具体细节可能描述的不是很准确,仅供参考 源码版本 1.2.0 技 ...

  2. 8. SOFAJRaft源码分析— 如何实现日志复制的pipeline机制?

    前言 前几天和腾讯的大佬一起吃饭聊天,说起我对SOFAJRaft的理解,我自然以为我是很懂了的,但是大佬问起了我那SOFAJRaft集群之间的日志是怎么复制的? 我当时哑口无言,说不出是怎么实现的,所 ...

  3. DolphinScheduler源码分析之EntityTestUtils类

    1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license ...

  4. 源码分析之spring-JdbcTemplate日志打印sql语句

    对于开源的项目来说的好处就是我们遇到什么问题可以通过看源码来解决. 比如近期有个同事问我说,为啥JdbcTemplate中只有在Error的时候才打印出sql语句呢.我一想,这和log的配置有关系吧. ...

  5. DolphinScheduler 源码分析之 DAG类

    1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license ...

  6. HDFS源码分析之编辑日志编辑相关双缓冲区EditsDoubleBuffer

    EditsDoubleBuffer是为edits准备的双缓冲区.新的编辑被写入第一个缓冲区,同时第二个缓冲区可以被flush.为edits准备的双缓冲区.新的编辑被写入第一个缓冲区,同时第二个缓冲区可 ...

  7. [Abp vNext 源码分析] - 文章目录

    一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...

  8. java 日志体系(四)log4j 源码分析

    java 日志体系(四)log4j 源码分析 logback.log4j2.jul 都是在 log4j 的基础上扩展的,其实现的逻辑都差不多,下面以 log4j 为例剖析一下日志框架的基本组件. 一. ...

  9. HDFS源码分析EditLog之获取编辑日志输入流

    在<HDFS源码分析之EditLogTailer>一文中,我们详细了解了编辑日志跟踪器EditLogTailer的实现,介绍了其内部编辑日志追踪线程EditLogTailerThread的 ...

随机推荐

  1. mysql--->mysql查看数据库操作记录

    mysql查看数据库操作记录 MySQL的查询日志记录了所有MySQL数据库请求的信息.无论这些请求是否得到了正确的执行.默认文件名为hostname.log.默认情况下MySQL查询日志是关闭的.生 ...

  2. Web 项目没有发布到我们安装的tomcat目录下

    新手做Web项目的时候,在Ecplise把app发布到tomcat,但最后项目并没有发布到我们自己安装的 tomcat目录下,而是在.metadata\.plugins\org.eclipse.wst ...

  3. 安装Eclipse activity插件 报异常 Cannot complete the install because one or more required items could not be

    下载插件:Activiti Designer 5.17 2)安装过程中错误处理 a.错误: Cannot complete the install because one or more requir ...

  4. Asp.Net Core 混合全球化与本地化支持

    前言 最近的新型冠状病毒流行让很多人主动在家隔离,希望疫情能快点消退.武汉加油,中国必胜! Asp.Net Core 提供了内置的网站国际化(全球化与本地化)支持,微软还内置了基于 resx 资源字符 ...

  5. 11、ACL

    IP访问控制列表 标准ACL 1)检查源地址 2)不能对协议簇作限定 扩展ACL 1)检查源和目标地址 2)能容许或拒绝特定的协议和应用(端口号) 区别列表类型: 1)ACL号 : 1-99,1300 ...

  6. Qt实现简易计算器

    麻烦到不能再麻烦的实现,简单到不能再简单的思路. calc.h #ifndef CALC_H #define CALC_H #include <QtWidgets/QMainWindow> ...

  7. Mplayer另类在线播放影音文件技巧【转】

    http://www.linuxsir.org/bbs/showthread.php?t=254467 本文介绍的Mplayer在线播放的方法,不是指在浏览器中安装Mplayer插件这种方法,而是在命 ...

  8. html作业记录

    <html> <head> <title>Hello World</title> </head> <body> <!-- ...

  9. sublime: javascript/css 的格式化

    Sublime Text 3 破解版 + 注册机 + 汉化包 + 教程 http://www.xiumu.org/note/sublime-text-3.shtml 1.sublime 如果控制菜单选 ...

  10. DataGuard---->主库和备库都配置 db_file_name_convert和log_file_name_convert的作用

    一.参数说明 [1] db_file_name_convert db_file_name_convert 主数据库和备用数据库的数据文件转换目录对映(如果两数据库的目录结构不一样),如果有多个对映,逐 ...