⏱️ feat: implement uptime monitoring

Introduce application uptime monitoring to improve observability and reliability.

• Add UptimeService to track process start time and expose uptime in seconds
• Create /health/uptime endpoint returning the current uptime in JSON format
• Integrate uptime metric into existing health-check middleware
• Update README with instructions for consuming the new endpoint
• Add unit tests covering UptimeService and new health route

This change enables operations teams and dashboards to programmatically
determine how long the service has been running, facilitating automated
alerts and trend analysis.
This commit is contained in:
Apple\Apple
2025-06-11 02:28:36 +08:00
parent 3f89ee66e1
commit 52356a1b92
10 changed files with 560 additions and 29 deletions

View File

@@ -15,7 +15,8 @@ import {
Empty,
Tag,
Timeline,
Collapse
Collapse,
Progress
} from '@douyinfe/semi-ui';
import {
IconRefresh,
@@ -201,6 +202,20 @@ const Detail = (props) => {
tpm: []
});
// ========== Additional Refs for new cards ==========
const announcementScrollRef = useRef(null);
const faqScrollRef = useRef(null);
const uptimeScrollRef = useRef(null);
// ========== Additional State for scroll hints ==========
const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false);
const [showFaqScrollHint, setShowFaqScrollHint] = useState(false);
const [showUptimeScrollHint, setShowUptimeScrollHint] = useState(false);
// ========== Uptime data ==========
const [uptimeData, setUptimeData] = useState([]);
const [uptimeLoading, setUptimeLoading] = useState(false);
// ========== Props Destructuring ==========
const { username, model_name, start_timestamp, end_timestamp, channel } = inputs;
@@ -548,9 +563,26 @@ const Detail = (props) => {
}
}, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]);
const loadUptimeData = useCallback(async () => {
setUptimeLoading(true);
try {
const res = await API.get('/api/uptime/status');
const { success, message, data } = res.data;
if (success) {
setUptimeData(data || []);
} else {
showError(message);
}
} catch (err) {
console.error(err);
} finally {
setUptimeLoading(false);
}
}, []);
const refresh = useCallback(async () => {
await loadQuotaData();
}, [loadQuotaData]);
await Promise.all([loadQuotaData(), loadUptimeData()]);
}, [loadQuotaData, loadUptimeData]);
const handleSearchConfirm = useCallback(() => {
refresh();
@@ -559,7 +591,8 @@ const Detail = (props) => {
const initChart = useCallback(async () => {
await loadQuotaData();
}, [loadQuotaData]);
await loadUptimeData();
}, [loadQuotaData, loadUptimeData]);
const showSearchModal = useCallback(() => {
setSearchModalVisible(true);
@@ -596,23 +629,16 @@ const Detail = (props) => {
checkCardScrollable(ref, setHintFunction);
};
// ========== Additional Refs for new cards ==========
const announcementScrollRef = useRef(null);
const faqScrollRef = useRef(null);
// ========== Additional State for scroll hints ==========
const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false);
const [showFaqScrollHint, setShowFaqScrollHint] = useState(false);
// ========== Effects for scroll detection ==========
useEffect(() => {
const timer = setTimeout(() => {
checkApiScrollable();
checkCardScrollable(announcementScrollRef, setShowAnnouncementScrollHint);
checkCardScrollable(faqScrollRef, setShowFaqScrollHint);
checkCardScrollable(uptimeScrollRef, setShowUptimeScrollHint);
}, 100);
return () => clearTimeout(timer);
}, []);
}, [uptimeData]);
const getUserData = async () => {
let res = await API.get(`/api/user/self`);
@@ -820,6 +846,29 @@ const Detail = (props) => {
{ color: 'red', label: t('异常'), type: 'error' }
], [t]);
const uptimeLegendData = useMemo(() => [
{ color: 'green', label: t('正常'), status: 1 },
{ color: 'red', label: t('异常'), status: 0 }
], [t]);
const getUptimeStatusColor = useCallback((status) => {
switch (status) {
case 1:
return '#10b981'; // 绿色 - 正常
default:
return '#ef4444'; // 红色 - 异常
}
}, []);
const getUptimeStatusText = useCallback((status) => {
switch (status) {
case 1:
return t('可用率');
default:
return t('有异常');
}
}, [t]);
const apiInfoData = useMemo(() => {
return statusState?.status?.api_info || [];
}, [statusState?.status?.api_info]);
@@ -1160,7 +1209,7 @@ const Detail = (props) => {
{/* 常见问答卡片 */}
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-2"
className="shadow-sm !rounded-2xl lg:col-span-1"
title={
<div className={FLEX_CENTER_GAP2}>
<HelpCircle size={16} />
@@ -1208,6 +1257,99 @@ const Detail = (props) => {
/>
</div>
</Card>
{/* 服务可用性卡片 */}
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-1"
title={
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
<div className="flex items-center gap-2">
<Gauge size={16} />
{t('服务可用性')}
</div>
<div className="flex items-center gap-3">
{/* 图例 */}
<div className="flex flex-wrap gap-3 text-xs">
{uptimeLegendData.map((legend, index) => (
<div key={index} className="flex items-center gap-1">
<div
className="w-2 h-2 rounded-full"
style={{
backgroundColor: legend.color === 'green' ? '#10b981' :
legend.color === 'red' ? '#ef4444' : '#8b9aa7'
}}
/>
<span className="text-gray-600">{legend.label}</span>
</div>
))}
</div>
<IconButton
icon={<IconRefresh />}
onClick={loadUptimeData}
loading={uptimeLoading}
size="small"
theme="borderless"
className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
/>
</div>
</div>
}
>
<div className="card-content-container">
<Spin spinning={uptimeLoading}>
<div
ref={uptimeScrollRef}
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
>
{uptimeData.length > 0 ? (
uptimeData.map((monitor, idx) => (
<div key={idx} className="p-2 hover:bg-white rounded-lg transition-colors">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<div
className="w-2 h-2 rounded-full flex-shrink-0"
style={{
backgroundColor: getUptimeStatusColor(monitor.status)
}}
/>
<span className="text-sm font-medium text-gray-900">{monitor.name}</span>
</div>
<span className="text-xs text-gray-500">{((monitor.uptime || 0) * 100).toFixed(2)}%</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">{getUptimeStatusText(monitor.status)}</span>
<div className="flex-1">
<Progress
percent={(monitor.uptime || 0) * 100}
showInfo={false}
aria-label={`${monitor.name} uptime`}
stroke={getUptimeStatusColor(monitor.status)}
/>
</div>
</div>
</div>
))
) : (
<div className="flex justify-center items-center py-8">
<Empty
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
title={t('暂无监控数据')}
description={t('请联系管理员在系统设置中配置Uptime')}
style={{ padding: '12px' }}
/>
</div>
)}
</div>
</Spin>
<div
className="card-content-fade-indicator"
style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
/>
</div>
</Card>
</div>
</div>
)}