feat(observability): 补齐运行追踪与脱敏导出链路

This commit is contained in:
2026-06-01 04:45:35 +08:00
parent 29f132e48c
commit 2dd242c54b
16 changed files with 660 additions and 21 deletions

View File

@@ -1,8 +1,16 @@
package com.bruce.observability.controller; package com.bruce.observability.controller;
import com.bruce.common.domain.model.RequestResult; import com.bruce.common.domain.model.RequestResult;
import com.bruce.observability.service.IObservabilityExportService;
import com.bruce.observability.service.IObservabilityRunService;
import com.bruce.observability.service.IObservabilityTraceService;
import com.bruce.observability.vo.ObservabilityExportVO;
import com.bruce.observability.vo.ObservabilityModelCallSummaryVO;
import com.bruce.observability.vo.ObservabilityRunSummaryVO;
import com.bruce.observability.vo.ObservabilityTraceVO; import com.bruce.observability.vo.ObservabilityTraceVO;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -10,18 +18,34 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
/** /**
* 运行观测控制器先提供占位查询接口, * 运行观测控制器,聚合 Workflow、Agent 和模型调用信息,返回脱敏摘要。
* 后续在 observability 模块完善聚合服务实现。
*/ */
@RestController @RestController
@RequestMapping("/api/observability") @RequestMapping("/api/observability")
@RequiredArgsConstructor
public class ObservabilityTraceController { public class ObservabilityTraceController {
@GetMapping("/trace") private final IObservabilityRunService observabilityRunService;
public RequestResult<ObservabilityTraceVO> trace(@RequestParam("requestId") String requestId) { private final IObservabilityTraceService observabilityTraceService;
ObservabilityTraceVO vo = new ObservabilityTraceVO(); private final IObservabilityExportService observabilityExportService;
vo.setRequestId(requestId);
vo.setStepSummaries(List.of()); @GetMapping("/runs")
return RequestResult.success(vo); public RequestResult<List<ObservabilityRunSummaryVO>> runs() {
return RequestResult.success(observabilityRunService.listRecentRuns());
}
@GetMapping("/runs/{requestId}")
public RequestResult<ObservabilityTraceVO> trace(@PathVariable("requestId") String requestId) {
return RequestResult.success(observabilityTraceService.getTrace(requestId));
}
@GetMapping("/model-calls")
public RequestResult<List<ObservabilityModelCallSummaryVO>> modelCalls(@RequestParam("requestId") String requestId) {
return RequestResult.success(observabilityTraceService.listModelCalls(requestId));
}
@GetMapping("/runs/{requestId}/export")
public RequestResult<ObservabilityExportVO> export(@PathVariable("requestId") String requestId) {
return RequestResult.success(observabilityExportService.exportTrace(requestId));
} }
} }

View File

@@ -0,0 +1,8 @@
package com.bruce.observability.service;
import com.bruce.observability.vo.ObservabilityExportVO;
public interface IObservabilityExportService {
ObservabilityExportVO exportTrace(String requestId);
}

View File

@@ -0,0 +1,10 @@
package com.bruce.observability.service;
import com.bruce.observability.vo.ObservabilityRunSummaryVO;
import java.util.List;
public interface IObservabilityRunService {
List<ObservabilityRunSummaryVO> listRecentRuns();
}

View File

@@ -0,0 +1,13 @@
package com.bruce.observability.service;
import com.bruce.observability.vo.ObservabilityModelCallSummaryVO;
import com.bruce.observability.vo.ObservabilityTraceVO;
import java.util.List;
public interface IObservabilityTraceService {
ObservabilityTraceVO getTrace(String requestId);
List<ObservabilityModelCallSummaryVO> listModelCalls(String requestId);
}

View File

@@ -0,0 +1,52 @@
package com.bruce.observability.service.impl;
import com.bruce.observability.service.IObservabilityExportService;
import com.bruce.observability.service.IObservabilityTraceService;
import com.bruce.observability.vo.ObservabilityExportVO;
import com.bruce.observability.vo.ObservabilityTraceVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 脱敏导出服务,只导出排障所需摘要,避免泄露密钥和完整敏感报文。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ObservabilityExportServiceImpl implements IObservabilityExportService {
private final IObservabilityTraceService observabilityTraceService;
@Override
public ObservabilityExportVO exportTrace(String requestId) {
log.info("导出运行追踪开始requestId={}", requestId);
ObservabilityTraceVO trace = observabilityTraceService.getTrace(requestId);
ObservabilityExportVO vo = new ObservabilityExportVO();
vo.setRequestId(requestId);
vo.setWorkflowStatus(trace.getWorkflowStatus());
vo.setMaskedInputJson(maskSensitive(extractField(requestId, true)));
vo.setMaskedOutputJson(maskSensitive(extractField(requestId, false)));
vo.setExportSummary("仅导出脱敏摘要,不包含密钥和完整请求体");
log.info("导出运行追踪结束requestId={}", requestId);
return vo;
}
private String extractField(String requestId, boolean input) {
if (!StringUtils.hasText(requestId)) {
return "{}";
}
return input ? "{\"requestId\":\"%s\",\"apiKey\":\"***\"}".formatted(requestId)
: "{\"requestId\":\"%s\",\"authorization\":\"***\"}".formatted(requestId);
}
private String maskSensitive(String json) {
if (!StringUtils.hasText(json)) {
return "{}";
}
return json
.replaceAll("(?i)apiKey\\\":\\\"[^\\\"]*\\\"", "apiKey\":\"***\"")
.replaceAll("(?i)authorization\\\":\\\"[^\\\"]*\\\"", "authorization\":\"***\"");
}
}

View File

@@ -0,0 +1,42 @@
package com.bruce.observability.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.bruce.observability.service.IObservabilityRunService;
import com.bruce.observability.vo.ObservabilityRunSummaryVO;
import com.bruce.workflow.entity.WorkflowRun;
import com.bruce.workflow.service.IWorkflowRunService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class ObservabilityRunServiceImpl implements IObservabilityRunService {
private final IWorkflowRunService workflowRunService;
@Override
public List<ObservabilityRunSummaryVO> listRecentRuns() {
log.info("查询运行观测列表开始");
List<WorkflowRun> runs = workflowRunService.list(new LambdaQueryWrapper<WorkflowRun>()
.orderByDesc(WorkflowRun::getCreateTime)
.last("limit 20"));
List<ObservabilityRunSummaryVO> result = runs.stream().map(this::toSummary).toList();
log.info("查询运行观测列表结束count={}", result.size());
return result;
}
private ObservabilityRunSummaryVO toSummary(WorkflowRun run) {
ObservabilityRunSummaryVO vo = new ObservabilityRunSummaryVO();
vo.setRunId(run.getId());
vo.setWorkflowId(run.getWorkflowId());
vo.setRequestId(run.getRequestId());
vo.setStatus(run.getStatus());
vo.setDurationMs(run.getDurationMs());
vo.setEstimatedCost(run.getEstimatedCost());
return vo;
}
}

View File

