Files
2026-07-13 12:24:33 +08:00

172 lines
4.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Metrics formatters — control *how* metrics are rendered.
A formatter converts a title + sections into a string (or dict).
Formatters are attached to handlers, separating rendering from destination.
"""
# Standard
from typing import Any
import abc
import inspect
import json
# First Party
from lmcache.cli.metrics.section import Section, sections_to_dict
class MetricsFormatter(abc.ABC):
"""Base class for metrics formatters."""
@abc.abstractmethod
def format(self, title: str, sections: list[Section]) -> str:
"""Render metrics into a string.
Args:
title: The report title.
sections: Ordered list of ``Section`` objects.
Returns:
The formatted string.
"""
# ---------------------------------------------------------------------------
# Formatter registry
# ---------------------------------------------------------------------------
_FORMATTER_REGISTRY: dict[str, type[MetricsFormatter]] = {}
def register_formatter(name: str):
"""Decorator that registers a ``MetricsFormatter`` subclass under *name*.
Args:
name: The format name used for CLI lookup (e.g. ``"json"``).
Returns:
A class decorator that registers the class and returns it unchanged.
"""
def decorator(cls: type[MetricsFormatter]) -> type[MetricsFormatter]:
_FORMATTER_REGISTRY[name] = cls
return cls
return decorator
def get_formatter(name: str, **kwargs: Any) -> MetricsFormatter:
"""Instantiate a formatter by its registered name.
Args:
name: Registered format name (e.g. ``"terminal"``, ``"json"``).
**kwargs: Forwarded to the formatter constructor (e.g. ``width``).
Returns:
A new formatter instance.
Raises:
ValueError: If *name* is not registered.
"""
cls = _FORMATTER_REGISTRY.get(name)
if cls is None:
available = ", ".join(sorted(_FORMATTER_REGISTRY))
raise ValueError(f"Unknown format {name!r}. Available: {available}")
# Only forward kwargs that the constructor accepts.
sig = inspect.signature(cls.__init__)
valid = {k: v for k, v in kwargs.items() if k in sig.parameters}
return cls(**valid)
# ---------------------------------------------------------------------------
# Built-in formatters
# ---------------------------------------------------------------------------
def _format_value(value: Any) -> str:
"""Format a metric value for terminal display."""
if value is None:
return "N/A"
if isinstance(value, float):
return f"{value:.2f}"
return str(value)
@register_formatter("terminal")
class TerminalFormatter(MetricsFormatter):
"""Plain ASCII table formatter for terminal output.
Title is centered in ``=`` borders, section headers are centered in
``-`` borders, key-value lines have left-aligned labels and
right-aligned values.
Args:
width: Target total character width for the output.
"""
def __init__(self, width: int = 48) -> None:
self._width = width
def format(self, title: str, sections: list[Section]) -> str:
"""Render metrics as an ASCII table.
Args:
title: The report title.
sections: Ordered list of ``Section`` objects.
Returns:
Rendered multi-line string.
"""
width = self._width
lines: list[str] = []
# Title bar
title_text = f" {title} "
lines.append(title_text.center(width, "="))
for section in sections:
# Section header (skip for unnamed section)
if section.label is not None:
header_text = f" {section.label} "
lines.append(header_text.center(width, "-"))
for _key, label, value in section.entries:
formatted = _format_value(value)
label_part = f"{label}:"
padding = width - len(label_part) - len(formatted)
if padding < 1:
padding = 1
lines.append(f"{label_part}{' ' * padding}{formatted}")
# Footer
lines.append("=" * width)
return "\n".join(lines)
@register_formatter("json")
class JsonFormatter(MetricsFormatter):
"""Renders metrics as a JSON string.
Args:
indent: JSON indentation level.
"""
def __init__(self, indent: int = 2) -> None:
self._indent = indent
def format(self, title: str, sections: list[Section]) -> str:
"""Render metrics as indented JSON.
Args:
title: The report title.
sections: Ordered list of ``Section`` objects.
Returns:
JSON string.
"""
return json.dumps(
sections_to_dict(title, sections),
indent=self._indent,
)