Appearance
transactionInfo 接口代码逻辑分析
接口路径:
GET /transactionInfo
所在类:com.apm.starry.web.controller.BusinessTransactionController
功能概述: 根据 traceId 查询一笔完整的分布式交易(Trace)详情,包含调用栈、应用拓扑图等信息,用于"交易快照"页面展示。
1. 接口签名与请求参数
java
@RequestMapping(value = "/transactionInfo", method = RequestMethod.GET)
@ResponseBody
public TransactionInfoViewModel transactionInfo(
@RequestParam("traceId") String traceIdParam,
@RequestParam(value = "focusTimestamp", required = false, defaultValue = "0") long focusTimestamp,
@RequestParam(value = "agentId", required = false) String agentId,
@RequestParam(value = "spanId", required = false, defaultValue = "-1") long spanId,
@RequestParam(value = "v", required = false, defaultValue = "0") int viewVersion
)| 参数名 | 类型 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|---|
traceId | String | 是 | - | 交易ID字符串,格式为 agentId^agentStartTime^transactionSequence |
focusTimestamp | long | 否 | 0 | 焦点时间戳,用于确定当事务包含多个 Span 时,以哪个 Span 为基准展示 |
agentId | String | 否 | null | Agent 标识,辅助定位焦点 Span |
spanId | long | 否 | -1 | Span 标识,辅助定位焦点 Span |
v | int | 否 | 0 | 视图版本号,影响应用拓扑图的构建方式 |
2. 整体执行流程
mermaid
flowchart TD
A["接收请求参数"] --> B["解析 traceId"]
B --> C["查询 Span 列表"]
C --> D["构建调用树 CallTree"]
D --> E["构建应用拓扑图 ApplicationMap"]
D --> F["创建 RecordSet 调用栈记录集"]
E --> G["组装 TransactionInfoViewModel"]
F --> G
G --> H["返回 JSON 响应"]3. 详细步骤分析
3.1 解析 traceId
java
final TransactionId transactionId = TransactionIdUtils.parseTransactionId(traceIdParam);- 将前端传入的
traceIdParam字符串解析为TransactionId对象。 TransactionId包含三个字段:agentId、agentStartTime、transactionSequence。- 同时生成
transactionStr用于后续数据库查询:
java
String transactionStr = transactionId.getAgentId() + "^" + transactionId.getAgentStartTime() + "^" + transactionId.getTransactionSequence();3.2 查询 Span 列表
java
List<SpanBo> spanBoList = this.spanService.selectSpanList(transactionId, focusTimestamp);调用链路: SpanService.selectSpanList(TransactionId, long) → SpanService.getSpanListNew(Map)
核心逻辑 (getSpanListNew):
- 构建查询参数: 以
transaction_str为条件,时间范围为focusTimestamp ± 12小时,查询 ClickHouse 的 Span 表。 - 查询所有 SpanBo: 调用
spanDao.getSpanListNew(params)获取原始数据。 - 拆分数据:
type == 0→ 主 Span(按collectorAcceptTime降序)type != 0→ SpanChunk(按collectorAcceptTime升序)
- 查询 SpanEvent: 批量查询所有 Span 和 SpanChunk 关联的事件数据。
- 组装调用关系: 将 SpanChunkBo 挂载到对应的 SpanBo 下,将 SpanEventBo 挂载到对应的 Span/SpanChunk 下。
- 构建 Annotation: 为每个 Span/SpanEvent 解析注解信息(支持 JSON 格式和简单 key-value 格式)。
- 数据清洗: 过滤掉没有 SpanChunk、SpanEvent 和 Annotation 的垃圾数据。
3.3 构建调用树 (CallTree)
java
final SpanResult spanResult = this.spanService.selectSpan(spanBoList, focusTimestamp);
final CallTreeIterator callTreeIterator = spanResult.getCallTree();调用链路: SpanService.selectSpan(List<SpanBo>, long) → SpanService.order(...) → 返回 SpanResult
selectSpan 核心逻辑:
- 空判断: 若
spanBoList为空,返回ERROR状态的空结果。 - 排序构建调用树: 调用
order()方法将平铺的 Span 列表组织成树形调用结构,生成CallTreeIterator。 - 元数据翻译(使用
CompletableFuture并发执行 5 个任务):
| 任务 | 方法 | 说明 |
|---|---|---|
| task1 | transitionDynamicApiId | 将 apiId 翻译为可读的 API 方法名(查询 api_meta_data 表) |
| task2 | transitionSqlId | 将 sqlId 翻译为完整 SQL 语句(查询 sql_meta_data 表) |
| task3 | transitionMongoJson | 翻译 MongoDB 查询的 JSON 表示 |
| task4 | transitionCachedString | 翻译缓存的字符串元数据(查询 string_meta_data 表) |
| task5 | transitionException | 处理异常信息的翻译 |
性能优化: 上述 5 个翻译任务通过线程池
querySpanChuckTask并发执行,且内部对批量 ID 做了分页查询(每次最多 1000 个),避免 SQL IN 子句过长。
3.4 构建应用拓扑图 (ApplicationMap)
java
ApplicationMap map = filteredMapService.selectApplicationMap(spanBoList, viewVersion);调用链路: FilteredMapService.selectApplicationMap(List<SpanBo>, int)
核心逻辑:
- 创建
FilteredMapBuilder,传入应用工厂、服务类型注册表、时间范围和视图版本。 - 调用
addTransaction(spanBoList)将 Span 数据添加到构建器。 - 调用
build()生成FilteredMap。 - 通过
createMap()方法:- 构建节点直方图(
NodeHistogramFactory):统计各应用节点的响应时间分布。 - 构建服务实例列表(
ServerInstanceListFactory):获取各 Agent 实例信息。 - 调用
ApplicationMapBuilder构建最终的ApplicationMap,包含 节点 (Nodes) 和 连线 (Links)。
- 构建节点直方图(
- 若配置了
serverMapDataFilter,对拓扑图数据做过滤。
返回的 ApplicationMap 包含:
Collection<Node> nodes— 应用节点(包含应用名、服务类型、Agent 信息、响应直方图等)Collection<Link> links— 调用关系连线(包含源/目标应用、调用次数、响应时间等)
3.5 创建 RecordSet
java
RecordSet recordSet = this.transactionInfoService.createRecordSet(callTreeIterator, focusTimestamp, agentId, spanId);调用链路: TransactionInfoService.createRecordSet(...)
核心逻辑:
查找焦点 ViewPoint:
- 遍历
alignList,依次匹配focusTimestamp、agentId、spanId。 - 若无法精确匹配,返回第一个 Span 作为 fallback。
- 从 ViewPoint 中提取
agentId、applicationId、applicationName。
- 遍历
计算时间范围: 从 alignList 的首条记录获取
startTime和endTime。检查日志事务标记: 遍历所有 Span 检查是否有
LoggingInfo.LOGGED标记。填充调用栈记录 (
SpanAlignPopulate.populateSpanRecord):- 遍历
CallTreeIterator的每个节点。 - 使用
RecordFactory为每个节点创建Record,包含:- 基本 Record: 方法名、耗时、参数等。
- 异常 Record: 若有异常,追加异常记录。
- 注解 Record: 追加 SQL、参数等注解信息。
- 远程地址 Record: 追加
REMOTE_ADDRESS。 - 端点 Record: 追加
ENDPOINT。
- 遍历
标记焦点记录: 在 RecordList 中标记与 ViewPoint 匹配的记录为
focused = true。
RecordSet 结构:
| 字段 | 说明 |
|---|---|
startTime | 调用栈开始时间 |
endTime | 调用栈结束时间 |
agentId | Agent 标识 |
applicationId | 应用 ID |
applicationName | 应用名称(RPC 参数或显示参数) |
beginTimestamp | ViewPoint 的开始时间戳 |
recordList | 调用栈记录列表 |
loggingTransactionInfo | 是否有日志事务标记 |
3.6 组装 ViewModel 并返回
java
return new TransactionInfoViewModel(
transactionStr, spanId,
map.getNodes(), map.getLinks(),
recordSet,
spanResult.getTraceState(),
logConfiguration
);4. 返回数据结构 (TransactionInfoViewModel)
| JSON 字段 | 类型 | 来源 | 说明 |
|---|---|---|---|
transactionId | String | 入参解析 | 交易ID字符串 |
spanId | long | 入参 | Span标识 |
applicationName | String | RecordSet | 应用名称 |
agentId | String | RecordSet | Agent标识 |
applicationId | String | RecordSet | 应用ID |
callStackStart | long | RecordSet.startTime | 调用栈起始时间 |
callStackEnd | long | RecordSet.endTime | 调用栈结束时间 |
completeState | String | SpanResult.traceState | 链路完整状态(如 COMPLETE / ERROR) |
loggingTransactionInfo | boolean | RecordSet | 是否包含日志事务信息 |
disableButtonMessage | String | LogConfiguration | 日志按钮禁用提示消息 |
callStackIndex | Map<String, Integer> | 静态定义 | 调用栈字段索引映射 |
callStack | List<CallStack> | RecordSet.recordList 转换 | 调用栈数据(序列化为数组格式) |
applicationMapData | Map<String, List<Object>> | Nodes + Links | 应用拓扑图数据 |
CallStack 字段说明(共 26 个字段):
| 索引 | 字段名 | 说明 |
|---|---|---|
| 0 | depth | 调用深度 |
| 1 | begin | 开始时间 |
| 2 | end | 结束时间 |
| 3 | excludeFromTimeline | 是否从时间线中排除 |
| 4 | applicationName | 应用名称 |
| 5 | tab | 缩进层级 |
| 6 | id | 记录ID |
| 7 | parentId | 父记录ID |
| 8 | isMethod | 是否为方法调用 |
| 9 | hasChild | 是否有子节点 |
| 10 | title | 方法/API名称 |
| 11 | arguments | 参数信息 |
| 12 | executeTime | 执行时间(格式化) |
| 13 | gap | 间隔时间 |
| 14 | elapsedTime | 耗时(ms) |
| 15 | barWidth | 时间条宽度(百分比) |
| 16 | executionMilliseconds | 执行毫秒数 |
| 17 | simpleClassName | 简短类名 |
| 18 | methodType | 方法类型代码 |
| 19 | apiType | API类型 |
| 20 | agent | Agent名称 |
| 21 | isFocused | 是否为焦点记录 |
| 22 | hasException | 是否有异常 |
| 23 | isAuthorized | 是否已授权 |
| 24 | fullApiDescription | 完整API描述 |
| 25 | exceptionClassRoute | 异常类路径 |
5. 核心依赖关系图
mermaid
graph LR
Controller["BusinessTransactionController"] --> SpanService["SpanService"]
Controller --> TransactionInfoService["TransactionInfoService"]
Controller --> FilteredMapService["FilteredMapService"]
Controller --> LogConfiguration["LogConfiguration"]
SpanService --> SpanDao["SpanDao (ClickHouse)"]
SpanService --> ApiMetaDataDao["ApiMetaDataDao"]
SpanService --> SqlMetaDataDao["SqlMetaDataDao"]
SpanService --> StringMetaDataDao["StringMetaDataDao"]
TransactionInfoService --> AnnotationKeyMatcherService["AnnotationKeyMatcherService"]
TransactionInfoService --> ServiceTypeRegistryService["ServiceTypeRegistryService"]
TransactionInfoService --> RecordFactory["RecordFactory"]
FilteredMapService --> FilteredMapBuilder["FilteredMapBuilder"]
FilteredMapService --> ApplicationMapBuilder["ApplicationMapBuilder"]
FilteredMapService --> AgentInfoService["AgentInfoService"]6. 关键数据库交互
| 步骤 | DAO 方法 | 数据源 | 说明 |
|---|---|---|---|
| 查询 Span 列表 | SpanDao.getSpanListNew | ClickHouse | 按 transaction_str 和时间范围查询 |
| 查询 SpanEvent | SpanDao.getSpanEventStrList | ClickHouse | 按 Span ID 批量查询事件 |
| 翻译 API 元数据 | ApiMetaDataDao.getApiMetaDataBatch | ClickHouse | 批量查询 api_meta_data 表 |
| 翻译 SQL 元数据 | SqlMetaDataDao.getSqlMetaDataBatch | ClickHouse | 批量查询 sql_meta_data 表 |
| 翻译字符串元数据 | StringMetaDataDao.getStringMetaDataBatch | ClickHouse | 批量查询 string_meta_data 表 |
7. 性能优化措施
- 并发元数据翻译: 5 个翻译任务(API、SQL、MongoDB、CachedString、Exception)通过
CompletableFuture并发执行。 - 批量查询: 所有元数据查询都做了分批处理(每批最多 1000 条),避免 SQL IN 子句过长导致报错。
- 共享线程池: 使用名为
querySpanChuckTask的ThreadPoolTaskExecutor,避免频繁创建线程池导致资源泄漏。 - SpanEvent 数量限制: 当 Span ID 超过 2000 个时进行截断,防止查询过于庞大。
- 时间范围限定: 查询时间窗口为
focusTimestamp ± 12小时,避免全表扫描。
8. 异常处理与容错
- TransactionId 解析失败:
TransactionIdUtils.parseTransactionId解析异常将直接抛出,由全局异常处理器捕获。 - 空 Span 列表: 若查询不到任何 Span,
selectSpan返回ERROR状态的空CallTreeIterator。 - API 元数据未找到: 记录到 Redis 缓存 (
SqlAndApiNotFoundCache),并在 Annotation 中标记ERROR_API_METADATA_NOT_FOUND。 - SQL 元数据未找到: 同上,标记
SQL-ID not found。 - .NET 探针兼容: 当
api_meta_data查不到时,尝试从 Annotation(key=12) 或 SpanStacktrace(key=9030) 中提取 API 名称。
9. 该接口的调用方
| 调用位置 | 场景 |
|---|---|
| 前端"交易快照"页面 | 直接发起 GET 请求展示交易详情 |
BusinessTransactionController.getFundamental() | 根因分析时内部调用,获取交易调用栈后发送给 RCA 引擎 |
OpenAnalysisTraceController.transactionInfo() | 开放 API 中的相同功能实现 |
10. 涉及数据库表总结
该接口一共涉及 5 张 ClickHouse 表 + 1 张 MySQL 表 + Redis,共计 6 个数据存储交互。
ClickHouse 表(5 张)
| # | 表名 | 调用步骤 | DAO 方法 | 用途 |
|---|---|---|---|---|
| 1 | span | 3.2 查询 Span 列表 | SpanDao.getSpanListNew | 查询主 Span 和 SpanChunk 原始数据(type=0 为主 Span,type!=0 为 SpanChunk),按 transaction_str 和时间范围查询 |
| 2 | span_event | 3.2 查询 SpanEvent | SpanDao.getSpanEventStrList | 查询 Span 关联的事件数据(方法调用、SQL 执行等),按 parent_id 批量查询 |
| 3 | api_meta_data | 3.3 元数据翻译 (task1) | MetaDataInfoDao.selectApiMetaDataBatch | 将 apiId 翻译为可读的 API 方法签名(如 com.example.UserService.getUser()) |
| 4 | sql_meta_data | 3.3 元数据翻译 (task2) | MetaDataInfoDao.selectSqlMetaDataBatch | 将 sqlId 翻译为完整的 SQL 语句文本 |
| 5 | string_meta_data | 3.3 元数据翻译 (task4) | MetaDataInfoDao.selectStringMetaDataBatch | 将缓存的 stringId 翻译为原始字符串值(如方法参数、HTTP Header 等) |
MySQL 表(1 张)
| # | 表名 | 调用步骤 | DAO 方法 | 用途 |
|---|---|---|---|---|
| 6 | agent_info | 3.4 构建应用拓扑图 | AgentInfoService.getAgentsByApplicationNameWithoutStatus | 构建 ApplicationMap 时查询 Agent 实例信息,用于填充拓扑图中的服务器实例列表 |
Redis
| # | 用途 | 调用步骤 | 说明 |
|---|---|---|---|
| 7 | 缓存未找到的元数据 ID | 3.3 元数据翻译 | 当 api_meta_data 或 sql_meta_data 中找不到对应 ID 时,通过 SqlAndApiNotFoundCache 写入 Redis 缓存,用于后续排查或补偿 |
表交互流程图
mermaid
flowchart LR
subgraph ClickHouse
T1["span"]
T2["span_event"]
T3["api_meta_data"]
T4["sql_meta_data"]
T5["string_meta_data"]
end
subgraph MySQL
T6["agent_info"]
end
subgraph Redis
R1["SqlAndApiNotFoundCache"]
end
A["transactionInfo 接口"] --> T1
T1 -->|"Span ID 列表"| T2
T1 -->|"apiId 列表"| T3
T1 -->|"sqlId 列表"| T4
T1 -->|"stringId 列表"| T5
A -->|"构建拓扑图"| T6
T3 -.->|"未找到时写入"| R1
T4 -.->|"未找到时写入"| R1