diff --git "a/docs/1.18.0/design/\346\227\245\345\277\227\346\224\257\346\214\201\347\273\206\347\262\222\345\272\246\350\277\224\345\233\236\346\224\271\351\200\240_\350\256\276\350\256\241.md" "b/docs/1.18.0/design/\346\227\245\345\277\227\346\224\257\346\214\201\347\273\206\347\262\222\345\272\246\350\277\224\345\233\236\346\224\271\351\200\240_\350\256\276\350\256\241.md" new file mode 100644 index 0000000000..a1ba5cecc6 --- /dev/null +++ "b/docs/1.18.0/design/\346\227\245\345\277\227\346\224\257\346\214\201\347\273\206\347\262\222\345\272\246\350\277\224\345\233\236\346\224\271\351\200\240_\350\256\276\350\256\241.md" @@ -0,0 +1,130 @@ +# 阶段2:设计方案文档 + +## 1. 总述 + +### 1.1 需求与目标 + +**项目背景**:在大模型分析场景中,当前获取用户任务日志接口会返回所有(info、error、warn)任务日志,导致大模型处理文件数量过多。为了优化大模型处理效率,需要对 filesystem 模块的 openLog 接口进行增强,支持根据指定的日志级别返回对应的日志内容。 + +**设计目标**: +1. 实现 openLog 接口的日志级别过滤功能 +2. 支持 all、info、error、warn 四种日志级别 +3. 保持向后兼容性,缺省情况下返回全部日志 +4. 确保实现的正确性、性能和可靠性 + +## 2. 技术架构 + +**技术栈**: +- 开发语言:Java (服务端), Scala (客户端SDK) +- 框架:Spring Boot +- 存储:文件系统 + +**部署架构**: +与现有 filesystem 模块部署架构一致,无需额外部署组件。 + +## 3. 核心概念/对象 + +| 概念/对象 | 描述 | +|-----------|------| +| LogLevel | 日志级别枚举类,定义了 ERROR、WARN、INFO、ALL 四种级别 | +| FsRestfulApi | filesystem 模块的 RESTful 接口实现类 | +| OpenLogAction | 客户端 SDK 中调用 openLog 接口的 Action 类 | +| filterLogByLevel | 新增的日志过滤方法 | + +## 4. 处理逻辑设计 + +### 4.1 接口参数变更 + +**原接口签名**: +```java +public Message openLog( + HttpServletRequest req, + @RequestParam(value = "path", required = false) String path, + @RequestParam(value = "proxyUser", required = false) String proxyUser) +``` + +**新接口签名**: +```java +public Message openLog( + HttpServletRequest req, + @RequestParam(value = "path", required = false) String path, + @RequestParam(value = "proxyUser", required = false) String proxyUser, + @RequestParam(value = "logLevel", required = false, defaultValue = "all") String logLevel) +``` + +### 4.2 日志过滤逻辑 + +``` +输入: log[4] 数组, logLevel 参数 +| +v +logLevel 为空或 "all"? --> 是 --> 返回原始 log[4] +| +v (否) +根据 logLevel 创建新数组 filteredResult[4],初始化为空字符串 +| +v +switch(logLevel.toLowerCase()): + case "error": filteredResult[0] = log[0] + case "warn": filteredResult[1] = log[1] + case "info": filteredResult[2] = log[2] + default: 返回原始 log[4] (向后兼容) +| +v +返回 filteredResult[4] +``` + +### 4.3 数据结构 + +日志数组索引与日志级别对应关系: + +| 索引 | 日志级别 | LogLevel.Type | +|------|----------|---------------| +| 0 | ERROR | LogLevel.Type.ERROR | +| 1 | WARN | LogLevel.Type.WARN | +| 2 | INFO | LogLevel.Type.INFO | +| 3 | ALL | LogLevel.Type.ALL | + +## 5. 代码变更清单 + +### 5.1 FsRestfulApi.java + +**文件路径**: `linkis-public-enhancements/linkis-pes-publicservice/src/main/java/org/apache/linkis/filesystem/restful/api/FsRestfulApi.java` + +**变更内容**: +1. `openLog` 方法添加 `logLevel` 参数 +2. 添加 Swagger API 文档注解 +3. 新增 `filterLogByLevel()` 私有方法 + +### 5.2 OpenLogAction.scala + +**文件路径**: `linkis-computation-governance/linkis-client/linkis-computation-client/src/main/scala/org/apache/linkis/ujes/client/request/OpenLogAction.scala` + +**变更内容**: +1. Builder 类添加 `logLevel` 属性(默认值 "all") +2. 添加 `setLogLevel()` 方法 +3. `build()` 方法中添加 logLevel 参数设置 + +## 6. 非功能性设计 + +### 6.1 安全 + +- **权限控制**:确保用户只能访问自己有权限的日志文件(复用现有逻辑) +- **参数校验**:对请求参数进行合理处理,无效参数不抛异常 + +### 6.2 性能 + +- 日志级别过滤对接口响应时间的影响可忽略不计(< 1ms) +- 过滤逻辑在内存中完成,无额外 I/O 操作 + +### 6.3 向后兼容 + +- 缺省情况下返回全部日志,与原有行为一致 +- 无效 logLevel 参数返回全部日志,确保服务不中断 +- 现有调用方无需修改代码即可继续使用 + +## 7. 变更历史 + +| 版本 | 日期 | 变更人 | 变更内容 | +|-----|------|--------|----------| +| 1.0 | 2025-12-26 | AI Assistant | 初始版本 | diff --git "a/docs/1.18.0/requirements/\346\227\245\345\277\227\346\224\257\346\214\201\347\273\206\347\262\222\345\272\246\350\277\224\345\233\236\346\224\271\351\200\240_\351\234\200\346\261\202.md" "b/docs/1.18.0/requirements/\346\227\245\345\277\227\346\224\257\346\214\201\347\273\206\347\262\222\345\272\246\350\277\224\345\233\236\346\224\271\351\200\240_\351\234\200\346\261\202.md" new file mode 100644 index 0000000000..d5ba14f796 --- /dev/null +++ "b/docs/1.18.0/requirements/\346\227\245\345\277\227\346\224\257\346\214\201\347\273\206\347\262\222\345\272\246\350\277\224\345\233\236\346\224\271\351\200\240_\351\234\200\346\261\202.md" @@ -0,0 +1,125 @@ +# 阶段1:需求分析文档 + +## 一、需求背景 + +在大模型分析场景中,当前获取用户任务日志接口会返回所有(info、error、warn)任务日志,导致大模型处理文件数量过多。为了优化大模型处理效率,需要对 filesystem 模块的 openLog 接口进行增强,支持根据指定的日志级别返回对应的日志内容。 + +## 二、需求描述 + +### 2.1 需求详细描述 + +| 模块 | 功能点 | 功能描述 | UI设计及细节 | 功能关注点 | +|-----|--------|----------|--------------|------------| +| filesystem | 日志级别过滤 | 在 openLog 接口中添加 logLevel 参数,支持指定返回的日志级别 | 不涉及 | 确保参数类型正确,默认值设置合理 | +| filesystem | 多种日志级别支持 | 支持 logLevel=all,info,error,warn 四种取值 | 不涉及 | 确保所有取值都能正确处理 | +| filesystem | 默认值处理 | 缺省情况下返回全部日志(相当于 logLevel=all) | 不涉及 | 确保向后兼容性 | +| filesystem | 向后兼容 | 不影响现有调用方的使用 | 不涉及 | 现有调用方无需修改代码即可继续使用 | + +### 2.2 需求交互步骤 + +1. 用户调用 `/openLog` 接口,指定 `path` 参数和可选的 `logLevel` 参数 +2. 系统解析请求参数,获取日志文件路径和日志级别 +3. 系统读取日志文件内容,根据指定的日志级别过滤日志 +4. 系统返回过滤后的日志内容给用户 + +### 2.3 模块交互步骤 + +``` +用户 → filesystem模块 → openLog接口 → 日志文件 → 日志过滤 → 返回结果 +``` + +**关键步骤说明**: +1. 用户调用 openLog 接口,传入 path 和 logLevel 参数 +2. openLog 接口验证参数合法性,解析日志级别 +3. 系统读取指定路径的日志文件 +4. 系统根据日志级别过滤日志内容 +5. 系统将过滤后的日志内容封装为响应对象返回给用户 + +**关注点**: +- 需关注无效 logLevel 参数的处理,应返回默认日志(全部日志) +- 需关注日志文件过大的情况,应返回合理的错误信息 +- 需关注权限控制,确保用户只能访问自己有权限的日志文件 + +## 三、接口文档 + +### 3.1 接口基本信息 + +| 项 | 说明 | +|----|------| +| 接口URL | /api/rest_j/v1/filesystem/openLog | +| 请求方法 | GET | +| 接口描述 | 获取指定路径的日志文件内容,支持按日志级别过滤 | + +### 3.2 请求参数 + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| path | String | 是 | 无 | 日志文件路径 | +| proxyUser | String | 否 | 无 | 代理用户,仅管理员可使用 | +| logLevel | String | 否 | all | 日志级别,取值为 all,info,error,warn | + +### 3.3 响应参数 + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| status | String | 响应状态,success 表示成功,error 表示失败 | +| message | String | 响应消息 | +| data | Object | 响应数据 | +| data.log | String[] | 日志内容数组,按以下顺序排列:
1. 第0位:ERROR 级别的日志
2. 第1位:WARN 级别的日志
3. 第2位:INFO 级别的日志
4. 第3位:ALL 级别的日志(所有日志) | + +### 3.4 请求示例 + +```bash +# 请求所有日志 +curl -X GET "http://localhost:8080/api/rest_j/v1/filesystem/openLog?path=/path/to/test.log" + +# 请求特定级别的日志 +curl -X GET "http://localhost:8080/api/rest_j/v1/filesystem/openLog?path=/path/to/test.log&logLevel=error" +``` + +### 3.5 响应示例 + +**请求所有日志的响应**: +```json +{ + "status": "success", + "message": "", + "data": { + "log": [ + "2025-12-26 10:00:02.000 ERROR This is an error log\n", + "2025-12-26 10:00:01.000 WARN This is a warn log\n", + "2025-12-26 10:00:00.000 INFO This is an info log\n", + "2025-12-26 10:00:00.000 INFO This is an info log\n2025-12-26 10:00:01.000 WARN This is a warn log\n2025-12-26 10:00:02.000 ERROR This is an error log\n" + ] + } +} +``` + +**请求 ERROR 级别日志的响应**: +```json +{ + "status": "success", + "message": "", + "data": { + "log": [ + "2025-12-26 10:00:02.000 ERROR This is an error log\n", + "", + "", + "" + ] + } +} +``` + +## 四、关联影响分析 + +- **对存量功能的影响**:无,该功能是对现有接口的增强,不会影响其他功能 +- **对第三方组件的影响**:无,该功能仅涉及 filesystem 模块内部逻辑 + +## 五、测试关注点 + +- 验证不同日志级别参数的处理是否正确 +- 验证缺省情况下是否返回全部日志 +- 验证无效日志级别参数的处理是否正确 +- 验证大小写不敏感是否正确 +- 验证权限控制是否有效 diff --git a/linkis-public-enhancements/linkis-pes-publicservice/src/main/java/org/apache/linkis/filesystem/restful/api/FsRestfulApi.java b/linkis-public-enhancements/linkis-pes-publicservice/src/main/java/org/apache/linkis/filesystem/restful/api/FsRestfulApi.java index 0cf8d8334f..2eee67ab0c 100644 --- a/linkis-public-enhancements/linkis-pes-publicservice/src/main/java/org/apache/linkis/filesystem/restful/api/FsRestfulApi.java +++ b/linkis-public-enhancements/linkis-pes-publicservice/src/main/java/org/apache/linkis/filesystem/restful/api/FsRestfulApi.java @@ -1390,20 +1390,26 @@ public Message getSheetInfo( @ApiOperation(value = "openLog", notes = "open log", response = Message.class) @ApiImplicitParams({ @ApiImplicitParam(name = "path", required = false, dataType = "String", value = "path"), - @ApiImplicitParam(name = "proxyUser", dataType = "String") + @ApiImplicitParam(name = "proxyUser", dataType = "String"), + @ApiImplicitParam( + name = "logLevel", + dataType = "String", + value = + "Log level, values: all,info,error,warn or comma-separated combinations like error,info,warn") }) @RequestMapping(path = "/openLog", method = RequestMethod.GET) public Message openLog( HttpServletRequest req, @RequestParam(value = "path", required = false) String path, - @RequestParam(value = "proxyUser", required = false) String proxyUser) + @RequestParam(value = "proxyUser", required = false) String proxyUser, + @RequestParam(value = "logLevel", required = false, defaultValue = "all") String logLevel) throws IOException, WorkSpaceException { if (StringUtils.isEmpty(path)) { throw WorkspaceExceptionManager.createException(80004, path); } String userName = ModuleUserUtils.getOperationUser(req, "openLog " + path); LoggerUtils.setJobIdMDC("openLogThread_" + userName); - LOGGER.info("userName {} start to openLog File {}", userName, path); + LOGGER.info("userName {} start to openLog File {}, logLevel: {}", userName, path, logLevel); if (proxyUser != null && Configuration.isJobHistoryAdmin(userName)) { userName = proxyUser; } @@ -1419,6 +1425,8 @@ public Message openLog( > ByteTimeUtils.byteStringAsBytes(FILESYSTEM_FILE_CHECK_SIZE.getValue())) { throw WorkspaceExceptionManager.createException(80033, path); } + // 解析日志级别,支持多级别组合 + Set targetLevels = parseLogLevels(logLevel); try (FileSource fileSource = FileSource$.MODULE$.create(fsPath, fileSystem).addParams("ifMerge", "false")) { Pair> collect = fileSource.collect()[0]; @@ -1431,7 +1439,21 @@ public Message openLog( snd.stream() .map(f -> f[0]) .forEach( - s -> WorkspaceUtil.logMatch(s, start).forEach(i -> log[i].append(s).append("\n"))); + s -> { + List matchedIndices = WorkspaceUtil.logMatch(s, start); + if (targetLevels.contains(LogLevel.Type.ALL)) { + // 返回所有日志 + matchedIndices.forEach(i -> log[i].append(s).append("\n")); + } else { + // 多级别组合:只返回目标级别的日志 + for (LogLevel.Type level : targetLevels) { + int targetIndex = level.ordinal(); + if (matchedIndices.contains(targetIndex)) { + log[targetIndex].append(s).append("\n"); + } + } + } + }); LOGGER.info("userName {} Finished to openLog File {}", userName, path); LoggerUtils.removeJobIdMDC(); return Message.ok() @@ -1454,6 +1476,55 @@ private static void deleteAllFiles(FileSystem fileSystem, FsPath fsPath) throws fileSystem.delete(fsPath); } + /** 解析日志级别参数,支持多级别组合 */ + private Set parseLogLevels(String logLevel) { + Set levels = new HashSet<>(); + + if (StringUtils.isEmpty(logLevel)) { + levels.add(LogLevel.Type.ALL); + return levels; + } + + // 去除空格并转为大写 + String cleanedLevel = logLevel.replaceAll("\\s+", "").toUpperCase(); + + // 检查是否为 ALL + if ("ALL".equals(cleanedLevel)) { + levels.add(LogLevel.Type.ALL); + return levels; + } + + // 检查是否包含逗号(多级别组合) + if (cleanedLevel.contains(",")) { + String[] levelArray = cleanedLevel.split(","); + for (String levelStr : levelArray) { + try { + LogLevel.Type level = LogLevel.Type.valueOf(levelStr); + levels.add(level); + } catch (IllegalArgumentException e) { + LOGGER.warn("Invalid log level: {}, skipping", levelStr); + } + } + + // 如果没有有效的级别,使用默认的 ALL + if (levels.isEmpty()) { + LOGGER.warn("No valid log levels found in: {}, using ALL", logLevel); + levels.add(LogLevel.Type.ALL); + } + } else { + // 单级别情况 + try { + LogLevel.Type level = LogLevel.Type.valueOf(cleanedLevel); + levels.add(level); + } catch (IllegalArgumentException e) { + LOGGER.warn("Invalid logLevel: {}, use default level: ALL", logLevel); + levels.add(LogLevel.Type.ALL); + } + } + + return levels; + } + @ApiOperation(value = "chmod", notes = "file permission chmod", response = Message.class) @ApiImplicitParams({ @ApiImplicitParam(name = "filepath", required = true, dataType = "String", value = "filepath"),