Skip to content

feat: add log_context param for labeled outcome logs#253

Open
geetikakay wants to merge 1 commit into
RedHatQE:mainfrom
geetikakay:log_context_callers
Open

feat: add log_context param for labeled outcome logs#253
geetikakay wants to merge 1 commit into
RedHatQE:mainfrom
geetikakay:log_context_callers

Conversation

@geetikakay

@geetikakay geetikakay commented Jul 4, 2026

Copy link
Copy Markdown

Add optional log_context to TimeoutSampler for grep-friendly outcome lines in CI logs. Giving agents and users more accurate data.Generic Elapsed time: lines are hard to tie to a specific VM or callers.
When log_context is set, the sampler logs once at terminal exit:

INFO: {log_context} succeeded after {N.N}s.
ERROR: {log_context} failed after {N.N}s: {exception} (non-retried exception)
ERROR: {log_context} timed out after {N.N}s.
Retries are not logged (ignored exceptions stay silent by design).

if not set, we continue to see same elapsed logs and is backward compatable.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added an optional log_context setting for timeout and retry workflows, producing clearer success, failure, and timeout messages.
    • Updated usage examples to show waiting for SSH with retry handling and contextual logging.
  • Bug Fixes

    • Improved log output so elapsed-time details are no longer duplicated in outcome messages.
    • Refined failure handling so timeout-related errors report consistent, cleaner messages.

Walkthrough

This PR adds log_context support to TimeoutSampler and retry, changes outcome logging to use contextual success/failure/timeout messages, suppresses elapsed-time output in those paths, and updates tests and README examples.

Changes

log_context logging behavior

Layer / File(s) Summary
TimeoutSampler log_context parameter and iteration logging
timeout_sampler/__init__.py
Adds log_context to TimeoutSampler, stores it, and updates iteration logging to emit contextual success, failure, and timeout messages while suppressing per-attempt elapsed-time logs and omitting elapsed_time in the relevant errors.
retry decorator wiring and tests
timeout_sampler/__init__.py, tests/test_timeout_sampler.py
Adds log_context to retry, forwards it to TimeoutSampler, and extends tests to cover success, non-retried exception, timeout, and retry-decorator logging behavior.
README usage examples
README.md
Updates the usage section with an SSH example and revised retry example showing log_context and exceptions_dict.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: commented-coderabbitai[bot]

Suggested reviewers: rnetser, myakove

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a log_context parameter for labeled outcome logs.
Description check ✅ Passed The description is related to the changes and explains the new log_context behavior in TimeoutSampler.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@redhat-qe-bot

Copy link
Copy Markdown
Collaborator

Report bugs in Issues

Welcome! 🎉

This pull request will be automatically processed with the following features:

🔄 Automatic Actions

  • Reviewer Assignment: Reviewers are automatically assigned based on the OWNERS file in the repository root
  • Size Labeling: PR size labels (XS, S, M, L, XL, XXL) are automatically applied based on changes
  • Issue Creation: Disabled for this repository
  • Branch Labeling: Branch-specific labels are applied to track the target branch
  • Auto-verification: Auto-verified users have their PRs automatically marked as verified
  • Labels: All label categories are enabled (default configuration)

📋 Available Commands

PR Status Management

  • /wip - Mark PR as work in progress (adds WIP: prefix to title)
  • /wip cancel - Remove work in progress status
  • /hold - Block PR merging (approvers only)
  • /hold cancel - Unblock PR merging
  • /verified - Mark PR as verified
  • /verified cancel - Remove verification status
  • /reprocess - Trigger complete PR workflow reprocessing (useful if webhook failed or configuration changed)
  • /regenerate-welcome - Regenerate this welcome message
  • /security-override - Set security check runs to pass (maintainers only)
  • /security-override cancel - Re-run security checks

Review & Approval

  • /lgtm - Approve changes (looks good to me)
  • /approve - Approve PR (approvers only)
  • /automerge - Enable automatic merging when all requirements are met (maintainers and approvers only)
  • /assign-reviewers - Assign reviewers based on OWNERS file
  • /assign-reviewer @username - Assign specific reviewer
  • /check-can-merge - Check if PR meets merge requirements

Testing & Validation

  • /retest tox - Run Python test suite with tox
  • /retest python-module-install - Test Python package installation
  • /retest all - Run all available tests

Cherry-pick Operations

  • /cherry-pick <branch> - Schedule cherry-pick to target branch when PR is merged
    • Multiple branches: /cherry-pick branch1 branch2 branch3
  • /cherry-pick-retry <branch> - Retry a failed cherry-pick (merged PRs only)

Branch Management

  • /rebase - Rebase this PR branch onto its base branch

Label Management

  • /<label-name> - Add a label to the PR
  • /<label-name> cancel - Remove a label from the PR

✅ Merge Requirements

This PR will be automatically approved when the following conditions are met:

  1. Approval: /approve from at least one approver
  2. Status Checks: All required status checks must pass
  3. No Blockers: No wip, hold, has-conflicts labels and PR must be mergeable (no conflicts)
  4. Verified: PR must be marked as verified

📊 Review Process

Approvers and Reviewers

Approvers:

  • myakove
  • rnetser

Reviewers:

  • myakove
  • rnetser
Available Labels
  • hold
  • verified
  • wip
  • lgtm
  • approve
  • automerge
AI Features
  • Conventional Title: Mode: fix (claude/claude-opus-4-6-1m)
  • Cherry-Pick Conflict Resolution: Enabled (claude/claude-opus-4-6-1m)
Security Checks
  • Suspicious Path Detection: Monitors paths: .claude/, .vscode/, .cursor/, .devcontainer/, .pi/, .github/workflows/, .github/actions/
  • Committer Identity Check: Verifies last committer matches PR author
  • Mandatory: Security checks block merge (use /security-override to bypass — maintainers only)

💡 Tips

  • WIP Status: Use /wip when your PR is not ready for review
  • Verification: The verified label is removed on new commits unless the push is detected as a clean rebase
  • Cherry-picking: Cherry-pick labels are processed when the PR is merged
  • Permission Levels: Some commands require approver permissions
  • Auto-verified Users: Certain users have automatic verification and merge privileges

For more information, please refer to the project documentation or contact the maintainers.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
timeout_sampler/__init__.py (1)

300-304: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Chain the re-raised exception (raise ... from last_exp).

Raising TimeoutExpiredError inside the except block without from obscures the original cause in tracebacks. Ruff (B904) flags this.

♻️ Proposed change
-                    raise TimeoutExpiredError(
-                        self._get_exception_log(exp=last_exp),
-                        last_exp=last_exp,
-                        elapsed_time=elapsed_time if not (self.print_log and self.log_context) else None,
-                    )
+                    raise TimeoutExpiredError(
+                        self._get_exception_log(exp=last_exp),
+                        last_exp=last_exp,
+                        elapsed_time=elapsed_time if not (self.print_log and self.log_context) else None,
+                    ) from last_exp
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@timeout_sampler/__init__.py` around lines 300 - 304, The TimeoutExpiredError
re-raise in the timeout handling path is missing explicit exception chaining,
which obscures the original cause in tracebacks. Update the raise inside the
except block in the timeout sampler logic to chain from last_exp using Python’s
exception chaining syntax, keeping the existing TimeoutExpiredError construction
and _get_exception_log / elapsed_time handling unchanged.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@timeout_sampler/__init__.py`:
- Around line 288-291: In TimeoutSampler.__iter__ and the GeneratorExit
handling, the success log is too broad and also drops valid 0.0s timings. Update
the logging condition so it only records success for actual accepted samples,
not merely when the generator is closed, and change the elapsed_time check to
use an explicit None test instead of relying on truthiness. Keep the fix
localized to the GeneratorExit block and the TimeoutSampler logic that sets or
logs elapsed_time.

---

Nitpick comments:
In `@timeout_sampler/__init__.py`:
- Around line 300-304: The TimeoutExpiredError re-raise in the timeout handling
path is missing explicit exception chaining, which obscures the original cause
in tracebacks. Update the raise inside the except block in the timeout sampler
logic to chain from last_exp using Python’s exception chaining syntax, keeping
the existing TimeoutExpiredError construction and _get_exception_log /
elapsed_time handling unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1be0983-d1cf-4c26-a897-6e6b23a4d052