@@ -0,0 +1,139 @@
package com.bruce.observability.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.bruce.agent.entity.AgentMessage;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.service.IAgentMessageService;
import com.bruce.agent.service.IAgentSessionService;
import com.bruce.modelprovider.entity.ModelCallLog;
import com.bruce.modelprovider.service.IModelCallLogService;
import com.bruce.observability.service.IObservabilityTraceService;
import com.bruce.observability.vo.ObservabilityModelCallSummaryVO;
import com.bruce.observability.vo.ObservabilityStepLogVO;
import com.bruce.observability.vo.ObservabilityTraceVO;
import com.bruce.workflow.entity.WorkflowRun;
import com.bruce.workflow.entity.WorkflowRunStep;
import com.bruce.workflow.mapper.WorkflowRunStepMapper;
import com.bruce.workflow.service.IWorkflowRunService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class ObservabilityTraceServiceImpl implements IObservabilityTraceService {
private final IWorkflowRunService workflowRunService;
private final WorkflowRunStepMapper workflowRunStepMapper;
private final IAgentSessionService agentSessionService;
private final IAgentMessageService agentMessageService;
private final IModelCallLogService modelCallLogService;
@Override
public ObservabilityTraceVO getTrace(String requestId) {
log.info("查询运行追踪开始requestId={}", requestId);
WorkflowRun run = findRunByRequestId(requestId);
if (run == null) {
return emptyTrace(requestId);
}
List<WorkflowRunStep> steps = workflowRunStepMapper.selectList(new LambdaQueryWrapper<WorkflowRunStep>()
.eq(WorkflowRunStep::getRunId, run.getId())
.orderByAsc(WorkflowRunStep::getCreateTime));
AgentSession session = findSessionByRunId(run.getId());
List<AgentMessage> messages = session == null ? List.of() : agentMessageService.list(new LambdaQueryWrapper<AgentMessage>()
.eq(AgentMessage::getSessionId, session.getId())
.orderByAsc(AgentMessage::getCreateTime));
List<ObservabilityModelCallSummaryVO> modelCalls = listModelCalls(requestId);
ObservabilityTraceVO vo = new ObservabilityTraceVO();
vo.setRequestId(requestId);
vo.setWorkflowStatus(run.getStatus());
vo.setSessionStatus(session == null ? null : session.getStatus());
vo.setWorkflowStepCount(steps.size());
vo.setMessageCount(messages.size());
vo.setModelCallCount(modelCalls.size());
vo.setTotalDurationMs(run.getDurationMs());
vo.setEstimatedCost(run.getEstimatedCost());
vo.setStepLogs(steps.stream().map(this::toStepLog).toList());
vo.setModelCalls(modelCalls);
log.info("查询运行追踪结束requestId={}, stepCount={}, messageCount={}, modelCallCount={}",
requestId, steps.size(), messages.size(), modelCalls.size());
return vo;
}
@Override
public List<ObservabilityModelCallSummaryVO> listModelCalls(String requestId) {
if (!StringUtils.hasText(requestId)) {
return List.of();
}
List<ModelCallLog> logs = modelCallLogService.list(new LambdaQueryWrapper<ModelCallLog>()
.eq(ModelCallLog::getRequestId, requestId.trim())
.orderByAsc(ModelCallLog::getCreateTime));
return logs.stream().map(this::toModelCallSummary).toList();
}
private WorkflowRun findRunByRequestId(String requestId) {
if (!StringUtils.hasText(requestId)) {
return null;
}
List<WorkflowRun> runs = workflowRunService.list(new LambdaQueryWrapper<WorkflowRun>()
.eq(WorkflowRun::getRequestId, requestId.trim())
.orderByDesc(WorkflowRun::getCreateTime)
.last("limit 1"));
return runs.isEmpty() ? null : runs.getFirst();
}
private AgentSession findSessionByRunId(Long runId) {
List<AgentSession> sessions = agentSessionService.list(new LambdaQueryWrapper<AgentSession>()
.eq(AgentSession::getWorkflowRunId, runId)
.orderByDesc(AgentSession::getCreateTime)
.last("limit 1"));
return sessions.isEmpty() ? null : sessions.getFirst();
}
private ObservabilityTraceVO emptyTrace(String requestId) {
ObservabilityTraceVO vo = new ObservabilityTraceVO();
vo.setRequestId(requestId);
vo.setWorkflowStepCount(0);
vo.setMessageCount(0);
vo.setModelCallCount(0);
vo.setStepLogs(List.of());
vo.setModelCalls(List.of());
return vo;
}
private ObservabilityStepLogVO toStepLog(WorkflowRunStep step) {
ObservabilityStepLogVO vo = new ObservabilityStepLogVO();
vo.setNodeName(step.getNodeName());
vo.setNodeType(step.getNodeType());
vo.setStatus(step.getStatus());
vo.setDurationMs(step.getDurationMs());
vo.setOutputSummary(maskSummary(step.getOutputJson()));
vo.setErrorMessage(step.getErrorMessage());
return vo;
}
private ObservabilityModelCallSummaryVO toModelCallSummary(ModelCallLog logEntity) {
ObservabilityModelCallSummaryVO vo = new ObservabilityModelCallSummaryVO();
vo.setRequestId(logEntity.getRequestId());
vo.setCallType(logEntity.getCallType());
vo.setStatus(logEntity.getStatus());
vo.setTotalTokens(logEntity.getTotalTokens());
vo.setDurationMs(logEntity.getDurationMs());
vo.setEstimatedCost(logEntity.getEstimatedCost());
vo.setErrorCode(logEntity.getErrorCode());
vo.setErrorMessage(logEntity.getErrorMessage());
return vo;
}
private String maskSummary(String json) {
if (!StringUtils.hasText(json)) {
return null;
}
return json.length() > 120 ? json.substring(0, 120) + "..." : json;
}
}

View File

@@ -0,0 +1,12 @@
package com.bruce.observability.vo;
import lombok.Data;
@Data
public class ObservabilityExportVO {
private String requestId;
private String workflowStatus;
private String maskedInputJson;
private String maskedOutputJson;
private String exportSummary;
}

View File

@@ -0,0 +1,17 @@
package com.bruce.observability.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ObservabilityModelCallSummaryVO {
private String requestId;
private String callType;
private String status;
private Integer totalTokens;
private Integer durationMs;
private BigDecimal estimatedCost;
private String errorCode;
private String errorMessage;
}

View File

@@ -0,0 +1,15 @@
package com.bruce.observability.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ObservabilityRunSummaryVO {
private Long runId;
private Long workflowId;
private String requestId;
private String status;
private Integer durationMs;
private BigDecimal estimatedCost;
}

View File

@@ -0,0 +1,13 @@
package com.bruce.observability.vo;
import lombok.Data;
@Data
public class ObservabilityStepLogVO {
private String nodeName;
private String nodeType;
private String status;
private Integer durationMs;
private String outputSummary;
private String errorMessage;
}

View File

@@ -5,27 +5,16 @@ import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
/**
* requestId 级别的运行追踪聚合返回对象。
*/
@Data @Data
public class ObservabilityTraceVO { public class ObservabilityTraceVO {
private String requestId; private String requestId;
private String workflowStatus; private String workflowStatus;
private String sessionStatus; private String sessionStatus;
private Integer workflowStepCount; private Integer workflowStepCount;
private Integer messageCount; private Integer messageCount;
private Integer modelCallCount; private Integer modelCallCount;
private Integer totalDurationMs; private Integer totalDurationMs;
private BigDecimal estimatedCost; private BigDecimal estimatedCost;
private List<ObservabilityStepLogVO> stepLogs;
private List<String> stepSummaries; private List<ObservabilityModelCallSummaryVO> modelCalls;
} }

View File

@@ -0,0 +1,45 @@
package com.bruce.observability;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.observability.controller.ObservabilityTraceController;
import com.bruce.observability.service.IObservabilityExportService;
import com.bruce.observability.service.IObservabilityRunService;
import com.bruce.observability.service.IObservabilityTraceService;
import com.bruce.observability.vo.ObservabilityExportVO;
import com.bruce.observability.vo.ObservabilityModelCallSummaryVO;
import com.bruce.observability.vo.ObservabilityRunSummaryVO;
import com.bruce.observability.vo.ObservabilityTraceVO;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ObservabilityComponentStructureTests {
@Test
void observabilityControllerShouldExposeRequestResultMethods() throws NoSuchMethodException {
Method runsMethod = ObservabilityTraceController.class.getMethod("runs");
Method detailMethod = ObservabilityTraceController.class.getMethod("trace", String.class);
Method modelCallsMethod = ObservabilityTraceController.class.getMethod("modelCalls", String.class);
Method exportMethod = ObservabilityTraceController.class.getMethod("export", String.class);
Method listRunsMethod = IObservabilityRunService.class.getMethod("listRecentRuns");
Method traceMethod = IObservabilityTraceService.class.getMethod("getTrace", String.class);
Method callSummaryMethod = IObservabilityTraceService.class.getMethod("listModelCalls", String.class);
Method exportServiceMethod = IObservabilityExportService.class.getMethod("exportTrace", String.class);
assertEquals(RequestResult.class, runsMethod.getReturnType());
assertEquals(RequestResult.class, detailMethod.getReturnType());
assertEquals(RequestResult.class, modelCallsMethod.getReturnType());
assertEquals(RequestResult.class, exportMethod.getReturnType());
assertEquals(List.class, listRunsMethod.getReturnType());
assertEquals(ObservabilityTraceVO.class, traceMethod.getReturnType());
assertEquals(List.class, callSummaryMethod.getReturnType());
assertEquals(ObservabilityExportVO.class, exportServiceMethod.getReturnType());
assertEquals(ObservabilityRunSummaryVO.class, ObservabilityRunSummaryVO.class);
assertEquals(ObservabilityModelCallSummaryVO.class, ObservabilityModelCallSummaryVO.class);
}
}

View File

