feat(web): show source provider freshness on alerts
This commit is contained in:
@@ -747,8 +747,13 @@
|
|||||||
"sourceCoverageSentry": "Sentry refs",
|
"sourceCoverageSentry": "Sentry refs",
|
||||||
"sourceCoverageSigNoz": "SigNoz refs",
|
"sourceCoverageSigNoz": "SigNoz refs",
|
||||||
"sourceCoverageRatio": "source refs coverage {ratio} / total {total}",
|
"sourceCoverageRatio": "source refs coverage {ratio} / total {total}",
|
||||||
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}",
|
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}, latest {latest} ({age})",
|
||||||
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}"
|
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}, latest {latest} ({age})",
|
||||||
|
"sourceCoverageFreshness": "{provider} latest {latest} ({age})",
|
||||||
|
"sourceCoverageFresh": "fresh",
|
||||||
|
"sourceCoverageStaleHours": "stale {hours}h",
|
||||||
|
"sourceCoverageStaleDays": "stale {days}d",
|
||||||
|
"sourceCoverageNoEvents": "no events"
|
||||||
},
|
},
|
||||||
"navSection": {
|
"navSection": {
|
||||||
"aiCore": "AI Core",
|
"aiCore": "AI Core",
|
||||||
|
|||||||
@@ -748,8 +748,13 @@
|
|||||||
"sourceCoverageSentry": "Sentry refs",
|
"sourceCoverageSentry": "Sentry refs",
|
||||||
"sourceCoverageSigNoz": "SigNoz refs",
|
"sourceCoverageSigNoz": "SigNoz refs",
|
||||||
"sourceCoverageRatio": "source refs 覆蓋率 {ratio} / total {total}",
|
"sourceCoverageRatio": "source refs 覆蓋率 {ratio} / total {total}",
|
||||||
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}",
|
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}, latest {latest} ({age})",
|
||||||
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}"
|
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}, latest {latest} ({age})",
|
||||||
|
"sourceCoverageFreshness": "{provider} latest {latest} ({age})",
|
||||||
|
"sourceCoverageFresh": "fresh",
|
||||||
|
"sourceCoverageStaleHours": "stale {hours}h",
|
||||||
|
"sourceCoverageStaleDays": "stale {days}d",
|
||||||
|
"sourceCoverageNoEvents": "no events"
|
||||||
},
|
},
|
||||||
"navSection": {
|
"navSection": {
|
||||||
"aiCore": "AI 核心",
|
"aiCore": "AI 核心",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { Bell, BellOff, RefreshCw, AlertTriangle, AlertCircle, Info, ChevronLeft
|
|||||||
const SEVERITY_ORDER: Record<string, number> = { P0: 0, P1: 1, P2: 2, P3: 3 }
|
const SEVERITY_ORDER: Record<string, number> = { P0: 0, P1: 1, P2: 2, P3: 3 }
|
||||||
const ALERTS_PAGE_SIZE = 50
|
const ALERTS_PAGE_SIZE = 50
|
||||||
const SOURCE_COVERAGE_LIMIT = 100
|
const SOURCE_COVERAGE_LIMIT = 100
|
||||||
|
const SOURCE_FRESHNESS_STALE_HOURS = 24
|
||||||
|
|
||||||
const SEVERITY_STYLE: Record<string, { bg: string; text: string }> = {
|
const SEVERITY_STYLE: Record<string, { bg: string; text: string }> = {
|
||||||
P0: { bg: 'bg-status-critical/10', text: 'text-status-critical' },
|
P0: { bg: 'bg-status-critical/10', text: 'text-status-critical' },
|
||||||
@@ -91,6 +92,7 @@ interface SourceCoverageProvider {
|
|||||||
sentry_ref_total: number
|
sentry_ref_total: number
|
||||||
signoz_ref_total: number
|
signoz_ref_total: number
|
||||||
alert_ref_total: number
|
alert_ref_total: number
|
||||||
|
latest_received_at?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceCoverageResponse {
|
interface SourceCoverageResponse {
|
||||||
@@ -103,6 +105,38 @@ function percentLabel(value: number, total: number) {
|
|||||||
return `${Math.round((value / total) * 100)}%`
|
return `${Math.round((value / total) * 100)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseApiTimestamp(value?: string | null) {
|
||||||
|
if (!value) return null
|
||||||
|
const normalized = /(?:z|[+-]\d{2}:?\d{2})$/i.test(value) ? value : `${value}Z`
|
||||||
|
const date = new Date(normalized)
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(value: string | null | undefined, locale: string, emptyLabel: string) {
|
||||||
|
const date = parseApiTimestamp(value)
|
||||||
|
if (!date) return emptyLabel
|
||||||
|
return new Intl.DateTimeFormat(locale, {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
function freshnessAgeHours(value?: string | null) {
|
||||||
|
const date = parseApiTimestamp(value)
|
||||||
|
if (!date) return null
|
||||||
|
return Math.max(0, (Date.now() - date.getTime()) / 3_600_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function freshnessToneClass(ageHours: number | null) {
|
||||||
|
if (ageHours === null || ageHours > SOURCE_FRESHNESS_STALE_HOURS) {
|
||||||
|
return 'border-status-warning/30 bg-status-warning/5 text-status-warning'
|
||||||
|
}
|
||||||
|
return 'border-status-healthy/30 bg-status-healthy/5 text-status-healthy'
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Page
|
// Page
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -206,6 +240,17 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
|||||||
const topCoverageProviders = sourceCoverage?.providers?.slice(0, 3) ?? []
|
const topCoverageProviders = sourceCoverage?.providers?.slice(0, 3) ?? []
|
||||||
const sentryCoverageSummary = providerCoverage.sentry?.summary
|
const sentryCoverageSummary = providerCoverage.sentry?.summary
|
||||||
const signozCoverageSummary = providerCoverage.signoz?.summary
|
const signozCoverageSummary = providerCoverage.signoz?.summary
|
||||||
|
const emptyFreshnessLabel = tAlerts('sourceCoverageNoEvents')
|
||||||
|
const formatFreshnessAge = (value?: string | null) => {
|
||||||
|
const ageHours = freshnessAgeHours(value)
|
||||||
|
if (ageHours === null) return emptyFreshnessLabel
|
||||||
|
if (ageHours <= SOURCE_FRESHNESS_STALE_HOURS) return tAlerts('sourceCoverageFresh')
|
||||||
|
if (ageHours < 48) return tAlerts('sourceCoverageStaleHours', { hours: Math.floor(ageHours) })
|
||||||
|
return tAlerts('sourceCoverageStaleDays', { days: Math.floor(ageHours / 24) })
|
||||||
|
}
|
||||||
|
const coverageFreshnessAge = freshnessAgeHours(coverageSummary?.latest_received_at)
|
||||||
|
const sentryFreshnessAge = freshnessAgeHours(sentryCoverageSummary?.latest_received_at)
|
||||||
|
const signozFreshnessAge = freshnessAgeHours(signozCoverageSummary?.latest_received_at)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout locale={params.locale}>
|
<AppLayout locale={params.locale}>
|
||||||
@@ -289,10 +334,23 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
|||||||
total: coverageSummary?.source_count ?? 0,
|
total: coverageSummary?.source_count ?? 0,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
<span className={cn(
|
||||||
|
'rounded border px-2 py-1',
|
||||||
|
freshnessToneClass(coverageFreshnessAge)
|
||||||
|
)}>
|
||||||
|
{tAlerts('sourceCoverageFreshness', {
|
||||||
|
provider: 'overall',
|
||||||
|
latest: formatTimestamp(coverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
|
||||||
|
age: formatFreshnessAge(coverageSummary?.latest_received_at),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
{topCoverageProviders.map(provider => (
|
{topCoverageProviders.map(provider => (
|
||||||
<span
|
<span
|
||||||
key={provider.provider}
|
key={provider.provider}
|
||||||
className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1"
|
className={cn(
|
||||||
|
'rounded border px-2 py-1',
|
||||||
|
freshnessToneClass(freshnessAgeHours(provider.latest_received_at))
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{tAlerts('sourceCoverageProvider', {
|
{tAlerts('sourceCoverageProvider', {
|
||||||
provider: provider.provider,
|
provider: provider.provider,
|
||||||
@@ -300,23 +358,35 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
|||||||
missing: provider.missing_source_refs_total,
|
missing: provider.missing_source_refs_total,
|
||||||
sentry: provider.sentry_ref_total,
|
sentry: provider.sentry_ref_total,
|
||||||
signoz: provider.signoz_ref_total,
|
signoz: provider.signoz_ref_total,
|
||||||
|
latest: formatTimestamp(provider.latest_received_at, params.locale, emptyFreshnessLabel),
|
||||||
|
age: formatFreshnessAge(provider.latest_received_at),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
|
<span className={cn(
|
||||||
|
'rounded border px-2 py-1',
|
||||||
|
freshnessToneClass(sentryFreshnessAge)
|
||||||
|
)}>
|
||||||
{tAlerts('sourceCoverageProviderWindow', {
|
{tAlerts('sourceCoverageProviderWindow', {
|
||||||
provider: 'sentry',
|
provider: 'sentry',
|
||||||
total: sentryCoverageSummary?.source_count ?? 0,
|
total: sentryCoverageSummary?.source_count ?? 0,
|
||||||
withRefs: sentryCoverageSummary?.with_source_refs_total ?? 0,
|
withRefs: sentryCoverageSummary?.with_source_refs_total ?? 0,
|
||||||
missing: sentryCoverageSummary?.missing_source_refs_total ?? 0,
|
missing: sentryCoverageSummary?.missing_source_refs_total ?? 0,
|
||||||
|
latest: formatTimestamp(sentryCoverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
|
||||||
|
age: formatFreshnessAge(sentryCoverageSummary?.latest_received_at),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
|
<span className={cn(
|
||||||
|
'rounded border px-2 py-1',
|
||||||
|
freshnessToneClass(signozFreshnessAge)
|
||||||
|
)}>
|
||||||
{tAlerts('sourceCoverageProviderWindow', {
|
{tAlerts('sourceCoverageProviderWindow', {
|
||||||
provider: 'signoz',
|
provider: 'signoz',
|
||||||
total: signozCoverageSummary?.source_count ?? 0,
|
total: signozCoverageSummary?.source_count ?? 0,
|
||||||
withRefs: signozCoverageSummary?.with_source_refs_total ?? 0,
|
withRefs: signozCoverageSummary?.with_source_refs_total ?? 0,
|
||||||
missing: signozCoverageSummary?.missing_source_refs_total ?? 0,
|
missing: signozCoverageSummary?.missing_source_refs_total ?? 0,
|
||||||
|
latest: formatTimestamp(signozCoverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
|
||||||
|
age: formatFreshnessAge(signozCoverageSummary?.latest_received_at),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user