Skip to content

[release] print the observability agent analysis in its own buildkite group - #65910

Open
sai-miduthuri wants to merge 11 commits into
sai-miduthuri/add-obs-agent-release-testsfrom
sai-miduthuri/obs-agent-analysis-log-group
Open

[release] print the observability agent analysis in its own buildkite group#65910
sai-miduthuri wants to merge 11 commits into
sai-miduthuri/add-obs-agent-release-testsfrom
sai-miduthuri/obs-agent-analysis-log-group

Conversation

@sai-miduthuri

@sai-miduthuri sai-miduthuri commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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 of
the +++ :memo: Reporting results group — between the other reporters' output and
the traceback of the failure it is explaining. This moves it to the end of the
step, under its own group:

+++ :memo: Reporting results
    reporters ... traceback ... "Script failed on try 1/1 ..." epilogue
+++ :robot_face: Observability agent analysis
    summary, feedback ask, slack thread link

The reporter writes its message to the file named by RELEASE_TEST_OBS_AGENT_FILE
and run_release_test.sh prints that file as its last action. Buildkite log groups
are 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:

  • When the file is written, the reporter logs a one-line pointer to it rather than
    the message, so the analysis is not duplicated in one log while still leaving a
    breadcrumb if the group never appears.
  • The default path is /tmp/obs_agent_analysis.txt, outside RELEASE_RESULTS_DIR,
    so it is not uploaded to the Artifacts tab. Putting it under RELEASE_RESULTS_DIR
    would 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:

  • The analysis is written as UTF-8. open() without an explicit encoding uses the
    container locale, and real agent summaries contain em dashes, so an ascii locale
    raised UnicodeEncodeError — not an OSError, so it escaped the write guard.
    glue.py does not guard the reporting loop, so that would have failed the test.
  • Null fields in the agent's response no longer raise. response.get("result", {})
    returns None when the key is present and null, and the next .get raised an
    AttributeError out of the reporter.
  • The analysis body is indented when printed. It is prose written by an LLM, and a
    line starting with ---, +++ or ~~~ would otherwise open a buildkite group of
    its own and file the rest of the analysis under it.
  • The unit tests no longer inherit RELEASE_TEST_OBS_AGENT_FILE from the
    environment, which this PR makes run_release_test.sh export.

Not duplicating existing work

gh pr list --repo ray-project/ray --state open --search "observability agent buildkite group"
gh pr list --repo ray-project/ray --state open --search "run_release_test.sh"

Neither returns anything touching this area; the only related PR is #65906, which
this one is stacked on.

Tests

$ bazel test --test_tag_filters=release_unit //release/... --test_output=errors
Executed 29 out of 29 tests: 29 tests pass.

$ bazel test //release:test_observability_agent_reporter //release:test_run_script
//release:test_observability_agent_reporter                              PASSED
//release:test_run_script                                                PASSED

$ pre-commit run --files release/ray_release/reporter/observability_agent.py \
    release/ray_release/tests/test_observability_agent_reporter.py \
    release/ray_release/tests/test_run_script.py release/run_release_test.sh
# all hooks passed, shellcheck included

New coverage:

Behavior Test
The analysis is written to the configured file test_analysis_written_to_file
It is logged inline when no file is configured test_analysis_logged_when_no_file_is_configured
A write failure falls back to logging, and never propagates test_analysis_logged_when_the_file_cannot_be_written, test_write_failures_never_propagate
Non-ascii summaries round-trip test_analysis_file_handles_non_ascii
Null result / analysis / metadata do not raise test_null_fields_in_the_response_do_not_raise
The group is printed, and printed last test_obs_agent_analysis_is_printed_in_its_own_group
No group when there is no analysis test_no_group_when_there_is_no_analysis
Markup in the summary cannot open a group test_analysis_cannot_open_a_buildkite_group_of_its_own

The 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.

@sai-miduthuri sai-miduthuri changed the title sai miduthuri/obs agent analysis log group [release] print the observability agent analysis in its own buildkite group Sep 4, 2026
@sai-miduthuri
sai-miduthuri marked this pull request as ready for review September 4, 2026 07:31
@sai-miduthuri
sai-miduthuri requested a review from a team as a code owner September 4, 2026 07:31

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment thread release/run_release_test.sh Outdated
Comment thread release/run_release_test.sh Outdated
# 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")

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.

medium

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".

Suggested change
summary = (query_result.get("analysis") or {}).get("summary")
summary = (query_result.get("analysis") or {}).get("summary") or "No summary provided."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@ray-gardener ray-gardener Bot added core Issues that should be addressed in Ray Core devprod release-test release test labels Sep 4, 2026
@sai-miduthuri

Copy link
Copy Markdown
Contributor Author

The Observability Agent response can be viewed in the logs of the forced hello_world.aws test failure on buildkite/release on commit b8d3428.

The agent's summary and thread URL are printed onto the logs more prominently, and given their own Observability agent analysis group in the Buildkite job logs.

@sai-miduthuri
sai-miduthuri force-pushed the sai-miduthuri/obs-agent-analysis-log-group branch from b8d3428 to 0838f6a Compare September 4, 2026 18:53
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Issues that should be addressed in Ray Core devprod release-test release test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant