diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d9f8133..457a320 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,12 +1,13 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { QueryClientProvider } from '@tanstack/react-query'; -import { ThemeProvider, useTheme } from '@/components/theme-provider'; -import { Layout } from '@/components/layout/layout'; -import { Dashboard } from '@/pages/Dashboard'; -import { Buckets } from '@/pages/Buckets'; -import { AccessControl } from '@/pages/AccessControl'; -import { Toaster } from 'sonner'; -import { queryClient } from '@/lib/query-client'; +import {BrowserRouter, Route, Routes} from 'react-router-dom'; +import {QueryClientProvider} from '@tanstack/react-query'; +import {ThemeProvider, useTheme} from '@/components/theme-provider'; +import {Layout} from '@/components/layout/layout'; +import {Dashboard} from '@/pages/Dashboard'; +import {Buckets} from '@/pages/Buckets'; +import {Cluster} from '@/pages/Cluster'; +import {AccessControl} from '@/pages/AccessControl'; +import {Toaster} from 'sonner'; +import {queryClient} from '@/lib/query-client'; function ThemedToaster() { const { theme } = useTheme(); @@ -23,6 +24,7 @@ function App() { }> } /> } /> + } /> } /> diff --git a/frontend/src/components/buckets/ObjectsTable.tsx b/frontend/src/components/buckets/ObjectsTable.tsx index 4d998f1..f196fa4 100644 --- a/frontend/src/components/buckets/ObjectsTable.tsx +++ b/frontend/src/components/buckets/ObjectsTable.tsx @@ -280,7 +280,7 @@ export function ObjectsTable({ - {obj.isFolder ? 'Folder' : getFileType(obj.key.replace(currentPath, ''))} + {obj.isFolder ? 'Directory' : getFileType(obj.key.replace(currentPath, ''))} {obj.storageClass && ( diff --git a/frontend/src/components/charts/BucketUsageChart.tsx b/frontend/src/components/charts/BucketUsageChart.tsx index da28505..470e9f3 100644 --- a/frontend/src/components/charts/BucketUsageChart.tsx +++ b/frontend/src/components/charts/BucketUsageChart.tsx @@ -1,8 +1,8 @@ -import { useEffect, useState } from 'react'; -import { PieChart, Pie, Cell, Legend, Tooltip, ResponsiveContainer } from 'recharts'; -import type { BucketUsage } from '@/types'; -import { formatBytes } from '@/lib/utils'; -import { chartColorPalette, getTextColor, getTooltipStyle } from '@/lib/chart-colors'; +import {useEffect, useState} from 'react'; +import {Cell, Legend, Pie, PieChart, ResponsiveContainer, Tooltip} from 'recharts'; +import type {BucketUsage} from '@/types'; +import {formatBytes} from '@/lib/utils'; +import {chartColorPalette, getTextColor, getTooltipStyle} from '@/lib/chart-colors'; interface BucketUsageChartProps { data: BucketUsage[]; diff --git a/frontend/src/components/charts/ClusterHealthChart.tsx b/frontend/src/components/charts/ClusterHealthChart.tsx index 11a4cdc..486ee0e 100644 --- a/frontend/src/components/charts/ClusterHealthChart.tsx +++ b/frontend/src/components/charts/ClusterHealthChart.tsx @@ -1,7 +1,7 @@ -import { useEffect, useState } from 'react'; -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; -import type { ClusterHealth } from '@/types'; -import { grafanaColors, getTextColor, getGridColor, getTooltipStyle } from '@/lib/chart-colors'; +import {useEffect, useState} from 'react'; +import {Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import type {ClusterHealth} from '@/types'; +import {getGridColor, getTextColor, getTooltipStyle, grafanaColors} from '@/lib/chart-colors'; interface ClusterHealthChartProps { data: ClusterHealth; @@ -32,13 +32,13 @@ export function ClusterHealthChart({ data }: ClusterHealthChartProps) { const chartData = [ { metric: 'Nodes', - healthy: data.healthyStorageNodes, - unhealthy: data.declaredStorageNodes - data.healthyStorageNodes, + healthy: data.storageNodesUp, + unhealthy: data.storageNodes - data.storageNodesUp, }, { metric: 'Partitions', - healthy: data.healthyPartitions, - unhealthy: data.totalPartitions - data.healthyPartitions, + healthy: data.partitionsAllOk, + unhealthy: data.partitions - data.partitionsAllOk, }, { metric: 'Connected', diff --git a/frontend/src/components/layout/sidebar.tsx b/frontend/src/components/layout/sidebar.tsx index d972cd7..8d5c3a3 100644 --- a/frontend/src/components/layout/sidebar.tsx +++ b/frontend/src/components/layout/sidebar.tsx @@ -1,6 +1,6 @@ import {Link, useLocation} from 'react-router-dom'; import {cn} from '@/lib/utils'; -import {Database, Key, LayoutDashboard,} from 'lucide-react'; +import {Database, Key, LayoutDashboard, Server} from 'lucide-react'; interface NavItem { title: string; @@ -19,6 +19,11 @@ const navItems: NavItem[] = [ href: '/buckets', icon: Database, }, + { + title: 'Cluster', + href: '/cluster', + icon: Server, + }, { title: 'Access Control', href: '/access', @@ -71,17 +76,17 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { ); })} -
-
-
- AD -
-
-

Admin User

-

admin@garage.local

-
-
-
+ {/*
*/} + {/*
*/} + {/*
*/} + {/* AD*/} + {/*
*/} + {/*
*/} + {/*

Admin User

*/} + {/*

admin@garage.local

*/} + {/*
*/} + {/*
*/} + {/*
*/} ); } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index c468910..22a42b7 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -6,8 +6,10 @@ import type { BucketDetails, ClusterHealth, ClusterStatistics, + ClusterStatus, GarageMetrics, - NodeInfo, + MultiNodeResponse, + MultiNodeStatisticsResponse, ObjectListResponse, ObjectMetadata, S3Object, @@ -261,8 +263,7 @@ export const garageApi = { return response.data.data; }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getClusterStatus: async (): Promise => { + getClusterStatus: async (): Promise => { const response = await api.get('/v1/cluster/status'); return response.data.data; }, @@ -272,23 +273,21 @@ export const garageApi = { return response.data.data; }, - getNodeInfo: async (nodeId: string = 'self'): Promise => { + getNodeInfo: async (nodeId: string = 'self'): Promise => { const response = await api.get(`/v1/cluster/nodes/${nodeId}`); return response.data.data; }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getNodeStatistics: async (nodeId: string): Promise => { + getNodeStatistics: async (nodeId: string): Promise => { const response = await api.get(`/v1/cluster/nodes/${nodeId}/statistics`); return response.data.data; }, getFullMetrics: async (): Promise => { // Fetch all cluster-related metrics - const [health, statistics, nodeInfo, storageMetrics] = await Promise.all([ + const [health, statistics, storageMetrics] = await Promise.all([ garageApi.getClusterHealth(), garageApi.getClusterStatistics(), - garageApi.getNodeInfo(), analyticsApi.getMetrics(), ]); @@ -296,7 +295,6 @@ export const garageApi = { ...storageMetrics, clusterHealth: health, clusterStatistics: statistics, - nodeInfo: nodeInfo, }; }, }; diff --git a/frontend/src/pages/Cluster.tsx b/frontend/src/pages/Cluster.tsx new file mode 100644 index 0000000..68359ef --- /dev/null +++ b/frontend/src/pages/Cluster.tsx @@ -0,0 +1,489 @@ +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; +import {Header} from '@/components/layout/header'; +import {formatBytes} from '@/lib/utils'; +import {Activity, AlertCircle, CheckCircle2, Clock, Cpu, Database, Info, Network, Server, XCircle,} from 'lucide-react'; +import {useQuery} from '@tanstack/react-query'; +import {garageApi} from '@/lib/api'; +import {Badge} from '@/components/ui/badge'; +import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; +import type {ClusterNode, LocalNodeInfo, NodeStatistics} from '@/types'; +import {useState} from 'react'; + +export function Cluster() { + const [selectedNodeId, setSelectedNodeId] = useState(null); + + const { data: health, isLoading: healthLoading } = useQuery({ + queryKey: ['cluster-health'], + queryFn: () => garageApi.getClusterHealth(), + refetchInterval: 10000, + }); + + const { data: status, isLoading: statusLoading } = useQuery({ + queryKey: ['cluster-status'], + queryFn: () => garageApi.getClusterStatus(), + refetchInterval: 15000, + }); + + const { data: statistics, isLoading: statisticsLoading } = useQuery({ + queryKey: ['cluster-statistics'], + queryFn: () => garageApi.getClusterStatistics(), + refetchInterval: 30000, + }); + + const { data: nodeInfo, isLoading: nodeInfoLoading } = useQuery({ + queryKey: ['node-info', selectedNodeId || '*'], + queryFn: () => garageApi.getNodeInfo(selectedNodeId || '*'), + enabled: !!selectedNodeId || selectedNodeId === null, + }); + + const { data: nodeStats } = useQuery({ + queryKey: ['node-statistics', selectedNodeId || '*'], + queryFn: () => garageApi.getNodeStatistics(selectedNodeId || '*'), + enabled: !!selectedNodeId, + }); + + const isLoading = healthLoading || statusLoading || statisticsLoading; + + const getHealthStatus = () => { + if (!health) return { color: 'text-gray-500', bgColor: 'bg-gray-100', label: 'Unknown', icon: AlertCircle }; + if ( + health.storageNodesUp === health.storageNodes && + health.partitionsAllOk === health.partitions && + health.connectedNodes === health.knownNodes + ) { + return { color: 'text-green-600', bgColor: 'bg-green-100', label: 'Healthy', icon: CheckCircle2 }; + } + if (health.storageNodesUp > 0 && health.partitionsQuorum > 0) { + return { color: 'text-yellow-600', bgColor: 'bg-yellow-100', label: 'Degraded', icon: AlertCircle }; + } + return { color: 'text-red-600', bgColor: 'bg-red-100', label: 'Unhealthy', icon: XCircle }; + }; + + const healthStatus = getHealthStatus(); + const HealthIcon = healthStatus.icon; + + const getNodeStatus = (node: ClusterNode) => { + if (!node.isUp) { + return { color: 'text-red-600', bgColor: 'bg-red-100', label: 'Down', icon: XCircle }; + } + if (node.draining) { + return { color: 'text-yellow-600', bgColor: 'bg-yellow-100', label: 'Draining', icon: AlertCircle }; + } + return { color: 'text-green-600', bgColor: 'bg-green-100', label: 'Up', icon: CheckCircle2 }; + }; + + const formatUptime = (seconds?: number) => { + if (!seconds) return 'N/A'; + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return `${days}d ${hours}h ${minutes}m`; + }; + + if (isLoading) { + return ( +
+
+
+
+
+

Loading cluster information...

+
+
+
+ ); + } + + return ( +
+
+
+ {/* Cluster Health Overview */} +
+ + + Cluster Status + + + +
{healthStatus.label}
+

+ Layout v{status?.layoutVersion || 0} +

+
+
+ + + + Connected Nodes + + + +
+ {health?.connectedNodes || 0}/{health?.knownNodes || 0} +
+

+ Nodes online +

+
+
+ + + + Storage Nodes + + + +
+ {health?.storageNodesUp || 0}/{health?.storageNodes || 0} +
+

+ Healthy storage nodes +

+
+
+ + + + Partitions + + + +
+ {health?.partitionsAllOk || 0}/{health?.partitions || 0} +
+

+ Healthy partitions +

+
+
+
+ + {/* Tabs for different views */} + + + Nodes + Statistics + Details + + + {/* Nodes Tab */} + + + + Cluster Nodes + + Overview of all nodes in the Garage cluster + + + +
+ {status?.nodes && status.nodes.length > 0 ? ( + status.nodes.map((node) => { + const nodeStatus = getNodeStatus(node); + const NodeIcon = nodeStatus.icon; + const dataUsage = node.dataPartition + ? ((node.dataPartition.total - node.dataPartition.available) / node.dataPartition.total) * 100 + : 0; + const metadataUsage = node.metadataPartition + ? ((node.metadataPartition.total - node.metadataPartition.available) / node.metadataPartition.total) * 100 + : 0; + + return ( + setSelectedNodeId(node.id)} + > + +
+
+
+ +
+
+ {node.id.substring(0, 16)}... +
+ {node.hostname && ( +
{node.hostname}
+ )} +
+
+ +
+
+
Status
+ + {nodeStatus.label} + +
+ + {node.addr && ( +
+
Address
+
{node.addr}
+
+ )} + + {node.garageVersion && ( +
+
Version
+
{node.garageVersion}
+
+ )} + + {node.role && ( +
+
Zone
+
{node.role.zone}
+
+ )} +
+ + {node.role?.capacity && ( +
+
+ Capacity: {formatBytes(node.role.capacity)} +
+
+ )} + + {(node.dataPartition || node.metadataPartition) && ( +
+ {node.dataPartition && ( +
+
+ Data Partition: {formatBytes(node.dataPartition.total - node.dataPartition.available)} / {formatBytes(node.dataPartition.total)} +
+
+
90 ? 'bg-red-500' : dataUsage > 70 ? 'bg-yellow-500' : 'bg-green-500' + }`} + style={{ width: `${dataUsage}%` }} + /> +
+
+ )} + + {node.metadataPartition && ( +
+
+ Metadata Partition: {formatBytes(node.metadataPartition.total - node.metadataPartition.available)} / {formatBytes(node.metadataPartition.total)} +
+
+
90 ? 'bg-red-500' : metadataUsage > 70 ? 'bg-yellow-500' : 'bg-green-500' + }`} + style={{ width: `${metadataUsage}%` }} + /> +
+
+ )} +
+ )} + + {!node.isUp && node.lastSeenSecsAgo !== undefined && ( +
+ + Last seen: {node.lastSeenSecsAgo === null ? 'Never' : formatUptime(node.lastSeenSecsAgo) + ' ago'} +
+ )} +
+
+ + + ); + }) + ) : ( +
+ +

No nodes found in the cluster

+
+ )} +
+ + + + + {/* Statistics Tab */} + + + + Cluster Statistics + + Detailed statistics and metrics from the Garage cluster + + + + {statistics ? ( +
+
+
+                        {statistics.freeform}
+                      
+
+
+ ) : ( +
+ +

No statistics available

+
+ )} +
+
+
+ + {/* Details Tab */} + + {selectedNodeId ? ( + <> + + + Node Information + + Detailed information for node: {selectedNodeId.substring(0, 16)}... + + + + {nodeInfoLoading ? ( +
+
+

Loading node info...

+
+ ) : nodeInfo ? ( +
+ {/* Success responses */} + {Object.entries(nodeInfo.success || {}).map(([nodeId, info]) => ( +
+
+ +

+ Node: {nodeId.substring(0, 16)}... +

+
+ +
+
+
Node ID
+
{(info as LocalNodeInfo).nodeId}
+
+ +
+
Garage Version
+
{(info as LocalNodeInfo).garageVersion}
+
+ +
+
Rust Version
+
{(info as LocalNodeInfo).rustVersion}
+
+ +
+
Database Engine
+
{(info as LocalNodeInfo).dbEngine}
+
+
+ + {(info as LocalNodeInfo).garageFeatures && (info as LocalNodeInfo).garageFeatures!.length > 0 && ( +
+
Garage Features
+
+ {(info as LocalNodeInfo).garageFeatures!.map((feature) => ( + + {feature} + + ))} +
+
+ )} +
+ ))} + + {/* Error responses */} + {Object.entries(nodeInfo.error || {}).map(([nodeId, error]) => ( +
+
+ +
Error for node {nodeId.substring(0, 16)}...
+
+
{error}
+
+ ))} +
+ ) : ( +
+ +

No node information available

+
+ )} +
+
+ + {nodeStats && ( + + + Node Statistics + + Performance metrics for the selected node + + + +
+ {/* Success responses */} + {Object.entries(nodeStats.success || {}).map(([nodeId, stats]) => ( +
+
+ +

+ Statistics for: {nodeId.substring(0, 16)}... +

+
+ +
+
+                                {(stats as NodeStatistics).freeform}
+                              
+
+
+ ))} + + {/* Error responses */} + {Object.entries(nodeStats.error || {}).map(([nodeId, error]) => ( +
+
+ +
Error for node {nodeId.substring(0, 16)}...
+
+
{error}
+
+ ))} +
+
+
+ )} + + ) : ( + + +
+ +

Select a Node

+

+ Click on a node in the Nodes tab to view detailed information and statistics +

+
+
+
+ )} +
+ +
+
+ ); +} + + diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 05f68c3..0772591 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,11 +1,10 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Header } from '@/components/layout/header'; -import { formatBytes } from '@/lib/utils'; -import { Database, FolderOpen, HardDrive, Activity, Server, Zap, AlertCircle } from 'lucide-react'; -import { BucketUsageChart } from '@/components/charts/BucketUsageChart'; -import { RequestMetricsChart } from '@/components/charts/RequestMetricsChart'; -import { useDashboardData } from '@/hooks/useApi'; -import type { ClusterHealth } from '@/types'; +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; +import {Header} from '@/components/layout/header'; +import {formatBytes} from '@/lib/utils'; +import {AlertCircle, Database, FolderOpen, HardDrive, Server, Zap} from 'lucide-react'; +import {BucketUsageChart} from '@/components/charts/BucketUsageChart'; +import {useDashboardData} from '@/hooks/useApi'; +import type {ClusterHealth} from '@/types'; export function Dashboard() { const { metrics: metricsQuery, buckets: bucketsQuery, health: healthQuery, isLoading } = useDashboardData(); @@ -17,15 +16,15 @@ export function Dashboard() { const getHealthStatus = (health: ClusterHealth | null) => { if (!health) return { color: 'text-gray-500', label: 'Unknown', icon: AlertCircle }; if ( - health.healthyStorageNodes === health.declaredStorageNodes && - health.healthyPartitions === health.totalPartitions && + health.storageNodesUp === health.storageNodes && + health.partitionsAllOk === health.partitions && health.connectedNodes === health.knownNodes ) { return { color: 'text-green-500', label: 'Healthy', icon: Zap }; } if ( - health.healthyStorageNodes > 0 && - health.healthyPartitions > 0 + health.storageNodesUp > 0 && + health.partitionsQuorum > 0 ) { return { color: 'text-yellow-500', label: 'Degraded', icon: AlertCircle }; } @@ -53,7 +52,7 @@ export function Dashboard() {
{/* Top Stats Grid */} -
+
Total Storage @@ -96,28 +95,6 @@ export function Dashboard() {

- - - - Requests (24h) - - - -
- {metrics - ? ( - metrics.requestMetrics.getRequests + - metrics.requestMetrics.putRequests + - metrics.requestMetrics.deleteRequests + - metrics.requestMetrics.listRequests - ).toLocaleString() - : null} -
-

- GET, PUT, DELETE, LIST -

-
-
{/* Cluster Status */} @@ -146,7 +123,7 @@ export function Dashboard() {
- {clusterHealth?.healthyStorageNodes || 0}/{clusterHealth?.declaredStorageNodes || 0} + {clusterHealth?.storageNodesUp || 0}/{clusterHealth?.storageNodes || 0}

Healthy storage nodes @@ -161,7 +138,7 @@ export function Dashboard() {

- {clusterHealth?.healthyPartitions || 0}/{clusterHealth?.totalPartitions || 0} + {clusterHealth?.partitionsAllOk || 0}/{clusterHealth?.partitions || 0}

Healthy partitions @@ -187,59 +164,44 @@ export function Dashboard() { - {/* Request Metrics Chart */} + {/* Bucket Details Table */} - Request Metrics - API request distribution (24h) + Storage Usage by Bucket (Table) + Detailed breakdown of storage across all buckets - {metrics?.requestMetrics ? ( - - ) : ( -

No data available
- )} -
- -
- - {/* Bucket Details Table */} - - - Storage Usage by Bucket (Table) - Detailed breakdown of storage across all buckets - - -
- {metrics?.usageByBucket && metrics.usageByBucket.length > 0 ? ( - metrics.usageByBucket.map((bucket) => ( -
-
- {bucket.bucketName} -
+
+ {metrics?.usageByBucket && metrics.usageByBucket.length > 0 ? ( + metrics.usageByBucket.map((bucket) => ( +
+
+ {bucket.bucketName} +
{bucket.objectCount.toLocaleString()} objects - {formatBytes(bucket.size)} - + {formatBytes(bucket.size)} + {bucket.percentage.toFixed(1)}% -
-
-
-
-
-
- )) - ) : ( -
No buckets available
- )} -
- - +
+
+
+
+
+
+ )) + ) : ( +
No buckets available
+ )} +
+ + +
{/* Recent Buckets */} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index bbffe3f..c03a67a 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -160,10 +160,11 @@ export interface ClusterHealth { status: string; connectedNodes: number; knownNodes: number; - healthyStorageNodes: number; - declaredStorageNodes: number; - healthyPartitions: number; - totalPartitions: number; + storageNodes: number; + storageNodesUp: number; + partitions: number; + partitionsQuorum: number; + partitionsAllOk: number; } export interface ClusterStatistics { @@ -173,6 +174,57 @@ export interface ClusterStatistics { [key: string]: any; } +export interface ClusterStatus { + layoutVersion: number; + nodes: ClusterNode[]; +} + +export interface ClusterNode { + id: string; + isUp: boolean; + lastSeenSecsAgo?: number; + hostname?: string; + addr?: string; + garageVersion?: string; + role?: NodeRole; + draining: boolean; + dataPartition?: FreeSpaceInfo; + metadataPartition?: FreeSpaceInfo; +} + +export interface NodeRole { + zone: string; + capacity?: number; + tags: string[]; +} + +export interface FreeSpaceInfo { + available: number; + total: number; +} + +export interface LocalNodeInfo { + nodeId: string; + garageVersion: string; + rustVersion: string; + dbEngine: string; + garageFeatures?: string[]; +} + +export interface MultiNodeResponse { + success: Record; + error: Record; +} + +export interface NodeStatistics { + freeform: string; +} + +export interface MultiNodeStatisticsResponse { + success: Record; + error: Record; +} + export interface NodeInfo { nodeId: string; version: string; diff --git a/internal/handlers/monitoring.go b/internal/handlers/monitoring.go index c8a5ce0..b1bfb00 100644 --- a/internal/handlers/monitoring.go +++ b/internal/handlers/monitoring.go @@ -139,13 +139,6 @@ func (h *MonitoringHandler) GetDashboardMetrics(c fiber.Ctx) error { ObjectCount: totalObjects, BucketCount: len(buckets), UsageByBucket: usageByBucket, - RequestMetrics: models.RequestMetrics{ - GetRequests: 0, - PutRequests: 0, - DeleteRequests: 0, - ListRequests: 0, - Period: "last-24h", - }, } return c.JSON(models.SuccessResponse(dashboardMetrics)) diff --git a/internal/models/responses.go b/internal/models/responses.go index 3a15262..bf9f513 100644 --- a/internal/models/responses.go +++ b/internal/models/responses.go @@ -4,11 +4,10 @@ import "time" // DashboardMetrics represents aggregated metrics for the dashboard type DashboardMetrics struct { - TotalSize int64 `json:"totalSize"` - ObjectCount int64 `json:"objectCount"` - BucketCount int `json:"bucketCount"` - UsageByBucket []BucketUsage `json:"usageByBucket"` - RequestMetrics RequestMetrics `json:"requestMetrics"` + TotalSize int64 `json:"totalSize"` + ObjectCount int64 `json:"objectCount"` + BucketCount int `json:"bucketCount"` + UsageByBucket []BucketUsage `json:"usageByBucket"` } // BucketUsage represents storage usage for a single bucket @@ -19,15 +18,6 @@ type BucketUsage struct { Percentage float64 `json:"percentage"` } -// RequestMetrics represents API request statistics -type RequestMetrics struct { - GetRequests int64 `json:"getRequests"` - PutRequests int64 `json:"putRequests"` - DeleteRequests int64 `json:"deleteRequests"` - ListRequests int64 `json:"listRequests"` - Period string `json:"period"` -} - // APIResponse is the standard response structure for all API endpoints type APIResponse struct { Success bool `json:"success"`