Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions docs/1.18.0/design/日志支持细粒度返回改造_设计.md
Original file line number Diff line number Diff line change
@@ -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 | 初始版本 |
125 changes: 125 additions & 0 deletions docs/1.18.0/requirements/日志支持细粒度返回改造_需求.md
Original file line number Diff line number Diff line change
@@ -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[] | 日志内容数组,按以下顺序排列:<br>1. 第0位:ERROR 级别的日志<br>2. 第1位:WARN 级别的日志<br>3. 第2位:INFO 级别的日志<br>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 模块内部逻辑

## 五、测试关注点

- 验证不同日志级别参数的处理是否正确
- 验证缺省情况下是否返回全部日志
- 验证无效日志级别参数的处理是否正确
- 验证大小写不敏感是否正确
- 验证权限控制是否有效
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -1419,6 +1425,8 @@ public Message openLog(
> ByteTimeUtils.byteStringAsBytes(FILESYSTEM_FILE_CHECK_SIZE.getValue())) {
throw WorkspaceExceptionManager.createException(80033, path);
}
// 解析日志级别,支持多级别组合
Set<LogLevel.Type> targetLevels = parseLogLevels(logLevel);
try (FileSource fileSource =
FileSource$.MODULE$.create(fsPath, fileSystem).addParams("ifMerge", "false")) {
Pair<Object, ArrayList<String[]>> collect = fileSource.collect()[0];
Expand All @@ -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<Integer> 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()
Expand All @@ -1454,6 +1476,55 @@ private static void deleteAllFiles(FileSystem fileSystem, FsPath fsPath) throws
fileSystem.delete(fsPath);
}

/** 解析日志级别参数,支持多级别组合 */
private Set<LogLevel.Type> parseLogLevels(String logLevel) {
Set<LogLevel.Type> 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"),
Expand Down
Loading