📥 Commits

Reviewing files that changed from the base of the PR and between 49cfa8b and 0e45a3f.

📒 Files selected for processing (2)
  • tests/test_timeout_sampler.py
  • timeout_sampler/__init__.py

Comment thread timeout_sampler/__init__.py
Comment thread timeout_sampler/__init__.py

@myakove myakove 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

Found 4 issue(s) in this PR:

💡 Suggestions (4)

File Line Issue
timeout_sampler/__init__.py 128 To clarify this suggestion: the idea is to change the type to str with default
timeout_sampler/__init__.py 103 [CRITICAL] Missing README.md documentation for log_context
tests/test_timeout_sampler.py 177 [WARNING] wait_timeout=5 exceeds recommended test timeout
tests/test_timeout_sampler.py 168 [SUGGESTION] No test for retry decorator with log_context

Review generated by pi

Comment thread timeout_sampler/__init__.py
Comment thread timeout_sampler/__init__.py
Comment thread tests/test_timeout_sampler.py
Comment thread tests/test_timeout_sampler.py

@myakove myakove 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

Found 1 issue(s) in this PR:

💡 Suggestions (1)

File Line Issue
timeout_sampler/__init__.py 128 Good point — you're right that str = "" would change the legacy log format for

Review generated by pi

Comment thread timeout_sampler/__init__.py
Logs "<log_context> succeeded/failed/timed out after Xs" at the three
terminal exits of the sampling loop. Retries stay silent & behavior is
same when log_context is unset.

Signed-off-by: Geetika Kapoor <gkapoor@redhat.com>
Comment thread tests/test_timeout_sampler.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_timeout_sampler.py (1)

196-212: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Brittle mock: exact-count side_effect list couples test to internal call count.

side_effect = [2.0, 2.0] will raise StopIteration if TimeoutWatch.remaining_time() is called more than twice by the implementation. Since both values are identical, return_value=2.0 achieves the same elapsed_time=0.0 result without hardcoding call counts, making the test resilient to internal implementation changes.

♻️ Proposed simplification
     mock_watch = MagicMock()
