Skip to content

Commit 7907aa0

Browse files
authored
676 feature request display total index size per customer in indicesmanagement (#687)
* feat: add endpoint to fetch total indices size per customer * feat: add component to display total indices size per customer * feat: enhance customer indices display with popover for index details * feat: enable index selection in CustomerIndicesSize component * precommit fixes * lint fixes * chore: update CURRENT_VERSION to 0.1.43
1 parent 45edd6b commit 7907aa0

File tree

7 files changed

+454
-69
lines changed

7 files changed

+454
-69
lines changed

backend/app/connectors/wazuh_indexer/routes/monitoring.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
from app.auth.utils import AuthHandler
88
from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealthResponse
9+
from app.connectors.wazuh_indexer.schema.monitoring import CustomerIndicesSizeResponse
910
from app.connectors.wazuh_indexer.schema.monitoring import IndicesStatsResponse
1011
from app.connectors.wazuh_indexer.schema.monitoring import NodeAllocationResponse
1112
from app.connectors.wazuh_indexer.schema.monitoring import ShardsResponse
1213

1314
# from app.connectors.wazuh_indexer.schema import WazuhIndexerResponse, WazuhIndexerListResponse
1415
from app.connectors.wazuh_indexer.services.monitoring import cluster_healthcheck
16+
from app.connectors.wazuh_indexer.services.monitoring import indices_size_per_customer
1517
from app.connectors.wazuh_indexer.services.monitoring import indices_stats
1618
from app.connectors.wazuh_indexer.services.monitoring import node_allocation
1719
from app.connectors.wazuh_indexer.services.monitoring import (
@@ -101,6 +103,35 @@ async def get_indices_stats() -> Union[IndicesStatsResponse, HTTPException]:
101103
raise HTTPException(status_code=500, detail="Failed to retrieve indices stats.")
102104

103105

106+
@wazuh_indexer_router.get(
107+
"/indices/size-per-customer",
108+
response_model=CustomerIndicesSizeResponse,
109+
description="Fetch Wazuh Indexer indices size aggregated per customer",
110+
dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
111+
)
112+
async def get_indices_size_per_customer() -> Union[CustomerIndicesSizeResponse, HTTPException]:
113+
"""
114+
Fetch Wazuh Indexer indices size per customer.
115+
116+
This endpoint retrieves the total indices size aggregated per customer,
117+
where customer is extracted from index names (e.g., wazuh-copilot_37 -> copilot).
118+
119+
Returns:
120+
CustomerIndicesSizeResponse: A Pydantic model representing the indices size per customer.
121+
122+
Raises:
123+
HTTPException: An exception with a 500 status code is raised if the data cannot be retrieved.
124+
"""
125+
try:
126+
response = await indices_size_per_customer()
127+
return response
128+
except Exception as e:
129+
raise HTTPException(
130+
status_code=500,
131+
detail=f"Failed to retrieve indices size per customer: {str(e)}",
132+
)
133+
134+
104135
@wazuh_indexer_router.get(
105136
"/shards",
106137
response_model=ShardsResponse,

backend/app/connectors/wazuh_indexer/schema/monitoring.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,17 @@ class ShardsResponse(BaseModel):
7878
shards: Optional[List[Shards]]
7979
message: str
8080
success: bool
81+
82+
83+
class CustomerIndicesSize(BaseModel):
84+
customer: str
85+
total_size_bytes: int
86+
total_size_human: str
87+
index_count: int
88+
indices: List[str]
89+
90+
91+
class CustomerIndicesSizeResponse(BaseModel):
92+
customer_sizes: Optional[List[CustomerIndicesSize]]
93+
message: str
94+
success: bool

backend/app/connectors/wazuh_indexer/services/monitoring.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import re
12
from typing import Dict
23
from typing import Union
34

45
from loguru import logger
56

67
from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealth
78
from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealthResponse
9+
from app.connectors.wazuh_indexer.schema.monitoring import CustomerIndicesSize
10+
from app.connectors.wazuh_indexer.schema.monitoring import CustomerIndicesSizeResponse
811
from app.connectors.wazuh_indexer.schema.monitoring import IndicesStats
912
from app.connectors.wazuh_indexer.schema.monitoring import IndicesStatsResponse
1013
from app.connectors.wazuh_indexer.schema.monitoring import NodeAllocation
@@ -153,3 +156,127 @@ async def output_shard_number_to_be_set_based_on_nodes() -> int:
153156
logger.error(f"Shards check failed with error: {e}")
154157
e = f"Shards check failed with error: {e}"
155158
raise Exception(str(e))
159+
160+
161+
def parse_size_to_bytes(size_str: str) -> int:
162+
"""
163+
Convert a human-readable size string to bytes.
164+
Handles formats like '1.2gb', '500mb', '100kb', '1024b'.
165+
"""
166+
if not size_str or size_str == "Store size not found":
167+
return 0
168+
169+
size_str = size_str.lower().strip()
170+
171+
# Define multipliers
172+
multipliers = {
173+
"b": 1,
174+
"kb": 1024,
175+
"mb": 1024**2,
176+
"gb": 1024**3,
177+
"tb": 1024**4,
178+
}
179+
180+
# Match number and unit
181+
match = re.match(r"^([\d.]+)\s*([a-z]+)$", size_str)
182+
if match:
183+
value = float(match.group(1))
184+
unit = match.group(2)
185+
return int(value * multipliers.get(unit, 1))
186+
187+
# Try to parse as pure number (bytes)
188+
try:
189+
return int(float(size_str))
190+
except ValueError:
191+
return 0
192+
193+
194+
def bytes_to_human_readable(size_bytes: int) -> str:
195+
"""Convert bytes to human-readable format."""
196+
for unit in ["b", "kb", "mb", "gb", "tb"]:
197+
if abs(size_bytes) < 1024.0:
198+
return f"{size_bytes:.2f}{unit}"
199+
size_bytes /= 1024.0
200+
return f"{size_bytes:.2f}pb"
201+
202+
203+
def extract_customer_from_index(index_name: str) -> str:
204+
"""
205+
Extract customer name from index name.
206+
Pattern: after dash or underscore, before the next underscore or end.
207+
Examples:
208+
- wazuh-copilot_37 -> copilot
209+
- dev-taylor_37 -> taylor
210+
- wazuh-509dine2v_0 -> 509dine2v
211+
"""
212+
# Match pattern: prefix-customer_suffix or prefix_customer_suffix
213+
match = re.match(r"^[^-_]+-([^_]+)_", index_name)
214+
if match:
215+
return match.group(1)
216+
217+
# Fallback: try underscore as first separator
218+
match = re.match(r"^[^_]+_([^_]+)_", index_name)
219+
if match:
220+
return match.group(1)
221+
222+
return "unknown"
223+
224+
225+
async def indices_size_per_customer() -> Union[CustomerIndicesSizeResponse, Dict[str, str]]:
226+
"""
227+
Returns the total indices size aggregated per customer.
228+
229+
Returns:
230+
CustomerIndicesSizeResponse: A Pydantic model containing the indices size per customer.
231+
232+
Raises:
233+
Exception: An exception is raised if the indices stats cannot be retrieved.
234+
"""
235+
logger.info("Collecting Wazuh Indexer indices size per customer")
236+
es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
237+
try:
238+
raw_indices_stats_data = es_client.cat.indices(format="json")
239+
240+
formatted_indices_stats_data = await format_indices_stats(raw_indices_stats_data)
241+
242+
# Aggregate by customer
243+
customer_data: Dict[str, Dict] = {}
244+
245+
for index_data in formatted_indices_stats_data:
246+
index_name = index_data.get("index", "")
247+
store_size = index_data.get("store_size", "0b")
248+
249+
customer = extract_customer_from_index(index_name)
250+
size_bytes = parse_size_to_bytes(store_size)
251+
252+
if customer not in customer_data:
253+
customer_data[customer] = {
254+
"total_size_bytes": 0,
255+
"index_count": 0,
256+
"indices": [],
257+
}
258+
259+
customer_data[customer]["total_size_bytes"] += size_bytes
260+
customer_data[customer]["index_count"] += 1
261+
customer_data[customer]["indices"].append(index_name)
262+
263+
# Convert to response models
264+
customer_sizes = [
265+
CustomerIndicesSize(
266+
customer=customer,
267+
total_size_bytes=data["total_size_bytes"],
268+
total_size_human=bytes_to_human_readable(data["total_size_bytes"]),
269+
index_count=data["index_count"],
270+
indices=data["indices"],
271+
)
272+
for customer, data in sorted(customer_data.items())
273+
]
274+
275+
return CustomerIndicesSizeResponse(
276+
customer_sizes=customer_sizes,
277+
success=True,
278+
message="Successfully collected Wazuh Indexer indices size per customer",
279+
)
280+
except Exception as e:
281+
logger.error(f"Indices size per customer check failed with error: {e}")
282+
raise Exception(f"Indices size per customer check failed with error: {e}")

backend/app/version/services/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from packaging.version import Version
88

99
# Current version - update this with each release
10-
CURRENT_VERSION = "0.1.42"
10+
CURRENT_VERSION = "0.1.43"
1111
VERSION_CHECK_URL = "https://api.github.com/repos/socfortress/CoPilot/releases/latest"
1212

1313

frontend/src/api/endpoints/wazuh/indices.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,18 @@ export default {
1414
},
1515
getClusterHealth() {
1616
return HttpClient.get<FlaskBaseResponse & { cluster_health: ClusterHealth }>("/wazuh_indexer/health")
17-
}
17+
},
18+
getIndicesSizePerCustomer() {
19+
return HttpClient.get<{
20+
customer_sizes: {
21+
customer: string
22+
total_size_bytes: number
23+
total_size_human: string
24+
index_count: number
25+
indices: string[]
26+
}[]
27+
message: string
28+
success: boolean
29+
}>(`/wazuh_indexer/indices/size-per-customer`)
30+
}
1831
}

0 commit comments

Comments
 (0)