diff --git a/README.md b/README.md index c400f57b..47e5fb9d 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ XXL-JOB 是一个开源且免费项目,其正在进行的开发完全得益于 - 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; - 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; - 37、AI任务:原生提供AI执行器,并内置多个AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务。 +- 38、审计日志:记录任务操作敏感信息,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题。 ## Development 于2015年中,我在github上创建XXL-JOB项目仓库并提交第一个commit,随之进行系统结构设计,UI选型,交互设计…… @@ -845,6 +846,20 @@ XXL-JOB 是一个开源且免费项目,其正在进行的开发完全得益于 - 691、联通云 - 692、北京爱话本科技有限公司 - 693、北京起创科技有限公司 + - 694、平安证券【平安证券】 + - 695、合肥中科类脑智能技术有限公司 + - 696、南京同仁堂健康产业有限公司【同仁堂】 + - 697、铜仁市碧江区智惠加油站 + - 698、惟客数据 + - 699、凤凰新闻【凤凰新闻】 + - 700、深圳王力智能 + - 701、返利网数字科技股份有限公司 + - 702、上海阜能信息科技有限公司 + - 703、深圳市极能超电数字科技有限公司 + - 704、海目星激光科技集团股份有限公司 + - 705、深圳市极能超电数字科技有限公司 + - 706、安克创新科技股份有限公司【安克】 + - 707、大庆点神科技有限公司 - …… > 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。 diff --git a/doc/XXL-JOB官方文档.md b/doc/XXL-JOB官方文档.md index 97e407a8..2035d81c 100644 --- a/doc/XXL-JOB官方文档.md +++ b/doc/XXL-JOB官方文档.md @@ -58,6 +58,8 @@ XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅 - 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; - 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; - 37、AI任务:原生提供AI执行器,并内置多个AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务。 +- 38、审计日志:记录任务操作敏感信息,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题。 + ### 1.4 发展 于2015年中,我在github上创建XXL-JOB项目仓库并提交第一个commit,随之进行系统结构设计,UI选型,交互设计…… @@ -788,6 +790,20 @@ XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅 - 691、联通云 - 692、北京爱话本科技有限公司 - 693、北京起创科技有限公司 + - 694、平安证券【平安证券】 + - 695、合肥中科类脑智能技术有限公司 + - 696、南京同仁堂健康产业有限公司【同仁堂】 + - 697、铜仁市碧江区智惠加油站 + - 698、惟客数据 + - 699、凤凰新闻【凤凰新闻】 + - 700、深圳王力智能 + - 701、返利网数字科技股份有限公司 + - 702、上海阜能信息科技有限公司 + - 703、深圳市极能超电数字科技有限公司 + - 704、海目星激光科技集团股份有限公司 + - 705、深圳市极能超电数字科技有限公司 + - 706、安克创新科技股份有限公司【安克】 + - 707、大庆点神科技有限公司 - …… > 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。 @@ -2606,11 +2622,11 @@ public void execute() { - 10、【修复】脚本任务process销毁逻辑优化,解决风险情况下脚本进程无法终止问题; - 11、【修复】调度预读任务数量调整,改为调度线程池大小x10,降低事务颗粒度,提升性能及稳定性; - 12、【修复】合并PR-2369,修复脚本任务参数取值问题; -- 13、【强化】通用HTTP任务(httpJobHandler)强化,支持更丰富请求参数设置,完整参数示例如下: +- 13、【强化】通用HTTP任务(httpJobHandler)强化,支持更丰富请求参数设置,完整参数示例如下:
- 完整参数示例参考: - + 完整参数示例参考: + ``` { "url": "http://www.baidu.com", @@ -2636,15 +2652,19 @@ public void execute() { - 16、【重构】规范API交互协议,通用响应结构体调整为Response,调度中心API统一为Response封装数据; (注意:响应结构体从ReturnT升级为Response,其中属性值“content”会调整为“data”,取值逻辑需注意) - 17、【升级】Http通讯组件升级,基于接口代理方式重构通讯组件,提升组件性能及扩展性; -- 18、【ING】UI框架重构升级,提升交互体验; -- 19、【ING】调整资源加载逻辑,移除不必要的拦截器逻辑,提升页面加载效率; +- 18、【新增】任务审计日志,记录任务操作敏感日志信息,如任务新建/更新/删除/启停/触发以及GLUE代码更新等,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题等。 + (当前任务审计日志以Info级别输出在系统日志中,可通过关键词 "xxl-job operation log:" 检索过滤) +- 19、【ING】UI框架重构升级,提升交互体验; +- 20、【ING】调整资源加载逻辑,移除不必要的拦截器逻辑,提升页面加载效率; + ### TODO LIST - 1、调度隔离:调度中心针对不同执行器,各自维护不同的调度和远程触发组件。 - 2、任务优先级:调度与执行阶段按照优先级分配资源。 - 3、多数据库支持,DAO层通过JPA实现,不限制数据库类型。 -- 4、执行器Log清理功能:调度中心Log删除时同步删除执行器中的Log文件; +- 4、OpenApi: + - 执行器Log文件清理:支持调度中心远程删除执行器中指定任务的Log文件; - 5、性能优化:任务、执行器数据全量本地缓存;新增消息表广播通知; - 6、DAG流程任务 - 子任务:废弃 @@ -2653,10 +2673,8 @@ public void execute() { - 分片任务:全部完成后才会出发后置节点; - 配置并列的"a-b、b-c"路径列表,构成串行、并行、dag任务流程,"dagre-d3"绘图;任务依赖,流程图,子任务+会签任务,各节点日志;支持根据成功、失败选择分支; - 7、任务标签:方便搜索; -- 8、告警增强: - - 邮件告警:支持自定义标题、模板格式; - - webhook告警:支持自定义告警URL、请求体格式; -- 9、安全强化:AccessToken动态生成、动态启停;控制调度、回调; +- 8、GLUE 模式 Web Ide 版本对比功能; +- 9、自定义失败重试时间间隔; - 10、任务导入导出工具,灵活支持版本升级、迁移等场景。 - 11、任务日志重构:一次调度只记录一条主任务,维护起止时间和状态。 - 普通任务:只记录一条主任务; @@ -2664,13 +2682,21 @@ public void execute() { - 重试任务:失败时,新增主任务。所有调度记录,包括入口调度和重试调度,均挂载主任务上。 - 12、分片任务:全部完成后才会出发后置节点; - 13、日期过滤:支持多个时间段排除; -- 13、GLUE 模式 Web Ide 版本对比功能; - 14、提供执行器Docker镜像; - 15、脚本任务,支持数据参数,新版本仅支持单参数不支持需要兼容; - 17、批量调度:调度请求入queue,调度线程批量获取调度请求并发起远程调度;提高线程效率; - 18、执行器端口复用,复用容器端口提供通讯服务; -- 19、自定义失败重试时间间隔; -- 20、安全功能增强,通讯加密参数改用加密数据避免AccessToken明文, 降低token泄漏风险; +- 19、安全功能增强,通讯加密参数改用加密数据避免AccessToken明文, 降低token泄漏风险; +- 20、告警增强: + - 邮件告警:支持自定义标题、模板格式; + - webhook告警:支持自定义告警URL、请求体格式; +- 21、公共告警策略:执行器维度设置多告警策略,任务勾选启用;待评估任务或执行器维度; +- 20、日志策略: + - 调度日志:全局配置:废弃; 新增“调度日志策略”:任务维度自定义,保留3天、7天、1个月、3个月、一年、永久; + - 执行日志:新增“执行RollingLog开关”:任务维度自定义,支持:RollingLog、普通日志(slf4j输出)、关闭(不输出); +- 21、AccessToken:废弃全局配置;支持在线管理,动态生成、动态启停; + + ## 八、其他 diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobCodeController.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobCodeController.java index 7097d3e4..ffb4b4f6 100644 --- a/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobCodeController.java +++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobCodeController.java @@ -7,10 +7,14 @@ import com.xxl.job.admin.model.XxlJobLogGlue; import com.xxl.job.admin.util.I18nUtil; import com.xxl.job.admin.util.JobGroupPermissionUtil; import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.StringTool; +import com.xxl.tool.gson.GsonTool; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @@ -27,6 +31,7 @@ import java.util.List; @Controller @RequestMapping("/jobcode") public class JobCodeController { + private static final Logger logger = LoggerFactory.getLogger(JobCodeController.class); @Resource private XxlJobInfoMapper xxlJobInfoMapper; @@ -79,7 +84,7 @@ public class JobCodeController { } // valid jobGroup permission - JobGroupPermissionUtil.validJobGroupPermission(request, existsJobInfo.getJobGroup()); + LoginInfo loginInfo = JobGroupPermissionUtil.validJobGroupPermission(request, existsJobInfo.getJobGroup()); // update new code existsJobInfo.setGlueSource(glueSource); @@ -103,7 +108,10 @@ public class JobCodeController { // remove code backup more than 30 xxlJobLogGlueMapper.removeOld(existsJobInfo.getId(), 30); + // write operation log + logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", + loginInfo.getUserName(), "jobcode-update", GsonTool.toJson(xxlJobLogGlue)); return Response.ofSuccess(); } - + } diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java index f10a6dbd..40cf80ce 100644 --- a/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java +++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java @@ -18,6 +18,8 @@ import com.xxl.job.core.constant.ExecutorBlockStrategyEnum; import com.xxl.job.core.glue.GlueTypeEnum; import com.xxl.sso.core.model.LoginInfo; import com.xxl.tool.core.DateTool; +import com.xxl.tool.core.StringTool; +import com.xxl.tool.gson.GsonTool; import com.xxl.tool.response.PageModel; import com.xxl.tool.response.Response; import jakarta.annotation.Resource; @@ -70,10 +72,10 @@ public class XxlJobServiceImpl implements XxlJobService { if (group == null) { return Response.ofFail (I18nUtil.getString("system_please_choose")+I18nUtil.getString("jobinfo_field_jobgroup")); } - if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + if (StringTool.isBlank(jobInfo.getJobDesc())) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); } - if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + if (StringTool.isBlank(jobInfo.getAuthor())) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); } @@ -91,7 +93,7 @@ public class XxlJobServiceImpl implements XxlJobService { return Response.ofFail ( (I18nUtil.getString("schedule_type")) ); } try { - int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + int fixSecond = Integer.parseInt(jobInfo.getScheduleConf()); if (fixSecond < 1) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } @@ -104,7 +106,7 @@ public class XxlJobServiceImpl implements XxlJobService { if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_gluetype")+I18nUtil.getString("system_unvalid")) ); } - if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && (jobInfo.getExecutorHandler()==null || jobInfo.getExecutorHandler().trim().length()==0) ) { + if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && StringTool.isBlank(jobInfo.getExecutorHandler()) ) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+"JobHandler") ); } // 》fix "\r" in shell @@ -124,10 +126,10 @@ public class XxlJobServiceImpl implements XxlJobService { } // 》ChildJobId valid - if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + if (StringTool.isNotBlank(jobInfo.getChildJobId())) { String[] childJobIds = jobInfo.getChildJobId().split(","); for (String childJobIdItem: childJobIds) { - if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + if (StringTool.isNotBlank(childJobIdItem) && StringTool.isNumeric(childJobIdItem)) { XxlJobInfo childJobInfo = xxlJobInfoMapper.loadById(Integer.parseInt(childJobIdItem)); if (childJobInfo==null) { return Response.ofFail ( @@ -165,26 +167,21 @@ public class XxlJobServiceImpl implements XxlJobService { return Response.ofFail ( (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) ); } - return Response.ofSuccess(String.valueOf(jobInfo.getId())); - } + // write operation log + logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", + loginInfo.getUserName(), "jobinfo-save", GsonTool.toJson(jobInfo)); - private boolean isNumeric(String str){ - try { - int result = Integer.valueOf(str); - return true; - } catch (NumberFormatException e) { - return false; - } + return Response.ofSuccess(String.valueOf(jobInfo.getId())); } @Override public Response update(XxlJobInfo jobInfo, LoginInfo loginInfo) { // valid base - if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + if (StringTool.isBlank(jobInfo.getJobDesc())) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); } - if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + if (StringTool.isBlank(jobInfo.getAuthor())) { return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); } @@ -202,7 +199,7 @@ public class XxlJobServiceImpl implements XxlJobService { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } try { - int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + int fixSecond = Integer.parseInt(jobInfo.getScheduleConf()); if (fixSecond < 1) { return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); } @@ -223,10 +220,10 @@ public class XxlJobServiceImpl implements XxlJobService { } // 》ChildJobId valid - if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + if (StringTool.isNotBlank(jobInfo.getChildJobId())) { String[] childJobIds = jobInfo.getChildJobId().split(","); for (String childJobIdItem: childJobIds) { - if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + if (StringTool.isNotBlank(childJobIdItem) && StringTool.isNumeric(childJobIdItem)) { // parse child int childJobId = Integer.parseInt(childJobIdItem); if (childJobId == jobInfo.getId()) { @@ -310,6 +307,10 @@ public class XxlJobServiceImpl implements XxlJobService { exists_jobInfo.setUpdateTime(new Date()); xxlJobInfoMapper.update(exists_jobInfo); + // write operation log + logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", + loginInfo.getUserName(), "jobinfo-update", GsonTool.toJson(exists_jobInfo)); + return Response.ofSuccess(); } @@ -329,6 +330,11 @@ public class XxlJobServiceImpl implements XxlJobService { xxlJobInfoMapper.delete(id); xxlJobLogMapper.delete(id); xxlJobLogGlueMapper.deleteByJobId(id); + + // write operation log + logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", + loginInfo.getUserName(), "jobinfo-remove", id); + return Response.ofSuccess(); } @@ -372,6 +378,11 @@ public class XxlJobServiceImpl implements XxlJobService { xxlJobInfo.setUpdateTime(new Date()); xxlJobInfoMapper.update(xxlJobInfo); + + // write operation log + logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", + loginInfo.getUserName(), "jobinfo-start", id); + return Response.ofSuccess(); } @@ -395,6 +406,11 @@ public class XxlJobServiceImpl implements XxlJobService { xxlJobInfo.setUpdateTime(new Date()); xxlJobInfoMapper.update(xxlJobInfo); + + // write operation log + logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", + loginInfo.getUserName(), "jobinfo-stop", id); + return Response.ofSuccess(); } @@ -417,6 +433,11 @@ public class XxlJobServiceImpl implements XxlJobService { } XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobId, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList); + + // write operation log + logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}", + loginInfo.getUserName(), "jobinfo-trigger", jobId); + return Response.ofSuccess(); }