130 lines
4.0 KiB
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>
|