Skip to content

feat: add timestamp parsing for container execution logs (#1343) - #1740

Open
sufirehman wants to merge 4 commits into
green-coding-solutions:mainfrom
sufirehman:main
Open

feat: add timestamp parsing for container execution logs (#1343)#1740
sufirehman wants to merge 4 commits into
green-coding-solutions:mainfrom
sufirehman:main

Conversation

@sufirehman

@sufirehman sufirehman commented Jun 23, 2026

Copy link
Copy Markdown

Description

Implements Option 1 (Integrated Timestamp Storage) from the issue, scoped specifically to LogType.CONTAINER_EXECUTION. Adds the --timestamps flag to the docker logs call in _read_container_logs(), parses each line's RFC3339Nano timestamp prefix into {timestamp, content} entries, and stores them as stdout_entries/stderr_entries for container logs only. SETUP_COMMAND and FLOW_COMMAND log types keep their existing plain stdout/stderr string structure completely unchanged.

Related Issue

Closes #1343

Motivation and Context

Container logs span multiple phases (BOOT, IDLE, RUNTIME) and are currently stored as a single undifferentiated block per phase. Docker's --timestamps flag gives per-line RFC3339Nano timestamps for free, which lets us time-key container log lines for future phase-splitting and analysis, as proposed by @ArneTR.

How Has This Been Tested?

Added 6 tests: timestamp parsing on normal docker --timestamps output, fallback to {timestamp: None, content: line} for any line that doesn't match the expected format, empty input handling, and three tests confirming CONTAINER_EXECUTION entries use the new structure while SETUP_COMMAND and FLOW_COMMAND entries remain plain strings, completely unaffected. All 6 tests pass. (Note: the existing test teardown calls Tests.reset_db(), which requires the test Postgres container to be running; this is unrelated to the change and fails identically on main without the test Docker stack started.)

Scope notes

Per discussion with @ArneTR, this PR does not touch frontend display. Per his guidance, the plan for a follow-up is to simply prepend each line with its timestamp in the Logs tab, similar to how docker logs -t displays output, with no interleaving logic for now.

Types of changes

  • New feature (non-breaking change which adds functionality)

Summary

  • Added Docker timestamp support for container execution logs.
  • Parses RFC3339Nano prefixes into {timestamp, content} entries, with None for unmatched lines.
  • Stores container logs as stdout_entries/stderr_entries while preserving plain strings for setup and flow commands.
  • Added tests covering parsing, fallback behavior, empty input, and log-type-specific storage.

Frontend display and phase detection remain out of scope.

…reen-coding-solutions#1343)

Pass `--timestamps` to `docker logs` so each line is prefixed with an
RFC3339Nano timestamp. A new module-level helper
`_parse_timestamped_log_lines()` parses those prefixed lines into
`{timestamp, content}` dicts, falling back to `{timestamp: None, ...}`
for lines that don't match.

`_add_to_current_run_log()` now branches on log type: CONTAINER_EXECUTION
entries store `stdout_entries`/`stderr_entries` (lists of the parsed
dicts) instead of plain strings. All other log types (SETUP_COMMAND,
FLOW_COMMAND, NETWORK_STATS, EXCEPTION) are unchanged and continue to
use plain `stdout`/`stderr` keys.

Tests cover the parser (normal, fallback, empty input) and confirm the
correct key shape for CONTAINER_EXECUTION, SETUP_COMMAND, and
FLOW_COMMAND log entries.
@sufirehman

Copy link
Copy Markdown
Author

Hi @ArneTR, this PR has been open since June 23 without any feedback yet, so just checking in. I've since resolved a merge conflict in lib/scenario_runner.py that came up from recent upstream changes, and the timestamp parsing tests still pass cleanly against the merged code.

No rush at all if you're busy, just wanted to make sure it hadn't slipped through unnoticed. Happy to adjust the approach if anything needs changing.

@ArneTR

ArneTR commented Jul 25, 2026

Copy link
Copy Markdown
Member

Hey @sufirehman,

sorry for the this. The PR simply slipped through 😨

Reviewing now

@ArneTR

ArneTR commented Jul 25, 2026

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Docker container logs are now requested with timestamps and parsed into timestamp/content entries with null-byte normalization and unmatched-line fallback handling. Container execution records store structured stdout and stderr entry arrays, while setup and flow command records retain sanitized stdout and stderr strings. Tests cover parsing behavior, empty input, fallback cases, and the log-record shapes for each supported log type.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: timestamp parsing for container execution logs.
Linked Issues check ✅ Passed The PR implements the listed requirements: Docker timestamps, RFC3339Nano parsing, timestamped container entries, fallback handling, and plain setup/flow logs.
Out of Scope Changes check ✅ Passed The changes stay within the stated scope and only touch log parsing/storage plus tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_runner.py (1)

981-1060: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

test_logs_structure will now fail: it still asserts plain stdout/stderr for container-execution entries.

_add_to_current_run_log (lib/scenario_runner.py, lines 2063-2086) now stores stdout_entries/stderr_entries instead of stdout/stderr for LogType.CONTAINER_EXECUTION. This test wasn't updated: line 1016's assert "stdout" in log_entry or "stderr" in log_entry will fail on the container-execution entry produced by the capture_logs.yml run, and the content check at line 1051 ("stdout" in log_entry and "Test log from container" in log_entry["stdout"]) will never match, so found_container_execution stays False, failing line 1060 too.

Please update this test to branch on log_entry["type"] and check stdout_entries/stderr_entries (with per-entry content) for CONTAINER_EXECUTION, while keeping the plain stdout/stderr checks for the other types. Worth also checking the sibling capture_logs_with_null_bytes.yml / capture_logs_with_invalid_character.yml tests for the same pattern.

🧹 Nitpick comments (1)
lib/scenario_runner.py (1)

2063-2102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated str/bytes coercion into one helper.

The str/decode/fallback logic (isinstance(..., str)hasattr(..., 'decode')str(...)) is duplicated four times across the CONTAINER_EXECUTION and non-container branches. This also triggers the four Ruff B009 warnings ("Do not call getattr with a constant attribute value") flagged on lines 2071, 2080, 2090, 2098. Consolidating into one helper removes the duplication and the lint warnings at once.

♻️ Proposed refactor
+def _coerce_to_text(data):
+    if isinstance(data, str):
+        return data
+    if hasattr(data, 'decode'):
+        return data.decode('UTF-8', errors='replace')
+    return str(data)
+
 def _add_to_current_run_log(self, container_name, log_type, log_id, cmd, phase, stdout=None, stderr=None, flow=None, exception_class=None):
     ...
     if log_type == LogType.CONTAINER_EXECUTION:
         if stdout is not None:
-            if isinstance(stdout, str):
-                stdout_str = stdout
-            elif hasattr(stdout, 'decode') and callable(getattr(stdout, 'decode')):
-                stdout_str = stdout.decode('UTF-8', errors='replace')
-            else:
-                stdout_str = str(stdout)
-            stdout_str = utils.filter_sensitive_data(stdout_str)
+            stdout_str = utils.filter_sensitive_data(_coerce_to_text(stdout))
             log_entry['stdout_entries'] = _parse_timestamped_log_lines(stdout_str)
         if stderr is not None:
-            if isinstance(stderr, str):
-                stderr_str = stderr
-            elif hasattr(stderr, 'decode') and callable(getattr(stderr, 'decode')):
-                stderr_str = stderr.decode('UTF-8', errors='replace')
-            else:
-                stderr_str = str(stderr)
-            stderr_str = utils.filter_sensitive_data(stderr_str)
+            stderr_str = utils.filter_sensitive_data(_coerce_to_text(stderr))
             log_entry['stderr_entries'] = _parse_timestamped_log_lines(stderr_str)
     else:
         if stdout is not None:
-            if isinstance(stdout, str):
-                log_entry['stdout'] = stdout.replace('\x00', '0x00')
-            elif hasattr(stdout, 'decode') and callable(getattr(stdout, 'decode')):
-                log_entry['stdout'] = stdout.decode('UTF-8', errors='replace').replace('\x00', '0x00')
-            else:
-                log_entry['stdout'] = str(stdout).replace('\x00', '0x00')
-            log_entry['stdout'] = utils.filter_sensitive_data(log_entry['stdout'])
+            log_entry['stdout'] = utils.filter_sensitive_data(_coerce_to_text(stdout).replace('\x00', '0x00'))
         if stderr is not None:
-            ... (same pattern)
+            log_entry['stderr'] = utils.filter_sensitive_data(_coerce_to_text(stderr).replace('\x00', '0x00'))

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e26cb20-4cf1-4d98-ba08-e66bae94de0f

📥 Commits

Reviewing files that changed from the base of the PR and between 881bb63 and 695268c.

📒 Files selected for processing (2)
  • lib/scenario_runner.py
  • tests/test_runner.py

@ArneTR ArneTR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR. I now had time to review it.

I made remarks in the code directly where appropriate.

But overall this PR does not fully implement the idea intended.

  • It does not show in the frontend. Although discussed that we do not want to touch the interleaving and phase splitting in the frontend, it should still show in the legacy box. I made an idea in the code how to achieve this
  • Not implemented is the fact that docker exec calls will not have timestamps. Since they typically make up the majority of our logs I feel this must be part of the implementation. Would be great if you supply some ideas here how to achieve this. As David said: This might be a trickier one though :)
    If you feel this is too tough to implement I can understand. But I feel for only having timestamps in the docker logs this PR is currently not enough to merge.

Comment thread lib/scenario_runner.py Outdated
else:
log_entry['stderr'] = str(stderr).replace('\x00', '0x00') # we just force it to a string. This can garble output a bit though
log_entry['stderr'] = utils.filter_sensitive_data(log_entry['stderr'])
if log_type == LogType.CONTAINER_EXECUTION:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is very duplicated code. I feel this whole function should be refactored to be more compact and not branch into two big branches with identical sub-branches

Comment thread lib/scenario_runner.py Outdated
else:
stdout_str = str(stdout)
stdout_str = utils.filter_sensitive_data(stdout_str)
log_entry['stdout_entries'] = _parse_timestamped_log_lines(stdout_str)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Storing in this element is useful. But in the end it still must be copied as string only int stdout / stderr. Otherwise the logs currently do not show in the frontend

@sufirehman

Copy link
Copy Markdown
Author

Hey @ArneTR, thanks for the review!

Points 1 and 2 make sense, I'll fix the frontend display and clean up the branching in _add_to_current_run_log.

For the docker exec timestamps, I looked into it and the issue is structural: exec output bypasses the Docker daemon's log driver so there's no --timestamps equivalent available. I'll add a comment explaining this as a known gap rather than pushing a streaming workaround that would only give host-receipt timestamps with buffering issues. Let me know if you'd rather I take a different approach there.

Will push the fix shortly.

@sufirehman

Copy link
Copy Markdown
Author

Hi @ArneTR, pushed the updated implementation:

  1. Fixed the frontend display issue, _add_to_current_run_log now populates both stdout_entries/stderr_entries (structured, timestamped) and plain stdout/stderr (content joined for the legacy log box). The existing test_logs_structure integration test, which already expected this behavior, now passes.

  2. Refactored _add_to_current_run_log using a _stringify_process_output helper, collapsing the duplicated decode/str-fallback logic that appeared four times into one loop over stdout and stderr.

  3. For docker exec timestamps: confirmed this is a structural limitation, exec output bypasses the Docker daemon log driver entirely so there's no --timestamps equivalent. Added comments at both exec call sites explaining this. Happy to discuss further if you'd prefer a different approach.

Let me know if anything else needs adjusting.

…utput handling

Container log entries stored stdout_entries/stderr_entries (timestamped,
parsed from `docker logs -t`) but never populated the plain stdout/stderr
fields, so the frontend's legacy log box showed nothing for container
output. _add_to_current_run_log now also joins the parsed entries'
content into stdout/stderr for that log type.

Collapse the near-duplicate stdout/stderr branches in
_add_to_current_run_log into a single loop, extracting the repeated
isinstance/decode/str-fallback conversion into a new module-level
_stringify_process_output helper.

Document why docker exec output (setup-commands and flow-commands)
can't get the same per-line timestamps as `docker logs -t`: exec
bypasses the daemon's logging driver entirely, and output is only ever
read back as one fully-buffered blob after the process exits, so even
host-side receipt timestamps would be false precision.

@ArneTR ArneTR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The function is a lot clear now, ty.

I have made a remark where I think it can further be improved.

Will follow up on how to extend the PR.

Comment thread lib/scenario_runner.py
log_entry[f'{key}_entries'] = entries
# Must also be copied as plain string: the (legacy) frontend log box reads
# stdout/stderr directly and does not know about the *_entries structure.
log_entry[key] = '\n'.join(entry['content'] for entry in entries)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why this operation? and not just have the following else-branch unconditional. joining the entries here should be the same as setting log_entry[key] = value_str or?

@ArneTR

ArneTR commented Jul 27, 2026

Copy link
Copy Markdown
Member

The remark that docker exec simply cannot do what we need at the moment is nice to have in the comments.
However I feel in the end this PR needs to address this to fulfill the open issue.

To make this achievable I think GMT should inject the functionality we need into the container. Here an idea, happy for your thoughs and remarks:

  • For every exec call we do not simply execute the program, but rather inject a program. Let's call it ts_x for now.
  • The program will be very similar to ts from moreutils with two important distinctions:
    • It can run in a container without any dependencies. THis means also no dynamically linked libc. It should bring its own libc statically linked
    • It does not require a pipe on the CLI (which is shell feature ... we cannot assume a shell in the container), but does process inheritance (similar to how timeout or stdbuf work)

This way we can then run ts docker exec MY_PROGRAM and it will prepend everything with timestamps, similar to how docker logs works.

Considerations

  • Measurement of overhead in a heavy logging file
  • Measurement of overhead in a file that does no logging at all

This makes the PR bigger, but would fulfill the whole functionality. It requires some C work, but I guess also something a bit out of the ordinary :)

Let me know how you feel about extending the PR like this and if you feel you want to take this on.

@sufirehman

Copy link
Copy Markdown
Author

Hey @ArneTR, good question on the join.

They're actually not equivalent in this case. _parse_timestamped_log_lines reads value_str but doesn't mutate it, so value_str at that point still contains the raw docker logs -t output with RFC3339Nano timestamps prefixed on every line (e.g. "2025-09-17T06:46:17.013138795Z hello from container"). Using value_str directly for the plain field would put raw timestamped text into stdout/stderr, which breaks the legacy frontend display. The join reconstructs the timestamp-stripped version, which is what the frontend actually needs. I've added a clarifying comment in the code to make this explicit.

On the ts_x idea: I think it's a genuinely interesting approach and worth doing properly. A statically linked C binary with process inheritance, no shell dependency, and overhead benchmarking is meaningfully different in scope from what this PR set out to address though. Rather than extending this PR further, would it make sense to merge what's here (docker logs timestamps working and tested end to end) and open a follow-up issue for ts_x with your design notes as the starting point? Happy to open that issue if that works for you.

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.

Time-splitting of container logs

2 participants