Files
common_agent/frontend/src/pages/studio/ObservabilityPage.vue

130 lines
4.0 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { ElMessage } from 'element-plus';
import type { ObservabilityRunSummary, ObservabilityTrace } from '@/api/observability';
import { exportObservabilityTrace, getObservabilityTrace, listObservabilityRuns } from '@/api/observability';
const loading = ref(false);
const runs = ref<ObservabilityRunSummary[]>([]);
const trace = ref<ObservabilityTrace | null>(null);
const exportSummary = ref('');
const selectedRequestId = computed(() => trace.value?.requestId || runs.value[0]?.requestId || '');
function formatLatency(durationMs?: number) {
if (durationMs == null) {
return '-';
}
if (durationMs >= 1000) {
return `${(durationMs / 1000).toFixed(2)}s`;
}
return `${durationMs}ms`;
}
function formatCost(cost?: number) {
if (cost == null) {
return '-';
}
return `¥${cost}`;
}
function formatStatus(status?: string) {
if (status === 'SUCCESS') {
return '成功';
}
if (status === 'FAILED') {
return '失败';
}
if (status === 'RUNNING') {
return '运行中';
}
return status || '-';
}
async function loadRuns() {
loading.value = true;
try {
const response = await listObservabilityRuns();
runs.value = response.data ?? [];
if (runs.value.length > 0) {
await loadTrace(runs.value[0].requestId);
}
} finally {
loading.value = false;
}
}
async function loadTrace(requestId: string) {
const response = await getObservabilityTrace(requestId);
trace.value = response.data;
}
async function exportTrace() {
if (!selectedRequestId.value) {
return;
}
const response = await exportObservabilityTrace(selectedRequestId.value);
exportSummary.value = response.data.exportSummary ?? '';
ElMessage.success('脱敏日志导出成功');
}
onMounted(loadRuns);
</script>
<template>
<section class="studio-page observability-page">
<header class="page-title-row">
<div>
<p class="studio-kicker">ObservabilityView</p>
<h1>运行观测</h1>
</div>
<el-button data-test="observability-export" @click="exportTrace">导出日志</el-button>
</header>
<div class="observability-layout" v-loading="loading">
<section class="studio-panel">
<div class="panel-heading">
<h2>运行记录</h2>
<span>workflow_run / workflow_run_step / model_call_log</span>
</div>
<div class="run-table">
<div class="run-row run-head">
<span>请求ID</span><span>状态</span><span>延迟</span><span>成本</span>
</div>
<div
v-for="run in runs"
:key="run.requestId"
class="run-row"
:data-test="`observability-run-${run.requestId}`"
@click="loadTrace(run.requestId)"
>
<strong>{{ run.requestId }}</strong>
<span class="status-cell">
<span class="status-pill" :class="formatStatus(run.status) === '成功' ? 'is-success' : 'is-warning'">
{{ formatStatus(run.status) }}
</span>
</span>
<span>{{ formatLatency(run.durationMs) }}</span>
<span>{{ formatCost(run.estimatedCost) }}</span>
</div>
</div>
</section>
<aside class="studio-panel">
<div class="panel-heading compact">
<h2>步骤日志</h2>
<span data-test="observability-trace-id">{{ selectedRequestId || '暂无请求' }}</span>
</div>
<p v-if="exportSummary" class="export-summary" data-test="observability-export-summary">{{ exportSummary }}</p>
<ol class="log-list">
<li v-for="step in trace?.stepLogs ?? []" :key="`${step.nodeName}-${step.durationMs}`" :data-test="`observability-step-${step.nodeName}`">
<time>{{ formatLatency(step.durationMs) }}</time>
<span>{{ step.nodeName }} · {{ formatStatus(step.status) }} · {{ step.outputSummary || step.errorMessage || '-' }}</span>
</li>
</ol>
</aside>
</div>
</section>
</template>