239 lines
6.5 KiB
TypeScript
239 lines
6.5 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* useGlobalPulseMetrics - 全局脈搏真實數據 Hook
|
|
* =============================================
|
|
* 專案鐵律: 禁止假數據!所有指標必須來自 production 真實資料源
|
|
*
|
|
* Features:
|
|
* - Fail Fast 錯誤處理
|
|
* - 自動輪詢 (30 秒)
|
|
* - 誠實渲染 (無數據顯示 "--")
|
|
* - 專案鐵律: 禁止 Hardcode API URL
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
import type { PulseMetric } from '@/components/charts/global-pulse-chart'
|
|
|
|
// =============================================================================
|
|
// Types (對應後端 Response)
|
|
// =============================================================================
|
|
|
|
interface GoldMetricItem {
|
|
label: string
|
|
value: number | string
|
|
unit?: string
|
|
trend: number[]
|
|
status: 'healthy' | 'warning' | 'critical'
|
|
}
|
|
|
|
interface GoldMetricsResponse {
|
|
timestamp: string
|
|
service_name: string
|
|
metrics: GoldMetricItem[]
|
|
raw_data?: Record<string, unknown>
|
|
}
|
|
|
|
interface UseGlobalPulseMetricsOptions {
|
|
/** 輪詢間隔 (ms),預設 30000 */
|
|
pollInterval?: number
|
|
/** 是否啟用自動輪詢,預設 true */
|
|
enablePolling?: boolean
|
|
/** 服務名稱,預設 "awoooi-api" */
|
|
serviceName?: string
|
|
}
|
|
|
|
interface UseGlobalPulseMetricsResult {
|
|
/** 指標數據 (格式化給 GlobalPulseChart 使用) */
|
|
metrics: PulseMetric[]
|
|
/** 是否載入中 */
|
|
isLoading: boolean
|
|
/** 錯誤訊息 */
|
|
error: string | null
|
|
/** 最後更新時間 */
|
|
lastUpdated: Date | null
|
|
/** 手動刷新 */
|
|
refresh: () => Promise<void>
|
|
/** 是否有真實數據 (非全 0) */
|
|
hasRealData: boolean
|
|
}
|
|
|
|
// =============================================================================
|
|
// API Helper (專案鐵律: 禁止 Hardcode)
|
|
// =============================================================================
|
|
|
|
const getApiBaseUrl = (): string => {
|
|
if (typeof window === 'undefined') return ''
|
|
|
|
const url = process.env.NEXT_PUBLIC_API_URL
|
|
if (!url) {
|
|
console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL configuration.')
|
|
return ''
|
|
}
|
|
return url
|
|
}
|
|
|
|
// =============================================================================
|
|
// Hook Implementation
|
|
// =============================================================================
|
|
|
|
export function useGlobalPulseMetrics(
|
|
options: UseGlobalPulseMetricsOptions = {}
|
|
): UseGlobalPulseMetricsResult {
|
|
const {
|
|
pollInterval = 30000,
|
|
enablePolling = true,
|
|
serviceName = 'awoooi-api',
|
|
} = options
|
|
|
|
// State
|
|
const [metrics, setMetrics] = useState<PulseMetric[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
|
|
const [hasRealData, setHasRealData] = useState(false)
|
|
|
|
// Polling timer ref
|
|
const pollTimerRef = useRef<NodeJS.Timeout | null>(null)
|
|
|
|
// ==========================================================================
|
|
// Fetch Metrics
|
|
// ==========================================================================
|
|
const fetchMetrics = useCallback(async () => {
|
|
const apiBaseUrl = getApiBaseUrl()
|
|
|
|
// Fail Fast: 無 API URL 則報錯
|
|
if (!apiBaseUrl) {
|
|
setError('NEXT_PUBLIC_API_URL 未設定')
|
|
setIsLoading(false)
|
|
setHasRealData(false)
|
|
// 誠實渲染: 顯示 "--"
|
|
setMetrics(createEmptyMetrics())
|
|
return
|
|
}
|
|
|
|
try {
|
|
setError(null)
|
|
|
|
const response = await fetch(
|
|
`${apiBaseUrl}/api/v1/metrics/gold?service_name=${encodeURIComponent(serviceName)}`,
|
|
{
|
|
headers: { 'Content-Type': 'application/json' },
|
|
// 10 秒超時
|
|
signal: AbortSignal.timeout(10000),
|
|
}
|
|
)
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
}
|
|
|
|
const data: GoldMetricsResponse = await response.json()
|
|
|
|
// 轉換為 PulseMetric 格式
|
|
const pulseMetrics: PulseMetric[] = data.metrics.map((m) => ({
|
|
label: m.label,
|
|
value: m.value,
|
|
unit: m.unit,
|
|
trend: m.trend,
|
|
status: m.status,
|
|
}))
|
|
|
|
setMetrics(pulseMetrics)
|
|
setLastUpdated(new Date())
|
|
|
|
// 檢查是否有真實數據 (非全 0)
|
|
const hasReal = pulseMetrics.some(
|
|
(m) => typeof m.value === 'number' && m.value > 0
|
|
)
|
|
setHasRealData(hasReal)
|
|
|
|
console.log('[GlobalPulse] Metrics fetched:', pulseMetrics.length, 'items')
|
|
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
console.error('[GlobalPulse] Fetch error:', message)
|
|
setError(message)
|
|
setHasRealData(false)
|
|
|
|
// 誠實渲染: 錯誤時顯示 "--"
|
|
setMetrics(createEmptyMetrics())
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [serviceName])
|
|
|
|
// ==========================================================================
|
|
// Polling Effect
|
|
// ==========================================================================
|
|
useEffect(() => {
|
|
// Initial fetch
|
|
fetchMetrics()
|
|
|
|
// Start polling if enabled
|
|
if (enablePolling && pollInterval > 0) {
|
|
pollTimerRef.current = setInterval(fetchMetrics, pollInterval)
|
|
}
|
|
|
|
// Cleanup
|
|
return () => {
|
|
if (pollTimerRef.current) {
|
|
clearInterval(pollTimerRef.current)
|
|
pollTimerRef.current = null
|
|
}
|
|
}
|
|
}, [fetchMetrics, enablePolling, pollInterval])
|
|
|
|
return {
|
|
metrics,
|
|
isLoading,
|
|
error,
|
|
lastUpdated,
|
|
refresh: fetchMetrics,
|
|
hasRealData,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helper: Create Empty Metrics (誠實渲染用)
|
|
// =============================================================================
|
|
|
|
function createEmptyMetrics(): PulseMetric[] {
|
|
return [
|
|
{
|
|
label: 'RPS',
|
|
value: '--',
|
|
unit: 'req/s',
|
|
trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
status: 'critical',
|
|
},
|
|
{
|
|
label: 'Error Rate',
|
|
value: '--',
|
|
unit: '%',
|
|
trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
status: 'critical',
|
|
},
|
|
{
|
|
label: 'P99 Latency',
|
|
value: '--',
|
|
unit: 'ms',
|
|
trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
status: 'critical',
|
|
},
|
|
{
|
|
label: 'AI Success',
|
|
value: '--',
|
|
unit: '%',
|
|
trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
status: 'critical',
|
|
},
|
|
]
|
|
}
|
|
|
|
// =============================================================================
|
|
// Export
|
|
// =============================================================================
|
|
|
|
export default useGlobalPulseMetrics
|