优化系统健康评分与缓存监测
This commit is contained in:
@@ -15,6 +15,17 @@ vi.mock("@/api/projectPermissions", () => ({
|
||||
cache_metrics: {
|
||||
total_accesses: 1000, cache_hits: 850, cache_misses: 150,
|
||||
cache_invalidations: 5, hit_rate: 85.0, miss_rate: 15.0,
|
||||
window_seconds: 3600, scope: "sliding_window",
|
||||
by_cache: {
|
||||
project_permissions: {
|
||||
total_accesses: 800, cache_hits: 700, cache_misses: 100,
|
||||
cache_invalidations: 5, hit_rate: 87.5, miss_rate: 12.5,
|
||||
},
|
||||
member_roles: {
|
||||
total_accesses: 200, cache_hits: 150, cache_misses: 50,
|
||||
cache_invalidations: 5, hit_rate: 75, miss_rate: 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
uptime_seconds: 3600,
|
||||
},
|
||||
@@ -22,7 +33,21 @@ vi.mock("@/api/projectPermissions", () => ({
|
||||
fetchPermissionHealth: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
status: "healthy", health_score: 95, issues: [],
|
||||
metrics: {}, cache_stats: { cache_items: { total_count: 60 }, cache_metrics: {} },
|
||||
sample_sufficient: true,
|
||||
score_breakdown: {
|
||||
performance: { weight: 40, deduction: 0 },
|
||||
deny_rate: { weight: 20, deduction: 0 },
|
||||
cache: {
|
||||
weight: 25, deduction: 5, sample_sufficient: true,
|
||||
scope: "sliding_window", window_seconds: 3600,
|
||||
},
|
||||
alerts: { weight: 15, deduction: 0 },
|
||||
},
|
||||
last_hour: {
|
||||
total_checks: 30, denied_checks: 2, deny_rate: 6.67,
|
||||
avg_elapsed_ms: 3.5, error_alerts: 0, warning_alerts: 0,
|
||||
},
|
||||
cache_stats: { cache_items: { total_count: 60 }, cache_metrics: {} },
|
||||
},
|
||||
}),
|
||||
fetchPermissionAlerts: vi.fn().mockResolvedValue({ data: { total: 0, alerts: [] } }),
|
||||
@@ -94,7 +119,7 @@ describe("PermissionMonitoring.vue", () => {
|
||||
it("resizes the source-analysis chart when its tab becomes active", () => {
|
||||
const source = readFileSync(resolve(__dirname, "./PermissionMonitoring.vue"), "utf8");
|
||||
|
||||
expect(source).toContain('import { ref, computed, h, nextTick, onMounted } from "vue"');
|
||||
expect(source).toContain("onUnmounted");
|
||||
expect(source).toContain("type IpLocationsExpose");
|
||||
expect(source).toContain("resize: () => void | Promise<void>");
|
||||
expect(source).toContain('if (tab === "ip-locations")');
|
||||
|
||||
@@ -44,24 +44,37 @@
|
||||
</template>
|
||||
</el-progress>
|
||||
</div>
|
||||
<div class="score-breakdown">
|
||||
<span v-for="item in healthScoreItems" :key="item.label">
|
||||
{{ item.label }} {{ item.deduction ? `-${item.deduction}` : "正常" }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!health.sample_sufficient" class="sample-warning">
|
||||
近 1 小时仅 {{ health.last_hour.total_checks }} 次检查,样本不足,当前评分仅供参考
|
||||
</div>
|
||||
<div v-if="health.issues.length > 0" class="health-issues">
|
||||
<div v-for="(issue, index) in health.issues" :key="index" class="issue-item">
|
||||
<span class="issue-dot" />
|
||||
<span>{{ issue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="health-ok">
|
||||
<div v-else-if="health.sample_sufficient" class="health-ok">
|
||||
<span class="ok-icon">✓</span>
|
||||
<span>系统运行正常,未发现问题</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="metrics" class="cache-card">
|
||||
<h3>缓存效率</h3>
|
||||
<div class="cache-title">
|
||||
<h3>缓存效率</h3>
|
||||
<span>近 1 小时</span>
|
||||
</div>
|
||||
<div class="cache-stats">
|
||||
<div class="cache-stat primary">
|
||||
<div class="cache-stat-header">
|
||||
<span class="stat-label">命中率</span>
|
||||
<span class="stat-label">
|
||||
综合命中率({{ metrics.cache_metrics.cache_hits }}/{{ metrics.cache_metrics.total_accesses }})
|
||||
</span>
|
||||
<strong class="stat-value" :style="{ color: getProgressColor(metrics.cache_metrics.hit_rate) }">
|
||||
{{ metrics.cache_metrics.hit_rate.toFixed(1) }}%
|
||||
</strong>
|
||||
@@ -73,6 +86,22 @@
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="cache-scope-details">
|
||||
<span>
|
||||
项目权限
|
||||
{{ metrics.cache_metrics.by_cache.project_permissions.hit_rate.toFixed(1) }}%
|
||||
({{ metrics.cache_metrics.by_cache.project_permissions.cache_hits }}/{{
|
||||
metrics.cache_metrics.by_cache.project_permissions.total_accesses
|
||||
}})
|
||||
</span>
|
||||
<span>
|
||||
成员角色
|
||||
{{ metrics.cache_metrics.by_cache.member_roles.hit_rate.toFixed(1) }}%
|
||||
({{ metrics.cache_metrics.by_cache.member_roles.cache_hits }}/{{
|
||||
metrics.cache_metrics.by_cache.member_roles.total_accesses
|
||||
}})
|
||||
</span>
|
||||
</div>
|
||||
<div class="cache-stat-row">
|
||||
<div class="cache-stat-mini">
|
||||
<span class="stat-label">缓存项目</span>
|
||||
@@ -173,7 +202,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h, nextTick, onMounted } from "vue";
|
||||
import { ref, computed, h, nextTick, onMounted, onUnmounted } from "vue";
|
||||
import type {
|
||||
PermissionMetricsResponse,
|
||||
AlertsResponse,
|
||||
@@ -304,6 +333,16 @@ const summaryCards = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
const healthScoreItems = computed(() => {
|
||||
if (!health.value) return [];
|
||||
return [
|
||||
{ label: "性能", deduction: health.value.score_breakdown.performance.deduction },
|
||||
{ label: "拒绝率", deduction: health.value.score_breakdown.deny_rate.deduction },
|
||||
{ label: "缓存", deduction: health.value.score_breakdown.cache.deduction },
|
||||
{ label: "告警", deduction: health.value.score_breakdown.alerts.deduction },
|
||||
];
|
||||
});
|
||||
|
||||
const loadOverviewData = async () => {
|
||||
const [metricsRes, healthRes, alertsRes, deniedRes, summaryRes] = await Promise.allSettled([
|
||||
fetchPermissionMetrics(),
|
||||
@@ -335,7 +374,18 @@ const refresh = () => {
|
||||
else if (activeTab.value === "ip-locations") ipLocationsRef.value?.refresh();
|
||||
};
|
||||
|
||||
onMounted(loadOverviewData);
|
||||
let overviewRefreshTimer: number | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
loadOverviewData();
|
||||
overviewRefreshTimer = window.setInterval(() => {
|
||||
if (activeTab.value === "overview") loadOverviewData();
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (overviewRefreshTimer !== undefined) window.clearInterval(overviewRefreshTimer);
|
||||
});
|
||||
|
||||
defineExpose({ refresh });
|
||||
</script>
|
||||
@@ -632,6 +682,32 @@ defineExpose({ refresh });
|
||||
padding: 8px 0 16px;
|
||||
}
|
||||
|
||||
.score-breakdown {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: -6px 0 12px;
|
||||
}
|
||||
|
||||
.score-breakdown span {
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: var(--mon-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sample-warning {
|
||||
margin-bottom: 10px;
|
||||
padding: 9px 12px;
|
||||
border-radius: 10px;
|
||||
background: #fffbeb;
|
||||
color: #b45309;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -699,6 +775,36 @@ defineExpose({ refresh });
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cache-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cache-title h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cache-title span {
|
||||
color: var(--mon-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cache-scope-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cache-scope-details span {
|
||||
flex: 1 1 180px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: var(--mon-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cache-stat.primary {
|
||||
padding: 14px;
|
||||
background: #f8fafc;
|
||||
|
||||
Reference in New Issue
Block a user