[SDK] Migrate deployment log reading to logs_v4 and add pod listing - #148
Conversation
27c180e to
b692578
Compare
83d0bac to
ebedf60
Compare
michaelshin
left a comment
There was a problem hiding this comment.
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.
ebedf60 to
fd4d005
Compare
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>
fd4d005 to
45212d1
Compare
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>
michaelshin
left a comment
There was a problem hiding this comment.
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.
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>
Problem
The SDK reads deployment logs through the CloudWatch-backed
logs_v3endpoint, which is being retired in favor of the Loki-backedlogs_v4read API (platform #4182, merged 2026-08-13).logs_v4is per-pod and cursor-less: there is no server page token, pagination is driven by an exclusive epoch-millisecondtimestampboundary minted from the events themselves, and fetch-newer requests re-deliver a ~15s look-behind window that consumers must deduplicate by eventid. The SDK also had no way to discover which pods a revision has logged from.Change
get_deployment_pods(deployment_id, revision_number) -> List[str], exposingGET /deployments/pods/{deployment_id}/{revision_number}(includes terminated pods within log retention; empty list is normal for a fresh deployment).get_deployment_logsonlogs_v4as 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).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 eventids against the passed events, so callers get no duplicates while still receiving genuinely late-arriving lines. An empty anchor list raisesValueError(it silently meant two different scans before); a head read is expressed explicitly asafter=0.afteranchor holds no event ids, so the re-delivered span arrives undeduplicated — filter by timestamp or useget_deployment_logs_range).beforeandafterraisesValueError.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=Nonereads every pod of the revision and merges the streams chronologically by event id; each returnedDeploymentLogEvent(a small SDK dataclass) carries itspodname, whichlogs_v4events 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 exclusivetimestampboundary already expresses both).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.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),.eventsis 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 optionalevents=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.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 thelogs_v4endpoints, from platform release v4.25.0).mainalready pins exactly that version, so this PR no longer touchesrequirements.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,.eventscopy semantics, per-callmax_lines; and for the stateless layer: tail request shape,beforeanchoring on the oldest held timestamp, emptybeforepage as begin-of-history,afteranchoring on the newest held timestamp, look-behind dedup that keeps late arrivals,after=0head read, empty-anchor-listValueError, emptyafterpage as nothing-new,max_linespass-through, mutual-exclusionValueError, production-shaped 19-digit-ns id ordering, boundary/dedup correctness with the trailing-anchor slice, and a generated-client contract check (hasattron 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) emittingSEQ=<n>lines, verified, then deleted them.cd tests && pytest --sanity./scripts/format.sh --diff --check./scripts/lint.sh./scripts/typecheck.shget_deployment_podsbeforemax_lines=7afterafter=0after=history[:50]fetch_older()loopfetch_newer()polls.eventsfetch_newer()/fetch_older()get_deployment_logs_range(pod)start_time/end_timefrom mid-history eventsafter=T-1,before=Tpod=Noneover 2 podspod=in every pump message matches the attributed pod, later per-pod reads fully cover the mergeEach 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.txtextras (torch) were not installed;pytest --sanityskips the torch-importing test files by design, everything else mirrors the CI recipes exactly.