Skip to content

Commit c92523b

Browse files
authored
Merge pull request #535 from mmahut/development
Support for rich log color output and rich tables
2 parents 6af269d + 3a9f6c7 commit c92523b

9 files changed

Lines changed: 470 additions & 57 deletions

File tree

src/badfish/helpers/logger.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import json
22
import sys
3+
from io import StringIO
34

45
import yaml
6+
from rich.console import Console as RichConsole
57

68
try:
79
# Python 3.7 and newer, fast reentrant implementation
@@ -41,6 +43,9 @@ def emit(self, record):
4143
self.formatted_msg.append(self.formatter.format(record))
4244
return
4345

46+
if getattr(record, "is_table", False):
47+
return
48+
4449
if record.levelno == INFO and record.msg != "*" * 48:
4550
if record.name not in self.messages:
4651
self.messages.update({record.name: record.msg + "\n"})
@@ -176,8 +181,56 @@ def output(self, output_type, host_order=None):
176181
return "\n".join(sorted_msg)
177182

178183

184+
_LEVEL_MARKUP = {
185+
"DEBUG": "[dim cyan]DEBUG[/dim cyan]",
186+
"INFO": "[green]INFO[/green]",
187+
"WARNING": "[yellow]WARNING[/yellow]",
188+
"ERROR": "[bold red]ERROR[/bold red]",
189+
"CRITICAL": "[bold red]CRITICAL[/bold red]",
190+
}
191+
192+
193+
class BadfishFormatter(Formatter):
194+
def __init__(self, fmt, use_color=True):
195+
super().__init__(fmt)
196+
self.use_color = use_color
197+
self._colors = self._build_colors() if use_color else {}
198+
199+
def _build_colors(self):
200+
buf = StringIO()
201+
# no_color=False overrides NO_COLOR env var: we've already decided to
202+
# use color (use_color=True gate), so the builder must emit ANSI codes.
203+
console = RichConsole(
204+
file=buf,
205+
highlight=False,
206+
markup=True,
207+
force_terminal=True,
208+
no_color=False,
209+
color_system="standard",
210+
)
211+
colors = {}
212+
for level, markup in _LEVEL_MARKUP.items():
213+
buf.seek(0)
214+
buf.truncate(0)
215+
console.print(markup, end="")
216+
padding = " " * max(0, 8 - len(level))
217+
colors[level] = buf.getvalue() + padding
218+
return colors
219+
220+
def format(self, record):
221+
if getattr(record, "is_table", False):
222+
return record.getMessage()
223+
if not self.use_color:
224+
return super().format(record)
225+
original = record.levelname
226+
record.levelname = self._colors.get(original, f"{original:<8}")
227+
result = super().format(record)
228+
record.levelname = original
229+
return result
230+
231+
179232
class BadfishLogger:
180-
def __init__(self, verbose=False, multi_host=False, log_file=None, output=None):
233+
def __init__(self, verbose=False, multi_host=False, log_file=None, output=None, console=None):
181234
self.log_level = DEBUG if verbose else INFO
182235
self.multi_host = multi_host
183236
self.log_file = log_file
@@ -186,8 +239,11 @@ def __init__(self, verbose=False, multi_host=False, log_file=None, output=None):
186239
_format_str = f"{_host_name_tag}- %(levelname)-8s - %(message)s"
187240
_file_format_str = f"%(asctime)-12s: {_host_name_tag}- %(levelname)-8s - %(message)s"
188241

242+
use_color = bool(console and console.is_terminal and not console.no_color)
243+
console_formatter = BadfishFormatter(_format_str, use_color=use_color)
244+
189245
self.badfish_handler = BadfishHandler(True if output else False)
190-
self.badfish_handler.setFormatter(Formatter(_format_str))
246+
self.badfish_handler.setFormatter(console_formatter)
191247
self.badfish_handler.setLevel(INFO)
192248

193249
_queue = Queue()

src/badfish/main.py

Lines changed: 170 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
import tempfile
1414
from urllib.parse import urlparse
1515

16+
from io import StringIO
17+
1618
from rich.console import Console
19+
from rich.table import Table
1720

