From 9d04d79d777c5f898d53f819377562d978d7a385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Fri, 23 Oct 2026 20:15:05 +0800 Subject: [PATCH] feat(site-replication): show replication health on the console page Operators could not tell from the Site Replication page whether replication was working. The server already reports peer reachability, failures, queue depth, undelivered configuration changes, unfinished lifecycle operations, and configuration drift, but the page only rendered configuration fields, and three of them read like health signals while carrying none: - the black "Enabled" sync badge is the per-peer sync setting, not link health, and stays "Enabled" while the peer is down; - "Replicated Objects/Size" are in-memory counters of the node that answered the request, reset on restart and not limited to site replication targets; - "Last Online" was taken from the local entry and always read "now". Add a health summary (healthy / needs attention / unhealthy / unknown) with the reasons behind it, a per-peer connection column with last-online time and downtime, a failures and backlog section, a consistency list of buckets, IAM entities, and expiry rules that differ between sites, an on-demand resync status view, and a 30-second quiet auto refresh while the tab is visible. Drop the misleading summary fields and render the sync setting as plain text. Health is derived in lib/site-replication-health.ts so the rules are unit tested: an unreachable peer is critical and suppresses the drift comparison (it reports empty metadata), and peers without a metric entry on servers older than rustfs/rustfs#7024 stay "unknown" instead of reading healthy. Resync status is fetched on demand only, because the server takes the lifecycle lock and persists state on every status call. The duplicated duration formatter moves from the status page to lib/functions.ts so both pages share it. Refs rustfs/backlog#2922 --- app/(dashboard)/site-replication/page.tsx | 456 ++++++++++++++++++---- app/(dashboard)/status/page.tsx | 15 +- hooks/use-site-replication.ts | 132 +++++++ i18n/locales/ar-MA.json | 42 +- i18n/locales/de-DE.json | 42 +- i18n/locales/en-US.json | 42 +- i18n/locales/es-ES.json | 42 +- i18n/locales/fr-FR.json | 42 +- i18n/locales/id-ID.json | 42 +- i18n/locales/it-IT.json | 42 +- i18n/locales/ja-JP.json | 42 +- i18n/locales/ko-KR.json | 42 +- i18n/locales/pt-BR.json | 42 +- i18n/locales/ru-RU.json | 42 +- i18n/locales/tr-TR.json | 42 +- i18n/locales/vi-VN.json | 42 +- i18n/locales/zh-CN.json | 42 +- lib/functions.ts | 15 + lib/site-replication-health.ts | 238 +++++++++++ tests/lib/functions.test.ts | 10 + tests/lib/site-replication-health.test.ts | 183 +++++++++ types/site-replication.ts | 80 +++- 22 files changed, 1620 insertions(+), 97 deletions(-) create mode 100644 lib/site-replication-health.ts create mode 100644 tests/lib/site-replication-health.test.ts diff --git a/app/(dashboard)/site-replication/page.tsx b/app/(dashboard)/site-replication/page.tsx index 77ce88e5..21598364 100644 --- a/app/(dashboard)/site-replication/page.tsx +++ b/app/(dashboard)/site-replication/page.tsx @@ -1,12 +1,13 @@ "use client" import * as React from "react" -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { RiAddLine, RiArrowLeftRightLine, RiDeleteBin5Line, + RiFileList3Line, RiMore2Line, RiRefreshLine, RiRestartLine, @@ -36,16 +37,29 @@ import { useDataTable } from "@/hooks/use-data-table" import { usePermissions } from "@/hooks/use-permissions" import { useSiteReplication } from "@/hooks/use-site-replication" import { useSystem } from "@/hooks/use-system" -import { formatDateTime, niceBytes } from "@/lib/functions" +import { formatDateTime, formatDuration, formatInteger, niceBytes } from "@/lib/functions" import { useDialog } from "@/lib/feedback/dialog" import { useMessage } from "@/lib/feedback/message" +import { + assessSiteReplicationHealth, + siteReplicationFailures, + type SiteReplicationBucketMismatch, + type SiteReplicationConnection, + type SiteReplicationDriftEntity, + type SiteReplicationDriftItem, + type SiteReplicationHealthIssue, + type SiteReplicationHealthLevel, +} from "@/lib/site-replication-health" import { extractServerDeploymentId, resolveSiteReplicationLocalSite } from "@/lib/site-replication-local-site" import { isHttpsSiteReplicationEndpoint } from "@/lib/site-replication-tls" +import { cn } from "@/lib/utils" import type { SiteReplicationInfo, SiteReplicationPeerInfo, SiteReplicationResyncOperation, + SiteReplicationResyncResponse, SiteReplicationStatus, + SiteReplicationVolume, } from "@/types/site-replication" const STATUS_QUERY = { @@ -58,22 +72,178 @@ const STATUS_QUERY = { "ilm-expiry-rules": "true", } +const AUTO_REFRESH_MS = 30_000 +const DRIFT_PREVIEW_LIMIT = 10 + +type Translate = (key: string, options?: Record) => string + function isNotImplementedError(error: unknown) { const message = (error as Error | undefined)?.message ?? "" return /notimplemented|not implemented/i.test(message) } -function formatSyncBadge(t: (key: string) => string, syncState: SiteReplicationPeerInfo["syncState"]) { - switch (syncState) { - case "enable": - return {t("Enabled")} - case "disable": - return {t("Disabled")} - default: - return {t("Unknown")} +// The sync flag is a per-peer setting, not link health, so it must not look +// like a status badge (the connection column carries health). +function formatSyncSetting(t: Translate, syncState: SiteReplicationPeerInfo["syncState"]) { + const label = syncState === "enable" ? t("Enabled") : syncState === "disable" ? t("Disabled") : t("Unknown") + return {label} +} + +const HEALTH_BADGES: Record< + SiteReplicationHealthLevel, + { label: string; variant: "secondary" | "destructive" | "outline"; className?: string } +> = { + healthy: { label: "Healthy", variant: "secondary" }, + degraded: { label: "Needs attention", variant: "outline", className: "border-amber-500/40 bg-amber-500/10" }, + critical: { label: "Unhealthy", variant: "destructive" }, + unknown: { label: "Unknown", variant: "outline" }, +} + +function HealthBadge({ t, level }: { t: Translate; level: SiteReplicationHealthLevel }) { + const badge = HEALTH_BADGES[level] + return ( + + {t(badge.label)} + + ) +} + +function ConnectionCell({ t, connection }: { t: Translate; connection?: SiteReplicationConnection }) { + if (!connection || connection.state === "unknown") { + return {t("Unknown")} + } + if (connection.state === "online") { + return {t("Online")} + } + + return ( +
+ {t("Offline")} + {connection.lastOnline ? ( +

+ {t("Last Online")}: {formatDateTime(connection.lastOnline)} +

+ ) : null} + {connection.downtimeNs > 0 ? ( +

+ {t("Downtime")}: {formatDuration(connection.downtimeNs / 1e9, t)} +

+ ) : null} +
+ ) +} + +const OPERATION_LABELS: Record = { + remove: "Site removal", + "endpoint-refresh": "Endpoint update", + "rotate-svc-acct": "Service account rotation", +} + +function describeIssue(t: Translate, issue: SiteReplicationHealthIssue, siteName: (deploymentId: string) => string) { + switch (issue.kind) { + case "peer-offline": + return t("Site {site} is unreachable, so nothing replicates to it.", { site: siteName(issue.deploymentId) }) + case "pending-operation": { + const { operation, pendingPeers } = issue.operation + const label = OPERATION_LABELS[operation] ? t(OPERATION_LABELS[operation]) : operation + return pendingPeers.length + ? t("{operation} has not finished; waiting for {sites}.", { + operation: label, + sites: pendingPeers.map(siteName).join(", "), + }) + : t("{operation} has not finished.", { operation: label }) + } + case "retry-backlog": + return t("{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.", { + pending: issue.retryStats.pending, + failed: issue.retryStats.failed, + }) + case "recent-failures": + return t("{count} object replication failures in the last hour.", { count: issue.count }) + case "drift": + return t("{count} buckets, IAM entities, or expiry rules differ between sites.", { count: issue.count }) } } +// Server errors can be long debug strings; they are shown clamped, in full on hover. +function issueError(issue: SiteReplicationHealthIssue) { + if (issue.kind === "peer-offline") return issue.error + if (issue.kind === "retry-backlog") return issue.retryStats.lastError || undefined + return undefined +} + +const DRIFT_ENTITY_LABELS: Record = { + bucket: "Bucket", + policy: "Policy", + user: "User", + group: "Group", + ilmExpiryRule: "ILM Expiry Rule", +} + +const DRIFT_MISMATCH_LABELS: Record = { + markedDeleted: "Marked for deletion", + tags: "Tags", + versioning: "Versioning", + objectLock: "Object Lock", + policy: "Bucket Policy", + encryption: "Encryption", + replication: "Replication", + quota: "Quota", + cors: "CORS", + content: "Policy document", +} + +function describeDrift(t: Translate, item: SiteReplicationDriftItem, siteName: (deploymentId: string) => string) { + return [ + item.missingOn.length ? t("Missing on {sites}", { sites: item.missingOn.map(siteName).join(", ") }) : "", + item.mismatches.length + ? t("Differs: {settings}", { + settings: item.mismatches.map((mismatch) => t(DRIFT_MISMATCH_LABELS[mismatch])).join(", "), + }) + : "", + ] + .filter(Boolean) + .join("; ") +} + +function formatVolume(volume: SiteReplicationVolume) { + return `${formatInteger(volume.count)} (${niceBytes(String(volume.bytes))})` +} + +const RESYNC_STATE_LABELS: Record = { + pending: "Waiting", + running: "Running", + canceling: "Canceling", + canceled: "Canceled", + completed: "Done", + failed: "Error", +} + +function describeResync(t: Translate, resync: SiteReplicationResyncResponse) { + if (!resync.resyncId) { + return t("No resync has been started for this site.") + } + + const state = RESYNC_STATE_LABELS[resync.state] ? t(RESYNC_STATE_LABELS[resync.state]) : resync.state + return [ + t( + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.", + { + state, + completed: resync.completedBuckets, + total: resync.totalBuckets, + failed: resync.failedBuckets, + objects: resync.replicatedObjects, + failedObjects: resync.failedObjects, + }, + ), + resync.updatedAt ? `${t("Updated")}: ${formatDateTime(resync.updatedAt)}` : "", + resync.errorDetail, + ] + .filter(Boolean) + .join(" ") +} + function formatBandwidth(t: (key: string) => string, peer: SiteReplicationPeerInfo) { if (!peer.defaultBandwidth.set) { return t("Not configured") @@ -133,67 +303,96 @@ export default function SiteReplicationPage() { const [pendingResyncDeploymentId, setPendingResyncDeploymentId] = useState("") const [pendingRemoveDeploymentId, setPendingRemoveDeploymentId] = useState("") const [pendingIlmToggle, setPendingIlmToggle] = useState(false) + const [updatedAt, setUpdatedAt] = useState(null) + const loadingRef = useRef(false) const canManageSites = isAdmin || hasPermission("admin:SiteReplicationAdd") const canRemoveSites = isAdmin || hasPermission("admin:SiteReplicationRemove") const canResyncSites = isAdmin || hasPermission("admin:SiteReplicationResync") - const loadData = useCallback(async () => { - setLoading(true) - setError(null) - setFeatureUnavailable(false) - setStatusUnavailable(false) - - // The local deployment ID is the only stable way to tell which entry of - // the shared, deployment-ID-sorted site list belongs to this server. Fetch - // it alongside the replication data; a failure here only degrades the - // "current site" identification, so it must not fail the whole page. - const serverDeploymentIdPromise = getSystemInfo(undefined, { suppress403Redirect: true }) - .then(extractServerDeploymentId) - .catch(() => "") + const loadData = useCallback( + async ({ quiet = false }: { quiet?: boolean } = {}) => { + // A quiet refresh never overlaps another load and keeps the current view + // (no spinner) so the periodic poll does not flicker the page. + if (quiet && loadingRef.current) { + return + } + loadingRef.current = true + if (!quiet) { + setLoading(true) + } + setError(null) + setFeatureUnavailable(false) + setStatusUnavailable(false) - try { - let nextInfo: SiteReplicationInfo | null = null - let nextStatus: SiteReplicationStatus | null = null + // The local deployment ID is the only stable way to tell which entry of + // the shared, deployment-ID-sorted site list belongs to this server. Fetch + // it alongside the replication data; a failure here only degrades the + // "current site" identification, so it must not fail the whole page. + const serverDeploymentIdPromise = getSystemInfo(undefined, { suppress403Redirect: true }) + .then(extractServerDeploymentId) + .catch(() => "") try { - nextInfo = await getSiteReplicationInfo() - } catch (loadError) { - if (isNotImplementedError(loadError)) { - setFeatureUnavailable(true) - setInfo(null) - setStatus(null) - return - } + let nextInfo: SiteReplicationInfo | null = null + let nextStatus: SiteReplicationStatus | null = null - throw loadError - } + try { + nextInfo = await getSiteReplicationInfo() + } catch (loadError) { + if (isNotImplementedError(loadError)) { + setFeatureUnavailable(true) + setInfo(null) + setStatus(null) + return + } - try { - nextStatus = await getSiteReplicationStatus(STATUS_QUERY) - } catch (loadError) { - if (isNotImplementedError(loadError)) { - setStatusUnavailable(true) - nextStatus = null - } else { throw loadError } - } - setServerDeploymentId(await serverDeploymentIdPromise) - setInfo(nextInfo) - setStatus(nextStatus) - } catch (loadError) { - setError((loadError as Error).message || t("Load Failed")) - } finally { - setLoading(false) - } - }, [getSiteReplicationInfo, getSiteReplicationStatus, getSystemInfo, t]) + try { + nextStatus = await getSiteReplicationStatus(STATUS_QUERY) + } catch (loadError) { + if (isNotImplementedError(loadError)) { + setStatusUnavailable(true) + nextStatus = null + } else { + throw loadError + } + } + + setServerDeploymentId(await serverDeploymentIdPromise) + setInfo(nextInfo) + setStatus(nextStatus) + setUpdatedAt(new Date()) + } catch (loadError) { + setError((loadError as Error).message || t("Load Failed")) + } finally { + loadingRef.current = false + setLoading(false) + } + }, + [getSiteReplicationInfo, getSiteReplicationStatus, getSystemInfo, t], + ) useEffect(() => { void loadData() }, [loadData]) + const replicationEnabled = !!info?.enabled + useEffect(() => { + if (!replicationEnabled) return + const refresh = () => { + if (document.visibilityState === "visible") void loadData({ quiet: true }) + } + const interval = window.setInterval(refresh, AUTO_REFRESH_MS) + document.addEventListener("visibilitychange", refresh) + return () => { + window.clearInterval(interval) + document.removeEventListener("visibilitychange", refresh) + } + }, [loadData, replicationEnabled]) + const peers = info?.sites ?? [] const ilmExpiryEnabled = peers.some((peer) => peer.replicateIlmExpiry) // Never pick the local site by array or key position: every site returns @@ -207,6 +406,32 @@ export default function SiteReplicationPage() { const localPeer = peers.find((peer) => peer.deploymentId === localDeploymentId) ?? null const localSummary = (localDeploymentId && status?.statsSummary[localDeploymentId]) || null const localMetric = (localDeploymentId && status?.metrics.metrics[localDeploymentId]) || null + const health = useMemo( + () => assessSiteReplicationHealth({ info, status, localDeploymentId }), + [info, status, localDeploymentId], + ) + const failures = siteReplicationFailures(status) + const siteName = (deploymentId: string) => + peers.find((peer) => peer.deploymentId === deploymentId)?.name || deploymentId + + const showResyncStatus = useCallback( + async (peer: SiteReplicationPeerInfo) => { + setPendingResyncDeploymentId(peer.deploymentId) + try { + const resync = await resyncSiteReplication("status", peer) + dialog.info({ + title: `${t("Resync Status")}: ${peer.name || peer.endpoint}`, + content: describeResync(t, resync), + positiveText: t("Close"), + }) + } catch (resyncError) { + message.error((resyncError as Error).message || t("Load Failed")) + } finally { + setPendingResyncDeploymentId("") + } + }, + [dialog, message, resyncSiteReplication, t], + ) const handleResync = useCallback( async (peer: SiteReplicationPeerInfo, operation: SiteReplicationResyncOperation) => { @@ -331,6 +556,16 @@ export default function SiteReplicationPage() { ) }, }, + { + id: "connection", + header: () => t("Connection"), + cell: ({ row }) => + row.original.deploymentId === localDeploymentId ? ( + - + ) : ( + + ), + }, { id: "endpoint", header: () => t("Endpoint"), @@ -350,9 +585,9 @@ export default function SiteReplicationPage() { }, { id: "syncState", - header: () => t("Sync"), + header: () => t("Sync Setting"), accessorKey: "syncState", - cell: ({ row }) => formatSyncBadge(t, row.original.syncState), + cell: ({ row }) => formatSyncSetting(t, row.original.syncState), }, { id: "ilmExpiry", @@ -414,6 +649,12 @@ export default function SiteReplicationPage() { {t("Cancel Resync")} ) : null} + {canResyncSites ? ( + void showResyncStatus(peer)}> + + {t("Resync Status")} + + ) : null} {canRemoveSites ? : null} {canRemoveSites ? ( @@ -434,9 +675,11 @@ export default function SiteReplicationPage() { canResyncSites, confirmRemoveSite, handleResync, + health.connections, localDeploymentId, pendingRemoveDeploymentId, pendingResyncDeploymentId, + showResyncStatus, t, ], ) @@ -611,6 +854,52 @@ export default function SiteReplicationPage() { ) : ( <> +
+
+
+

+ {t("Replication Health")} +

+ +
+

+ {t("Updated")}: {formatDateTime(updatedAt)} · {t("Refreshes every 30 seconds")} +

+
+ {health.issues.length ? ( +
    + {health.issues.map((issue, index) => { + const error = issueError(issue) + return ( +
  • + {describeIssue(t, issue, siteName)} + {error ? ( +

    + {t("Last error")}: {error} +

    + ) : null} +
  • + ) + })} +
+ ) : ( +

+ {health.level === "healthy" + ? t("Every peer site is reachable and nothing is waiting to be replicated or repaired.") + : t("Peer reachability is not reported by this server, so replication health cannot be confirmed.")} +

+ )} +
+
@@ -667,27 +956,33 @@ export default function SiteReplicationPage() {
-

{t("Replication Summary")}

+

{t("Failures and Backlog")}

- {t("Overview of replicated configuration and object replication activity.")} + {t("Object counters come from the node that answered this request and reset when it restarts.")}

+ + + + {t("Enabled")} - ) : ( - {t("Not configured")} - ) + status + ? `${formatInteger(status.metrics.activeWorkers.curr)} / ${formatInteger(status.metrics.activeWorkers.max)}` + : "--" } /> - -
@@ -718,6 +1013,32 @@ export default function SiteReplicationPage() { )}

+ {status ? ( +
+

{t("Consistency")}

+ {health.drift === null ? ( +

+ {t("Consistency cannot be checked until every peer site is reachable.")} +

+ ) : health.drift.length === 0 ? ( +

{t("Every site holds the same buckets, IAM entities, and expiry rules.")}

+ ) : ( +
    + {health.drift.slice(0, DRIFT_PREVIEW_LIMIT).map((item) => ( +
  • + {t(DRIFT_ENTITY_LABELS[item.entity])}{" "} + {item.name} — {describeDrift(t, item, siteName)} +
  • + ))} + {health.drift.length > DRIFT_PREVIEW_LIMIT ? ( +
  • + {t("and {count} more", { count: health.drift.length - DRIFT_PREVIEW_LIMIT })} +
  • + ) : null} +
+ )} +
+ ) : null}
@@ -730,7 +1051,6 @@ export default function SiteReplicationPage() { label={t("ILM Expiry Rules")} value={status?.maxIlmExpiryRules ?? localSummary?.totalIlmExpiryRulesCount ?? 0} /> -
diff --git a/app/(dashboard)/status/page.tsx b/app/(dashboard)/status/page.tsx index 01b00a22..f20dcdbe 100644 --- a/app/(dashboard)/status/page.tsx +++ b/app/(dashboard)/status/page.tsx @@ -12,6 +12,7 @@ import { isScannerCycleActive, resolveUsageFreshness, } from "@/lib/performance-data" +import { formatDuration } from "@/lib/functions" import { cn } from "@/lib/utils" import { RiArchiveDrawerFill, @@ -32,20 +33,6 @@ import { PerformanceStatusSources } from "../_components/performance-status-sour import { PerformanceStatusSummary } from "../_components/performance-status-summary" import { PerformanceUsageCard } from "../_components/performance-usage-card" -function formatDuration(seconds: number | undefined, t: (key: string) => string) { - if (seconds === undefined || !Number.isFinite(seconds) || seconds < 0) return t("Unknown") - const totalMinutes = Math.floor(seconds / 60) - const days = Math.floor(totalMinutes / 1440) - const hours = Math.floor((totalMinutes % 1440) / 60) - const minutes = totalMinutes % 60 - const parts = [ - days ? `${days} ${t("Days")}` : "", - hours ? `${hours} ${t("Hours")}` : "", - !days && minutes ? `${minutes} ${t("Minutes")}` : "", - ].filter(Boolean) - return parts.slice(0, 2).join(" ") || `0 ${t("Minutes")}` -} - export default function PerformancePage() { const { t, i18n } = useTranslation() const { canAccessPath } = usePermissions() diff --git a/hooks/use-site-replication.ts b/hooks/use-site-replication.ts index 86f3e0fd..09c2e972 100644 --- a/hooks/use-site-replication.ts +++ b/hooks/use-site-replication.ts @@ -5,19 +5,28 @@ import { useApi } from "@/contexts/api-context" import type { SiteReplicationAddResponse, SiteReplicationAddSiteInput, + SiteReplicationBucketStats, SiteReplicationEditResponse, + SiteReplicationEntityStats, SiteReplicationInfo, SiteReplicationMetric, SiteReplicationMetricsSummary, + SiteReplicationPeerError, SiteReplicationPeerInfo, + SiteReplicationPendingOperation, + SiteReplicationPolicyStats, + SiteReplicationQueueStat, SiteReplicationRemoveInput, SiteReplicationRemoveResponse, SiteReplicationResyncOperation, SiteReplicationResyncResponse, + SiteReplicationRetryStats, SiteReplicationSiteSummary, SiteReplicationStateInfo, SiteReplicationStatus, SiteReplicationSyncState, + SiteReplicationTimedErrors, + SiteReplicationVolume, SiteReplicationWorkerStat, } from "@/types/site-replication" @@ -39,6 +48,14 @@ function asBoolean(value: unknown): boolean { return typeof value === "boolean" ? value : false } +function asStringArray(value: unknown): string[] { + return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [] +} + +function mapRecord(value: unknown, normalize: (item: unknown) => T): Record { + return Object.fromEntries(Object.entries(asRecord(value)).map(([key, item]) => [key, normalize(item)])) +} + function normalizeSyncState(value: unknown): SiteReplicationSyncState { const normalized = asString(value).toLowerCase() if (normalized === "enable" || normalized === "disable") { @@ -57,6 +74,102 @@ function normalizeWorkerStat(value: unknown): SiteReplicationWorkerStat { } } +function normalizeVolume(value: unknown): SiteReplicationVolume { + const record = asRecord(value) + + return { count: asNumber(record.count), bytes: asNumber(record.bytes) } +} + +function normalizeTimedErrors(value: unknown): SiteReplicationTimedErrors { + const record = asRecord(value) + + return { + lastMinute: normalizeVolume(record.lastMinute), + lastHour: normalizeVolume(record.lastHour), + totals: normalizeVolume(record.totals), + } +} + +function normalizeQueueStat(value: unknown): SiteReplicationQueueStat { + const record = asRecord(value) + + return { + curr: normalizeVolume(record.curr), + avg: normalizeVolume(record.avg), + max: normalizeVolume(record.max), + } +} + +// Both fields are omitted by the server while nothing is outstanding, so an +// absent value means "healthy", not "unknown". +function normalizePendingOperation(value: unknown): SiteReplicationPendingOperation | undefined { + const record = asRecord(value) + const operation = asString(record.operation) + if (!operation) { + return undefined + } + + return { + operation, + id: asString(record.id), + pendingPeers: asStringArray(record.pendingPeers), + ackedPeers: asStringArray(record.ackedPeers), + updatedAt: asString(record.updatedAt) || undefined, + } +} + +function normalizeRetryStats(value: unknown): SiteReplicationRetryStats | undefined { + if (!value || typeof value !== "object") { + return undefined + } + const record = asRecord(value) + + return { + pending: asNumber(record.pending), + failed: asNumber(record.failed), + lastError: asString(record.lastError), + } +} + +function normalizePeerError(value: unknown): SiteReplicationPeerError { + const record = asRecord(value) + + return { name: asString(record.name), endpoint: asString(record.endpoint), error: asString(record.error) } +} + +function normalizeBucketStats(value: unknown): SiteReplicationBucketStats { + const record = asRecord(value) + + return { + hasBucket: asBoolean(record.HasBucket), + bucketMarkedDeleted: asBoolean(record.BucketMarkedDeleted), + tagMismatch: asBoolean(record.TagMismatch), + versioningConfigMismatch: asBoolean(record.VersioningConfigMismatch), + objectLockConfigMismatch: asBoolean(record.OLockConfigMismatch), + policyMismatch: asBoolean(record.PolicyMismatch), + sseConfigMismatch: asBoolean(record.SSEConfigMismatch), + replicationCfgMismatch: asBoolean(record.ReplicationCfgMismatch), + quotaCfgMismatch: asBoolean(record.QuotaCfgMismatch), + corsCfgMismatch: asBoolean(record.CorsCfgMismatch), + } +} + +function normalizePolicyStats(value: unknown): SiteReplicationPolicyStats { + const record = asRecord(value) + + return { hasPolicy: asBoolean(record.HasPolicy), policyMismatch: asBoolean(record.PolicyMismatch) } +} + +function normalizeEntityStats(value: unknown, normalize: (item: unknown) => T): SiteReplicationEntityStats { + return mapRecord(value, (entity) => mapRecord(entity, normalize)) +} + +// User, group, and ILM expiry rule entries only exist on the sites that hold +// the entity, so the inner keys are the holding deployment IDs. +function normalizeEntitySites(value: unknown): Record { + return mapRecord(value, (entity) => Object.keys(asRecord(entity))) +} + function normalizePeerInfo(value: unknown): SiteReplicationPeerInfo { const record = asRecord(value) const bandwidth = asRecord(record.defaultbandwidth) @@ -174,6 +287,7 @@ function normalizeMetric(value: unknown): SiteReplicationMetric { online: asBoolean(record.isOnline), replicatedSize: asNumber(record.replicatedSize), replicatedCount: asNumber(record.replicatedCount), + failed: normalizeTimedErrors(record.failed), } } @@ -190,6 +304,7 @@ function normalizeMetricsSummary(value: unknown): SiteReplicationMetricsSummary activeWorkers: normalizeWorkerStat(record.activeWorkers), replicaSize: asNumber(record.replicaSize), replicaCount: asNumber(record.replicaCount), + queued: normalizeQueueStat(record.queued), metrics: normalizeMetricMap(record.replMetrics), } } @@ -203,6 +318,8 @@ function normalizeInfo(value: unknown): SiteReplicationInfo { name: asString(record.name), sites, serviceAccountAccessKey: asString(record.serviceAccountAccessKey), + pendingOperation: normalizePendingOperation(record.pendingOperation), + retryStats: normalizeRetryStats(record.retryStats), apiVersion: asString(record.apiVersion) || undefined, } } @@ -220,7 +337,15 @@ function normalizeStatus(value: unknown): SiteReplicationStatus { sites: normalizePeerMap(record.Sites), statsSummary: normalizeSiteSummaryMap(record.StatsSummary), peerStates: normalizeStateInfoMap(record.PeerStates), + peerErrors: mapRecord(record.PeerErrors, normalizePeerError), + pendingOperation: normalizePendingOperation(record.PendingOperation), + retryStats: normalizeRetryStats(record.RetryStats), metrics: normalizeMetricsSummary(record.Metrics), + bucketStats: normalizeEntityStats(record.BucketStats, normalizeBucketStats), + policyStats: normalizeEntityStats(record.PolicyStats, normalizePolicyStats), + userSites: normalizeEntitySites(record.UserStats), + groupSites: normalizeEntitySites(record.GroupStats), + ilmExpiryRuleSites: normalizeEntitySites(record.ILMExpiryStats), apiVersion: asString(record.apiVersion) || undefined, } } @@ -275,7 +400,14 @@ function normalizeResyncResponse(value: unknown): SiteReplicationResyncResponse opType: asString(record.op), resyncId: asString(record.id), status: asString(record.status), + state: asString(record.state), buckets, + totalBuckets: asNumber(record.totalBuckets), + completedBuckets: asNumber(record.completedBuckets), + failedBuckets: asNumber(record.failedBuckets), + replicatedObjects: asNumber(record.replicatedObjects), + failedObjects: asNumber(record.failedObjects), + updatedAt: asString(record.updatedAt) || undefined, errorDetail: asString(record.errorDetail), } } diff --git a/i18n/locales/ar-MA.json b/i18n/locales/ar-MA.json index c293e768..b0b0151b 100644 --- a/i18n/locales/ar-MA.json +++ b/i18n/locales/ar-MA.json @@ -2241,5 +2241,45 @@ "Operations": "العمليات", "Resources": "الموارد", "Bucket size distribution": "توزيع أحجام الحاويات", - "Other buckets": "حاويات أخرى" + "Other buckets": "حاويات أخرى", + "Active workers": "العمال النشطون", + "Configuration deliveries": "تسليم الإعدادات", + "Connection": "الاتصال", + "Consistency": "الاتساق", + "Consistency cannot be checked until every peer site is reachable.": "لا يمكن التحقق من الاتساق حتى يصبح كل موقع نظير قابلاً للوصول.", + "Differs: {settings}": "مختلف: {settings}", + "Downtime": "مدة الانقطاع", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "كل المواقع النظيرة قابلة للوصول ولا يوجد ما ينتظر النسخ أو الإصلاح.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "تحتوي كل المواقع على نفس الحاويات وكيانات IAM وقواعد الانتهاء.", + "Failed in the last hour": "فشل خلال الساعة الأخيرة", + "Failed since start": "فشل منذ البدء", + "Failures and Backlog": "الإخفاقات والتراكم", + "Missing on {sites}": "مفقود في {sites}", + "No resync has been started for this site.": "لم تبدأ أي إعادة مزامنة لهذا الموقع.", + "Object counters come from the node that answered this request and reset when it restarts.": "تأتي عدادات الكائنات من العقدة التي أجابت على هذا الطلب وتُعاد إلى الصفر عند إعادة تشغيلها.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "لا يبلّغ هذا الخادم عن إمكانية الوصول إلى النظراء، لذا لا يمكن تأكيد سلامة النسخ.", + "Queued now": "في الانتظار الآن", + "Queued peak": "ذروة الانتظار", + "Refreshes every 30 seconds": "يتحدث كل 30 ثانية", + "Replication Health": "سلامة النسخ", + "Resync Status": "حالة إعادة المزامنة", + "Site {site} is unreachable, so nothing replicates to it.": "الموقع {site} غير قابل للوصول، لذا لا يتم نسخ أي شيء إليه.", + "Sync Setting": "إعداد المزامنة", + "and {count} more": "و{count} أخرى", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} من الحاويات أو كيانات IAM أو قواعد الانتهاء تختلف بين المواقع.", + "{count} object replication failures in the last hour.": "{count} من حالات فشل نسخ الكائنات خلال الساعة الأخيرة.", + "{operation} has not finished.": "{operation} لم تكتمل.", + "{operation} has not finished; waiting for {sites}.": "{operation} لم تكتمل؛ في انتظار {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} من تغييرات الإعدادات بانتظار التسليم إلى المواقع النظيرة، وفشل {failed}.", + "{pending} waiting, {failed} failed": "{pending} قيد الانتظار، {failed} فشل", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: اكتملت {completed} من {total} حاوية، وفشلت {failed}. تم نسخ {objects} كائن، وفشل {failedObjects}.", + "Site removal": "إزالة الموقع", + "Endpoint update": "تحديث نقطة النهاية", + "Group": "المجموعة", + "ILM Expiry Rule": "قاعدة انتهاء ILM", + "Marked for deletion": "محدد للحذف", + "Quota": "الحصة", + "CORS": "CORS", + "Policy document": "مستند السياسة", + "Service account rotation": "تدوير حساب الخدمة" } diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index 40b40014..389ae254 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -2241,5 +2241,45 @@ "Operations": "Betrieb", "Resources": "Ressourcen", "Bucket size distribution": "Verteilung der Bucket-Größen", - "Other buckets": "Weitere Buckets" + "Other buckets": "Weitere Buckets", + "Active workers": "Aktive Worker", + "Configuration deliveries": "Konfigurationszustellungen", + "Connection": "Verbindung", + "Consistency": "Konsistenz", + "Consistency cannot be checked until every peer site is reachable.": "Die Konsistenz kann erst geprüft werden, wenn alle Peer-Standorte erreichbar sind.", + "Differs: {settings}": "Abweichend: {settings}", + "Downtime": "Ausfallzeit", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Alle Peer-Standorte sind erreichbar, und nichts wartet auf Replikation oder Reparatur.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Alle Standorte enthalten dieselben Buckets, IAM-Entitäten und Ablaufregeln.", + "Failed in the last hour": "Fehlgeschlagen in der letzten Stunde", + "Failed since start": "Fehlgeschlagen seit Start", + "Failures and Backlog": "Fehler und Rückstand", + "Missing on {sites}": "Fehlt auf {sites}", + "No resync has been started for this site.": "Für diesen Standort wurde noch keine Neusynchronisierung gestartet.", + "Object counters come from the node that answered this request and reset when it restarts.": "Die Objektzähler stammen vom Knoten, der diese Anfrage beantwortet hat, und werden bei dessen Neustart zurückgesetzt.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Dieser Server meldet die Erreichbarkeit der Peers nicht, daher kann der Replikationszustand nicht bestätigt werden.", + "Queued now": "Aktuell in Warteschlange", + "Queued peak": "Höchststand Warteschlange", + "Refreshes every 30 seconds": "Aktualisierung alle 30 Sekunden", + "Replication Health": "Replikationszustand", + "Resync Status": "Status der Neusynchronisierung", + "Site {site} is unreachable, so nothing replicates to it.": "Standort {site} ist nicht erreichbar, daher wird nichts dorthin repliziert.", + "Sync Setting": "Sync-Einstellung", + "and {count} more": "und {count} weitere", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} Buckets, IAM-Entitäten oder Ablaufregeln unterscheiden sich zwischen den Standorten.", + "{count} object replication failures in the last hour.": "{count} fehlgeschlagene Objektreplikationen in der letzten Stunde.", + "{operation} has not finished.": "{operation} ist nicht abgeschlossen.", + "{operation} has not finished; waiting for {sites}.": "{operation} ist nicht abgeschlossen; wartet auf {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} Konfigurationsänderungen warten auf Zustellung an Peer-Standorte, {failed} fehlgeschlagen.", + "{pending} waiting, {failed} failed": "{pending} wartend, {failed} fehlgeschlagen", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed} von {total} Buckets abgeschlossen, {failed} fehlgeschlagen. {objects} Objekte repliziert, {failedObjects} fehlgeschlagen.", + "Site removal": "Standortentfernung", + "Endpoint update": "Endpunkt-Aktualisierung", + "Group": "Gruppe", + "ILM Expiry Rule": "ILM-Ablaufregel", + "Marked for deletion": "Zum Löschen markiert", + "Quota": "Kontingent", + "CORS": "CORS", + "Policy document": "Richtliniendokument", + "Service account rotation": "Rotation des Dienstkontos" } diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index 7d1f570e..fb0318fb 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -2241,5 +2241,45 @@ "Operations": "Operations", "Resources": "Resources", "Bucket size distribution": "Bucket size distribution", - "Other buckets": "Other buckets" + "Other buckets": "Other buckets", + "Active workers": "Active workers", + "Configuration deliveries": "Configuration deliveries", + "Connection": "Connection", + "Consistency": "Consistency", + "Consistency cannot be checked until every peer site is reachable.": "Consistency cannot be checked until every peer site is reachable.", + "Differs: {settings}": "Differs: {settings}", + "Downtime": "Downtime", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Every peer site is reachable and nothing is waiting to be replicated or repaired.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Every site holds the same buckets, IAM entities, and expiry rules.", + "Failed in the last hour": "Failed in the last hour", + "Failed since start": "Failed since start", + "Failures and Backlog": "Failures and Backlog", + "Missing on {sites}": "Missing on {sites}", + "No resync has been started for this site.": "No resync has been started for this site.", + "Object counters come from the node that answered this request and reset when it restarts.": "Object counters come from the node that answered this request and reset when it restarts.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Peer reachability is not reported by this server, so replication health cannot be confirmed.", + "Queued now": "Queued now", + "Queued peak": "Queued peak", + "Refreshes every 30 seconds": "Refreshes every 30 seconds", + "Replication Health": "Replication Health", + "Resync Status": "Resync Status", + "Site {site} is unreachable, so nothing replicates to it.": "Site {site} is unreachable, so nothing replicates to it.", + "Sync Setting": "Sync Setting", + "and {count} more": "and {count} more", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} buckets, IAM entities, or expiry rules differ between sites.", + "{count} object replication failures in the last hour.": "{count} object replication failures in the last hour.", + "{operation} has not finished.": "{operation} has not finished.", + "{operation} has not finished; waiting for {sites}.": "{operation} has not finished; waiting for {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.", + "{pending} waiting, {failed} failed": "{pending} waiting, {failed} failed", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.", + "Site removal": "Site removal", + "Endpoint update": "Endpoint update", + "Group": "Group", + "ILM Expiry Rule": "ILM Expiry Rule", + "Marked for deletion": "Marked for deletion", + "Quota": "Quota", + "CORS": "CORS", + "Policy document": "Policy document", + "Service account rotation": "Service account rotation" } diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index e807fc96..80f61a35 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -2241,5 +2241,45 @@ "Operations": "Operaciones", "Resources": "Recursos", "Bucket size distribution": "Distribución del tamaño de los buckets", - "Other buckets": "Otros buckets" + "Other buckets": "Otros buckets", + "Active workers": "Workers activos", + "Configuration deliveries": "Entregas de configuración", + "Connection": "Conexión", + "Consistency": "Coherencia", + "Consistency cannot be checked until every peer site is reachable.": "No se puede comprobar la coherencia hasta que todos los sitios pares sean accesibles.", + "Differs: {settings}": "Difiere: {settings}", + "Downtime": "Tiempo de inactividad", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Todos los sitios pares son accesibles y no hay nada pendiente de replicar o reparar.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Todos los sitios tienen los mismos buckets, entidades IAM y reglas de expiración.", + "Failed in the last hour": "Fallidos en la última hora", + "Failed since start": "Fallidos desde el inicio", + "Failures and Backlog": "Fallos y pendientes", + "Missing on {sites}": "Falta en {sites}", + "No resync has been started for this site.": "No se ha iniciado ninguna resincronización para este sitio.", + "Object counters come from the node that answered this request and reset when it restarts.": "Los contadores de objetos provienen del nodo que respondió a esta solicitud y se reinician cuando este se reinicia.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Este servidor no informa la accesibilidad de los pares, por lo que no se puede confirmar el estado de la replicación.", + "Queued now": "En cola ahora", + "Queued peak": "Pico en cola", + "Refreshes every 30 seconds": "Se actualiza cada 30 segundos", + "Replication Health": "Estado de la replicación", + "Resync Status": "Estado de resincronización", + "Site {site} is unreachable, so nothing replicates to it.": "El sitio {site} no es accesible, por lo que no se replica nada hacia él.", + "Sync Setting": "Ajuste de sincronización", + "and {count} more": "y {count} más", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} buckets, entidades IAM o reglas de expiración difieren entre los sitios.", + "{count} object replication failures in the last hour.": "{count} fallos de replicación de objetos en la última hora.", + "{operation} has not finished.": "{operation} no ha terminado.", + "{operation} has not finished; waiting for {sites}.": "{operation} no ha terminado; esperando a {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} cambios de configuración esperan ser entregados a los sitios pares; {failed} fallaron.", + "{pending} waiting, {failed} failed": "{pending} en espera, {failed} fallidos", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed} de {total} buckets completados, {failed} fallidos. {objects} objetos replicados, {failedObjects} fallidos.", + "Site removal": "Eliminación del sitio", + "Endpoint update": "Actualización del endpoint", + "Group": "Grupo", + "ILM Expiry Rule": "Regla de expiración ILM", + "Marked for deletion": "Marcado para eliminación", + "Quota": "Cuota", + "CORS": "CORS", + "Policy document": "Documento de política", + "Service account rotation": "Rotación de la cuenta de servicio" } diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 80d72f17..26ff8034 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -2241,5 +2241,45 @@ "Operations": "Opérations", "Resources": "Ressources", "Bucket size distribution": "Répartition de la taille des buckets", - "Other buckets": "Autres buckets" + "Other buckets": "Autres buckets", + "Active workers": "Workers actifs", + "Configuration deliveries": "Livraisons de configuration", + "Connection": "Connexion", + "Consistency": "Cohérence", + "Consistency cannot be checked until every peer site is reachable.": "La cohérence ne peut pas être vérifiée tant que tous les sites pairs ne sont pas joignables.", + "Differs: {settings}": "Diffère : {settings}", + "Downtime": "Durée d'indisponibilité", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Tous les sites pairs sont joignables et rien n'attend d'être répliqué ou réparé.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Tous les sites contiennent les mêmes buckets, entités IAM et règles d'expiration.", + "Failed in the last hour": "Échecs au cours de la dernière heure", + "Failed since start": "Échecs depuis le démarrage", + "Failures and Backlog": "Échecs et file d'attente", + "Missing on {sites}": "Absent sur {sites}", + "No resync has been started for this site.": "Aucune resynchronisation n'a été lancée pour ce site.", + "Object counters come from the node that answered this request and reset when it restarts.": "Les compteurs d'objets proviennent du nœud qui a répondu à cette requête et sont remis à zéro à son redémarrage.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Ce serveur ne signale pas la joignabilité des pairs ; l'état de la réplication ne peut donc pas être confirmé.", + "Queued now": "En file actuellement", + "Queued peak": "Pic de file", + "Refreshes every 30 seconds": "Actualisé toutes les 30 secondes", + "Replication Health": "État de la réplication", + "Resync Status": "État de la resynchronisation", + "Site {site} is unreachable, so nothing replicates to it.": "Le site {site} est injoignable ; rien n'y est répliqué.", + "Sync Setting": "Paramètre de synchronisation", + "and {count} more": "et {count} de plus", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} buckets, entités IAM ou règles d'expiration diffèrent entre les sites.", + "{count} object replication failures in the last hour.": "{count} échecs de réplication d'objets au cours de la dernière heure.", + "{operation} has not finished.": "{operation} n'est pas terminée.", + "{operation} has not finished; waiting for {sites}.": "{operation} n'est pas terminée ; en attente de {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} modifications de configuration attendent d'être livrées aux sites pairs ; {failed} ont échoué.", + "{pending} waiting, {failed} failed": "{pending} en attente, {failed} en échec", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state} : {completed} buckets sur {total} terminés, {failed} en échec. {objects} objets répliqués, {failedObjects} en échec.", + "Site removal": "Suppression du site", + "Endpoint update": "Mise à jour du point de terminaison", + "Group": "Groupe", + "ILM Expiry Rule": "Règle d'expiration ILM", + "Marked for deletion": "Marqué pour suppression", + "Quota": "Quota", + "CORS": "CORS", + "Policy document": "Document de stratégie", + "Service account rotation": "Rotation du compte de service" } diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index 141b8939..40e50b33 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -2241,5 +2241,45 @@ "Operations": "Operasi", "Resources": "Sumber daya", "Bucket size distribution": "Distribusi ukuran bucket", - "Other buckets": "Bucket lainnya" + "Other buckets": "Bucket lainnya", + "Active workers": "Worker aktif", + "Configuration deliveries": "Pengiriman konfigurasi", + "Connection": "Koneksi", + "Consistency": "Konsistensi", + "Consistency cannot be checked until every peer site is reachable.": "Konsistensi tidak dapat diperiksa sampai semua situs peer dapat dijangkau.", + "Differs: {settings}": "Berbeda: {settings}", + "Downtime": "Waktu henti", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Semua situs peer dapat dijangkau dan tidak ada yang menunggu untuk direplikasi atau diperbaiki.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Semua situs memiliki bucket, entitas IAM, dan aturan kedaluwarsa yang sama.", + "Failed in the last hour": "Gagal dalam satu jam terakhir", + "Failed since start": "Gagal sejak mulai", + "Failures and Backlog": "Kegagalan dan antrean", + "Missing on {sites}": "Tidak ada di {sites}", + "No resync has been started for this site.": "Belum ada sinkronisasi ulang yang dimulai untuk situs ini.", + "Object counters come from the node that answered this request and reset when it restarts.": "Penghitung objek berasal dari node yang menjawab permintaan ini dan direset saat node dimulai ulang.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Server ini tidak melaporkan keterjangkauan peer, sehingga kesehatan replikasi tidak dapat dipastikan.", + "Queued now": "Antre saat ini", + "Queued peak": "Puncak antrean", + "Refreshes every 30 seconds": "Diperbarui setiap 30 detik", + "Replication Health": "Kesehatan replikasi", + "Resync Status": "Status sinkronisasi ulang", + "Site {site} is unreachable, so nothing replicates to it.": "Situs {site} tidak dapat dijangkau, sehingga tidak ada yang direplikasi ke sana.", + "Sync Setting": "Pengaturan sinkronisasi", + "and {count} more": "dan {count} lainnya", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} bucket, entitas IAM, atau aturan kedaluwarsa berbeda antar situs.", + "{count} object replication failures in the last hour.": "{count} kegagalan replikasi objek dalam satu jam terakhir.", + "{operation} has not finished.": "{operation} belum selesai.", + "{operation} has not finished; waiting for {sites}.": "{operation} belum selesai; menunggu {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} perubahan konfigurasi menunggu dikirim ke situs peer, {failed} gagal.", + "{pending} waiting, {failed} failed": "{pending} menunggu, {failed} gagal", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed} dari {total} bucket selesai, {failed} gagal. {objects} objek direplikasi, {failedObjects} gagal.", + "Site removal": "Penghapusan situs", + "Endpoint update": "Pembaruan endpoint", + "Group": "Grup", + "ILM Expiry Rule": "Aturan kedaluwarsa ILM", + "Marked for deletion": "Ditandai untuk dihapus", + "Quota": "Kuota", + "CORS": "CORS", + "Policy document": "Dokumen kebijakan", + "Service account rotation": "Rotasi akun layanan" } diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index dad754da..89f2100e 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -2241,5 +2241,45 @@ "Operations": "Operazioni", "Resources": "Risorse", "Bucket size distribution": "Distribuzione delle dimensioni dei bucket", - "Other buckets": "Altri bucket" + "Other buckets": "Altri bucket", + "Active workers": "Worker attivi", + "Configuration deliveries": "Consegne di configurazione", + "Connection": "Connessione", + "Consistency": "Coerenza", + "Consistency cannot be checked until every peer site is reachable.": "La coerenza non può essere verificata finché tutti i siti peer non sono raggiungibili.", + "Differs: {settings}": "Diverso: {settings}", + "Downtime": "Tempo di inattività", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Tutti i siti peer sono raggiungibili e nulla è in attesa di replica o riparazione.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Tutti i siti contengono gli stessi bucket, entità IAM e regole di scadenza.", + "Failed in the last hour": "Non riusciti nell'ultima ora", + "Failed since start": "Non riusciti dall'avvio", + "Failures and Backlog": "Errori e arretrato", + "Missing on {sites}": "Mancante su {sites}", + "No resync has been started for this site.": "Non è stata avviata alcuna risincronizzazione per questo sito.", + "Object counters come from the node that answered this request and reset when it restarts.": "I contatori degli oggetti provengono dal nodo che ha risposto a questa richiesta e si azzerano al suo riavvio.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Questo server non segnala la raggiungibilità dei peer, quindi lo stato della replica non può essere confermato.", + "Queued now": "In coda ora", + "Queued peak": "Picco in coda", + "Refreshes every 30 seconds": "Aggiornamento ogni 30 secondi", + "Replication Health": "Stato della replica", + "Resync Status": "Stato della risincronizzazione", + "Site {site} is unreachable, so nothing replicates to it.": "Il sito {site} non è raggiungibile, quindi non viene replicato nulla verso di esso.", + "Sync Setting": "Impostazione di sincronizzazione", + "and {count} more": "e altri {count}", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} bucket, entità IAM o regole di scadenza differiscono tra i siti.", + "{count} object replication failures in the last hour.": "{count} errori di replica degli oggetti nell'ultima ora.", + "{operation} has not finished.": "{operation} non è terminata.", + "{operation} has not finished; waiting for {sites}.": "{operation} non è terminata; in attesa di {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} modifiche di configurazione attendono la consegna ai siti peer, {failed} non riuscite.", + "{pending} waiting, {failed} failed": "{pending} in attesa, {failed} non riusciti", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed} di {total} bucket completati, {failed} non riusciti. {objects} oggetti replicati, {failedObjects} non riusciti.", + "Site removal": "Rimozione del sito", + "Endpoint update": "Aggiornamento dell'endpoint", + "Group": "Gruppo", + "ILM Expiry Rule": "Regola di scadenza ILM", + "Marked for deletion": "Contrassegnato per l'eliminazione", + "Quota": "Quota", + "CORS": "CORS", + "Policy document": "Documento della policy", + "Service account rotation": "Rotazione dell'account di servizio" } diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index 7ee516f2..8e3b6f25 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -2241,5 +2241,45 @@ "Operations": "運用", "Resources": "リソース", "Bucket size distribution": "バケット容量の内訳", - "Other buckets": "その他のバケット" + "Other buckets": "その他のバケット", + "Active workers": "アクティブなワーカー", + "Configuration deliveries": "設定の配信", + "Connection": "接続", + "Consistency": "整合性", + "Consistency cannot be checked until every peer site is reachable.": "すべてのピアサイトに到達できるまで整合性を確認できません。", + "Differs: {settings}": "相違: {settings}", + "Downtime": "ダウンタイム", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "すべてのピアサイトに到達でき、レプリケーションや修復を待つものはありません。", + "Every site holds the same buckets, IAM entities, and expiry rules.": "すべてのサイトが同じバケット、IAM エンティティ、有効期限ルールを保持しています。", + "Failed in the last hour": "直近 1 時間の失敗", + "Failed since start": "起動以降の失敗", + "Failures and Backlog": "失敗とバックログ", + "Missing on {sites}": "{sites} に存在しません", + "No resync has been started for this site.": "このサイトの再同期はまだ開始されていません。", + "Object counters come from the node that answered this request and reset when it restarts.": "オブジェクトのカウンターはこのリクエストに応答したノードのもので、ノードの再起動時にリセットされます。", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "このサーバーはピアへの到達性を報告しないため、レプリケーションの健全性を確認できません。", + "Queued now": "現在のキュー", + "Queued peak": "キューのピーク", + "Refreshes every 30 seconds": "30 秒ごとに更新", + "Replication Health": "レプリケーションの健全性", + "Resync Status": "再同期の状態", + "Site {site} is unreachable, so nothing replicates to it.": "サイト {site} に到達できないため、何もレプリケートされません。", + "Sync Setting": "同期設定", + "and {count} more": "ほか {count} 件", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} 件のバケット、IAM エンティティ、有効期限ルールがサイト間で異なります。", + "{count} object replication failures in the last hour.": "直近 1 時間にオブジェクトのレプリケーションが {count} 件失敗しました。", + "{operation} has not finished.": "{operation} が完了していません。", + "{operation} has not finished; waiting for {sites}.": "{operation} が完了していません。{sites} を待っています。", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} 件の設定変更がピアサイトへの配信を待っており、{failed} 件が失敗しました。", + "{pending} waiting, {failed} failed": "待機中 {pending} 件、失敗 {failed} 件", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {total} 個中 {completed} 個のバケットが完了、{failed} 個が失敗。{objects} 個のオブジェクトをレプリケート、{failedObjects} 個が失敗。", + "Site removal": "サイトの削除", + "Endpoint update": "エンドポイントの更新", + "Group": "グループ", + "ILM Expiry Rule": "ILM 有効期限ルール", + "Marked for deletion": "削除予定", + "Quota": "クォータ", + "CORS": "CORS", + "Policy document": "ポリシードキュメント", + "Service account rotation": "サービスアカウントのローテーション" } diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index 239c7d0b..e1d462ac 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -2241,5 +2241,45 @@ "Operations": "운영", "Resources": "리소스", "Bucket size distribution": "버킷 용량 비율", - "Other buckets": "기타 버킷" + "Other buckets": "기타 버킷", + "Active workers": "활성 워커", + "Configuration deliveries": "구성 전달", + "Connection": "연결", + "Consistency": "일관성", + "Consistency cannot be checked until every peer site is reachable.": "모든 피어 사이트에 연결할 수 있어야 일관성을 확인할 수 있습니다.", + "Differs: {settings}": "차이: {settings}", + "Downtime": "중단 시간", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "모든 피어 사이트에 연결할 수 있으며 복제나 복구를 기다리는 항목이 없습니다.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "모든 사이트가 동일한 버킷, IAM 엔터티, 만료 규칙을 가지고 있습니다.", + "Failed in the last hour": "최근 1시간 실패", + "Failed since start": "시작 이후 실패", + "Failures and Backlog": "실패 및 적체", + "Missing on {sites}": "{sites}에 없음", + "No resync has been started for this site.": "이 사이트에 대해 시작된 재동기화가 없습니다.", + "Object counters come from the node that answered this request and reset when it restarts.": "객체 카운터는 이 요청에 응답한 노드의 값이며 해당 노드가 다시 시작되면 초기화됩니다.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "이 서버는 피어 연결 상태를 보고하지 않으므로 복제 상태를 확인할 수 없습니다.", + "Queued now": "현재 대기열", + "Queued peak": "대기열 최대치", + "Refreshes every 30 seconds": "30초마다 새로 고침", + "Replication Health": "복제 상태", + "Resync Status": "재동기화 상태", + "Site {site} is unreachable, so nothing replicates to it.": "사이트 {site}에 연결할 수 없어 아무것도 복제되지 않습니다.", + "Sync Setting": "동기화 설정", + "and {count} more": "외 {count}개", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count}개의 버킷, IAM 엔터티 또는 만료 규칙이 사이트 간에 다릅니다.", + "{count} object replication failures in the last hour.": "최근 1시간 동안 객체 복제가 {count}건 실패했습니다.", + "{operation} has not finished.": "{operation}이(가) 완료되지 않았습니다.", + "{operation} has not finished; waiting for {sites}.": "{operation}이(가) 완료되지 않았습니다. {sites}을(를) 기다리는 중입니다.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending}개의 구성 변경이 피어 사이트로 전달을 기다리고 있으며 {failed}개가 실패했습니다.", + "{pending} waiting, {failed} failed": "대기 {pending}개, 실패 {failed}개", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: 버킷 {total}개 중 {completed}개 완료, {failed}개 실패. 객체 {objects}개 복제, {failedObjects}개 실패.", + "Site removal": "사이트 제거", + "Endpoint update": "엔드포인트 업데이트", + "Group": "그룹", + "ILM Expiry Rule": "ILM 만료 규칙", + "Marked for deletion": "삭제 표시됨", + "Quota": "할당량", + "CORS": "CORS", + "Policy document": "정책 문서", + "Service account rotation": "서비스 계정 교체" } diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index e03b09be..6ac31b56 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -2241,5 +2241,45 @@ "Operations": "Operações", "Resources": "Recursos", "Bucket size distribution": "Distribuição do tamanho dos buckets", - "Other buckets": "Outros buckets" + "Other buckets": "Outros buckets", + "Active workers": "Workers ativos", + "Configuration deliveries": "Entregas de configuração", + "Connection": "Conexão", + "Consistency": "Consistência", + "Consistency cannot be checked until every peer site is reachable.": "A consistência só pode ser verificada quando todos os sites pares estiverem acessíveis.", + "Differs: {settings}": "Difere: {settings}", + "Downtime": "Tempo de inatividade", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Todos os sites pares estão acessíveis e nada aguarda replicação ou reparo.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Todos os sites têm os mesmos buckets, entidades IAM e regras de expiração.", + "Failed in the last hour": "Falhas na última hora", + "Failed since start": "Falhas desde o início", + "Failures and Backlog": "Falhas e pendências", + "Missing on {sites}": "Ausente em {sites}", + "No resync has been started for this site.": "Nenhuma ressincronização foi iniciada para este site.", + "Object counters come from the node that answered this request and reset when it restarts.": "Os contadores de objetos vêm do nó que respondeu a esta solicitação e são zerados quando ele reinicia.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Este servidor não informa a acessibilidade dos pares, portanto a saúde da replicação não pode ser confirmada.", + "Queued now": "Na fila agora", + "Queued peak": "Pico na fila", + "Refreshes every 30 seconds": "Atualiza a cada 30 segundos", + "Replication Health": "Saúde da replicação", + "Resync Status": "Status da ressincronização", + "Site {site} is unreachable, so nothing replicates to it.": "O site {site} está inacessível, então nada é replicado para ele.", + "Sync Setting": "Configuração de sincronização", + "and {count} more": "e mais {count}", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} buckets, entidades IAM ou regras de expiração diferem entre os sites.", + "{count} object replication failures in the last hour.": "{count} falhas de replicação de objetos na última hora.", + "{operation} has not finished.": "{operation} não foi concluída.", + "{operation} has not finished; waiting for {sites}.": "{operation} não foi concluída; aguardando {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} alterações de configuração aguardam entrega aos sites pares; {failed} falharam.", + "{pending} waiting, {failed} failed": "{pending} aguardando, {failed} com falha", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed} de {total} buckets concluídos, {failed} com falha. {objects} objetos replicados, {failedObjects} com falha.", + "Site removal": "Remoção do site", + "Endpoint update": "Atualização do endpoint", + "Group": "Grupo", + "ILM Expiry Rule": "Regra de expiração ILM", + "Marked for deletion": "Marcado para exclusão", + "Quota": "Cota", + "CORS": "CORS", + "Policy document": "Documento da política", + "Service account rotation": "Rotação da conta de serviço" } diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index 221e475e..16e56d35 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -2241,5 +2241,45 @@ "Operations": "Операции", "Resources": "Ресурсы", "Bucket size distribution": "Распределение объёма бакетов", - "Other buckets": "Другие бакеты" + "Other buckets": "Другие бакеты", + "Active workers": "Активные обработчики", + "Configuration deliveries": "Доставка конфигурации", + "Connection": "Подключение", + "Consistency": "Согласованность", + "Consistency cannot be checked until every peer site is reachable.": "Согласованность нельзя проверить, пока все одноранговые сайты не станут доступны.", + "Differs: {settings}": "Отличается: {settings}", + "Downtime": "Время простоя", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Все одноранговые сайты доступны, и ничто не ожидает репликации или восстановления.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Все сайты содержат одинаковые бакеты, сущности IAM и правила истечения срока.", + "Failed in the last hour": "Сбоев за последний час", + "Failed since start": "Сбоев с момента запуска", + "Failures and Backlog": "Сбои и очередь", + "Missing on {sites}": "Отсутствует на {sites}", + "No resync has been started for this site.": "Для этого сайта повторная синхронизация не запускалась.", + "Object counters come from the node that answered this request and reset when it restarts.": "Счётчики объектов берутся с узла, ответившего на этот запрос, и сбрасываются при его перезапуске.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Этот сервер не сообщает о доступности одноранговых узлов, поэтому состояние репликации подтвердить нельзя.", + "Queued now": "Сейчас в очереди", + "Queued peak": "Пик очереди", + "Refreshes every 30 seconds": "Обновляется каждые 30 секунд", + "Replication Health": "Состояние репликации", + "Resync Status": "Статус повторной синхронизации", + "Site {site} is unreachable, so nothing replicates to it.": "Сайт {site} недоступен, поэтому на него ничего не реплицируется.", + "Sync Setting": "Настройка синхронизации", + "and {count} more": "и ещё {count}", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} бакетов, сущностей IAM или правил истечения срока различаются между сайтами.", + "{count} object replication failures in the last hour.": "{count} сбоев репликации объектов за последний час.", + "{operation} has not finished.": "{operation} не завершена.", + "{operation} has not finished; waiting for {sites}.": "{operation} не завершена; ожидание {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} изменений конфигурации ожидают доставки на одноранговые сайты, {failed} завершились сбоем.", + "{pending} waiting, {failed} failed": "{pending} ожидают, {failed} со сбоем", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: завершено {completed} из {total} бакетов, {failed} со сбоем. Реплицировано объектов: {objects}, со сбоем: {failedObjects}.", + "Site removal": "Удаление сайта", + "Endpoint update": "Обновление конечной точки", + "Group": "Группа", + "ILM Expiry Rule": "Правило истечения ILM", + "Marked for deletion": "Помечен на удаление", + "Quota": "Квота", + "CORS": "CORS", + "Policy document": "Документ политики", + "Service account rotation": "Смена сервисной учётной записи" } diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index 96bac012..6dfe3504 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -2241,5 +2241,45 @@ "Operations": "İşlemler", "Resources": "Kaynaklar", "Bucket size distribution": "Kova boyutu dağılımı", - "Other buckets": "Diğer kovalar" + "Other buckets": "Diğer kovalar", + "Active workers": "Etkin çalışanlar", + "Configuration deliveries": "Yapılandırma teslimatları", + "Connection": "Bağlantı", + "Consistency": "Tutarlılık", + "Consistency cannot be checked until every peer site is reachable.": "Tüm eş sitelere ulaşılabilir olana kadar tutarlılık denetlenemez.", + "Differs: {settings}": "Farklı: {settings}", + "Downtime": "Kesinti süresi", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Tüm eş sitelere ulaşılabiliyor ve çoğaltılmayı ya da onarılmayı bekleyen bir şey yok.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Tüm siteler aynı bucket'ları, IAM varlıklarını ve süre sonu kurallarını içeriyor.", + "Failed in the last hour": "Son bir saatte başarısız", + "Failed since start": "Başlangıçtan beri başarısız", + "Failures and Backlog": "Hatalar ve birikim", + "Missing on {sites}": "{sites} üzerinde eksik", + "No resync has been started for this site.": "Bu site için yeniden eşitleme başlatılmadı.", + "Object counters come from the node that answered this request and reset when it restarts.": "Nesne sayaçları bu isteği yanıtlayan düğümden gelir ve düğüm yeniden başlatıldığında sıfırlanır.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Bu sunucu eşlere erişilebilirliği bildirmiyor, bu yüzden çoğaltma durumu doğrulanamıyor.", + "Queued now": "Şu an kuyrukta", + "Queued peak": "Kuyruk zirvesi", + "Refreshes every 30 seconds": "30 saniyede bir yenilenir", + "Replication Health": "Çoğaltma durumu", + "Resync Status": "Yeniden eşitleme durumu", + "Site {site} is unreachable, so nothing replicates to it.": "{site} sitesine ulaşılamıyor, bu yüzden oraya hiçbir şey çoğaltılmıyor.", + "Sync Setting": "Eşitleme ayarı", + "and {count} more": "ve {count} tane daha", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} bucket, IAM varlığı veya süre sonu kuralı siteler arasında farklı.", + "{count} object replication failures in the last hour.": "Son bir saatte {count} nesne çoğaltma hatası.", + "{operation} has not finished.": "{operation} tamamlanmadı.", + "{operation} has not finished; waiting for {sites}.": "{operation} tamamlanmadı; {sites} bekleniyor.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} yapılandırma değişikliği eş sitelere teslim edilmeyi bekliyor, {failed} başarısız.", + "{pending} waiting, {failed} failed": "{pending} bekliyor, {failed} başarısız", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {total} bucket'tan {completed} tamamlandı, {failed} başarısız. {objects} nesne çoğaltıldı, {failedObjects} başarısız.", + "Site removal": "Site kaldırma", + "Endpoint update": "Uç nokta güncellemesi", + "Group": "Grup", + "ILM Expiry Rule": "ILM süre sonu kuralı", + "Marked for deletion": "Silinmek üzere işaretli", + "Quota": "Kota", + "CORS": "CORS", + "Policy document": "İlke belgesi", + "Service account rotation": "Hizmet hesabı döndürme" } diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index 1c16b3cb..4ad23a25 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -2241,5 +2241,45 @@ "Operations": "Vận hành", "Resources": "Tài nguyên", "Bucket size distribution": "Phân bố dung lượng bucket", - "Other buckets": "Các bucket khác" + "Other buckets": "Các bucket khác", + "Active workers": "Worker đang hoạt động", + "Configuration deliveries": "Phân phối cấu hình", + "Connection": "Kết nối", + "Consistency": "Tính nhất quán", + "Consistency cannot be checked until every peer site is reachable.": "Không thể kiểm tra tính nhất quán cho đến khi mọi site ngang hàng đều truy cập được.", + "Differs: {settings}": "Khác nhau: {settings}", + "Downtime": "Thời gian ngừng", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "Mọi site ngang hàng đều truy cập được và không có gì đang chờ sao chép hoặc sửa chữa.", + "Every site holds the same buckets, IAM entities, and expiry rules.": "Mọi site đều có cùng bucket, thực thể IAM và quy tắc hết hạn.", + "Failed in the last hour": "Thất bại trong giờ qua", + "Failed since start": "Thất bại kể từ khi khởi động", + "Failures and Backlog": "Lỗi và tồn đọng", + "Missing on {sites}": "Thiếu trên {sites}", + "No resync has been started for this site.": "Chưa có lần đồng bộ lại nào được bắt đầu cho site này.", + "Object counters come from the node that answered this request and reset when it restarts.": "Bộ đếm đối tượng lấy từ nút đã trả lời yêu cầu này và được đặt lại khi nút khởi động lại.", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "Máy chủ này không báo cáo khả năng truy cập peer nên không thể xác nhận tình trạng sao chép.", + "Queued now": "Đang xếp hàng", + "Queued peak": "Đỉnh hàng đợi", + "Refreshes every 30 seconds": "Làm mới mỗi 30 giây", + "Replication Health": "Tình trạng sao chép", + "Resync Status": "Trạng thái đồng bộ lại", + "Site {site} is unreachable, so nothing replicates to it.": "Không truy cập được site {site} nên không có gì được sao chép tới đó.", + "Sync Setting": "Cài đặt đồng bộ", + "and {count} more": "và {count} mục khác", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "{count} bucket, thực thể IAM hoặc quy tắc hết hạn khác nhau giữa các site.", + "{count} object replication failures in the last hour.": "{count} lỗi sao chép đối tượng trong giờ qua.", + "{operation} has not finished.": "{operation} chưa hoàn tất.", + "{operation} has not finished; waiting for {sites}.": "{operation} chưa hoàn tất; đang chờ {sites}.", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "{pending} thay đổi cấu hình đang chờ gửi tới các site ngang hàng, {failed} thất bại.", + "{pending} waiting, {failed} failed": "{pending} đang chờ, {failed} thất bại", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}: {completed}/{total} bucket hoàn tất, {failed} thất bại. Đã sao chép {objects} đối tượng, {failedObjects} thất bại.", + "Site removal": "Gỡ site", + "Endpoint update": "Cập nhật endpoint", + "Group": "Nhóm", + "ILM Expiry Rule": "Quy tắc hết hạn ILM", + "Marked for deletion": "Đã đánh dấu xóa", + "Quota": "Hạn mức", + "CORS": "CORS", + "Policy document": "Tài liệu chính sách", + "Service account rotation": "Xoay vòng tài khoản dịch vụ" } diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index c4b932e2..b7b1983c 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -2241,5 +2241,45 @@ "Operations": "运行管理", "Resources": "资源", "Bucket size distribution": "桶容量占比", - "Other buckets": "其他桶" + "Other buckets": "其他桶", + "Active workers": "活跃工作线程", + "Configuration deliveries": "配置下发", + "Connection": "连接状态", + "Consistency": "一致性", + "Consistency cannot be checked until every peer site is reachable.": "需等所有对等站点都可达后才能检查一致性。", + "Differs: {settings}": "不一致:{settings}", + "Downtime": "累计离线时长", + "Every peer site is reachable and nothing is waiting to be replicated or repaired.": "所有对等站点均可达,没有等待复制或修复的内容。", + "Every site holds the same buckets, IAM entities, and expiry rules.": "各站点的存储桶、IAM 实体和过期规则完全一致。", + "Failed in the last hour": "近 1 小时失败", + "Failed since start": "启动以来失败", + "Failures and Backlog": "失败与积压", + "Missing on {sites}": "{sites} 缺失", + "No resync has been started for this site.": "该站点尚未发起过重同步。", + "Object counters come from the node that answered this request and reset when it restarts.": "对象计数来自响应本次请求的节点,该节点重启后清零。", + "Peer reachability is not reported by this server, so replication health cannot be confirmed.": "该服务器未上报对等站点的可达性,无法确认复制是否健康。", + "Queued now": "当前排队", + "Queued peak": "排队峰值", + "Refreshes every 30 seconds": "每 30 秒自动刷新", + "Replication Health": "复制健康状态", + "Resync Status": "重同步进度", + "Site {site} is unreachable, so nothing replicates to it.": "站点 {site} 不可达,任何内容都无法复制到该站点。", + "Sync Setting": "同步开关", + "and {count} more": "以及另外 {count} 项", + "{count} buckets, IAM entities, or expiry rules differ between sites.": "有 {count} 个存储桶、IAM 实体或过期规则在各站点间不一致。", + "{count} object replication failures in the last hour.": "近 1 小时有 {count} 次对象复制失败。", + "{operation} has not finished.": "{operation}尚未完成。", + "{operation} has not finished; waiting for {sites}.": "{operation}尚未完成,正在等待 {sites} 确认。", + "{pending} configuration changes are waiting to be delivered to peer sites, {failed} failed.": "有 {pending} 项配置变更等待下发到对等站点,{failed} 项失败。", + "{pending} waiting, {failed} failed": "等待 {pending},失败 {failed}", + "{state}: {completed} of {total} buckets completed, {failed} failed. {objects} objects replicated, {failedObjects} failed.": "{state}:{total} 个存储桶中已完成 {completed} 个,失败 {failed} 个;已复制 {objects} 个对象,失败 {failedObjects} 个。", + "Site removal": "站点移除", + "Endpoint update": "终端节点变更", + "Group": "用户组", + "ILM Expiry Rule": "ILM 过期规则", + "Marked for deletion": "已标记删除", + "Quota": "配额", + "CORS": "CORS", + "Policy document": "策略内容", + "Service account rotation": "服务账户轮换" } diff --git a/lib/functions.ts b/lib/functions.ts index 0ea77e8c..13386cbf 100644 --- a/lib/functions.ts +++ b/lib/functions.ts @@ -33,6 +33,21 @@ export function formatInteger(value?: number | null, fallback = "--"): string { return new Intl.NumberFormat(getRuntimeLocale(), { maximumFractionDigits: 0 }).format(value) } +/** Whole days, hours, and minutes, at most two units, e.g. "2 Days 3 Hours". */ +export function formatDuration(seconds: number | undefined, t: (key: string) => string): string { + if (seconds === undefined || !Number.isFinite(seconds) || seconds < 0) return t("Unknown") + const totalMinutes = Math.floor(seconds / 60) + const days = Math.floor(totalMinutes / 1440) + const hours = Math.floor((totalMinutes % 1440) / 60) + const minutes = totalMinutes % 60 + const parts = [ + days ? `${days} ${t("Days")}` : "", + hours ? `${hours} ${t("Hours")}` : "", + !days && minutes ? `${minutes} ${t("Minutes")}` : "", + ].filter(Boolean) + return parts.slice(0, 2).join(" ") || `0 ${t("Minutes")}` +} + function parseDateTime(value?: string | number | Date | null): Date | null { if (value == null || value === "") return null diff --git a/lib/site-replication-health.ts b/lib/site-replication-health.ts new file mode 100644 index 00000000..b33f5390 --- /dev/null +++ b/lib/site-replication-health.ts @@ -0,0 +1,238 @@ +import type { + SiteReplicationBucketStats, + SiteReplicationInfo, + SiteReplicationPendingOperation, + SiteReplicationRetryStats, + SiteReplicationStatus, + SiteReplicationTimedErrors, + SiteReplicationVolume, +} from "@/types/site-replication" + +export type SiteReplicationHealthLevel = "healthy" | "degraded" | "critical" | "unknown" + +export type SiteReplicationConnectionState = "online" | "offline" | "unknown" + +export interface SiteReplicationConnection { + state: SiteReplicationConnectionState + lastOnline?: string + downtimeNs: number + error?: string +} + +export type SiteReplicationHealthIssue = + | { kind: "peer-offline"; deploymentId: string; error?: string } + | { kind: "pending-operation"; operation: SiteReplicationPendingOperation } + | { kind: "retry-backlog"; retryStats: SiteReplicationRetryStats } + | { kind: "recent-failures"; count: number } + | { kind: "drift"; count: number } + +export type SiteReplicationDriftEntity = "bucket" | "policy" | "user" | "group" | "ilmExpiryRule" + +export type SiteReplicationBucketMismatch = + | "markedDeleted" + | "tags" + | "versioning" + | "objectLock" + | "policy" + | "encryption" + | "replication" + | "quota" + | "cors" + +export interface SiteReplicationDriftItem { + entity: SiteReplicationDriftEntity + name: string + /** Deployment IDs of the sites that do not hold the entity. */ + missingOn: string[] + /** Settings that differ between sites; `policy` entities report "content". */ + mismatches: (SiteReplicationBucketMismatch | "content")[] +} + +export interface SiteReplicationHealth { + level: SiteReplicationHealthLevel + issues: SiteReplicationHealthIssue[] + connections: Record + /** Null when an unreachable peer makes the comparison meaningless. */ + drift: SiteReplicationDriftItem[] | null +} + +const BUCKET_MISMATCH_FLAGS: [keyof SiteReplicationBucketStats, SiteReplicationBucketMismatch][] = [ + ["bucketMarkedDeleted", "markedDeleted"], + ["tagMismatch", "tags"], + ["versioningConfigMismatch", "versioning"], + ["objectLockConfigMismatch", "objectLock"], + ["policyMismatch", "policy"], + ["sseConfigMismatch", "encryption"], + ["replicationCfgMismatch", "replication"], + ["quotaCfgMismatch", "quota"], + ["corsCfgMismatch", "cors"], +] + +function resolveConnection( + status: SiteReplicationStatus | null, + deploymentId: string, + isLocal: boolean, +): SiteReplicationConnection { + if (isLocal) { + return { state: "online", downtimeNs: 0 } + } + + const peerError = status?.peerErrors[deploymentId] + const metric = status?.metrics.metrics[deploymentId] + if (peerError || metric?.online === false) { + return { + state: "offline", + lastOnline: metric?.lastOnline, + downtimeNs: metric?.totalDowntimeNs ?? 0, + error: peerError?.error || undefined, + } + } + if (metric) { + return { state: "online", lastOnline: metric.lastOnline, downtimeNs: metric.totalDowntimeNs } + } + + // Servers before rustfs/rustfs#7024 only report the local metric entry. + return { state: "unknown", downtimeNs: 0 } +} + +/** + * Object replication failures counted by the node that answered the status + * request. Only the local metric entry carries them; summing every entry keeps + * the result independent of whether the local site could be identified. + */ +export function siteReplicationFailures(status: SiteReplicationStatus | null): SiteReplicationTimedErrors { + const add = (left: SiteReplicationVolume, right: SiteReplicationVolume) => ({ + count: left.count + right.count, + bytes: left.bytes + right.bytes, + }) + const empty = { count: 0, bytes: 0 } + + return Object.values(status?.metrics.metrics ?? {}).reduce( + (total, metric) => ({ + lastMinute: add(total.lastMinute, metric.failed.lastMinute), + lastHour: add(total.lastHour, metric.failed.lastHour), + totals: add(total.totals, metric.failed.totals), + }), + { lastMinute: empty, lastHour: empty, totals: empty }, + ) +} + +function missingSites(siteIds: string[], holders: Iterable) { + const held = new Set(holders) + return siteIds.filter((deploymentId) => !held.has(deploymentId)) +} + +function collectDrift(status: SiteReplicationStatus, siteIds: string[]): SiteReplicationDriftItem[] { + const drift: SiteReplicationDriftItem[] = [] + + // The server already prunes in-sync buckets and policies from these maps. + for (const [name, perSite] of Object.entries(status.bucketStats)) { + const stats = Object.values(perSite) + const missingOn = missingSites( + siteIds, + Object.entries(perSite) + .filter(([, item]) => item.hasBucket) + .map(([deploymentId]) => deploymentId), + ) + const mismatches = BUCKET_MISMATCH_FLAGS.filter(([flag]) => stats.some((item) => item[flag])).map( + ([, mismatch]) => mismatch, + ) + if (missingOn.length || mismatches.length) { + drift.push({ entity: "bucket", name, missingOn, mismatches }) + } + } + + for (const [name, perSite] of Object.entries(status.policyStats)) { + const missingOn = missingSites( + siteIds, + Object.entries(perSite) + .filter(([, item]) => item.hasPolicy) + .map(([deploymentId]) => deploymentId), + ) + const mismatched = Object.values(perSite).some((item) => item.policyMismatch) + if (missingOn.length || mismatched) { + drift.push({ entity: "policy", name, missingOn, mismatches: mismatched ? ["content"] : [] }) + } + } + + const presence: [SiteReplicationDriftEntity, Record][] = [ + ["user", status.userSites], + ["group", status.groupSites], + ["ilmExpiryRule", status.ilmExpiryRuleSites], + ] + for (const [entity, sitesByName] of presence) { + for (const [name, holders] of Object.entries(sitesByName)) { + const missingOn = missingSites(siteIds, holders) + if (missingOn.length) { + drift.push({ entity, name, missingOn, mismatches: [] }) + } + } + } + + return drift +} + +/** + * Turn the site replication info and status payloads into the answer an + * operator needs first: is replication working, and if not, why. + * + * `critical` means a peer cannot be reached, so nothing replicates to it. + * `degraded` means replication runs but something needs attention: an + * unfinished lifecycle operation, failing peer deliveries, object + * replication failures in the last hour, or configuration drift. + */ +export function assessSiteReplicationHealth(input: { + info: SiteReplicationInfo | null + status: SiteReplicationStatus | null + localDeploymentId: string +}): SiteReplicationHealth { + const { info, status, localDeploymentId } = input + const siteIds = (info?.sites ?? []).map((peer) => peer.deploymentId).filter(Boolean) + const connections = Object.fromEntries( + siteIds.map((deploymentId) => [ + deploymentId, + resolveConnection(status, deploymentId, deploymentId === localDeploymentId), + ]), + ) + + const issues: SiteReplicationHealthIssue[] = [] + for (const [deploymentId, connection] of Object.entries(connections)) { + if (connection.state === "offline") { + issues.push({ kind: "peer-offline", deploymentId, error: connection.error }) + } + } + + const pendingOperation = status?.pendingOperation ?? info?.pendingOperation + if (pendingOperation) { + issues.push({ kind: "pending-operation", operation: pendingOperation }) + } + + const retryStats = status?.retryStats ?? info?.retryStats + if (retryStats && (retryStats.pending > 0 || retryStats.failed > 0)) { + issues.push({ kind: "retry-backlog", retryStats }) + } + + const recentFailures = siteReplicationFailures(status).lastHour.count + if (recentFailures > 0) { + issues.push({ kind: "recent-failures", count: recentFailures }) + } + + // An unreachable peer reports empty metadata, which would flag every + // bucket and user as missing there. + const allPeersReachable = Object.values(connections).every((connection) => connection.state === "online") + const drift = status && allPeersReachable ? collectDrift(status, siteIds) : null + if (drift?.length) { + issues.push({ kind: "drift", count: drift.length }) + } + + let level: SiteReplicationHealthLevel + if (issues.some((issue) => issue.kind === "peer-offline")) { + level = "critical" + } else if (issues.length) { + level = "degraded" + } else { + level = status && allPeersReachable ? "healthy" : "unknown" + } + + return { level, issues, connections, drift } +} diff --git a/tests/lib/functions.test.ts b/tests/lib/functions.test.ts index ebec5ca3..c266a268 100644 --- a/tests/lib/functions.test.ts +++ b/tests/lib/functions.test.ts @@ -107,3 +107,13 @@ test("date formatting preserves half-hour timezone offsets", () => { ["2026-09-24 05:30:00", "2026-09-24 05:30:00 UTC+05:30"], ]) }) + +test("formatDuration keeps the two largest whole units", async () => { + const { formatDuration } = await import("../../lib/functions") + const t = (key: string) => key + + assert.equal(formatDuration(90, t), "1 Minutes") + assert.equal(formatDuration(3 * 86400 + 5 * 3600 + 7 * 60, t), "3 Days 5 Hours") + assert.equal(formatDuration(20, t), "0 Minutes") + assert.equal(formatDuration(-1, t), "Unknown") +}) diff --git a/tests/lib/site-replication-health.test.ts b/tests/lib/site-replication-health.test.ts new file mode 100644 index 00000000..497fc56b --- /dev/null +++ b/tests/lib/site-replication-health.test.ts @@ -0,0 +1,183 @@ +import assert from "node:assert/strict" +import test from "node:test" + +const loadHealth = () => import(new URL("../../lib/site-replication-health.ts", import.meta.url).href) + +const LOCAL = "dep-local" +const PEER = "dep-peer" + +function volume(count = 0) { + return { count, bytes: count * 1024 } +} + +function metric(deploymentId: string, overrides: Record = {}) { + return { + deploymentId, + endpoint: `http://${deploymentId}:9000`, + totalDowntimeNs: 0, + lastOnline: "2026-10-23T10:00:00Z", + online: true, + replicatedSize: 0, + replicatedCount: 0, + failed: { lastMinute: volume(), lastHour: volume(), totals: volume() }, + ...overrides, + } +} + +function info(overrides: Record = {}) { + return { + enabled: true, + name: "site-local", + sites: [ + { deploymentId: LOCAL, name: "site-local", endpoint: "http://local:9000" }, + { deploymentId: PEER, name: "site-peer", endpoint: "http://peer:9000" }, + ], + serviceAccountAccessKey: "site-replicator-0", + ...overrides, + } +} + +function status(overrides: Record = {}, metrics?: Record) { + return { + enabled: true, + peerErrors: {}, + metrics: { + activeWorkers: { curr: 0, avg: 0, max: 0 }, + replicaSize: 0, + replicaCount: 0, + queued: { curr: volume(), avg: volume(), max: volume() }, + metrics: metrics ?? { [LOCAL]: metric(LOCAL), [PEER]: metric(PEER) }, + }, + bucketStats: {}, + policyStats: {}, + userSites: {}, + groupSites: {}, + ilmExpiryRuleSites: {}, + ...overrides, + } +} + +async function assess(input: { info?: unknown; status?: unknown; localDeploymentId?: string }) { + const { assessSiteReplicationHealth } = await loadHealth() + return assessSiteReplicationHealth({ + info: "info" in input ? input.info : info(), + status: "status" in input ? input.status : status(), + localDeploymentId: input.localDeploymentId ?? LOCAL, + }) +} + +test("a reachable group with nothing outstanding is healthy", async () => { + const health = await assess({}) + + assert.equal(health.level, "healthy") + assert.deepEqual(health.issues, []) + assert.deepEqual(health.drift, []) + assert.equal(health.connections[PEER].state, "online") +}) + +test("an offline peer is critical and keeps its last-online time and downtime", async () => { + const offline = metric(PEER, { online: false, totalDowntimeNs: 90e9, lastOnline: "2026-10-23T09:58:30Z" }) + const health = await assess({ status: status({}, { [LOCAL]: metric(LOCAL), [PEER]: offline }) }) + + assert.equal(health.level, "critical") + assert.deepEqual(health.connections[PEER], { + state: "offline", + lastOnline: "2026-10-23T09:58:30Z", + downtimeNs: 90e9, + error: undefined, + }) + assert.deepEqual(health.issues, [{ kind: "peer-offline", deploymentId: PEER, error: undefined }]) +}) + +test("a peer whose metainfo fetch failed is offline with the reported error", async () => { + const peerErrors = { [PEER]: { name: "site-peer", endpoint: "http://peer:9000", error: "connection refused" } } + const health = await assess({ status: status({ peerErrors }, { [LOCAL]: metric(LOCAL) }) }) + + assert.equal(health.connections[PEER].state, "offline") + assert.equal(health.connections[PEER].error, "connection refused") + assert.equal(health.level, "critical") +}) + +test("an unreachable peer suppresses the drift comparison instead of flagging everything missing", async () => { + const bucketStats = { photos: { [LOCAL]: { hasBucket: true }, [PEER]: { hasBucket: false } } } + const offline = metric(PEER, { online: false }) + const health = await assess({ status: status({ bucketStats }, { [LOCAL]: metric(LOCAL), [PEER]: offline }) }) + + assert.equal(health.drift, null) + assert.ok(!health.issues.some((issue: { kind: string }) => issue.kind === "drift")) +}) + +test("peers without a metric entry on older servers are unknown, never reported healthy", async () => { + const health = await assess({ status: status({}, { [LOCAL]: metric(LOCAL) }) }) + + assert.equal(health.connections[PEER].state, "unknown") + assert.equal(health.level, "unknown") + assert.equal(health.drift, null) +}) + +test("an unfinished lifecycle operation degrades health", async () => { + const pendingOperation = { operation: "remove", id: "op-1", pendingPeers: [PEER], ackedPeers: [] } + const health = await assess({ status: status({ pendingOperation }) }) + + assert.equal(health.level, "degraded") + assert.deepEqual(health.issues, [{ kind: "pending-operation", operation: pendingOperation }]) +}) + +test("failing peer deliveries degrade health while an empty retry queue does not", async () => { + const failing = await assess({ status: status({ retryStats: { pending: 2, failed: 1, lastError: "timeout" } }) }) + const idle = await assess({ status: status({ retryStats: { pending: 0, failed: 0, lastError: "" } }) }) + + assert.equal(failing.level, "degraded") + assert.equal(failing.issues[0].kind, "retry-backlog") + assert.equal(idle.level, "healthy") +}) + +test("object replication failures in the last hour degrade health but older ones do not", async () => { + const recent = metric(LOCAL, { failed: { lastMinute: volume(), lastHour: volume(3), totals: volume(3) } }) + const old = metric(LOCAL, { failed: { lastMinute: volume(), lastHour: volume(), totals: volume(7) } }) + + const recentHealth = await assess({ status: status({}, { [LOCAL]: recent, [PEER]: metric(PEER) }) }) + const oldHealth = await assess({ status: status({}, { [LOCAL]: old, [PEER]: metric(PEER) }) }) + + assert.deepEqual(recentHealth.issues, [{ kind: "recent-failures", count: 3 }]) + assert.equal(oldHealth.level, "healthy") +}) + +test("drift lists missing and mismatched buckets, policies, and users per site", async () => { + const bucketStats = { + photos: { + [LOCAL]: { hasBucket: true, policyMismatch: true, tagMismatch: false }, + [PEER]: { hasBucket: true, policyMismatch: true, tagMismatch: false }, + }, + logs: { [LOCAL]: { hasBucket: true } }, + } + const policyStats = { readonly: { [LOCAL]: { hasPolicy: true, policyMismatch: true }, [PEER]: { hasPolicy: true } } } + const userSites = { alice: [LOCAL, PEER], bob: [LOCAL] } + + const health = await assess({ status: status({ bucketStats, policyStats, userSites }) }) + + assert.equal(health.level, "degraded") + assert.deepEqual(health.drift, [ + { entity: "bucket", name: "photos", missingOn: [], mismatches: ["policy"] }, + { entity: "bucket", name: "logs", missingOn: [PEER], mismatches: [] }, + { entity: "policy", name: "readonly", missingOn: [], mismatches: ["content"] }, + { entity: "user", name: "bob", missingOn: [PEER], mismatches: [] }, + ]) + assert.deepEqual(health.issues, [{ kind: "drift", count: 4 }]) +}) + +test("without the status endpoint the info payload still surfaces outstanding work", async () => { + const pendingOperation = { operation: "remove", id: "op-1", pendingPeers: [PEER], ackedPeers: [] } + const withoutWork = await assess({ status: null }) + const withWork = await assess({ status: null, info: info({ pendingOperation }) }) + + assert.equal(withoutWork.level, "unknown") + assert.equal(withWork.level, "degraded") +}) + +test("the local site is online even when its own metric entry is missing", async () => { + const health = await assess({ status: status({}, { [PEER]: metric(PEER) }) }) + + assert.equal(health.connections[LOCAL].state, "online") + assert.equal(health.level, "healthy") +}) diff --git a/types/site-replication.ts b/types/site-replication.ts index abdef5e1..753da746 100644 --- a/types/site-replication.ts +++ b/types/site-replication.ts @@ -19,11 +19,27 @@ export interface SiteReplicationPeerInfo { apiVersion?: string } +export interface SiteReplicationPendingOperation { + operation: string + id: string + pendingPeers: string[] + ackedPeers: string[] + updatedAt?: string +} + +export interface SiteReplicationRetryStats { + pending: number + failed: number + lastError: string +} + export interface SiteReplicationInfo { enabled: boolean name: string sites: SiteReplicationPeerInfo[] serviceAccountAccessKey: string + pendingOperation?: SiteReplicationPendingOperation + retryStats?: SiteReplicationRetryStats apiVersion?: string } @@ -66,6 +82,17 @@ export interface SiteReplicationStateInfo { apiVersion?: string } +export interface SiteReplicationVolume { + count: number + bytes: number +} + +export interface SiteReplicationTimedErrors { + lastMinute: SiteReplicationVolume + lastHour: SiteReplicationVolume + totals: SiteReplicationVolume +} + export interface SiteReplicationMetric { deploymentId: string endpoint: string @@ -74,6 +101,7 @@ export interface SiteReplicationMetric { online: boolean replicatedSize: number replicatedCount: number + failed: SiteReplicationTimedErrors } export interface SiteReplicationWorkerStat { @@ -82,13 +110,47 @@ export interface SiteReplicationWorkerStat { max: number } +export interface SiteReplicationQueueStat { + curr: SiteReplicationVolume + avg: SiteReplicationVolume + max: SiteReplicationVolume +} + export interface SiteReplicationMetricsSummary { activeWorkers: SiteReplicationWorkerStat replicaSize: number replicaCount: number + queued: SiteReplicationQueueStat metrics: Record } +export interface SiteReplicationPeerError { + name: string + endpoint: string + error: string +} + +export interface SiteReplicationBucketStats { + hasBucket: boolean + bucketMarkedDeleted: boolean + tagMismatch: boolean + versioningConfigMismatch: boolean + objectLockConfigMismatch: boolean + policyMismatch: boolean + sseConfigMismatch: boolean + replicationCfgMismatch: boolean + quotaCfgMismatch: boolean + corsCfgMismatch: boolean +} + +export interface SiteReplicationPolicyStats { + hasPolicy: boolean + policyMismatch: boolean +} + +/** Per-entity details keyed by entity name, then by deployment ID. */ +export type SiteReplicationEntityStats = Record> + export interface SiteReplicationStatus { enabled: boolean maxBuckets: number @@ -99,7 +161,16 @@ export interface SiteReplicationStatus { sites: Record statsSummary: Record peerStates: Record + peerErrors: Record + pendingOperation?: SiteReplicationPendingOperation + retryStats?: SiteReplicationRetryStats metrics: SiteReplicationMetricsSummary + bucketStats: SiteReplicationEntityStats + policyStats: SiteReplicationEntityStats + /** Deployment IDs that hold each user, group, or ILM expiry rule. */ + userSites: Record + groupSites: Record + ilmExpiryRuleSites: Record apiVersion?: string } @@ -138,7 +209,7 @@ export interface SiteReplicationRemoveResponse { apiVersion?: string } -export type SiteReplicationResyncOperation = "start" | "cancel" +export type SiteReplicationResyncOperation = "start" | "cancel" | "status" export interface SiteReplicationResyncBucketStatus { bucket: string @@ -150,6 +221,13 @@ export interface SiteReplicationResyncResponse { opType: string resyncId: string status: string + state: string buckets: SiteReplicationResyncBucketStatus[] + totalBuckets: number + completedBuckets: number + failedBuckets: number + replicatedObjects: number + failedObjects: number + updatedAt?: string errorDetail: string }