mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 修复实时RPM/TPM指标显示为0的问题
- 添加调试日志以追踪数据读取过程 - 修复getRealtimeSystemMetrics中的数据验证逻辑 - 添加测试脚本用于验证时间戳匹配问题 - 改进错误处理和日志记录 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
69
scripts/test-realtime-metrics.js
Normal file
69
scripts/test-realtime-metrics.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const redis = require('../src/models/redis');
|
||||||
|
const logger = require('../src/utils/logger');
|
||||||
|
|
||||||
|
async function testRealtimeMetrics() {
|
||||||
|
try {
|
||||||
|
// 连接Redis
|
||||||
|
await redis.connect();
|
||||||
|
|
||||||
|
// 获取当前时间戳
|
||||||
|
const now = new Date();
|
||||||
|
const currentMinute = Math.floor(now.getTime() / 60000);
|
||||||
|
|
||||||
|
console.log('=== 时间戳测试 ===');
|
||||||
|
console.log('当前时间:', now.toISOString());
|
||||||
|
console.log('当前分钟时间戳:', currentMinute);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 检查最近5分钟的键
|
||||||
|
console.log('=== 检查Redis键 ===');
|
||||||
|
const client = redis.getClient();
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const minuteKey = `system:metrics:minute:${currentMinute - i}`;
|
||||||
|
const exists = await client.exists(minuteKey);
|
||||||
|
const data = await client.hgetall(minuteKey);
|
||||||
|
|
||||||
|
console.log(`键: ${minuteKey}`);
|
||||||
|
console.log(` 存在: ${exists ? '是' : '否'}`);
|
||||||
|
if (exists && data) {
|
||||||
|
console.log(` 数据: requests=${data.requests}, totalTokens=${data.totalTokens}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用getRealtimeSystemMetrics
|
||||||
|
console.log('=== 调用 getRealtimeSystemMetrics ===');
|
||||||
|
const metrics = await redis.getRealtimeSystemMetrics();
|
||||||
|
console.log('结果:', JSON.stringify(metrics, null, 2));
|
||||||
|
|
||||||
|
// 列出所有system:metrics:minute:*键
|
||||||
|
console.log('\n=== 所有系统指标键 ===');
|
||||||
|
const allKeys = await client.keys('system:metrics:minute:*');
|
||||||
|
console.log('找到的键数量:', allKeys.length);
|
||||||
|
if (allKeys.length > 0) {
|
||||||
|
// 排序并显示最新的10个
|
||||||
|
allKeys.sort((a, b) => {
|
||||||
|
const aNum = parseInt(a.split(':').pop());
|
||||||
|
const bNum = parseInt(b.split(':').pop());
|
||||||
|
return bNum - aNum;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('最新的10个键:');
|
||||||
|
for (let i = 0; i < Math.min(10, allKeys.length); i++) {
|
||||||
|
const key = allKeys[i];
|
||||||
|
const timestamp = parseInt(key.split(':').pop());
|
||||||
|
const timeDiff = currentMinute - timestamp;
|
||||||
|
console.log(` ${key} (${timeDiff}分钟前)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('测试失败:', error);
|
||||||
|
} finally {
|
||||||
|
await redis.disconnect();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
testRealtimeMetrics();
|
||||||
@@ -1008,18 +1008,25 @@ class RedisClient {
|
|||||||
async getRealtimeSystemMetrics() {
|
async getRealtimeSystemMetrics() {
|
||||||
try {
|
try {
|
||||||
const config = require('../../config/config');
|
const config = require('../../config/config');
|
||||||
const windowMinutes = config.system.metricsWindow;
|
const windowMinutes = config.system.metricsWindow || 5;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const currentMinute = Math.floor(now.getTime() / 60000);
|
const currentMinute = Math.floor(now.getTime() / 60000);
|
||||||
|
|
||||||
|
// 调试:打印当前时间和分钟时间戳
|
||||||
|
logger.debug(`🔍 Realtime metrics - Current time: ${now.toISOString()}, Minute timestamp: ${currentMinute}`);
|
||||||
|
|
||||||
// 使用Pipeline批量获取窗口内的所有分钟数据
|
// 使用Pipeline批量获取窗口内的所有分钟数据
|
||||||
const pipeline = this.client.pipeline();
|
const pipeline = this.client.pipeline();
|
||||||
|
const minuteKeys = [];
|
||||||
for (let i = 0; i < windowMinutes; i++) {
|
for (let i = 0; i < windowMinutes; i++) {
|
||||||
const minuteKey = `system:metrics:minute:${currentMinute - i}`;
|
const minuteKey = `system:metrics:minute:${currentMinute - i}`;
|
||||||
|
minuteKeys.push(minuteKey);
|
||||||
pipeline.hgetall(minuteKey);
|
pipeline.hgetall(minuteKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(`🔍 Realtime metrics - Checking keys: ${minuteKeys.join(', ')}`);
|
||||||
|
|
||||||
const results = await pipeline.exec();
|
const results = await pipeline.exec();
|
||||||
|
|
||||||
// 聚合计算
|
// 聚合计算
|
||||||
@@ -1029,23 +1036,32 @@ class RedisClient {
|
|||||||
let totalOutputTokens = 0;
|
let totalOutputTokens = 0;
|
||||||
let totalCacheCreateTokens = 0;
|
let totalCacheCreateTokens = 0;
|
||||||
let totalCacheReadTokens = 0;
|
let totalCacheReadTokens = 0;
|
||||||
|
let validDataCount = 0;
|
||||||
|
|
||||||
results.forEach(([err, data]) => {
|
results.forEach(([err, data], index) => {
|
||||||
if (!err && data) {
|
if (!err && data && Object.keys(data).length > 0) {
|
||||||
|
validDataCount++;
|
||||||
totalRequests += parseInt(data.requests || 0);
|
totalRequests += parseInt(data.requests || 0);
|
||||||
totalTokens += parseInt(data.totalTokens || 0);
|
totalTokens += parseInt(data.totalTokens || 0);
|
||||||
totalInputTokens += parseInt(data.inputTokens || 0);
|
totalInputTokens += parseInt(data.inputTokens || 0);
|
||||||
totalOutputTokens += parseInt(data.outputTokens || 0);
|
totalOutputTokens += parseInt(data.outputTokens || 0);
|
||||||
totalCacheCreateTokens += parseInt(data.cacheCreateTokens || 0);
|
totalCacheCreateTokens += parseInt(data.cacheCreateTokens || 0);
|
||||||
totalCacheReadTokens += parseInt(data.cacheReadTokens || 0);
|
totalCacheReadTokens += parseInt(data.cacheReadTokens || 0);
|
||||||
|
|
||||||
|
logger.debug(`🔍 Realtime metrics - Key ${minuteKeys[index]} data:`, {
|
||||||
|
requests: data.requests,
|
||||||
|
totalTokens: data.totalTokens
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.debug(`🔍 Realtime metrics - Valid data count: ${validDataCount}/${windowMinutes}, Total requests: ${totalRequests}, Total tokens: ${totalTokens}`);
|
||||||
|
|
||||||
// 计算平均值(每分钟)
|
// 计算平均值(每分钟)
|
||||||
const realtimeRPM = windowMinutes > 0 ? Math.round((totalRequests / windowMinutes) * 100) / 100 : 0;
|
const realtimeRPM = windowMinutes > 0 ? Math.round((totalRequests / windowMinutes) * 100) / 100 : 0;
|
||||||
const realtimeTPM = windowMinutes > 0 ? Math.round((totalTokens / windowMinutes) * 100) / 100 : 0;
|
const realtimeTPM = windowMinutes > 0 ? Math.round((totalTokens / windowMinutes) * 100) / 100 : 0;
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
realtimeRPM,
|
realtimeRPM,
|
||||||
realtimeTPM,
|
realtimeTPM,
|
||||||
windowMinutes,
|
windowMinutes,
|
||||||
@@ -1056,6 +1072,10 @@ class RedisClient {
|
|||||||
totalCacheCreateTokens,
|
totalCacheCreateTokens,
|
||||||
totalCacheReadTokens
|
totalCacheReadTokens
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.debug(`🔍 Realtime metrics - Final result:`, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting realtime system metrics:', error);
|
console.error('Error getting realtime system metrics:', error);
|
||||||
// 如果出错,返回历史平均值作为降级方案
|
// 如果出错,返回历史平均值作为降级方案
|
||||||
|
|||||||
@@ -1,5 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<!-- 自动刷新控制栏 -->
|
||||||
|
<div class="mb-6 bg-white rounded-lg shadow-sm p-4">
|
||||||
|
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-2 sm:gap-4">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800">系统仪表盘</h2>
|
||||||
|
<div v-if="refreshCountdownDisplay" class="text-sm text-gray-500 whitespace-nowrap">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
{{ refreshCountdownDisplay }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||||
|
<!-- 手动刷新按钮 -->
|
||||||
|
<button
|
||||||
|
@click="refreshAllData"
|
||||||
|
:disabled="isRefreshing"
|
||||||
|
class="px-4 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<i :class="['fas fa-sync-alt', { 'animate-spin': isRefreshing }]"></i>
|
||||||
|
{{ isRefreshing ? '刷新中...' : '刷新数据' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 自动刷新开关 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="autoRefreshEnabled"
|
||||||
|
class="sr-only peer"
|
||||||
|
>
|
||||||
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
<span class="ml-3 text-sm font-medium text-gray-700">
|
||||||
|
自动刷新
|
||||||
|
<span class="text-xs text-gray-500">(30秒)</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 主要统计 -->
|
<!-- 主要统计 -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
@@ -221,8 +261,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button @click="refreshChartsData()" class="btn btn-primary px-4 py-2 flex items-center gap-2">
|
<button
|
||||||
<i class="fas fa-sync-alt"></i>刷新
|
@click="refreshAllData()"
|
||||||
|
:disabled="isRefreshing"
|
||||||
|
class="btn btn-primary px-4 py-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<i :class="['fas fa-sync-alt', { 'animate-spin': isRefreshing }]"></i>
|
||||||
|
{{ isRefreshing ? '刷新中' : '刷新' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -329,7 +374,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useDashboardStore } from '@/stores/dashboard'
|
import { useDashboardStore } from '@/stores/dashboard'
|
||||||
import Chart from 'chart.js/auto'
|
import Chart from 'chart.js/auto'
|
||||||
@@ -368,6 +413,20 @@ let modelUsageChartInstance = null
|
|||||||
let usageTrendChartInstance = null
|
let usageTrendChartInstance = null
|
||||||
let apiKeysUsageTrendChartInstance = null
|
let apiKeysUsageTrendChartInstance = null
|
||||||
|
|
||||||
|
// 自动刷新相关
|
||||||
|
const autoRefreshEnabled = ref(false)
|
||||||
|
const autoRefreshInterval = ref(30) // 秒
|
||||||
|
const autoRefreshTimer = ref(null)
|
||||||
|
const refreshCountdown = ref(0)
|
||||||
|
const countdownTimer = ref(null)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
|
|
||||||
|
// 计算倒计时显示
|
||||||
|
const refreshCountdownDisplay = computed(() => {
|
||||||
|
if (!autoRefreshEnabled.value || refreshCountdown.value <= 0) return ''
|
||||||
|
return `${refreshCountdown.value}秒后刷新`
|
||||||
|
})
|
||||||
|
|
||||||
// 格式化数字
|
// 格式化数字
|
||||||
function formatNumber(num) {
|
function formatNumber(num) {
|
||||||
if (num >= 1000000) {
|
if (num >= 1000000) {
|
||||||
@@ -778,13 +837,90 @@ watch(apiKeysTrendData, () => {
|
|||||||
nextTick(() => createApiKeysUsageTrendChart())
|
nextTick(() => createApiKeysUsageTrendChart())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 刷新所有数据
|
||||||
|
async function refreshAllData() {
|
||||||
|
if (isRefreshing.value) return
|
||||||
|
|
||||||
|
isRefreshing.value = true
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
loadDashboardData(),
|
||||||
|
refreshChartsData()
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动自动刷新
|
||||||
|
function startAutoRefresh() {
|
||||||
|
if (!autoRefreshEnabled.value) return
|
||||||
|
|
||||||
|
// 重置倒计时
|
||||||
|
refreshCountdown.value = autoRefreshInterval.value
|
||||||
|
|
||||||
|
// 清除现有定时器
|
||||||
|
if (countdownTimer.value) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
}
|
||||||
|
if (autoRefreshTimer.value) {
|
||||||
|
clearTimeout(autoRefreshTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动倒计时
|
||||||
|
countdownTimer.value = setInterval(() => {
|
||||||
|
refreshCountdown.value--
|
||||||
|
if (refreshCountdown.value <= 0) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// 设置刷新定时器
|
||||||
|
autoRefreshTimer.value = setTimeout(async () => {
|
||||||
|
await refreshAllData()
|
||||||
|
// 递归调用以继续自动刷新
|
||||||
|
if (autoRefreshEnabled.value) {
|
||||||
|
startAutoRefresh()
|
||||||
|
}
|
||||||
|
}, autoRefreshInterval.value * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止自动刷新
|
||||||
|
function stopAutoRefresh() {
|
||||||
|
if (countdownTimer.value) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
countdownTimer.value = null
|
||||||
|
}
|
||||||
|
if (autoRefreshTimer.value) {
|
||||||
|
clearTimeout(autoRefreshTimer.value)
|
||||||
|
autoRefreshTimer.value = null
|
||||||
|
}
|
||||||
|
refreshCountdown.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换自动刷新
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
autoRefreshEnabled.value = !autoRefreshEnabled.value
|
||||||
|
if (autoRefreshEnabled.value) {
|
||||||
|
startAutoRefresh()
|
||||||
|
} else {
|
||||||
|
stopAutoRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听自动刷新状态变化
|
||||||
|
watch(autoRefreshEnabled, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
startAutoRefresh()
|
||||||
|
} else {
|
||||||
|
stopAutoRefresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 加载所有数据
|
// 加载所有数据
|
||||||
await Promise.all([
|
await refreshAllData()
|
||||||
loadDashboardData(),
|
|
||||||
refreshChartsData() // 使用refreshChartsData来确保根据当前筛选条件加载数据
|
|
||||||
])
|
|
||||||
|
|
||||||
// 创建图表
|
// 创建图表
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -792,6 +928,21 @@ onMounted(async () => {
|
|||||||
createUsageTrendChart()
|
createUsageTrendChart()
|
||||||
createApiKeysUsageTrendChart()
|
createApiKeysUsageTrendChart()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAutoRefresh()
|
||||||
|
// 销毁图表实例
|
||||||
|
if (modelUsageChartInstance) {
|
||||||
|
modelUsageChartInstance.destroy()
|
||||||
|
}
|
||||||
|
if (usageTrendChartInstance) {
|
||||||
|
usageTrendChartInstance.destroy()
|
||||||
|
}
|
||||||
|
if (apiKeysUsageTrendChartInstance) {
|
||||||
|
apiKeysUsageTrendChartInstance.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -810,4 +961,18 @@ onMounted(async () => {
|
|||||||
.custom-date-picker :deep(.el-range-input) {
|
.custom-date-picker :deep(.el-range-input) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 旋转动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user