Skip to content

Log workflow output - #14301

Merged
erlenlh merged 3 commits into
mainfrom
log-workflow-output
Sep 8, 2026
Merged

Log workflow output#14301
erlenlh merged 3 commits into
mainfrom
log-workflow-output

Conversation

@erlenlh

@erlenlh erlenlh commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Issue
Part of #13320
Closes #14308

Approach
First of two PRs splitting the original "Store workflow output" work. This one
covers capturing workflow job output and writing it to the ERT log; the
follow-up (#14062) adds the
WorkflowEvent and persists events to experiment storage.

Workflow job output previously only reached the terminal ERT was started from,
so it was lost once that terminal was gone, and a job run outside a terminal
left no record at all.

  • Replaces sys.stdout/sys.stderr with a thread-aware _CaptureProxy while a
    job runs, recording what the calling thread writes while still forwarding it
    to the real stream. Concurrently running jobs do not pick up each other's
    output.
  • Adds WorkflowJobResult, recording name, index, arguments, output, status and
    timestamp per invocation. workflowReport() is keyed by job name, so a
    workflow running the same job twice previously kept only the last invocation's
    output.
  • Names workflows after their LOAD_WORKFLOW alias, so reporting can use the
    name users recognise rather than the source file path.
  • Writes one log entry per job invocation, naming the hook, workflow and job.
    Covers hooked workflows, ert workflow and the GUI Run workflow tool, since
    all three go through WorkflowRunner.

Also fixes two latent bugs found along the way: the running flag was not cleared
when a job was rejected for bad arguments, and a job that stopped the workflow
had its output dropped before it could be recorded.

  • PR title captures the intent of the changes, and is fitting for release notes.
  • Added appropriate release note label
  • Commit history is consistent and clean, in line with the contribution guidelines.
  • Make sure unit tests pass locally after every commit (git rebase -i main --exec 'just rapid-tests')

When applicable

  • When screenshots are changed: Review screenshot-PR in ert-testdata,
    merge screenshot-PR in ert-testdata before merging this PR.
  • When there are user facing changes: Updated documentation
  • New behavior or changes to existing untested code: Ensured that unit tests are added (See Ground Rules).
  • Large PR: Prepare changes in small commits for more convenient review
  • Bug fix: Add regression test for the bug
  • Bug fix: Add backport label to latest release (format: 'backport release-branch-name')

@erlenlh erlenlh mentioned this pull request Aug 27, 2026
10 tasks
@codecov-commenter

codecov-commenter commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.36066% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.20%. Comparing base (074eb57) to head (da83710).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/ert/config/_capture_output.py 96.05% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #14301      +/-   ##
==========================================
+ Coverage   91.17%   91.20%   +0.03%     
==========================================
  Files         495      496       +1     
  Lines       36095    36244     +149     
==========================================
+ Hits        32908    33055     +147     
- Misses       3187     3189       +2     
Flag Coverage Δ
cli-tests 35.92% <89.61%> (+0.22%) ⬆️
fuzz 43.74% <37.15%> (+<0.01%) ⬆️
gui-tests 58.62% <83.60%> (+0.11%) ⬆️
performance-and-unit-tests 81.13% <98.36%> (+0.07%) ⬆️
test 45.60% <81.42%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/ert/config/ert_config.py 95.95% <ø> (ø)
src/ert/config/ert_script.py 88.65% <100.00%> (+1.35%) ⬆️
src/ert/config/external_ert_script.py 100.00% <100.00%> (ø)
src/ert/config/workflow.py 98.52% <100.00%> (+0.19%) ⬆️
src/ert/run_models/run_model.py 92.48% <ø> (ø)
src/ert/workflow_runner.py 94.91% <100.00%> (+2.85%) ⬆️
src/ert/config/_capture_output.py 96.05% <96.05%> (ø)

Comment thread src/ert/config/ert_script.py Outdated
@contextlib.contextmanager
def capture(self) -> Iterator[io.StringIO]:
"""Record what the calling thread writes for the duration of the block."""
buffer = io.StringIO()

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.

shouldn't all of this be in the try block rather than only the yield?

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.

not sure i understand. The append should happen before, such that the remove in finally has something to remove. Remove should be in finally so that we know it happens. Follows pattern from contextmanager docs

Image

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.

I meant more like if this fails buffers = self._buffers, but I guess then we don't want to remove it from the list.
So it might be good 👍

Comment thread src/ert/config/ert_script.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
@codspeed-hq

codspeed-hq Bot commented Aug 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 36 untouched benchmarks


Comparing log-workflow-output (da83710) with main (074eb57)

Open in CodSpeed

Comment thread src/ert/config/workflow.py
Comment thread src/ert/workflow_runner.py Outdated
Comment thread docs/ert/reference/workflows/complete_workflows.rst Outdated
Comment thread src/ert/workflow_runner.py Outdated
Comment thread src/ert/workflow_runner.py Outdated
Comment thread src/ert/workflow_runner.py Outdated
Comment thread src/ert/workflow_runner.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
Comment thread src/ert/config/ert_script.py Outdated
Comment thread tests/ert/unit_tests/cli/test_cli_workflow.py Outdated
Comment thread tests/ert/unit_tests/config/test_ert_config.py Outdated
Comment thread src/ert/config/_capture_output.py Outdated
Comment thread src/ert/workflow_runner.py Outdated
CANCELLED = "cancelled"


def _job_description(

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.

This can't be a WorkflowJobResult member / function?

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.

i think putting it on result is wrong, since it is used before we actually run the runner.

Image

Maybe we can put it on the runner? agree that its weird to be free

Comment thread tests/ert/unit_tests/config/test_ert_config.py Outdated
Comment thread tests/ert/unit_tests/workflow_runner/test_ert_script.py Outdated
Comment thread tests/ert/unit_tests/workflow_runner/test_ert_script.py Outdated
Comment thread tests/ert/unit_tests/workflow_runner/test_ert_script.py Outdated

@xjules xjules 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.

It looks already very good!
Had only a smaller comments.
Also, there are way to many articles in the test names: remove the, a and an

@erlenlh
erlenlh force-pushed the log-workflow-output branch 4 times, most recently from 33034fb to 6b7bf43 Compare September 7, 2026 12:20

@xjules xjules 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 work @erlenlh ! 🚀

erlenlh and others added 3 commits September 7, 2026 22:15
Workflow carried no name of its own, so anything reporting on a workflow
had to fall back to the source file path. LOAD_WORKFLOW lets a workflow
be given a name, and that is the name users recognise.

Store the name on Workflow, defaulting to the file name when none is
given, so it can be reported alongside the jobs that were run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WorkflowJobRunner.run() set the running flag before validating its
arguments but only cleared it on the success path, so a job rejected
for too few or too many arguments kept reporting that it was still
running.

workflowReport() is also keyed by job name, so a workflow running the
same job more than once kept only the output of the last invocation.
Add WorkflowJobResult, recording name, index, arguments, output,
status and timestamp per invocation, and expose the ordered list
through workflow_job_results(). Status is modelled as a
WorkflowJobStatus enum rather than a failed/cancelled boolean pair,
since the booleans could express states that do not exist; a job
interrupted mid-run is now reported as cancelled instead of failed.

Jobs skipped because the workflow was cancelled are recorded too, so
a cancelled workflow can be told apart from one that never ran. A job
that stops the workflow is recorded before the error is raised, so
its output is not lost. workflowReport() is left as it is, since the
run workflow tool and the CLI still use it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Internal python workflow jobs wrote straight to sys.stdout/sys.stderr,
so their output was visible in the terminal but not available to ERT
itself, and was lost once that terminal was gone. External jobs
captured their subprocess output but did not pass stdout on to the
terminal.

Replace sys.stdout/sys.stderr with a proxy while a job runs, recording
what the calling thread writes while still forwarding it to the real
stream. Capture is per-thread, so concurrently running jobs do not
pick up each other's output. The proxy and its context manager live in
their own module, since they are self-contained and have nothing to
do with ErtScript beyond the single call that uses them.

Write one log entry per job invocation to the ERT log, naming the
hook, the workflow and the job, and quoting the arguments, stdout and
stderr, built on WorkflowRunner so the starting and result lines share
one format. This covers hooked workflows, 'ert workflow' and the GUI
Run workflow tool, since all three go through WorkflowRunner.

Also stop reporting a failing external job with an ERT stack trace:
the new ExternalScriptError carries the exit code, since the traceback
would only show ERT internals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@erlenlh
erlenlh force-pushed the log-workflow-output branch from 6b7bf43 to da83710 Compare September 7, 2026 20:23
@erlenlh
erlenlh merged commit d8d93af into main Sep 8, 2026
42 checks passed
@erlenlh
erlenlh deleted the log-workflow-output branch September 8, 2026 07:53
erlenlh added a commit that referenced this pull request Sep 8, 2026
Test names introduced in this PR used "a", "an" and "the", which
does not follow this repo's naming convention and was flagged
repeatedly on the sibling PR #14301. Rename them to read as plain
behaviour specifications.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Log output from workflows

4 participants