Compare commits

..
Author SHA1 Message Date
Hari DhanushkodiandRobin 3accef6173 fix(cli): use server-side deployment metrics summaries
Request summary=true and pass through the server summary instead of downloading and aggregating chart data locally. Preserve compact and raw output and existing time-window defaults.

Co-authored-by: Robin <open-swe@users.noreply.github.com>
2026-11-09 18:59:26 +00:00
5 changed files with 62 additions and 37 deletions

No files matched your search

+14 -9
View File
@@ -129,8 +129,9 @@ The top level includes `deployment_id`, `start_time`, `end_time`, and
point bounds. The CLI resolves omitted bounds once: end defaults to now (UTC),
and start defaults to 15 minutes before the resolved end. With only `--end-time`,
start is 15 minutes before that value; with only `--start-time`, end is now. Both
values are sent explicitly to the backend and output, even when there are no points. `step_seconds` is the most common gap between consecutive
timestamps within series, with ties broken by the smallest gap. Whole seconds
values are sent explicitly to the backend and included in default compact output,
even when there are no points. `step_seconds` is the most common gap between
consecutive timestamps within series, with ties broken by the smallest gap. Whole seconds
are integers; fractional seconds are preserved. It is `null` only when every
series has fewer than two timestamps. UI metadata and repeated series IDs are
omitted. The host-backend response itself is unchanged.
@@ -140,16 +141,20 @@ langgraph deploy metrics DEPLOYMENT_ID --summary
langgraph deploy metrics DEPLOYMENT_ID --raw
```
`--summary` returns the same shape without `points`. `--raw` returns the original
UI chart schema without reshaping or rounding. These flags are mutually exclusive.
Neither mode returns traces.
`--summary` requests server-side aggregation with `summary=true` and prints the
response without reshaping or rounding. It includes `deployment_id`,
`step_seconds`, and `sections`, but no `start_time`, `end_time`, or `points`.
Series contain `metric`, `unit`, `latest`, `min`, `max`, and `avg`; units include
`percent`, `count`, `milliseconds`, and `responses_per_second`.
`--raw` returns the original UI chart schema without reshaping or rounding.
These flags are mutually exclusive. Neither mode returns traces.
Size comparison using the realistic synthetic unit-test fixture (compact JSON
serialization for all modes; not a live deployment benchmark):
serialization; not a live deployment benchmark):
| Before: raw UI schema | After: default | After: summary |
| --- | --- | --- |
| 4,302 bytes | 1,226 bytes (72% smaller) | 806 bytes (81% smaller) |
| Before: raw UI schema | After: default |
| --- | --- |
| 4,302 bytes | 1,226 bytes (72% smaller) |
## Configuration
+3 -2
View File
@@ -2694,19 +2694,20 @@ def deploy_metrics(
}.items()
if value is not None
}
if summary:
params["summary"] = "true"
client = _create_host_backend_client(host_url, api_key, no_input=True)
result = _call_host_backend_with_optional_tenant(
client,
lambda c: c.get_deployment_metrics(str(deployment_id), params),
no_input=True,
)
if not raw:
if not raw and not summary:
result = compact_deployment_metrics(
result,
str(deployment_id),
start_time=start_time,
end_time=end_time,
summary=summary,
)
click.echo(json_mod.dumps(result, separators=(",", ":")))
+6 -8
View File
@@ -38,7 +38,6 @@ def compact_deployment_metrics(
*,
start_time: str,
end_time: str,
summary: bool = False,
) -> dict[str, Any]:
"""Reshape UI charts without mutating the response; step is in seconds."""
sections = {}
@@ -74,14 +73,13 @@ def compact_deployment_metrics(
"max": round(max(values), 3) if values else None,
"avg": round(fmean(values), 3) if values else None,
}
if not summary:
stats["points"] = [
[
_iso(timestamp),
round(value, 3) if value is not None else None,
]
for timestamp, value in points
stats["points"] = [
[
_iso(timestamp),
round(value, 3) if value is not None else None,
]
for timestamp, value in points
]
series_output[series["name"]] = stats
charts[chart["title"]] = series_output or {"_empty": True}
sections[section["title"]] = charts or {"_empty": True}
@@ -34,6 +34,30 @@ def invoke(monkeypatch, args, handler, input=None):
@pytest.mark.parametrize("mode", [None, "--summary", "--raw"])
def test_metrics(monkeypatch, bounds, mode, deployment_metrics_response):
charts = deployment_metrics_response
server_summary = {
"deployment_id": DEPLOYMENT_ID,
"step_seconds": 60,
"sections": {
"API": {
"Metrics": {
metric: {
"metric": metric,
"unit": unit,
"latest": 1.234567,
"min": 1.234567,
"max": 1.234567,
"avg": 1.234567,
}
for metric, unit in (
("cpu_usage", "percent"),
("replica_count", "count"),
("p95_latency", "milliseconds"),
("responses_per_second", "responses_per_second"),
)
}
}
},
}
class FrozenDatetime(datetime):
@classmethod
@@ -57,6 +81,8 @@ def test_metrics(monkeypatch, bounds, mode, deployment_metrics_response):
"end_time": "2026-10-28T23:02:33+00:00",
**options,
}
if mode == "--summary":
params["summary"] = "true"
def handler(request):
assert request.method == "GET"
@@ -64,7 +90,9 @@ def test_metrics(monkeypatch, bounds, mode, deployment_metrics_response):
assert dict(request.url.params) == params
assert request.headers["x-tenant-id"] == "workspace-id"
assert request.headers["x-api-key"] == "key"
return httpx.Response(200, json=charts)
return httpx.Response(
200, json=server_summary if mode == "--summary" else charts
)
args = ["metrics", DEPLOYMENT_ID]
for key, value in options.items():
@@ -73,19 +101,19 @@ def test_metrics(monkeypatch, bounds, mode, deployment_metrics_response):
args.append(mode)
result = invoke(monkeypatch, args, handler)
assert result.exit_code == 0, result.output
expected = (
charts
if mode == "--raw"
else compact_deployment_metrics(
if mode == "--summary":
expected = server_summary
elif mode == "--raw":
expected = charts
else:
expected = compact_deployment_metrics(
charts,
DEPLOYMENT_ID,
start_time=params.get("start_time"),
end_time=params.get("end_time"),
summary=mode == "--summary",
start_time=params["start_time"],
end_time=params["end_time"],
)
)
assert json.loads(result.output) == expected
if mode != "--raw":
if mode is None:
assert expected["start_time"] == params["start_time"]
assert expected["end_time"] == params["end_time"]
assert result.output.strip() == json.dumps(expected, separators=(",", ":"))
@@ -53,22 +53,15 @@ def test_compact_metrics(deployment_metrics_response):
assert result["sections"]["API"]["Latency"]["P95 Latency"]["unit"] is None
def test_summary_and_requested_bounds(deployment_metrics_response):
def test_requested_bounds(deployment_metrics_response):
result = reshape(
deployment_metrics_response,
"id",
start_time="2026-10-27T23:00:00Z",
end_time="2026-10-28T01:00:00Z",
summary=True,
)
assert result["start_time"] == "2026-10-27T23:00:00Z"
assert result["end_time"] == "2026-10-28T01:00:00Z"
for section in result["sections"].values():
if section == {"_empty": True}:
continue
for chart in section.values():
for series in chart.values():
assert set(series) == {"metric", "unit", "latest", "min", "max", "avg"}
@pytest.mark.parametrize("values", [[], [None], [float("nan"), float("inf")]])