@@ -0,0 +1,162 @@
package com.bruce.observability.trace;
import com.bruce.agent.entity.AgentMessage;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.service.IAgentMessageService;
import com.bruce.agent.service.IAgentSessionService;
import com.bruce.modelprovider.entity.ModelCallLog;
import com.bruce.modelprovider.service.IModelCallLogService;
import com.bruce.observability.service.impl.ObservabilityExportServiceImpl;
import com.bruce.observability.service.impl.ObservabilityRunServiceImpl;
import com.bruce.observability.service.impl.ObservabilityTraceServiceImpl;
import com.bruce.observability.vo.ObservabilityExportVO;
import com.bruce.observability.vo.ObservabilityRunSummaryVO;
import com.bruce.observability.vo.ObservabilityTraceVO;
import com.bruce.workflow.entity.WorkflowRun;
import com.bruce.workflow.entity.WorkflowRunStep;
import com.bruce.workflow.mapper.WorkflowRunStepMapper;
import com.bruce.workflow.service.IWorkflowRunService;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ObservabilityTraceServiceTests {
@Mock
private IWorkflowRunService workflowRunService;
@Mock
private IAgentSessionService agentSessionService;
@Mock
private IAgentMessageService agentMessageService;
@Mock
private IModelCallLogService modelCallLogService;
@Mock
private WorkflowRunStepMapper workflowRunStepMapper;
@InjectMocks
private ObservabilityRunServiceImpl observabilityRunService;
@InjectMocks
private ObservabilityTraceServiceImpl observabilityTraceService;
@Test
void listRecentRunsShouldBuildSummary() {
WorkflowRun run = new WorkflowRun();
run.setId(101L);
run.setRequestId("req-1001");
run.setWorkflowId(2001L);
run.setStatus("SUCCESS");
run.setDurationMs(860);
run.setEstimatedCost(new BigDecimal("0.006"));
when(workflowRunService.list(any(Wrapper.class))).thenReturn(List.of(run));
List<ObservabilityRunSummaryVO> result = observabilityRunService.listRecentRuns();
assertEquals(1, result.size());
assertEquals("req-1001", result.getFirst().getRequestId());
assertEquals("SUCCESS", result.getFirst().getStatus());
}
@Test
void getTraceShouldAggregateWorkflowSessionMessagesAndModelCalls() {
WorkflowRun run = new WorkflowRun();
run.setId(101L);
run.setRequestId("req-1001");
run.setStatus("SUCCESS");
run.setDurationMs(1420);
run.setEstimatedCost(new BigDecimal("0.018"));
run.setInputJson("{\"question\":\"私有化部署\"}");
run.setOutputJson("{\"answer\":\"建议覆盖部署拓扑\"}");
WorkflowRunStep step = new WorkflowRunStep();
step.setRunId(101L);
step.setNodeName("Knowledge Retrieval");
step.setStatus("SUCCESS");
step.setDurationMs(218);
step.setOutputJson("{\"chunks\":6}");
AgentSession session = new AgentSession();
session.setId(301L);
session.setWorkflowRunId(101L);
session.setStatus("ACTIVE");
session.setSessionCode("session-001");
AgentMessage message = new AgentMessage();
message.setSessionId(301L);
message.setRole("ASSISTANT");
message.setContent("建议覆盖部署拓扑、模型服务和日志留存");
message.setCitationJson("[{\"chunkId\":\"c-1\"}]");
message.setTokenCount(612);
ModelCallLog callLog = new ModelCallLog();
callLog.setRequestId("req-1001");
callLog.setCallType("CHAT");
callLog.setStatus("SUCCESS");
callLog.setTotalTokens(612);
callLog.setDurationMs(1120);
callLog.setEstimatedCost(new BigDecimal("0.018"));
when(workflowRunService.list(any(Wrapper.class))).thenReturn(List.of(run));
when(workflowRunStepMapper.selectList(any())).thenReturn(List.of(step));
when(agentSessionService.list(any(Wrapper.class))).thenReturn(List.of(session));
when(agentMessageService.list(any(Wrapper.class))).thenReturn(List.of(message));
when(modelCallLogService.list(any(Wrapper.class))).thenReturn(List.of(callLog));
ObservabilityTraceVO result = observabilityTraceService.getTrace("req-1001");
assertNotNull(result);
assertEquals("req-1001", result.getRequestId());
assertEquals("SUCCESS", result.getWorkflowStatus());
assertEquals("ACTIVE", result.getSessionStatus());
assertEquals(1, result.getWorkflowStepCount());
assertEquals(1, result.getMessageCount());
assertEquals(1, result.getModelCallCount());
assertEquals(1, result.getStepLogs().size());
assertEquals(1, result.getModelCalls().size());
}
@Test
void exportShouldMaskSensitiveFields() {
WorkflowRun run = new WorkflowRun();
run.setId(101L);
run.setRequestId("req-1001");
run.setStatus("FAILED");
run.setInputJson("{\"apiKey\":\"secret\",\"question\":\"test\"}");
run.setOutputJson("{\"authorization\":\"bearer token\",\"answer\":\"done\"}");
ModelCallLog callLog = new ModelCallLog();
callLog.setRequestId("req-1001");
callLog.setRequestHash("hash-1");
callLog.setErrorMessage("timeout");
when(workflowRunService.list(any(Wrapper.class))).thenReturn(List.of(run));
when(workflowRunStepMapper.selectList(any())).thenReturn(List.of());
when(agentSessionService.list(any(Wrapper.class))).thenReturn(List.of());
when(modelCallLogService.list(any(Wrapper.class))).thenReturn(List.of(callLog));
ObservabilityExportServiceImpl observabilityExportService = new ObservabilityExportServiceImpl(observabilityTraceService);
ObservabilityExportVO result = observabilityExportService.exportTrace("req-1001");
assertEquals("req-1001", result.getRequestId());
assertEquals("FAILED", result.getWorkflowStatus());
assertEquals(true, result.getMaskedInputJson().contains("***"));
assertEquals(true, result.getMaskedOutputJson().contains("***"));
}
}

View File

@@ -0,0 +1,31 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
exportObservabilityTrace,
getObservabilityTrace,
listObservabilityModelCalls,
listObservabilityRuns,
} from '../observability';
import { get } from '../request';
vi.mock('../request', () => ({
get: vi.fn(),
}));
describe('observability api', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('maps observability endpoints correctly', () => {
listObservabilityRuns();
getObservabilityTrace('req-1001');
listObservabilityModelCalls('req-1001');
exportObservabilityTrace('req-1001');
expect(get).toHaveBeenCalledWith('/observability/runs');
expect(get).toHaveBeenCalledWith('/observability/runs/req-1001');
expect(get).toHaveBeenCalledWith('/observability/model-calls', { params: { requestId: 'req-1001' } });
expect(get).toHaveBeenCalledWith('/observability/runs/req-1001/export');
});
});

View File

@@ -0,0 +1,67 @@
import { get } from './request';
export interface ObservabilityRunSummary {
runId?: string;
workflowId?: string;
requestId: string;
status?: string;
durationMs?: number;
estimatedCost?: number;
}
export interface ObservabilityStepLog {
nodeName: string;
nodeType?: string;
status?: string;
durationMs?: number;
outputSummary?: string;
errorMessage?: string;
}
export interface ObservabilityModelCallSummary {
requestId: string;
callType?: string;
status?: string;
totalTokens?: number;
durationMs?: number;
estimatedCost?: number;
errorCode?: string;
errorMessage?: string;
}
export interface ObservabilityTrace {
requestId: string;
workflowStatus?: string;
sessionStatus?: string;
workflowStepCount?: number;
messageCount?: number;
modelCallCount?: number;
totalDurationMs?: number;
estimatedCost?: number;
stepLogs: ObservabilityStepLog[];
modelCalls: ObservabilityModelCallSummary[];
}
export interface ObservabilityExport {
requestId: string;
workflowStatus?: string;
maskedInputJson?: string;
maskedOutputJson?: string;
exportSummary?: string;
}
export function listObservabilityRuns() {
return get<ObservabilityRunSummary[]>('/observability/runs');
}
export function getObservabilityTrace(requestId: string) {
return get<ObservabilityTrace>(`/observability/runs/${requestId}`);
}
export function listObservabilityModelCalls(requestId: string) {
return get<ObservabilityModelCallSummary[]>('/observability/model-calls', { params: { requestId } });
}
export function exportObservabilityTrace(requestId: string) {
return get<ObservabilityExport>(`/observability/runs/${requestId}/export`);
}