1821
from badfish.helpers import get_now
1922
from badfish.helpers.parser import parse_arguments
@@ -99,6 +102,15 @@ def __init__(
99102
self.vendor = None
100103
self.console = _console if _console is not None else Console()
101104
self._progress_disabled = _progress_disabled
105+
# Tables are useful only in the same conditions as progress bars: TTY,
106+
# single-host, no structured output, no log file.
107+
self._use_tables = not _progress_disabled
108+
109+
def _log_table(self, table):
110+
buf = StringIO()
111+
tmp = Console(file=buf, highlight=False, force_terminal=self.console.is_terminal, no_color=self.console.no_color)
112+
tmp.print(table)
113+
self.logger.info(buf.getvalue().rstrip("\n"), extra={"is_table": True})
102114

103115
async def __aenter__(self):
104116
await self.init()
@@ -1295,12 +1307,21 @@ async def check_boot(self, _interfaces_path):
12951307
return True
12961308
else:
12971309
self.logger.warning("Current boot order does not match any of the given.")
1298-
self.logger.info("Current boot order:")
1310+
1311+
self.logger.info("Current boot order:")
1312+
if self._use_tables:
1313+
table = Table(show_header=True, header_style="bold")
1314+
table.add_column("#", justify="right")
1315+
table.add_column("Device")
1316+
table.add_column("Status")
1317+
for device in sorted(self.boot_devices, key=lambda x: x["Index"]):
1318+
status = "Enabled" if device["Enabled"] else "Disabled"
1319+
table.add_row(str(int(device["Index"]) + 1), device["Name"], status)
1320+
self._log_table(table)
12991321
else:
1300-
self.logger.info("Current boot order:")
1301-
for device in sorted(self.boot_devices, key=lambda x: x["Index"]):
1302-
enabled = "" if device["Enabled"] else " (DISABLED)"
1303-
self.logger.info("%s: %s%s" % (int(device["Index"]) + 1, device["Name"], enabled))
1322+
for device in sorted(self.boot_devices, key=lambda x: x["Index"]):
1323+
enabled = "" if device["Enabled"] else " (DISABLED)"
1324+
self.logger.info("%s: %s%s" % (int(device["Index"]) + 1, device["Name"], enabled))
13041325
return True
13051326

13061327
async def check_device(self, device):
@@ -1387,6 +1408,8 @@ async def get_firmware_inventory(self):
13871408
if "Installed" in a:
13881409
installed_devices.append(a)
13891410

1411+
_SKIP = {"odata", "Description", "Oem"}
1412+
rows = []
13901413
for device in installed_devices:
13911414
self.logger.debug("Getting device info for %s" % device)
13921415
_uri = "%s/UpdateService/FirmwareInventory/%s" % (self.root_uri, device)
@@ -1397,11 +1420,32 @@ async def get_firmware_inventory(self):
13971420

13981421
raw = await _response.text("utf-8", "ignore")
13991422
data = json.loads(raw.strip())
1400-
for info in data.items():
1401-
if "Id" == info[0]:
1402-
self.logger.info("%s:" % info[1])
1403-
if "odata" not in info[0] and "Description" not in info[0] and "Oem" not in info[0]:
1404-
self.logger.info(" %s: %s" % (info[0], info[1]))
1423+
row = {k: v for k, v in data.items() if not any(s in k for s in _SKIP)}
1424+
rows.append(row)
1425+
1426+
if rows:
1427+
if self._use_tables:
1428+
cols = ["Id", "Name", "Version", "Manufacturer", "Status"]
1429+
table = Table(show_header=True, header_style="bold")
1430+
for col in cols:
1431+
table.add_column(col)
1432+
for row in rows:
1433+
status = row.get("Status")
1434+
if isinstance(status, dict):
1435+
status = status.get("State", "")
1436+
table.add_row(
1437+
str(row.get("Id", "")),
1438+
str(row.get("Name", "")),
1439+
str(row.get("Version", "")),
1440+
str(row.get("Manufacturer", "")),
1441+
str(status or ""),
1442+
)
1443+
self._log_table(table)
1444+
else:
1445+
for row in rows:
1446+
self.logger.info("%s:" % row.get("Id", ""))
1447+
for k, v in row.items():
1448+
self.logger.info(" %s: %s" % (k, v))
14051449

14061450
async def get_host_type_boot_device(self, host_type, _interfaces_path):
14071451
if _interfaces_path:
@@ -1844,22 +1888,49 @@ async def list_interfaces(self):
18441888
self.logger.error("Server does not support this functionality")
18451889
return False
18461890

1847-
for interface, properties in data.items():
1848-
self.logger.info(f"{interface}:")
1849-
for key, value in properties.items():
1850-
if key == "SupportedLinkCapabilities":
1851-
speed_key = "LinkSpeedMbps"
1852-
speed = value[0].get(speed_key)
1853-
if speed:
1854-
self.logger.info(f" {speed_key}: {speed}")
1855-
elif key == "Status":
1856-
health_key = "Health"
1857-
health = value.get(health_key)
1858-
if health:
1859-
self.logger.info(f" {health_key}: {health}")
1860-
else:
1861-
self.logger.info(f" {key}: {value}")
1891+
if self._use_tables:
1892+
cols = {}
1893+
for interface, properties in data.items():
1894+
for key, value in properties.items():
1895+
if key == "SupportedLinkCapabilities":
1896+
cols["LinkSpeedMbps"] = True
1897+
elif key == "Status":
1898+
cols["Health"] = True
1899+
else:
1900+
cols[key] = True
1901+
1902+
table = Table(show_header=True, header_style="bold")
1903+
table.add_column("Interface")
1904+
for col in cols:
1905+
table.add_column(col)
1906+
1907+
for interface, properties in data.items():
1908+
row_vals = {"Interface": interface}
1909+
for key, value in properties.items():
1910+
if key == "SupportedLinkCapabilities":
1911+
caps = value[0] if isinstance(value, list) and value else {}
1912+
row_vals["LinkSpeedMbps"] = str(caps.get("LinkSpeedMbps", ""))
1913+
elif key == "Status":
1914+
row_vals["Health"] = str(value.get("Health", ""))
1915+
else:
1916+
row_vals[key] = str(value)
1917+
table.add_row(*[row_vals.get(c, "") for c in ["Interface"] + list(cols)])
18621918

1919+
self._log_table(table)
1920+
else:
1921+
for interface, properties in data.items():
1922+
self.logger.info(f"{interface}:")
1923+
for key, value in properties.items():
1924+
if key == "SupportedLinkCapabilities":
1925+
speed = value[0].get("LinkSpeedMbps") if value else None
1926+
if speed:
1927+
self.logger.info(f" LinkSpeedMbps: {speed}")
1928+
elif key == "Status":
1929+
health = value.get("Health")
1930+
if health:
1931+
self.logger.info(f" Health: {health}")
1932+
else:
1933+
self.logger.info(f" {key}: {value}")
18631934
return True
18641935

18651936
async def get_processor_summary(self):
@@ -2129,15 +2200,33 @@ async def list_processors(self):
21292200
data = await self.get_processor_summary()
21302201

21312202
self.logger.info("Processor Summary:")
2132-
for _key, _value in data.items():
2133-
self.logger.info(f" {_key}: {_value}")
2203+
if self._use_tables:
2204+
summary = Table(show_header=True, header_style="bold")
2205+
summary.add_column("Property")
2206+
summary.add_column("Value")
2207+
for _key, _value in data.items():
2208+
summary.add_row(_key, str(_value))
2209+
self._log_table(summary)
2210+
else:
2211+
for _key, _value in data.items():
2212+
self.logger.info(f" {_key}: {_value}")
21342213

21352214
processor_data = await self.get_processor_details()
21362215

2137-
for _processor, _properties in processor_data.items():
2138-
self.logger.info(f"{_processor}:")
2139-
for _key, _value in _properties.items():
2140-
self.logger.info(f" {_key}: {_value}")
2216+
if self._use_tables:
2217+
detail = Table(show_header=True, header_style="bold")
2218+
detail.add_column("Processor")
2219+
all_keys = dict.fromkeys(k for props in processor_data.values() for k in props)
2220+
for col in all_keys:
2221+
detail.add_column(col)
2222+
for _processor, _properties in processor_data.items():
2223+
detail.add_row(_processor, *[str(_properties.get(k, "")) for k in all_keys])
2224+
self._log_table(detail)
2225+
else:
2226+
for _processor, _properties in processor_data.items():
2227+
self.logger.info(f"{_processor}:")
2228+
for _key, _value in _properties.items():
2229+
self.logger.info(f" {_key}: {_value}")
21412230

21422231
return True
21432232

@@ -2148,33 +2237,68 @@ async def list_gpu(self):
21482237
summary = await self.get_gpu_summary(gpu_responses)
21492238

21502239
self.logger.info("GPU Summary:")
2151-
for _key, _value in summary.items():
2152-
self.logger.info(f" Model: {_key} (Count: {_value})")
2153-
2154-
self.logger.info("Current GPU's on host:")
2240+
if self._use_tables:
2241+
summary_table = Table(show_header=True, header_style="bold")
2242+
summary_table.add_column("Model")
2243+
summary_table.add_column("Count", justify="right")
2244+
for _key, _value in summary.items():
2245+
summary_table.add_row(_key, str(_value))
2246+
self._log_table(summary_table)
2247+
else:
2248+
for _key, _value in summary.items():
2249+
self.logger.info(f" Model: {_key} (Count: {_value})")
21552250

21562251
gpu_data = await self.get_gpu_details(gpu_responses)
21572252

2158-
for _gpu, _properties in gpu_data.items():
2159-
self.logger.info(f" {_gpu}:")
2160-
for _key, _value in _properties.items():
2161-
self.logger.info(f" {_key}: {_value}")
2253+
self.logger.info("Current GPU's on host:")
2254+
if self._use_tables:
2255+
detail = Table(show_header=True, header_style="bold")
2256+
detail.add_column("GPU")
2257+
all_keys = dict.fromkeys(k for props in gpu_data.values() for k in props)
2258+
for col in all_keys:
2259+
detail.add_column(col)
2260+
for _gpu, _properties in gpu_data.items():
2261+
detail.add_row(_gpu, *[str(_properties.get(k, "")) for k in all_keys])
2262+
self._log_table(detail)
2263+
else:
2264+
for _gpu, _properties in gpu_data.items():
2265+
self.logger.info(f" {_gpu}:")
2266+
for _key, _value in _properties.items():
2267+
self.logger.info(f" {_key}: {_value}")
21622268

21632269
return True
21642270

21652271
async def list_memory(self):
21662272
data = await self.get_memory_summary()
21672273

21682274
self.logger.info("Memory Summary:")
2169-
for _key, _value in data.items():
2170-
self.logger.info(f" {_key}: {_value}")
2275+
if self._use_tables:
2276+
summary = Table(show_header=True, header_style="bold")
2277+
summary.add_column("Property")
2278+
summary.add_column("Value")
2279+
for _key, _value in data.items():
2280+
summary.add_row(_key, str(_value))
2281+
self._log_table(summary)
2282+
else:
2283+
for _key, _value in data.items():
2284+
self.logger.info(f" {_key}: {_value}")
21712285

21722286
memory_data = await self.get_memory_details()
21732287

2174-
for _memory, _properties in memory_data.items():
2175-
self.logger.info(f"{_memory}:")
2176-
for _key, _value in _properties.items():
2177-
self.logger.info(f" {_key}: {_value}")
2288+
if self._use_tables:
2289+
detail = Table(show_header=True, header_style="bold")
2290+
detail.add_column("DIMM")
2291+
all_keys = dict.fromkeys(k for props in memory_data.values() for k in props)
2292+
for col in all_keys:
2293+
detail.add_column(col)
2294+
for _memory, _properties in memory_data.items():
2295+
detail.add_row(_memory, *[str(_properties.get(k, "")) for k in all_keys])
2296+
self._log_table(detail)
2297+
else:
2298+
for _memory, _properties in memory_data.items():
2299+
self.logger.info(f"{_memory}:")
2300+
for _key, _value in _properties.items():
2301+
self.logger.info(f" {_key}: {_value}")
21782302

21792303
return True
21802304

@@ -2984,9 +3108,8 @@ def main(argv=None):
29843108
multi_host = True if host_list else False
29853109
result = True
29863110
output = _args["output"]
2987-
bfl = BadfishLogger(_args["verbose"], multi_host, _args["log"], output)
2988-
29893111
console = Console()
3112+
bfl = BadfishLogger(_args["verbose"], multi_host, _args["log"], output, console=console)
29903113
progress_disabled = bool(output) or multi_host or bool(_args["log"]) or not console.is_terminal
29913114

29923115
try:

0 commit comments

Comments
 (0)