[release] print the observability agent analysis in its own buildkite group - #65910
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the observability agent reporter to write its analysis to a file instead of logging it inline, and configures run_release_test.sh to print this analysis at the end of the test run under its own Buildkite group. Feedback on these changes includes adding || true to the rm -f cleanup command to prevent script crashes under set -e, ensuring the analysis path is a regular file before reading it, and providing a fallback string in the reporter if the agent returns a null summary.
| # and a null would otherwise raise out of a reporter, which glue.py | ||
| # does not guard against. | ||
| query_result = response.get("result") or {} | ||
| summary = (query_result.get("analysis") or {}).get("summary") |
There was a problem hiding this comment.
If the observability agent returns a response where analysis is present but summary is None or null, summary will be None. This results in the message containing the literal string "None" (e.g., "Observability agent analysis of job <job_id>:\nNone"). Providing a fallback string like "No summary provided." when summary is empty or None improves readability and prevents logging "None".
| summary = (query_result.get("analysis") or {}).get("summary") | |
| summary = (query_result.get("analysis") or {}).get("summary") or "No summary provided." |
There was a problem hiding this comment.
Fixed, but not with a placeholder string — or "No summary provided." would make the problem more visible rather than less. The file stays non-empty either way, so the step still ends with a prominent +++ :robot_face: Observability agent analysis group whose entire content is that sentence, and it reads as an intended state, so an API regression would look designed rather than broken. None is at least obviously wrong.
Instead every field the agent leaves out is named where the group is read: it states that the summary is missing and gives the debug session id, states when the slack thread is missing too, and is still emitted when the response is empty — so an agent that returned nothing does not look like an agent that never ran. That also matches how a missing slack thread was already handled, with a logger.error rather than a substituted value.
With both fields missing the group now reads:
Observability agent analysis of job prodjob_x:
>>> The agent returned no summary for this job.
>>> Debug session: oasess_y
>>> The agent returned no slack thread, so the full report
>>> and its feedback buttons cannot be reached from here.
Covered by test_missing_summary_is_named_in_the_analysis and test_empty_analysis_still_reports_the_failure, both asserting no "None" reaches the output.
|
The Observability Agent response can be viewed in the logs of the forced The agent's summary and thread URL are printed onto the logs more prominently, and given their own |
b8d3428 to
0838f6a
Compare
… group The analysis was logged inline, which put it in the middle of the ":memo: Reporting results" group, between the other reporters' output and the traceback of the failure it explains. Hand it to run_release_test.sh through a file instead, and have the script print it under its own group once everything else is done. Buildkite log groups are sequential rather than nested, so a group opened by the reporter would file the traceback and the exit status that follow it under the analysis heading; printing last is what avoids that. The reporter falls back to logging the analysis inline when no file is configured or the write fails, so the analysis is never lost, and a failure to write it cannot change the outcome of the test run. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The analysis is prose from the agent and carries non-ascii characters -- em dashes appear in real responses. open() without an explicit encoding uses the container locale, so an ascii locale raises UnicodeEncodeError, which is not an OSError and so escaped the write guard. The reporting loop in glue.py does not guard against a reporter raising, so that would have turned an observability agent problem into a failed release test. Write as utf-8 and swallow anything the write raises. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
…tate
Four fixes from review:
- The agent sends explicit nulls, so `response.get("result", {})` returns
None rather than the default and the following `.get` raised an
AttributeError out of the reporter. glue.py does not guard the reporting
loop, so a null in the response would have failed the release test.
- The summary is prose written by the agent. Printed verbatim, a line of
it starting with ---, +++ or ~~~ opens a buildkite group of its own and
files the rest of the analysis under it. Indent the body when printing.
- The unit tests inherited RELEASE_TEST_OBS_AGENT_FILE from the
environment, which this change makes run_release_test.sh export, so
three of them failed for anyone who had run the harness in that shell.
- The shell test helper concatenated two captured streams, which cannot
show the order they were written in; the assertion that the analysis is
printed last was not verifying anything. Merge stderr into stdout.
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Three fixes from review on the PR: - `rm -f` of the analysis file runs under `set -e`, so a failure -- a directory or an unremovable file at that path -- killed the release test script outright. The cleanup is defensive, so it is now `|| true`, like the line above it. - `-s` is true for a directory, and reading one aborts the run the same way. The print is guarded with `-f` as well, and the read itself cannot take the harness down either. - A response with no summary produced the literal string "None" in the group, which is both ugly and easy to mistake for something intentional. Each field the agent leaves out is now named in the message: the group says the summary is missing and gives the debug session id, and says when the slack thread is missing too. The group is still emitted when the response is empty, so an agent that returned nothing does not look like an agent that never ran. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Both tests assert only that something is absent from the script's output, and absence also holds when the script never ran -- a wrong path, an early exit or a syntax error leaves them green while the behaviour they guard is gone. The helper discards the return code, so nothing else catches it either. Assert the epilogue line the script only prints once it reaches the end, so the absence assertions mean something. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The existing test pins MAX_RETRIES to 1 and exits 40, which breaks out of the in-script loop, so the cleanup it exercises is the one before the first attempt -- a leftover from an earlier run on the same agent. Rename it to say that. Add the case the old name described: a first attempt that writes an analysis and exits 30, the only kind of exit that continues the loop, then a second attempt that writes nothing. Exit codes 30-33 become INFRA_TIMEOUT and never trigger the agent, so this cannot happen today -- which is exactly why the cleanup is defensive, and why the guard is worth pinning before a triggering status is ever added to that list. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The analysis lived at a fixed /tmp path, so it was never copied into /tmp/ray_release_test_artifacts and never uploaded: it existed only in the step's scrollback, while result.json and test_config.json outlived it. The path was also one global name, shared by every harness on a host that is not containerised. Defaulting it under RELEASE_RESULTS_DIR fixes both -- the existing copy carries it into the artifacts, and the path is per-run. The explicit cleanup in the retry loop stays: the wipe of that directory is skipped when NO_ARTIFACTS is set, and does not cover a caller-provided path. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
- The comment on the reporter's position described the mechanism this change replaced: the analysis no longer appears in the reporting output at all, and where it is printed is decided by run_release_test.sh. A reader could have reordered the list, or dropped the shell group, believing the ordering still controlled placement. - One round-trip read did not pass encoding="utf-8" while three others did. It survived only because that fixture is ascii; the write side pins the encoding precisely because the agent's prose is not. - test_write_failures_never_propagate patched builtins.open, so any incidental open in the process satisfied it and a refactor that stopped opening the file would have left it green. It now patches the module's own open. - _run_script_capturing had no type annotations, unlike everything else this change adds. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Each of the five repeated the same three-line path to the script and its own copy of the stub-writing boilerplate, and two of them differed only in what they seeded and what they asserted absent. Hoist the path and the epilogue string into constants, extract the stub writing into a helper, and parametrize those two into one test with ids that keep both cases named in the output. The other three keep their own functions: one needs a stateful stub across two attempts, one needs its own results directory, and one asserts a per-line property. Forcing those into the same parametrize would cost more scaffolding than the duplication does. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Rebasing this branch onto the updated base resolved the conflict in test_run_script.py by taking this branch's side of the file wholesale, which discarded the two tests PR 65907 added and merged to master: test_buildkite_max_retries_is_inherited and test_buildkite_max_retries_defaults_to_one. Nothing reported it, because the result was a clean file rather than a conflict -- the branch would simply have deleted two tests that exist on master. Restore them verbatim, at the end of the file, where the base has them and where this branch's own tests do not collide with them. Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
They were restored verbatim in the previous commit, so they still carried
their own copy of the script path and the env dict that this branch had
already extracted into RELEASE_TEST_SCRIPT, _write_stub and
_run_script_capturing, and they differed only in the budget they publish
and the value they expect. Parametrize them into one test using those
helpers.
The explicit `env.pop("BUILDKITE_MAX_RETRIES", None)` that guarded the
default case moves into the helper, which already did the same for
RELEASE_TEST_OBS_AGENT_FILE: a value inherited from the caller's shell
would otherwise decide the outcome of a test about what happens when the
variable is unset. Behaviour is unchanged -- the old test popped it too.
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
0838f6a to
1bd3d3d
Compare
Description
Stacked on #65906, which adds the observability agent reporter. Review that one
first; this PR's diff is only the change on top of it.
That reporter logs its analysis with
logger.info, which puts it in the middle ofthe
+++ :memo: Reporting resultsgroup — between the other reporters' output andthe traceback of the failure it is explaining. This moves it to the end of the
step, under its own group:
The reporter writes its message to the file named by
RELEASE_TEST_OBS_AGENT_FILEand
run_release_test.shprints that file as its last action. Buildkite log groupsare sequential, not nested — a group runs until the next header — so a group
opened by the reporter would file the traceback, the shell epilogue and the exit
status under the analysis heading. Printing last is what avoids that, and it is why
this is not simply a
buildkite_group()call inside the reporter.When no file is configured or the write fails, the reporter logs the message inline
exactly as before, so the analysis cannot be lost, and a failure to write it cannot
change the outcome of the test run.
Related issues
None.
Additional information
Two decisions worth a reviewer's opinion, both one-line changes:
the message, so the analysis is not duplicated in one log while still leaving a
breadcrumb if the group never appears.
/tmp/obs_agent_analysis.txt, outsideRELEASE_RESULTS_DIR,so it is not uploaded to the Artifacts tab. Putting it under
RELEASE_RESULTS_DIRwould make it retrievable as an artifact at the cost of one more artifact per
failing test.
Hardening that came out of self-review, each with a regression test that fails
when the fix is reverted:
open()without an explicit encoding uses thecontainer locale, and real agent summaries contain em dashes, so an ascii locale
raised
UnicodeEncodeError— not anOSError, so it escaped the write guard.glue.pydoes not guard the reporting loop, so that would have failed the test.response.get("result", {})returns
Nonewhen the key is present and null, and the next.getraised anAttributeErrorout of the reporter.line starting with
---,+++or~~~would otherwise open a buildkite group ofits own and file the rest of the analysis under it.
RELEASE_TEST_OBS_AGENT_FILEfrom theenvironment, which this PR makes
run_release_test.shexport.Not duplicating existing work
Neither returns anything touching this area; the only related PR is #65906, which
this one is stacked on.
Tests
New coverage:
test_analysis_written_to_filetest_analysis_logged_when_no_file_is_configuredtest_analysis_logged_when_the_file_cannot_be_written,test_write_failures_never_propagatetest_analysis_file_handles_non_asciiresult/analysis/metadatado not raisetest_null_fields_in_the_response_do_not_raisetest_obs_agent_analysis_is_printed_in_its_own_grouptest_no_group_when_there_is_no_analysistest_analysis_cannot_open_a_buildkite_group_of_its_ownThe buildkite rendering itself is not verifiable locally; the group markers are
asserted as text.
AI assistance
AI assistance (Claude Code) was used for this change.