172 lines
4.8 KiB
Python
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,
|
|
)
|