-    mock_watch.remaining_time.side_effect = [2.0, 2.0]
+    mock_watch.remaining_time.return_value = 2.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_timeout_sampler.py` around lines 196 - 212, The test in
TimeoutSampler is over-constrained by using a fixed two-item side_effect on
TimeoutWatch.remaining_time, which ties it to the current call count. Update the
mock setup in test_log_context_logs_success_when_elapsed_time_is_zero to use a
stable return value instead of an exact-count sequence, so the elapsed-time
assertion still works even if TimeoutSampler or TimeoutWatch calls
remaining_time more times. Keep the assertion on the success log text unchanged
and reference TimeoutWatch and TimeoutSampler when adjusting the mock.
timeout_sampler/__init__.py (1)

313-317: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Chain the re-raised TimeoutExpiredError to preserve the original cause.

Ruff flags B904 here. Raising from within the except block without from obscures the underlying exception in tracebacks.

♻️ Proposed change
                     raise TimeoutExpiredError(
                         self._get_exception_log(exp=last_exp),
                         last_exp=last_exp,
                         elapsed_time=elapsed_time if not (self.print_log and self.log_context) else None,
-                    )
+                    ) from last_exp
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@timeout_sampler/__init__.py` around lines 313 - 317, The TimeoutExpiredError
raised in the exception-handling path should be explicitly chained to preserve
the original cause and satisfy Ruff B904. Update the raise in the retry/timeout
flow around TimeoutExpiredError so it uses exception chaining from the caught
exception or last_exp, keeping the context clear in tracebacks while retaining
the existing log message and elapsed_time behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@timeout_sampler/__init__.py`:
- Around line 256-269: The non-retried failure log in _log_outcome currently
records only the outcome and elapsed time, but it must also include the
exception detail from the TimeoutExpiredError to match the documented
"{log_context} failed after {N.N}s: {exception}" format. Update _log_outcome so
the "failed" branch accepts and logs the exception text, and adjust the call
site that raises or handles TimeoutExpiredError to pass that exception
information into _log_outcome while keeping the succeeded and no-context paths
unchanged.

---

Nitpick comments:
In `@tests/test_timeout_sampler.py`:
- Around line 196-212: The test in TimeoutSampler is over-constrained by using a
fixed two-item side_effect on TimeoutWatch.remaining_time, which ties it to the
current call count. Update the mock setup in
test_log_context_logs_success_when_elapsed_time_is_zero to use a stable return
value instead of an exact-count sequence, so the elapsed-time assertion still
works even if TimeoutSampler or TimeoutWatch calls remaining_time more times.
Keep the assertion on the success log text unchanged and reference TimeoutWatch
and TimeoutSampler when adjusting the mock.

In `@timeout_sampler/__init__.py`:
- Around line 313-317: The TimeoutExpiredError raised in the exception-handling
path should be explicitly chained to preserve the original cause and satisfy
Ruff B904. Update the raise in the retry/timeout flow around TimeoutExpiredError
so it uses exception chaining from the caught exception or last_exp, keeping the
context clear in tracebacks while retaining the existing log message and
elapsed_time behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60f3a572-d48e-4ae4-b12d-56d138ab25ef

📥 Commits

Reviewing files that changed from the base of the PR and between 0e45a3f and c227a3b.

📒 Files selected for processing (3)
  • README.md
  • tests/test_timeout_sampler.py
  • timeout_sampler/__init__.py
✅ Files skipped from review due to trivial changes (1)
  • README.md

Comment thread timeout_sampler/__init__.py

@myakove myakove 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

Found 1 issue(s) in this PR:

💡 Suggestions (1)

File Line Issue
timeout_sampler/__init__.py 256 [CRITICAL] **Simplification request — prepend [log_context] to existing logs i

Review generated by pi


Assisted-by: PI (claude-opus-4-6-1m)

_func_name = self._get_func_info(_func=self.func, type_="__name__")
return f"Function: {_func_module}.{_func_name} {_func_args} {_func_kwargs}".strip()

def _log_outcome(self, elapsed_time: float, outcome: str | None = None) -> None:

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.

[CRITICAL] Simplification request — prepend [log_context] to existing logs instead of introducing new log formats

The current implementation adds significant complexity:

  • _log_outcome() helper method (13 lines)
  • GeneratorExit handler for success logging
  • elapsed_time=None suppression to avoid duplicate log lines
  • Complex ternary in TimeoutExpiredError
  • Multiple conditional branches

All of this stems from introducing new log formats (succeeded after, failed after, timed out after) that conflict with the existing Elapsed time: format.

Simpler approach: just prepend [log_context] to the existing log lines. No new formats, no suppression, no GeneratorExit handler:

# Without log_context (unchanged):
# Elapsed time: 1.5 [0:00:01.500000]

# With log_context:
# [SSH connectivity to test-vm] Elapsed time: 1.5 [0:00:01.500000]

This means:

  • No _log_outcome() method — delete it entirely
  • No GeneratorExit handler — not needed
  • No elapsed_time=None — always preserved in TimeoutExpiredError
  • No new log format — just a prefix on the existing format
  • Fully backward compatible — identical output when log_context is not set
  • Grep-friendlygrep "SSH connectivity" catches all related logs
  • Much simpler diff — the change becomes a few lines in _elapsed_time_log or the log call sites

The prefix also appears on retry logs, which actually helps in CI — you can see which sampler is retrying.

The only tradeoff is losing the explicit "succeeded/failed/timed out" wording, but that's inferrable from context (no exception = success, TimeoutExpiredError = timeout/failure).


Assisted-by: PI (claude-opus-4-6-1m)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Goal for keeping log_context was to reduce log noise for humans and AI-assisted triage. Each wait should end with one short, labeled sentence that answers what was waited on and how it ended, without parsing retry spam, Elapsed time: fragments, or full TimeoutExpiredError blocks.

example:
SSH connectivity to test-vm succeeded after 12.3s.
SSH connectivity to test-vm failed after 3.1s.
SSH connectivity to test-vm timed out after 60.0s.

Why prefix-only doesn't meet that goal

Outcome lines (this PR) [context] prefix on Elapsed time:
Lines per wait One, when the wait finishes One per logged iteration
Outcome Explicit: succeeded / failed / timed out Inferred from context
Retries Silent (by design) Logged with prefix
Timeout / non-retried failure in LOGGER Dedicated outcome line No line exists to prefix — info stays inside TimeoutExpiredError
Token cost for AI/human scan Low Higher — same retry volume, longer lines

Prefix helps grep which sampler logged. It doesn't compress how the wait ended into one line.

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.

Good points, but the "no line exists to prefix" claim isn't accurate.

The finally block fires Elapsed time: on every path — including the last iteration before timeout and before non-retried exception propagation. With the prefix approach + log level differentiation:

  • Success: LOGGER.success("[SSH connectivity to my-vm] Elapsed time: 12.3 [0:00:12.300000]")
  • Timeout: LOGGER.error("[SSH connectivity to my-vm] Elapsed time: 60.0 [0:01:00]") → then TimeoutExpiredError
  • Non-retried exception: LOGGER.error("[SSH connectivity to my-vm] Elapsed time: 3.1 [0:00:03.100000]") → then TimeoutExpiredError

The outcome is clear from the log level (success vs error) and context (exception follows or not). The prefix gives you grep-ability and outcome distinction without a parallel logging system.

The current approach adds ~30 lines of complexity (_log_outcome, GeneratorExit handler, elapsed_time=None suppression, conditional ternary) to produce one line that could instead be a small change to the existing log call sites.


Assisted-by: PI (claude-opus-4-6-1m)

@geetikakay geetikakay Jul 7, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

reason for this style is, it consumes lesser token and faster triage as AI reads verbs better .
my understanding is prefix + logger is lesser code but tokens will cost more. Whatever you think is better we can go with that.

I tried experimenting around both approaches with real agent

  1. SSH connectivity to test-vm succeeded after 12.3s.
  2. ERROR [SSH connectivity to my-vm] Elapsed time: 60.0 [0:01:00]

Format 1: SSH connectivity to test-vm succeeded after 12.3s.

  • Single unambiguous signal: succeeded
  • VM name and duration inline, natural language, easy to extract with a simple pattern
  • An agent reads this in one pass — success, vm=test-vm, duration=12.3s

Format 2: ERROR ... [SSH connectivity to my-vm] Elapsed time: 60.0 [0:01:00]

  • Failure signal is the ERROR prefix — but the reason is implicit (60s = timeout threshold, not stated)
  • VM name is buried inside brackets alongside the action description
  • Time is duplicated in two formats (60.0 seconds and [0:01:00]) — an agent has to reconcile these or pick one

The core problem: Format 2 requires the agent to infer that 60s means timeout — that's only meaningful if the agent already knows the timeout
value. Format 1 is self-describing.

Two components:

  1. Raw token count (small but real)

┌────────────────────────────────────────────────────────────────────┬───────────────┐
│ │ Approx tokens │
├────────────────────────────────────────────────────────────────────┼───────────────┤
│ SSH connectivity to test-vm succeeded after 12.3s. │ ~11 │
├────────────────────────────────────────────────────────────────────┼───────────────┤
│ ERROR ... [SSH connectivity to my-vm] Elapsed time: 60.0 [0:01:00] │ ~18 │
└────────────────────────────────────────────────────────────────────┴───────────────┘

Format 2 is ~60% more tokens per line. Multiplied across thousands of log lines in a test run, this adds up.

  1. Reasoning tokens (the bigger cost)

Format 1 — agent extracts in one pass, no reasoning needed.

Format 2 — agent must:

  • Parse ERROR → look for cause
  • Extract VM name from inside mixed-content brackets
  • Reconcile two time representations
  • Infer 60s = timeout (requires knowing the threshold — if that's not in context, agent makes an extra tool call to find it)

That inference step is where cost spikes. If the timeout value isn't already in the agent's context, it triggers a tool call — reading a config
file or AGENTS.md — adding potentially hundreds of tokens just to interpret one log line.

Summary: The raw token difference is minor. The real cost is the reasoning overhead and the potential extra tool call to resolve implicit
meaning. Format 1 is self-describing — zero extra reasoning, zero extra calls.

Note : with multiple elapsed line in logs i think it will be more expensive operation

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.

Thanks for the detailed analysis — I traced through every exit path in __iter__ to verify both approaches, and I want to share the full picture.

The 4 exit paths

# Path How it exits
1 Success Caller breaks out of for sample in samplerGeneratorExit
2 Non-retried exception _should_ignore_exception returns False → TimeoutExpiredError
3 Timeout (with retried exceptions) while loop ends → TimeoutExpiredError after the loop
4 Timeout (func returned falsy) Same as #3

Your approach (current PR) — what logs on each path

Path Log output Level
✅ Success (GeneratorExit) SSH connectivity succeeded after 12.3s. LOGGER.info
❌ Non-retried exception SSH connectivity failed after 3.1s: {exp} LOGGER.error
⏰ Timeout SSH connectivity timed out after 60.0s. LOGGER.error
🔄 Each retry Nothing (suppressed)

This works, but requires: _log_outcome() helper, GeneratorExit handler, elapsed_time=None suppression in TimeoutExpiredError, and conditional ternary (~30 lines of new code).

Prefix approach — tracing the gap you identified

You're right that the finally block doesn't fire on timeout — here's why:

# In the finally block (inside the while loop):
if self.print_log and elapsed_time and not self.log_context:
    LOGGER.info(_elapsed_time_log(elapsed_time=elapsed_time))

elapsed_time is set before yield, then reset to None after time.sleep(). On the last retry iteration, it gets cleared. When the while condition fails, the loop exits — the finally already ran and cleared. The timeout raise happens after the loop, outside try/finally.

Path elapsed_time in finally? Logs?
✅ Success (caller breaks) ✅ Set before yield, never cleared ✅ Yes
❌ Non-retried exception ✅ Set in except block ✅ Yes
🔄 Retry iteration ❌ Cleared after sleep ❌ Correct — no log
⏰ Timeout ❌ Cleared on last retry No log

So the gap is real — but the fix is trivial. The timeout section after the loop already exists:

timeout_elapsed = self.wait_timeout - timeout_watch.remaining_time()
# Just add the prefix here:
if self.print_log:
    _log = _elapsed_time_log(elapsed_time=timeout_elapsed)
    if self.log_context:
        _log = f"[{self.log_context}] {_log}"
    LOGGER.error(_log)
raise TimeoutExpiredError(...)

Prefix approach — result (all 3 paths covered)

Path Log output Level
✅ Success [SSH connectivity] Elapsed time: 12.3 [0:00:12.300000] LOGGER.success
❌ Non-retried exception [SSH connectivity] Elapsed time: 3.1 [0:00:03.100000] LOGGER.error
⏰ Timeout [SSH connectivity] Elapsed time: 60.0 [0:01:00.000000] LOGGER.error
🔄 Each retry Nothing (same as your approach)

What changes are needed

  1. Modify _elapsed_time_log (or the call sites) to prepend [log_context] when set
  2. Change LOGGER.infoLOGGER.success on the success path in finally
  3. Add prefix to the post-loop timeout log (already exists, just needs prefix)

No _log_outcome(), no GeneratorExit handler, no elapsed_time=None suppression.

Agent-friendliness

The log level (SUCCESS/ERROR) is the verb. Any agent parsing structured logs reads the level first — that's the outcome signal. The difference between:

  • SUCCESS [SSH connectivity] Elapsed time: 12.3 [...]
  • SSH connectivity succeeded after 12.3s.

is one inference step that costs near-zero tokens. Both are greppable, both are one line per outcome, both suppress retries.

The key tradeoff: your approach produces slightly more natural-language output, but at ~30 lines of added complexity. The prefix approach achieves the same goals (one outcome line, context label, retry suppression) with minimal code changes.

Let's go with the prefix approach — it covers all paths and keeps the code simple.


Assisted-by: PI (claude-opus-4-6-1m)

@myakove myakove 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

Found 1 issue(s) in this PR:

💡 Suggestions (1)

File Line Issue
tests/test_timeout_sampler.py 181 sleep=0 is fine for unit tests — no reason to add artificial delay when we're

Review generated by pi


Assisted-by: PI (claude-opus-4-6-1m)

Comment thread tests/test_timeout_sampler.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants