85742ab165
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
1105 lines
43 KiB
Python
1105 lines
43 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Lightweight benchmark report for the Prometheus + Grafana stack shipped with Agent Lightning."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as dt
|
|
import json
|
|
import math
|
|
from dataclasses import dataclass
|
|
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Sequence, Set, Tuple, cast
|
|
from urllib import error, parse, request
|
|
|
|
|
|
class PrometheusQueryError(RuntimeError):
|
|
"""Raised when Prometheus returns an error payload."""
|
|
|
|
|
|
class PrometheusClient:
|
|
"""Tiny helper around the Prometheus HTTP API."""
|
|
|
|
def __init__(
|
|
self,
|
|
base_url: str,
|
|
timeout: float = 10.0,
|
|
default_time: Optional[dt.datetime] = None,
|
|
):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.timeout = timeout
|
|
self.default_time = default_time
|
|
|
|
def query_vector(self, expr: str, eval_time: Optional[dt.datetime] = None) -> List[Mapping[str, object]]:
|
|
params: Dict[str, str] = {"query": expr}
|
|
query_time = eval_time or self.default_time
|
|
if query_time is not None:
|
|
params["time"] = query_time.isoformat()
|
|
payload = self._get("/api/v1/query", params)
|
|
status = payload.get("status")
|
|
if not isinstance(status, str) or status != "success":
|
|
error_msg = payload.get("error", "unknown error")
|
|
raise PrometheusQueryError(str(error_msg))
|
|
data_obj = payload.get("data", {})
|
|
if isinstance(data_obj, dict):
|
|
data = cast(Dict[str, Any], data_obj)
|
|
else:
|
|
data = {}
|
|
result_type_obj = data.get("resultType")
|
|
result_type = result_type_obj if isinstance(result_type_obj, str) else None
|
|
raw_result_obj = data.get("result", [])
|
|
raw_result: List[object]
|
|
if isinstance(raw_result_obj, list):
|
|
raw_result = cast(List[object], raw_result_obj)
|
|
else:
|
|
raw_result = []
|
|
if result_type == "scalar":
|
|
if len(raw_result) >= 2:
|
|
ts = raw_result[0]
|
|
value = raw_result[1]
|
|
return [{"metric": {}, "value": [ts, value]}]
|
|
return []
|
|
vector_result: List[Mapping[str, object]] = [
|
|
cast(Mapping[str, object], item) for item in raw_result if isinstance(item, Mapping)
|
|
]
|
|
if result_type == "matrix":
|
|
collapsed: List[Dict[str, object]] = []
|
|
for series in vector_result:
|
|
values_obj = series.get("values")
|
|
if isinstance(values_obj, list) and values_obj and isinstance(values_obj[-1], Sequence):
|
|
last = cast(Sequence[object], values_obj[-1])
|
|
else:
|
|
continue
|
|
metric_obj = series.get("metric")
|
|
if isinstance(metric_obj, Mapping):
|
|
metric: Dict[str, object] = dict(cast(Mapping[str, object], metric_obj))
|
|
else:
|
|
metric = {}
|
|
collapsed.append({"metric": metric, "value": list(last)})
|
|
return cast(List[Mapping[str, object]], collapsed)
|
|
if result_type == "vector":
|
|
return vector_result
|
|
return []
|
|
|
|
def query_scalar(self, expr: str, eval_time: Optional[dt.datetime] = None) -> Optional[float]:
|
|
samples = self.query_vector(expr, eval_time=eval_time)
|
|
if not samples:
|
|
return None
|
|
return _sample_value(samples[0])
|
|
|
|
def _get(self, path: str, data: Optional[Mapping[str, str]] = None) -> Dict[str, Any]:
|
|
encoded: Optional[bytes] = None
|
|
if data is not None:
|
|
encoded = parse.urlencode(data).encode()
|
|
req = request.Request(f"{self.base_url}{path}", data=encoded)
|
|
try:
|
|
with request.urlopen(req, timeout=self.timeout) as resp:
|
|
loaded = json.loads(resp.read().decode())
|
|
if isinstance(loaded, dict):
|
|
return cast(Dict[str, Any], loaded)
|
|
return {}
|
|
except error.URLError as exc: # pragma: no cover - network/infra issues
|
|
raise PrometheusQueryError(str(exc)) from exc
|
|
|
|
|
|
def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Summarize benchmark metrics from Prometheus.")
|
|
parser.add_argument("--prom-url", default="http://localhost:9090", help="Base URL for the Prometheus API.")
|
|
parser.add_argument(
|
|
"--store-url",
|
|
default="http://localhost:4747/v1/agl",
|
|
help="Base URL for the Lightning Store API (without the /statistics suffix).",
|
|
)
|
|
parser.add_argument("--timeout", type=float, default=10.0, help="HTTP timeout in seconds.")
|
|
parser.add_argument("--start", type=str, help="ISO timestamp (e.g. 2024-05-01T12:00:00Z).")
|
|
parser.add_argument("--end", type=str, help="ISO timestamp (default: now).")
|
|
parser.add_argument(
|
|
"--duration",
|
|
type=str,
|
|
default="5m",
|
|
help="Fallback duration (e.g. 5m, 1h) used when --start is omitted.",
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def parse_timestamp(value: Optional[str], default: Optional[dt.datetime] = None) -> Optional[dt.datetime]:
|
|
if value is None:
|
|
return default
|
|
try:
|
|
if value.endswith("Z"):
|
|
value = value[:-1] + "+00:00"
|
|
return dt.datetime.fromisoformat(value).astimezone(dt.timezone.utc)
|
|
except ValueError as exc: # pragma: no cover - invalid CLI input
|
|
raise SystemExit(f"Invalid timestamp '{value}': {exc}") from exc
|
|
|
|
|
|
def parse_duration(text: str) -> dt.timedelta:
|
|
units = {"s": 1, "m": 60, "h": 3600}
|
|
if text.isdigit():
|
|
return dt.timedelta(seconds=int(text))
|
|
suffix = text[-1]
|
|
if suffix not in units:
|
|
raise SystemExit(f"Unsupported duration '{text}'. Use Ns/Nm/Nh.")
|
|
try:
|
|
value = int(text[:-1])
|
|
except ValueError as exc: # pragma: no cover - invalid CLI input
|
|
raise SystemExit(f"Invalid duration '{text}': {exc}") from exc
|
|
return dt.timedelta(seconds=value * units[suffix])
|
|
|
|
|
|
def format_window(seconds: float) -> str:
|
|
seconds = max(int(seconds), 1)
|
|
return f"{seconds}s"
|
|
|
|
|
|
def clamp_window_seconds(duration_seconds: float) -> int:
|
|
return max(int(duration_seconds), 1)
|
|
|
|
|
|
def compute_peak_window(duration_seconds: float) -> str:
|
|
peak_seconds = max(min(int(duration_seconds), 60), 1)
|
|
return f"{peak_seconds}s"
|
|
|
|
|
|
def compute_subquery_step(duration_seconds: float) -> str:
|
|
step_seconds = max(int(duration_seconds / 60), 1)
|
|
step_seconds = min(step_seconds, 15)
|
|
return f"{step_seconds}s"
|
|
|
|
|
|
def _sample_value(sample: Mapping[str, object]) -> Optional[float]:
|
|
value_obj = sample.get("value")
|
|
if not isinstance(value_obj, Sequence):
|
|
return None
|
|
value_seq = cast(Sequence[object], value_obj)
|
|
if len(value_seq) < 2:
|
|
return None
|
|
candidate = value_seq[1]
|
|
if isinstance(candidate, (int, float)):
|
|
return float(candidate)
|
|
if isinstance(candidate, str):
|
|
try:
|
|
return float(candidate)
|
|
except ValueError:
|
|
return None
|
|
return None
|
|
|
|
|
|
def vector_to_map(
|
|
samples: Optional[Sequence[Mapping[str, object]]],
|
|
labels: Sequence[str],
|
|
) -> Dict[Any, float]:
|
|
mapping: Dict[Any, float] = {}
|
|
if not samples:
|
|
return mapping
|
|
for sample in samples:
|
|
metric_obj = sample.get("metric", {})
|
|
if isinstance(metric_obj, Mapping):
|
|
metric: Dict[str, object] = dict(cast(Mapping[str, object], metric_obj))
|
|
else:
|
|
metric = {}
|
|
if len(labels) == 1:
|
|
key: Any = str(metric.get(labels[0], ""))
|
|
else:
|
|
key = tuple(str(metric.get(label, "")) for label in labels)
|
|
value = _sample_value(sample)
|
|
if value is not None:
|
|
mapping[key] = value
|
|
return mapping
|
|
|
|
|
|
def _normalize_label_value(value: Any) -> str:
|
|
if value is None:
|
|
return "-"
|
|
text = str(value)
|
|
return text if text else "-"
|
|
|
|
|
|
def vector_to_labeled_map(
|
|
samples: Optional[Sequence[Mapping[str, object]]],
|
|
labels: Sequence[str],
|
|
) -> Dict[Tuple[str, ...], float]:
|
|
mapping: Dict[Tuple[str, ...], float] = {}
|
|
if not samples:
|
|
return mapping
|
|
for sample in samples:
|
|
metric_obj = sample.get("metric", {})
|
|
if isinstance(metric_obj, Mapping):
|
|
metric = dict(cast(Mapping[str, object], metric_obj))
|
|
else:
|
|
metric = {}
|
|
if labels:
|
|
key = tuple(_normalize_label_value(metric.get(label)) for label in labels)
|
|
else:
|
|
key = tuple[str, ...]()
|
|
value = _sample_value(sample)
|
|
if value is not None:
|
|
mapping[key] = value
|
|
return mapping
|
|
|
|
|
|
def sum_by_clause(labels: Sequence[str]) -> str:
|
|
if labels:
|
|
joined = ", ".join(labels)
|
|
return f"sum by ({joined})"
|
|
return "sum"
|
|
|
|
|
|
def histogram_sum_by_clause(labels: Sequence[str]) -> str:
|
|
le_prefixed = ("le", *labels)
|
|
joined = ", ".join(le_prefixed)
|
|
return f"sum by ({joined})"
|
|
|
|
|
|
def histogram_sum_metric_name(bucket_metric: str) -> str:
|
|
if bucket_metric.endswith("_bucket"):
|
|
return f"{bucket_metric[: -len('_bucket')]}_sum"
|
|
return f"{bucket_metric}_sum"
|
|
|
|
|
|
def histogram_count_metric_name(bucket_metric: str) -> str:
|
|
if bucket_metric.endswith("_bucket"):
|
|
return f"{bucket_metric[: -len('_bucket')]}_count"
|
|
return f"{bucket_metric}_count"
|
|
|
|
|
|
def divide_or_none(numerator: Optional[float], denominator: Optional[float]) -> Optional[float]:
|
|
if numerator is None or denominator is None:
|
|
return None
|
|
if denominator == 0:
|
|
return None
|
|
return numerator / denominator
|
|
|
|
|
|
def compute_average_time_map(
|
|
time_totals: Mapping[Tuple[str, ...], float],
|
|
count_totals: Mapping[Tuple[str, ...], float],
|
|
) -> Dict[Tuple[str, ...], float]:
|
|
averages: Dict[Tuple[str, ...], float] = {}
|
|
keys = set(time_totals.keys()).union(count_totals.keys())
|
|
for key in keys:
|
|
avg = divide_or_none(time_totals.get(key), count_totals.get(key))
|
|
if avg is not None:
|
|
averages[key] = avg
|
|
return averages
|
|
|
|
|
|
def safe_vector(client: PrometheusClient, expr: str) -> Optional[List[Mapping[str, object]]]:
|
|
try:
|
|
return client.query_vector(expr)
|
|
except PrometheusQueryError as exc:
|
|
print(f"[warn] Prometheus query failed: {exc} (expr={expr})")
|
|
return None
|
|
|
|
|
|
def safe_scalar(client: PrometheusClient, expr: str) -> Optional[float]:
|
|
try:
|
|
return client.query_scalar(expr)
|
|
except PrometheusQueryError as exc:
|
|
print(f"[warn] Prometheus query failed: {exc} (expr={expr})")
|
|
return None
|
|
|
|
|
|
def fetch_store_statistics(store_url: str, timeout: float) -> Optional[Dict[str, Any]]:
|
|
store_url = store_url.rstrip("/")
|
|
stats_url = f"{store_url}/statistics"
|
|
req = request.Request(stats_url)
|
|
try:
|
|
with request.urlopen(req, timeout=timeout) as resp:
|
|
loaded = json.loads(resp.read().decode())
|
|
if isinstance(loaded, Mapping):
|
|
return dict(cast(Mapping[str, Any], loaded))
|
|
return None
|
|
except error.URLError as exc:
|
|
print(f"[warn] Failed to fetch store statistics: {exc} (url={stats_url})")
|
|
return None
|
|
except json.JSONDecodeError as exc:
|
|
print(f"[warn] Failed to decode store statistics: {exc} (url={stats_url})")
|
|
return None
|
|
except TimeoutError as exc:
|
|
print(f"[warn] Timeout fetching store statistics: {exc} (url={stats_url})")
|
|
return None
|
|
|
|
|
|
@dataclass
|
|
class CollectionThroughput:
|
|
name: str
|
|
count: Optional[float]
|
|
per_sec: Optional[float]
|
|
|
|
|
|
@dataclass
|
|
class MetricRow:
|
|
label_values: Tuple[str, ...]
|
|
avg_rate: Optional[float]
|
|
max_rate: Optional[float]
|
|
min_rate: Optional[float]
|
|
p50: Optional[float]
|
|
p95: Optional[float]
|
|
p99: Optional[float]
|
|
max_latency: Optional[float]
|
|
time_per_sec: Optional[float]
|
|
time_per_request: Optional[float]
|
|
avg_rate_delta: Optional[float]
|
|
p50_delta: Optional[float]
|
|
p95_delta: Optional[float]
|
|
time_delta: Optional[float]
|
|
time_per_request_delta: Optional[float]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MetricGroupSpec:
|
|
title: str
|
|
histogram_bucket_metric: str
|
|
label_names: Tuple[str, ...]
|
|
label_headers: Tuple[str, ...]
|
|
selector: str = ""
|
|
sum_metric: Optional[str] = None
|
|
count_metric: Optional[str] = None
|
|
|
|
|
|
def metric_row_sort_key(row: MetricRow) -> Tuple[str, ...]:
|
|
return row.label_values
|
|
|
|
|
|
STORE_TOTAL_FIELDS = {
|
|
"rollouts": "total_rollouts",
|
|
"spans": "total_spans",
|
|
"attempts": "total_attempts",
|
|
"resources": "total_resources",
|
|
"workers": "total_workers",
|
|
}
|
|
STORE_TOTAL_COLLECTIONS = tuple(STORE_TOTAL_FIELDS.keys())
|
|
|
|
|
|
def _coerce_int(value: Any) -> Optional[int]:
|
|
if isinstance(value, bool):
|
|
return int(value)
|
|
if isinstance(value, int):
|
|
return value
|
|
if isinstance(value, float):
|
|
if math.isnan(value):
|
|
return None
|
|
return int(value)
|
|
if isinstance(value, str):
|
|
try:
|
|
return int(value)
|
|
except ValueError:
|
|
try:
|
|
return int(float(value))
|
|
except ValueError:
|
|
return None
|
|
return None
|
|
|
|
|
|
def extract_store_totals(stats: Optional[Mapping[str, Any]]) -> Dict[str, Optional[int]]:
|
|
totals: Dict[str, Optional[int]] = {}
|
|
if not stats:
|
|
return totals
|
|
for display_name, field_name in STORE_TOTAL_FIELDS.items():
|
|
if field_name in stats:
|
|
totals[display_name] = _coerce_int(stats.get(field_name))
|
|
else:
|
|
totals[display_name] = None
|
|
return totals
|
|
|
|
|
|
def gather_collection_throughput(
|
|
client: PrometheusClient, collections: Sequence[str], duration_seconds: float
|
|
) -> List[CollectionThroughput]:
|
|
rows: List[CollectionThroughput] = []
|
|
window = format_window(duration_seconds)
|
|
for collection in collections:
|
|
# Successful insert operations reflect the number of new records.
|
|
expr = (
|
|
"sum("
|
|
f'increase(mongo_operation_total{{collection="{collection}", operation="insert", status="ok"}}[{window}])'
|
|
")"
|
|
)
|
|
count = safe_scalar(client, expr)
|
|
if count is not None and count < 0:
|
|
count = 0.0
|
|
per_sec = (count / duration_seconds) if (count is not None and duration_seconds > 0) else None
|
|
rows.append(CollectionThroughput(collection, count, per_sec))
|
|
return rows
|
|
|
|
|
|
def gather_metric_group(
|
|
client: PrometheusClient,
|
|
spec: MetricGroupSpec,
|
|
*,
|
|
window: str,
|
|
window_seconds: int,
|
|
peak_window: str,
|
|
subquery_step: str,
|
|
half_window: Optional[str],
|
|
half_window_seconds: Optional[int],
|
|
) -> List[MetricRow]:
|
|
label_names = spec.label_names
|
|
sum_clause = sum_by_clause(label_names)
|
|
hist_clause = histogram_sum_by_clause(label_names)
|
|
bucket_metric = f"{spec.histogram_bucket_metric}{spec.selector}" if spec.selector else spec.histogram_bucket_metric
|
|
base_sum_metric = spec.sum_metric or histogram_sum_metric_name(spec.histogram_bucket_metric)
|
|
sum_metric = f"{base_sum_metric}{spec.selector}" if spec.selector else base_sum_metric
|
|
base_count_metric = spec.count_metric or histogram_count_metric_name(spec.histogram_bucket_metric)
|
|
count_metric = f"{base_count_metric}{spec.selector}" if spec.selector else base_count_metric
|
|
|
|
count_total_expr = f"{sum_clause}(increase({count_metric}[{window}]))"
|
|
count_total_map = vector_to_labeled_map(safe_vector(client, count_total_expr), label_names)
|
|
avg_map = {key: value / window_seconds for key, value in count_total_map.items()} if window_seconds > 0 else {}
|
|
|
|
peak_expr = f"{sum_clause}(irate({count_metric}[{peak_window}]))"
|
|
max_expr = f"max_over_time(({peak_expr})[{window}:{subquery_step}])"
|
|
min_expr = f"min_over_time(({peak_expr})[{window}:{subquery_step}])"
|
|
max_map = vector_to_labeled_map(safe_vector(client, max_expr), label_names)
|
|
min_map = vector_to_labeled_map(safe_vector(client, min_expr), label_names)
|
|
|
|
p50_map = vector_to_labeled_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.50, {hist_clause}(increase({bucket_metric}[{window}])))",
|
|
),
|
|
label_names,
|
|
)
|
|
p95_map = vector_to_labeled_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.95, {hist_clause}(increase({bucket_metric}[{window}])))",
|
|
),
|
|
label_names,
|
|
)
|
|
p99_map = vector_to_labeled_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.99, {hist_clause}(increase({bucket_metric}[{window}])))",
|
|
),
|
|
label_names,
|
|
)
|
|
max_latency_map = vector_to_labeled_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(1.00, {hist_clause}(increase({bucket_metric}[{window}])))",
|
|
),
|
|
label_names,
|
|
)
|
|
|
|
time_total_expr = f"{sum_clause}(increase({sum_metric}[{window}]))"
|
|
time_total_map = vector_to_labeled_map(safe_vector(client, time_total_expr), label_names)
|
|
time_rate_map = {key: value / window_seconds for key, value in time_total_map.items()} if window_seconds > 0 else {}
|
|
avg_time_map = compute_average_time_map(time_total_map, count_total_map)
|
|
|
|
if half_window and half_window_seconds and half_window_seconds > 0:
|
|
count_late_expr = f"{sum_clause}(increase({count_metric}[{half_window}]))"
|
|
count_early_expr = f"{sum_clause}(increase({count_metric}[{half_window}] offset {half_window}))"
|
|
count_late_total_map = vector_to_labeled_map(safe_vector(client, count_late_expr), label_names)
|
|
count_early_total_map = vector_to_labeled_map(safe_vector(client, count_early_expr), label_names)
|
|
avg_late_map = {key: value / half_window_seconds for key, value in count_late_total_map.items()}
|
|
avg_early_map = {key: value / half_window_seconds for key, value in count_early_total_map.items()}
|
|
|
|
p50_late_expr = f"histogram_quantile(0.50, {hist_clause}(increase({bucket_metric}[{half_window}])))"
|
|
p50_early_expr = (
|
|
f"histogram_quantile(0.50, {hist_clause}(increase({bucket_metric}[{half_window}] offset {half_window})))"
|
|
)
|
|
p50_late_map = vector_to_labeled_map(safe_vector(client, p50_late_expr), label_names)
|
|
p50_early_map = vector_to_labeled_map(safe_vector(client, p50_early_expr), label_names)
|
|
|
|
p95_late_expr = f"histogram_quantile(0.95, {hist_clause}(increase({bucket_metric}[{half_window}])))"
|
|
p95_early_expr = (
|
|
f"histogram_quantile(0.95, {hist_clause}(increase({bucket_metric}[{half_window}] offset {half_window})))"
|
|
)
|
|
p95_late_map = vector_to_labeled_map(safe_vector(client, p95_late_expr), label_names)
|
|
p95_early_map = vector_to_labeled_map(safe_vector(client, p95_early_expr), label_names)
|
|
|
|
time_late_expr = f"{sum_clause}(increase({sum_metric}[{half_window}]))"
|
|
time_early_expr = f"{sum_clause}(increase({sum_metric}[{half_window}] offset {half_window}))"
|
|
time_late_total_map = vector_to_labeled_map(safe_vector(client, time_late_expr), label_names)
|
|
time_early_total_map = vector_to_labeled_map(safe_vector(client, time_early_expr), label_names)
|
|
time_late_map = {key: value / half_window_seconds for key, value in time_late_total_map.items()}
|
|
time_early_map = {key: value / half_window_seconds for key, value in time_early_total_map.items()}
|
|
avg_time_late_map = compute_average_time_map(time_late_total_map, count_late_total_map)
|
|
avg_time_early_map = compute_average_time_map(time_early_total_map, count_early_total_map)
|
|
else:
|
|
count_late_total_map: Dict[Tuple[str, ...], float] = {}
|
|
count_early_total_map: Dict[Tuple[str, ...], float] = {}
|
|
avg_late_map: Dict[Tuple[str, ...], float] = {}
|
|
avg_early_map: Dict[Tuple[str, ...], float] = {}
|
|
p50_late_map: Dict[Tuple[str, ...], float] = {}
|
|
p50_early_map: Dict[Tuple[str, ...], float] = {}
|
|
p95_late_map: Dict[Tuple[str, ...], float] = {}
|
|
p95_early_map: Dict[Tuple[str, ...], float] = {}
|
|
time_late_map: Dict[Tuple[str, ...], float] = {}
|
|
time_early_map: Dict[Tuple[str, ...], float] = {}
|
|
time_late_total_map = {}
|
|
time_early_total_map = {}
|
|
avg_time_late_map = {}
|
|
avg_time_early_map = {}
|
|
|
|
all_keys: Set[Tuple[str, ...]] = set()
|
|
all_keys.update(count_total_map.keys())
|
|
all_keys.update(avg_map.keys())
|
|
all_keys.update(max_map.keys())
|
|
all_keys.update(min_map.keys())
|
|
all_keys.update(p50_map.keys())
|
|
all_keys.update(p95_map.keys())
|
|
all_keys.update(p99_map.keys())
|
|
all_keys.update(max_latency_map.keys())
|
|
all_keys.update(time_rate_map.keys())
|
|
all_keys.update(avg_time_map.keys())
|
|
all_keys.update(count_late_total_map.keys())
|
|
all_keys.update(count_early_total_map.keys())
|
|
all_keys.update(avg_late_map.keys())
|
|
all_keys.update(avg_early_map.keys())
|
|
all_keys.update(p50_late_map.keys())
|
|
all_keys.update(p50_early_map.keys())
|
|
all_keys.update(p95_late_map.keys())
|
|
all_keys.update(p95_early_map.keys())
|
|
all_keys.update(time_late_map.keys())
|
|
all_keys.update(time_early_map.keys())
|
|
all_keys.update(avg_time_late_map.keys())
|
|
all_keys.update(avg_time_early_map.keys())
|
|
|
|
if not all_keys:
|
|
return []
|
|
|
|
def build_delta(
|
|
late_map: Mapping[Tuple[str, ...], float],
|
|
early_map: Mapping[Tuple[str, ...], float],
|
|
key: Tuple[str, ...],
|
|
) -> Optional[float]:
|
|
late = late_map.get(key)
|
|
early = early_map.get(key)
|
|
if late is None or early is None:
|
|
return None
|
|
return late - early
|
|
|
|
rows: List[MetricRow] = []
|
|
for key in sorted(all_keys):
|
|
rows.append(
|
|
MetricRow(
|
|
label_values=key,
|
|
avg_rate=avg_map.get(key),
|
|
max_rate=max_map.get(key),
|
|
min_rate=min_map.get(key),
|
|
p50=p50_map.get(key),
|
|
p95=p95_map.get(key),
|
|
p99=p99_map.get(key),
|
|
max_latency=max_latency_map.get(key),
|
|
time_per_sec=time_rate_map.get(key),
|
|
time_per_request=avg_time_map.get(key),
|
|
avg_rate_delta=build_delta(avg_late_map, avg_early_map, key),
|
|
p50_delta=build_delta(p50_late_map, p50_early_map, key),
|
|
p95_delta=build_delta(p95_late_map, p95_early_map, key),
|
|
time_delta=build_delta(time_late_map, time_early_map, key),
|
|
time_per_request_delta=build_delta(avg_time_late_map, avg_time_early_map, key),
|
|
)
|
|
)
|
|
return rows
|
|
|
|
|
|
def gather_diagnostics(client: PrometheusClient, window: str) -> Dict[str, Any]:
|
|
diagnostics: Dict[str, Any] = {}
|
|
diagnostics["mongo_ops"] = vector_to_map(
|
|
safe_vector(
|
|
client,
|
|
f"sum by (operation)(rate(mongo_operation_total{{operation!='ensure_collection'}}[{window}]))",
|
|
),
|
|
("operation",),
|
|
)
|
|
diagnostics["mongo_latency_p50"] = vector_to_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.50, sum by (le, operation)(rate(mongo_operation_duration_seconds_bucket{{operation!='ensure_collection'}}[{window}])))",
|
|
),
|
|
("operation",),
|
|
)
|
|
diagnostics["mongo_latency_p95"] = vector_to_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.95, sum by (le, operation)(rate(mongo_operation_duration_seconds_bucket{{operation!='ensure_collection'}}[{window}])))",
|
|
),
|
|
("operation",),
|
|
)
|
|
diagnostics["mongo_latency_p99"] = vector_to_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.99, sum by (le, operation)(rate(mongo_operation_duration_seconds_bucket{{operation!='ensure_collection'}}[{window}])))",
|
|
),
|
|
("operation",),
|
|
)
|
|
opcounters_samples = safe_vector(client, f"sum by (legacy_op_type)(rate(mongodb_ss_opcounters[{window}]))")
|
|
mongo_opcounters: Dict[str, float] = {}
|
|
if opcounters_samples:
|
|
for sample in opcounters_samples:
|
|
metric_obj = sample.get("metric", {})
|
|
if isinstance(metric_obj, Mapping):
|
|
metric: Dict[str, object] = dict(cast(Mapping[str, object], metric_obj))
|
|
else:
|
|
metric = {}
|
|
label_value = metric.get("legacy_op_type") or metric.get("type")
|
|
label = str(label_value) if label_value is not None else ""
|
|
value = _sample_value(sample)
|
|
if value is not None:
|
|
mongo_opcounters[str(label or "-")] = value
|
|
diagnostics["mongo_opcounters"] = mongo_opcounters
|
|
diagnostics["mongo_connections"] = safe_scalar(client, "avg(mongodb_ss_connections{conn_type='current'})")
|
|
diagnostics["memory_lock_rate"] = vector_to_map(
|
|
safe_vector(client, f"sum by (collection)(rate(memory_collection_lock_rate_total[{window}]))"),
|
|
("collection",),
|
|
)
|
|
diagnostics["memory_lock_p50"] = vector_to_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.50, sum by (le, collection)(rate(memory_collection_lock_latency_seconds_bucket[{window}])))",
|
|
),
|
|
("collection",),
|
|
)
|
|
diagnostics["memory_lock_p95"] = vector_to_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.95, sum by (le, collection)(rate(memory_collection_lock_latency_seconds_bucket[{window}])))",
|
|
),
|
|
("collection",),
|
|
)
|
|
diagnostics["memory_lock_p99"] = vector_to_map(
|
|
safe_vector(
|
|
client,
|
|
f"histogram_quantile(0.99, sum by (le, collection)(rate(memory_collection_lock_latency_seconds_bucket[{window}])))",
|
|
),
|
|
("collection",),
|
|
)
|
|
diagnostics["cpu_usage"] = safe_scalar(client, f"1 - avg(rate(node_cpu_seconds_total{{mode='idle'}}[{window}]))")
|
|
diagnostics["memory_total"] = safe_scalar(client, "avg(node_memory_MemTotal_bytes)")
|
|
diagnostics["memory_available"] = safe_scalar(client, "avg(node_memory_MemAvailable_bytes)")
|
|
diagnostics["network_rx"] = safe_scalar(
|
|
client,
|
|
f"sum(rate(node_network_receive_bytes_total{{device!~'lo|docker.*'}}[{window}]))",
|
|
)
|
|
diagnostics["network_tx"] = safe_scalar(
|
|
client,
|
|
f"sum(rate(node_network_transmit_bytes_total{{device!~'lo|docker.*'}}[{window}]))",
|
|
)
|
|
diagnostics["disk_read_ops"] = safe_scalar(client, f"sum(rate(node_disk_reads_completed_total[{window}]))")
|
|
diagnostics["disk_write_ops"] = safe_scalar(client, f"sum(rate(node_disk_writes_completed_total[{window}]))")
|
|
diagnostics["disk_read_bytes"] = safe_scalar(client, f"sum(rate(node_disk_read_bytes_total[{window}]))")
|
|
diagnostics["disk_write_bytes"] = safe_scalar(client, f"sum(rate(node_disk_written_bytes_total[{window}]))")
|
|
return diagnostics
|
|
|
|
|
|
def render_table(headers: Sequence[str], rows: Sequence[Sequence[str]]) -> List[str]:
|
|
if not rows:
|
|
return [f"(no data for {headers})"]
|
|
widths = [len(h) for h in headers]
|
|
rendered: List[List[str]] = []
|
|
for row in rows:
|
|
rendered_row = [str(cell) for cell in row]
|
|
for idx, cell in enumerate(rendered_row):
|
|
widths[idx] = max(widths[idx], len(cell))
|
|
rendered.append(rendered_row)
|
|
|
|
lines = [
|
|
" | ".join(headers[idx].ljust(widths[idx]) for idx in range(len(headers))),
|
|
"-+-".join("-" * widths[idx] for idx in range(len(headers))),
|
|
]
|
|
for row in rendered:
|
|
lines.append(" | ".join(row[idx].ljust(widths[idx]) for idx in range(len(headers))))
|
|
return lines
|
|
|
|
|
|
def fmt_rate(value: Optional[float]) -> str:
|
|
if value is None or math.isnan(value):
|
|
return "-"
|
|
return f"{value:.2f}/s"
|
|
|
|
|
|
def fmt_latency(value: Optional[float]) -> str:
|
|
if value is None or math.isnan(value):
|
|
return "-"
|
|
if abs(value) < 10:
|
|
return f"{value * 1e3:.2f} ms"
|
|
return f"{value:.2f} s"
|
|
|
|
|
|
def fmt_bytes(value: Optional[float]) -> str:
|
|
if value is None or math.isnan(value):
|
|
return "-"
|
|
units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
|
idx = 0
|
|
current = value
|
|
while current >= 1024 and idx < len(units) - 1:
|
|
current /= 1024
|
|
idx += 1
|
|
return f"{current:.2f} {units[idx]}"
|
|
|
|
|
|
def fmt_percentage(value: Optional[float]) -> str:
|
|
if value is None or math.isnan(value):
|
|
return "-"
|
|
return f"{value * 100:4.1f}%"
|
|
|
|
|
|
def section(title: str, body: Iterable[str]) -> List[str]:
|
|
lines = [f"## {title}"]
|
|
lines.extend(body)
|
|
lines.append("")
|
|
return lines
|
|
|
|
|
|
def render_metric_group_table(
|
|
spec: MetricGroupSpec,
|
|
rows: Sequence[MetricRow],
|
|
extra_columns: Optional[Sequence[Tuple[str, Callable[[MetricRow], str]]]] = None,
|
|
) -> List[str]:
|
|
headers = list(spec.label_headers)
|
|
headers.extend(
|
|
[
|
|
"Avg Rate/s",
|
|
"Max Rate/s",
|
|
"Min Rate/s",
|
|
"P50",
|
|
"P95",
|
|
"P99",
|
|
"Max Latency",
|
|
"Time/s",
|
|
"Avg Time/req",
|
|
"Avg Rate Δ",
|
|
"P50 Δ",
|
|
"P95 Δ",
|
|
"Time Δ",
|
|
"Avg Time/req Δ",
|
|
]
|
|
)
|
|
column_renderers: Sequence[Tuple[str, Callable[[MetricRow], str]]] = extra_columns or ()
|
|
for header, _ in column_renderers:
|
|
headers.append(header)
|
|
if not rows:
|
|
return render_table(headers, [])
|
|
sorted_rows = sorted(rows, key=metric_row_sort_key)
|
|
rendered_rows: List[List[str]] = []
|
|
for row in sorted_rows:
|
|
label_cells = list(row.label_values) if spec.label_headers else []
|
|
metrics = [
|
|
fmt_rate(row.avg_rate),
|
|
fmt_rate(row.max_rate),
|
|
fmt_rate(row.min_rate),
|
|
fmt_latency(row.p50),
|
|
fmt_latency(row.p95),
|
|
fmt_latency(row.p99),
|
|
fmt_latency(row.max_latency),
|
|
fmt_latency(row.time_per_sec),
|
|
fmt_latency(row.time_per_request),
|
|
fmt_rate(row.avg_rate_delta),
|
|
fmt_latency(row.p50_delta),
|
|
fmt_latency(row.p95_delta),
|
|
fmt_latency(row.time_delta),
|
|
fmt_latency(row.time_per_request_delta),
|
|
]
|
|
extra_cells = [renderer(row) for _, renderer in column_renderers]
|
|
rendered_rows.append(label_cells + metrics + extra_cells)
|
|
return render_table(headers, rendered_rows)
|
|
|
|
|
|
def make_time_share_column(
|
|
*,
|
|
label_index: int,
|
|
column_title: str,
|
|
time_per_sec_map: Mapping[str, Optional[float]],
|
|
) -> Tuple[str, Callable[[MetricRow], str]]:
|
|
def render_cell(row: MetricRow) -> str:
|
|
if not row.label_values or len(row.label_values) <= label_index:
|
|
return "-"
|
|
label_value = row.label_values[label_index]
|
|
store_time = time_per_sec_map.get(label_value)
|
|
collection_time = row.time_per_sec
|
|
if (
|
|
store_time is None
|
|
or collection_time is None
|
|
or math.isnan(store_time)
|
|
or math.isnan(collection_time)
|
|
or store_time <= 0
|
|
):
|
|
return "-"
|
|
return fmt_percentage(collection_time / store_time)
|
|
|
|
return (column_title, render_cell)
|
|
|
|
|
|
def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
args = parse_args(argv)
|
|
end = parse_timestamp(args.end, default=dt.datetime.now(dt.timezone.utc))
|
|
if end is None:
|
|
raise SystemExit("End timestamp could not be determined.")
|
|
start = parse_timestamp(args.start)
|
|
if start is None:
|
|
duration = parse_duration(args.duration)
|
|
start = end - duration
|
|
assert start is not None
|
|
duration_seconds = max((end - start).total_seconds(), 1.0)
|
|
window_seconds = clamp_window_seconds(duration_seconds)
|
|
window = format_window(duration_seconds)
|
|
peak_window = compute_peak_window(duration_seconds)
|
|
subquery_step = compute_subquery_step(duration_seconds)
|
|
half_window_seconds = window_seconds // 2 if window_seconds // 2 >= 1 else None
|
|
half_window = format_window(half_window_seconds) if half_window_seconds else None
|
|
|
|
client = PrometheusClient(args.prom_url, timeout=args.timeout, default_time=end)
|
|
store_stats = fetch_store_statistics(args.store_url, timeout=args.timeout)
|
|
store_totals = extract_store_totals(store_stats)
|
|
lines: List[str] = [
|
|
f"Agent Lightning benchmark report",
|
|
f"Range: {start.isoformat()} — {end.isoformat()} ({duration_seconds:.0f}s window)",
|
|
f"Prometheus: {args.prom_url}",
|
|
f"Store: {args.store_url}",
|
|
"",
|
|
]
|
|
|
|
# Throughput
|
|
throughput_rows = gather_collection_throughput(
|
|
client, collections=STORE_TOTAL_COLLECTIONS, duration_seconds=duration_seconds
|
|
)
|
|
throughput_table: List[List[str]] = []
|
|
for item in throughput_rows:
|
|
store_total = store_totals.get(item.name)
|
|
if store_total is not None:
|
|
count_value: Optional[int] = store_total
|
|
elif item.count is not None:
|
|
count_value = int(item.count)
|
|
else:
|
|
count_value = None
|
|
if count_value is None:
|
|
count_str = "-"
|
|
else:
|
|
count_str = f"{count_value:,}"
|
|
if count_value is not None and duration_seconds > 0:
|
|
per_sec_value = float(count_value) / duration_seconds
|
|
else:
|
|
per_sec_value = item.per_sec
|
|
throughput_table.append([item.name, count_str, fmt_rate(per_sec_value)])
|
|
lines.extend(
|
|
section(
|
|
"Rollout / Attempt / Span / Resource / Worker Throughput",
|
|
render_table(["Collection", "Count", "Per Sec"], throughput_table),
|
|
)
|
|
)
|
|
|
|
metric_categories: Sequence[Tuple[str, Sequence[MetricGroupSpec]]] = [
|
|
(
|
|
"HTTP Metrics",
|
|
(
|
|
MetricGroupSpec(
|
|
title="agl.http ungrouped",
|
|
histogram_bucket_metric="agl_http_latency_bucket",
|
|
label_names=tuple(),
|
|
label_headers=tuple(),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.http grouped by path, method",
|
|
histogram_bucket_metric="agl_http_latency_bucket",
|
|
label_names=("path", "method"),
|
|
label_headers=("Path", "Method"),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.http grouped by path, method, status",
|
|
histogram_bucket_metric="agl_http_latency_bucket",
|
|
label_names=("path", "method", "status"),
|
|
label_headers=("Path", "Method", "Status"),
|
|
),
|
|
),
|
|
),
|
|
(
|
|
"Store Metrics",
|
|
(
|
|
MetricGroupSpec(
|
|
title="agl.store ungrouped",
|
|
histogram_bucket_metric="agl_store_latency_bucket",
|
|
label_names=tuple(),
|
|
label_headers=tuple(),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.store grouped by method",
|
|
histogram_bucket_metric="agl_store_latency_bucket",
|
|
label_names=("method",),
|
|
label_headers=("Method",),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.store grouped by method, status",
|
|
histogram_bucket_metric="agl_store_latency_bucket",
|
|
label_names=("method", "status"),
|
|
label_headers=("Method", "Status"),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.store grouped by store_pubmeth, method, status",
|
|
histogram_bucket_metric="agl_store_latency_bucket",
|
|
label_names=("store_pubmeth", "method", "status"),
|
|
label_headers=("Store Method", "Private Method", "Status"),
|
|
),
|
|
),
|
|
),
|
|
(
|
|
"Rollout Outcomes",
|
|
(
|
|
MetricGroupSpec(
|
|
title="agl.rollouts ungrouped",
|
|
histogram_bucket_metric="agl_rollouts_duration_bucket",
|
|
label_names=tuple(),
|
|
label_headers=tuple(),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.rollouts grouped by status",
|
|
histogram_bucket_metric="agl_rollouts_duration_bucket",
|
|
label_names=("status",),
|
|
label_headers=("Status",),
|
|
),
|
|
),
|
|
),
|
|
(
|
|
"Collection Metrics",
|
|
(
|
|
MetricGroupSpec(
|
|
title="agl.collections grouped by store_pubmeth, collection, operation",
|
|
histogram_bucket_metric="agl_collections_latency_bucket",
|
|
label_names=("store_pubmeth", "collection"),
|
|
label_headers=("Store Method", "Collection"),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.collections grouped by store_pubmeth, collection, operation, status",
|
|
histogram_bucket_metric="agl_collections_latency_bucket",
|
|
label_names=("store_pubmeth", "collection", "operation", "status"),
|
|
label_headers=("Store Method", "Collection", "Operation", "Status"),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.collections grouped by store_privmeth, collection, operation, status",
|
|
histogram_bucket_metric="agl_collections_latency_bucket",
|
|
label_names=("store_privmeth", "collection", "operation", "status"),
|
|
label_headers=("Store Priv Meth", "Collection", "Operation", "Status"),
|
|
),
|
|
MetricGroupSpec(
|
|
title="agl.collections grouped by collection, operation, status",
|
|
histogram_bucket_metric="agl_collections_latency_bucket",
|
|
label_names=("collection", "operation", "status"),
|
|
label_headers=("Collection", "Operation", "Status"),
|
|
),
|
|
),
|
|
),
|
|
]
|
|
|
|
store_method_time_per_sec: Dict[str, Optional[float]] = {}
|
|
|
|
for category_title, specs in metric_categories:
|
|
category_lines: List[str] = []
|
|
for idx, spec in enumerate(specs):
|
|
rows = gather_metric_group(
|
|
client,
|
|
spec,
|
|
window=window,
|
|
window_seconds=window_seconds,
|
|
peak_window=peak_window,
|
|
subquery_step=subquery_step,
|
|
half_window=half_window,
|
|
half_window_seconds=half_window_seconds,
|
|
)
|
|
if spec.histogram_bucket_metric == "agl_store_latency_bucket" and spec.label_names == ("method",):
|
|
store_method_time_per_sec = {
|
|
row.label_values[0]: row.time_per_sec
|
|
for row in rows
|
|
if row.label_values and len(row.label_values) == 1
|
|
}
|
|
extra_column_specs: List[Tuple[str, Callable[[MetricRow], str]]] = []
|
|
if "store_pubmeth" in spec.label_names:
|
|
pubmeth_index = spec.label_names.index("store_pubmeth")
|
|
extra_column_specs.append(
|
|
make_time_share_column(
|
|
label_index=pubmeth_index,
|
|
column_title="Share %",
|
|
time_per_sec_map=store_method_time_per_sec,
|
|
)
|
|
)
|
|
if "store_privmeth" in spec.label_names:
|
|
privmeth_index = spec.label_names.index("store_privmeth")
|
|
extra_column_specs.append(
|
|
make_time_share_column(
|
|
label_index=privmeth_index,
|
|
column_title="Share (Priv) %",
|
|
time_per_sec_map=store_method_time_per_sec,
|
|
)
|
|
)
|
|
extra_columns = extra_column_specs or None
|
|
category_lines.append("### " + spec.title)
|
|
category_lines.extend(render_metric_group_table(spec, rows, extra_columns=extra_columns))
|
|
if idx != len(specs) - 1:
|
|
category_lines.append("")
|
|
lines.extend(section(category_title, category_lines))
|
|
|
|
# Diagnostics
|
|
diag = gather_diagnostics(client, window)
|
|
diagnostics_blocks: List[List[str]] = []
|
|
|
|
mongo_ops = cast(Dict[str, float], diag.get("mongo_ops", {}))
|
|
mongo_latency_p50 = cast(Dict[str, float], diag.get("mongo_latency_p50", {}))
|
|
mongo_latency_p95 = cast(Dict[str, float], diag.get("mongo_latency_p95", {}))
|
|
mongo_latency_p99 = cast(Dict[str, float], diag.get("mongo_latency_p99", {}))
|
|
mongo_op_keys = sorted(
|
|
{
|
|
*mongo_ops.keys(),
|
|
*mongo_latency_p50.keys(),
|
|
*mongo_latency_p95.keys(),
|
|
*mongo_latency_p99.keys(),
|
|
},
|
|
key=str,
|
|
)
|
|
mongo_ops_rows = [
|
|
[
|
|
op or "-",
|
|
fmt_rate(mongo_ops.get(op)),
|
|
fmt_latency(mongo_latency_p50.get(op)),
|
|
fmt_latency(mongo_latency_p95.get(op)),
|
|
fmt_latency(mongo_latency_p99.get(op)),
|
|
]
|
|
for op in mongo_op_keys
|
|
]
|
|
diagnostics_blocks.append(render_table(["Mongo Operation", "Ops/s", "P50", "P95", "P99"], mongo_ops_rows))
|
|
|
|
mongo_opcounters = cast(Dict[str, float], diag.get("mongo_opcounters", {}))
|
|
mongo_opcounters_rows = [
|
|
[op_type or "-", fmt_rate(rate)]
|
|
for op_type, rate in sorted(mongo_opcounters.items(), key=lambda item: str(item[0]))
|
|
]
|
|
diagnostics_blocks.append(render_table(["MongoDB Opcounter", "Ops/s"], mongo_opcounters_rows))
|
|
|
|
mongo_misc_rows: List[List[str]] = []
|
|
if diag.get("mongo_connections") is not None:
|
|
mongo_misc_rows.append(["MongoDB connections (avg)", f"{diag['mongo_connections']:.2f}"])
|
|
if mongo_misc_rows:
|
|
diagnostics_blocks.append(render_table(["Mongo Metric", "Value"], mongo_misc_rows))
|
|
|
|
node_rows: List[List[str]] = []
|
|
if diag.get("cpu_usage") is not None:
|
|
node_rows.append(["CPU usage", fmt_percentage(diag["cpu_usage"])])
|
|
mem_total = diag.get("memory_total")
|
|
mem_available = diag.get("memory_available")
|
|
if mem_total and mem_available:
|
|
used = mem_total - mem_available
|
|
node_rows.append(
|
|
["Memory usage", f"{fmt_bytes(used)} / {fmt_bytes(mem_total)} ({fmt_percentage(used / mem_total)})"]
|
|
)
|
|
node_rows.append(["Network rx", f"{fmt_bytes(diag.get('network_rx'))}/s"])
|
|
node_rows.append(["Network tx", f"{fmt_bytes(diag.get('network_tx'))}/s"])
|
|
node_rows.append(["Disk read ops", fmt_rate(diag.get("disk_read_ops"))])
|
|
node_rows.append(["Disk read bytes", f"{fmt_bytes(diag.get('disk_read_bytes'))}/s"])
|
|
node_rows.append(["Disk write ops", fmt_rate(diag.get("disk_write_ops"))])
|
|
node_rows.append(["Disk write bytes", f"{fmt_bytes(diag.get('disk_write_bytes'))}/s"])
|
|
diagnostics_blocks.append(render_table(["Node Metric", "Value"], node_rows))
|
|
|
|
diagnostics_lines: List[str] = []
|
|
for idx, block in enumerate(diagnostics_blocks):
|
|
diagnostics_lines.extend(block)
|
|
if idx != len(diagnostics_blocks) - 1:
|
|
diagnostics_lines.append("")
|
|
|
|
lines.extend(section("Diagnostics", diagnostics_lines))
|
|
|
|
print("\n".join(lines))
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover - manual execution
|
|
main()
|