Skip to content

[SDK] Migrate deployment log reading to logs_v4 and add pod listing - #148

Merged
V2arK merged 3 commits into
mainfrom
honglin/logs-v4-sdk
Sep 1, 2026
Merged

[SDK] Migrate deployment log reading to logs_v4 and add pod listing#148
V2arK merged 3 commits into
mainfrom
honglin/logs-v4-sdk

Conversation

@V2arK

@V2arK V2arK commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Problem

The SDK reads deployment logs through the CloudWatch-backed logs_v3 endpoint, which is being retired in favor of the Loki-backed logs_v4 read API (platform #4182, merged 2026-08-13). logs_v4 is per-pod and cursor-less: there is no server page token, pagination is driven by an exclusive epoch-millisecond timestamp boundary minted from the events themselves, and fetch-newer requests re-deliver a ~15s look-behind window that consumers must deduplicate by event id. The SDK also had no way to discover which pods a revision has logged from.

Change

  • Add get_deployment_pods(deployment_id, revision_number) -> List[str], exposing GET /deployments/pods/{deployment_id}/{revision_number} (includes terminated pods within log retention; empty list is normal for a fresh deployment).
  • Reimplement get_deployment_logs on logs_v4 as a stateless page fetch anchored on events from a previous call: get_deployment_logs(deployment_id, revision_number, pod, before=None, after=None, max_lines=100).
    • No anchor: the newest page (tail). Events are always oldest-first within a page.
    • before=<events you hold>: the page strictly older than the oldest of them; an empty result means the beginning of history — prepend and repeat to reassemble full history.
    • after=<events you hold>: only lines strictly newer than the newest of them; an empty result means nothing new yet — poll again to tail. The SDK drops the server's ~15s look-behind re-deliveries by matching event ids against the passed events, so callers get no duplicates while still receiving genuinely late-arriving lines. An empty anchor list raises ValueError (it silently meant two different scans before); a head read is expressed explicitly as after=0.
    • Either anchor also accepts a bare epoch-millisecond int as the exclusive boundary itself (an int after anchor holds no event ids, so the re-delivered span arrives undeduplicated — filter by timestamp or use get_deployment_logs_range).
    • Passing both before and after raises ValueError.
  • Add get_deployment_logs_range(deployment_id, revision_number, pod=None, start_time=None, end_time=None) -> List[DeploymentLogEvent]: every line in the inclusive epoch-ms window, oldest first; open ends read to the beginning or the present. pod=None reads every pod of the revision and merges the streams chronologically by event id; each returned DeploymentLogEvent (a small SDK dataclass) carries its pod name, which logs_v4 events themselves do not — this restores the v3-style whole-deployment read and the time-window read as pure client-side composition (zero API changes; the server's exclusive timestamp boundary already expresses both).
    • The logs_v3-shaped parameters (start_time, end_time, line_count, start_from_head, token handling) are removed with the endpoint — a breaking SDK-surface change that should ride a minor version bump.
  • Add DeploymentLogSession (factory: cclient.deployment_log_session(deployment_id, revision_number, pod, events=None)), a stateful reader that anchors every request on the window it has already fetched, so pages can never overlap or leave gaps inside it: fetch_older() prepends history pages (empty = beginning reached), fetch_newer() merges only new lines and returns the delta (empty = nothing new; rare late arrivals are sorted into place by id), .events is the merged ordered window (a copy). A first call on an empty session fetches the tail page in either direction. Fetches anchor through a shared trailing-retention-window slice (_recent_anchor) rather than the whole held window, so long-running tails and range walks poll at O(recent) instead of O(window). The optional events= seed resumes a session across processes from previously fetched logs (seed is deduplicated by id and sorted; interior gaps in seeded data are undetectable in principle — log lines carry no sequence numbers). Precedent for a stateful protocol wrapper inside the SDK: centml/sdk/shell/session.py.
  • Rewrite examples/sdk/get_deployment_logs.py (pod discovery, session backfill + tail as the primary flow, the stateless anchors shown as the low-level alternative) and add a README section for the flow (mirrors the Dynamo example section from [SDK] Add Dynamo deployment support; bump platform-api-python-client to 4.23.1 #146).

Requires platform-api-python-client>=4.25.0 (the first release carrying the logs_v4 endpoints, from platform release v4.25.0). main already pins exactly that version, so this PR no longer touches requirements.txt; all testing below ran against the published 4.25.0 from PyPI.

Test plan

Unit tests (TDD) cover: int timestamp anchors on both directions, range window trimming with look-behind filtering, open-ended range, all-pod merge with pod attribution, inverted-window ValueError; session first-fetch tail unification, backfill prepending, delta merge with late-arrival ordering, empty-delta stability, seed canonicalization and anchoring, .events copy semantics, per-call max_lines; and for the stateless layer: tail request shape, before anchoring on the oldest held timestamp, empty before page as begin-of-history, after anchoring on the newest held timestamp, look-behind dedup that keeps late arrivals, after=0 head read, empty-anchor-list ValueError, empty after page as nothing-new, max_lines pass-through, mutual-exclusion ValueError, production-shaped 19-digit-ns id ordering, boundary/dedup correctness with the trailing-anchor slice, and a generated-client contract check (hasattr on the two new endpoint methods, mirroring the Dynamo contract test).

Live validation ran the real SDK code against the dev API (kubectl port-forward to svc/api-service, platform-team test org): created 2-replica log-pump inference_v3 deployments (ids 8704, 8706, and 8732, cluster 1036) emitting SEQ=<n> lines, verified, then deleted them.

Check Command / probe Result
Unit tests cd tests && pytest --sanity 93 passed
Format ./scripts/format.sh --diff --check 24 files unchanged
Lint ./scripts/lint.sh 10.00/10
Types ./scripts/typecheck.sh Success, 0 issues
Pods poll get_deployment_pods both pods at +20s
Tail page no anchor 99 events, ascending
History via before prepend until empty page SEQ 0..250 contiguous, unique ids, 3 pages
Small-page equivalence same walk at max_lines=7 earlier snapshot is an exact prefix; walk gap-free
Incremental after 5 polls against held events +174 lines, zero duplicates, combined SEQ contiguous (425 lines)
Head read after=0 exact oldest prefix
Mid-history resume after=history[:50] continues exactly at SEQ 50
Session backfill fetch_older() loop SEQ 0..249 gapless, unique ids
Session tail 5 fetch_newer() polls +75 lines, deltas disjoint, window stays gapless and id-ordered
Session seed resume new session seeded with saved .events catches up +25 lines, zero duplicates, contiguous
Unknown pod session fetch_newer()/fetch_older() empty lists, no error
Range vs primitive open-ended get_deployment_logs_range(pod) identical to the before-loop history (276/276)
Time window start_time/end_time from mid-history events exact match with the timestamp-filtered reference (101 lines)
Int anchors after=T-1, before=T inclusive/exclusive boundaries hold on live data
Merged read pod=None over 2 pods id-sorted, no dups, pod= in every pump message matches the attributed pod, later per-pod reads fully cover the merge

Each live run passed all of its probes (11-13 per run across the pump deployments and the vllm read-only run); every test deployment was deleted afterwards. The heavy requirements-dev.txt extras (torch) were not installed; pytest --sanity skips the torch-importing test files by design, everything else mirrors the CI recipes exactly.

@V2arK V2arK self-assigned this Aug 14, 2026
@V2arK
V2arK requested a review from michaelshin August 14, 2026 17:36
@V2arK
V2arK force-pushed the honglin/logs-v4-sdk branch 4 times, most recently from 27c180e to b692578 Compare August 14, 2026 19:38
@V2arK
V2arK marked this pull request as ready for review August 14, 2026 20:03
@V2arK
V2arK force-pushed the honglin/logs-v4-sdk branch 2 times, most recently from 83d0bac to ebedf60 Compare September 1, 2026 18:04

@michaelshin michaelshin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice migration. The session vs stateless split is clear and the tests cover the paging contract well.

A few notes on empty-anchor semantics, older-page dedup, and the ms timestamp cursor — inline below. This is also a breaking change to get_deployment_logs (the #132 start_time/end_time/stream surface is gone); it should ride a minor version bump with a short changelog note. setup.py is still 0.5.3.

Comment thread centml/sdk/api.py
Comment thread centml/sdk/api.py
Comment thread centml/sdk/api.py
Comment thread centml/sdk/api.py Outdated
Comment thread examples/sdk/get_deployment_logs.py
@V2arK
V2arK force-pushed the honglin/logs-v4-sdk branch from ebedf60 to fd4d005 Compare September 1, 2026 18:25
Adds get_deployment_pods, a page-based get_deployment_logs anchored on
previously fetched events or an epoch-ms boundary (before/after), a
get_deployment_logs_range time-window read that can merge all pods of a
revision, and a stateful DeploymentLogSession for backfill, tailing, and
cross-process resume.

Signed-off-by: Honglin Cao <hocao@nvidia.com>
@V2arK
V2arK force-pushed the honglin/logs-v4-sdk branch from fd4d005 to 45212d1 Compare September 1, 2026 18:31
Empty before/after lists now raise ValueError (after=[] silently meant a
head scan while before=[] meant the tail); head reads use after=0. The
session passes only the boundary event / trailing retention window to
the primitive instead of its whole window, and the example prints a
count plus the last lines instead of the full history.

Signed-off-by: Honglin Cao <hocao@nvidia.com>
@V2arK
V2arK requested a review from michaelshin September 1, 2026 18:58

@michaelshin michaelshin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up looks good. Empty-list rejection (after=0 for head), slim session anchors, production-shaped id test, the whole-ms docstring, and the example’s last-N print all address the previous round. Leaving fetch_older prepend undeduplicated is fine given the logs_v4 contract.

Two leftover nits inline. Still worth a minor version bump when this ships — setup.py is 0.5.3 and get_deployment_logs remains a breaking rewrite of the #132 surface.

Comment thread centml/sdk/api.py
Comment thread examples/sdk/get_deployment_logs.py Outdated
get_deployment_logs_range paged with the full accumulated list, the same
O(window) pattern just removed from the session; both now anchor via a
shared _recent_anchor helper. Example comment reworded — the [] shorthand
read as an input since empty anchor lists became a ValueError.

Signed-off-by: Honglin Cao <hocao@nvidia.com>
@V2arK
V2arK requested a review from michaelshin September 1, 2026 19:22
@V2arK
V2arK merged commit fbe6893 into main Sep 1, 2026
5 checks passed
@V2arK
V2arK deleted the honglin/logs-v4-sdk branch September 1, 2026 20:01
@V2arK V2arK mentioned this pull request Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants