Skip to content

feat: S13.5 walking-skeleton scenario green on Windows via OTel oracle#154

Merged
DavidCozens merged 2 commits into
mainfrom
feat/windows-bdd-oracle
Apr 18, 2026
Merged

feat: S13.5 walking-skeleton scenario green on Windows via OTel oracle#154
DavidCozens merged 2 commits into
mainfrom
feat/windows-bdd-oracle

Conversation

@DavidCozens

@DavidCozens DavidCozens commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • OTel Collector Contrib oracle as a Windows-native replacement for syslog-ng. RFC 5424 UDP receiver on 127.0.0.1:5514, file exporter writing JSON Lines. Pinned v0.150.1, SHA-256 verified, idempotently installed via Bdd/otel/Install-OtelCollector.ps1.
  • Step abstraction: same syslog_steps.py drives both runners. EXAMPLE_BINARY / RECEIVED_LOG / ORACLE_FORMAT env vars select binary path + oracle output file + parser. New parse_otel_jsonl_line produces the same flat field dict as parse_syslog_ng_line, so Then steps don't care which oracle is in use.
  • run_example portable — uses process.communicate() and an absolute binary path. No more select.select on a pipe fd (Windows doesn't support it) and no more forward-slash relative paths (Windows CreateProcess can't resolve them). The threaded example keeps the prompt-protocol path via the new run_threaded_example helper, since its service thread is async and needs synchronisation between send N and quit.
  • ExampleAppName_Set strips trailing .exe / .EXE so the syslog app name omits the OS executable extension regardless of build platform. TDD-driven by the failing walking-skeleton scenario which expected SolidSyslogExample but was getting SolidSyslogExample.exe. Four new unit tests cover lower/upper case, with-path, and non-.exe-extension control cases.
  • syslog.feature promoted out of @windows_wip.
  • docs/bdd.md documents the two-oracle architecture and the local Windows setup recipe.

TDD trail

  • BDD scenario red on Windows: app name "SolidSyslogExample" got SolidSyslogExample.exe.
  • Unit test red: Tests/Example/ExampleAppNameTest.cpp::ExeExtensionIsStrippedexpected <app> but was <app.exe>.
  • Green: added EndsWithDotExe helper and a separate appNameLength so the borrowed argv0 slice can be terminated short of the extension.
  • Refactor (still green): added 3 more regression cases (upper-case, with-path, non-.exe control) — total 8 unit tests for ExampleAppName.

Local end-to-end

$ EXAMPLE_BINARY=build/msvc-debug/Example/Debug/SolidSyslogExample.exe \
  RECEIVED_LOG=Bdd/output/received.jsonl \
  ORACLE_FORMAT=otel-jsonl \
  behave Bdd/features/syslog.feature
1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
7 steps passed, 0 failed, 0 skipped

Local gates

Gate Result
Linux BDD (docker compose -f ci/docker-compose.bdd.yml up) ✅ 14 features / 31 scenarios / 160 steps
Windows walking-skeleton via behave + otelcol locally ✅ 1 scenario / 7 steps
debug (530 unit tests)
sanitize (530 unit tests, ASan+UBSan)
tidy
cppcheck
coverage ✅ 99.9% lines, 100% functions
format (clang-format --Werror)
msvc-debug build of SolidSyslogWindowsExample

Deferred to next PR

  • bdd-windows CI job on windows-2025 — packaging the now-known-good local recipe. Requires a runner with Python + the install script. Kept separate so this PR's behavioural change (oracle abstraction + walking-skeleton green) lands independently of CI plumbing.
  • Promoting more scenarios out of @windows_wip (header_fields, prival, timestamp, message_fields, structured_data, origin, time_quality). Some will need STRUCTURED_DATA re-rendering in parse_otel_jsonl_line; others may need additional .exe / Windows-portability fixes — let the failing scenarios drive each one.

Refs #129

Test plan

  • All CI status checks pass: build-and-test, clang-build-and-test, sanitize, coverage, tidy, cppcheck, format, bdd, windows-build-and-test
  • Linux bdd job still passes — every scenario it ran before still runs (no regressions from the step refactor)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added OpenTelemetry Collector support for Windows BDD testing and JSONL log parsing
    • Included OTel Collector configuration and a Windows installer script for the collector
  • Documentation

    • Expanded BDD setup guide with Windows/OTel oracle instructions and environment variable details
  • Tests

    • Updated tests for executable name handling to strip .exe
  • Chores

    • Broadened .gitignore to ignore OTel collector binaries
    • Pinned BDD test dependency (behave==1.3.3)

Introduces an OpenTelemetry Collector Contrib oracle as a Windows-native
replacement for syslog-ng, makes the BDD step code oracle-agnostic, and
promotes the walking-skeleton scenario out of @windows_wip.

Oracle infrastructure:
- Bdd/otel/config.yaml — RFC 5424 UDP receiver on 127.0.0.1:5514, file
  exporter writing JSON Lines.
- Bdd/otel/Install-OtelCollector.ps1 — pinned otelcol-contrib v0.150.1,
  SHA-256 verified, idempotent.
- Bdd/requirements.txt — pins behave==1.3.3 (matches the Linux container
  image so both runners parse identically).

Step abstraction:
- environment.py reads EXAMPLE_BINARY, RECEIVED_LOG, ORACLE_FORMAT from
  the env so the same step file drives both runners.
- syslog_steps.py adds parse_otel_jsonl_line + parse_oracle_line dispatch.
  Walking-skeleton scope only — STRUCTURED_DATA re-rendering for OTel is
  added when the structured-data scenarios are promoted.
- run_example refactored to use process.communicate() and an absolute
  binary path — portable across Linux and Windows (no select.select on a
  pipe fd, which Windows doesn't support; Windows CreateProcess can't
  resolve forward-slash relative paths).
- run_threaded_example keeps the prompt-protocol path so the threaded
  example's service thread has time to drain the buffer between "send N"
  and "quit". POSIX-only — all threaded scenarios are tagged @Buffered
  and excluded from the Windows runner.

ExampleAppName_Set now strips a trailing .exe/.EXE so the syslog app
name omits the OS executable extension regardless of build platform.
TDD-driven by the failing walking-skeleton scenario which expected
"SolidSyslogExample" but was getting "SolidSyslogExample.exe". 4 new
unit tests cover lower/upper case, with-path, and non-.exe-extension
control cases.

Verified locally:
- Linux BDD via ci/docker-compose.bdd.yml: 14 features / 31 scenarios /
  160 steps all green.
- Windows walking-skeleton via behave + otelcol locally: 1 scenario /
  7 steps green.
- Linux gates: tidy, cppcheck, sanitize (530 tests), format, coverage
  (99.9%/100%) all green.

Refs #129

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds OpenTelemetry Collector as an alternative "oracle" for Windows BDD tests, expands BDD environment/config to select syslog-ng or OTel JSONL parsing, updates runners and parsers to be oracle-agnostic, adds OTel installer/config files and .gitignore rule, and strips trailing .exe in Example app-name handling.

Changes

Cohort / File(s) Summary
BDD infra & OTel artifacts
\.gitignore, Bdd/requirements.txt, Bdd/otel/Install-OtelCollector.ps1, Bdd/otel/config.yaml
Added OTel support: ignore rule for Bdd/otel/bin/; PowerShell installer (download, verify SHA256, extract, idempotent); OTel Collector YAML config exporting JSONL to Bdd/output/received.jsonl; pinned behave==1.3.3.
BDD environment & steps
Bdd/features/environment.py, Bdd/features/steps/syslog_steps.py, Bdd/features/syslog.feature
Environment exposes context.received_log and context.oracle_format (default syslog-ng), changed default context.example_binary; added parse_otel_jsonl_line and parse_oracle_line dispatch; renamed parse_syslog_lineparse_syslog_ng_line; refactored run_example to one-shot, added run_threaded_example for POSIX threaded cases; updated steps to read context.received_log and conditional syslog-ng assertions; removed @windows_wip tag.
Example app name handling & tests
Example/Common/ExampleAppName.c, Tests/Example/ExampleAppNameTest.cpp
Strip trailing .exe (case-insensitive) when storing app name length via new helper; pass precomputed length to formatter; updated/added tests covering .exe stripping, casing, and path cases.
Documentation
docs/bdd.md
Documented OTel collector runner option, ORACLE_FORMAT dispatch, Windows setup steps including installer usage, config and port notes, and updated tag guidance.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Behave as BDD Test Runner
    participant Example as Example Binary
    participant OTel as OTel Collector
    participant File as File Exporter
    participant Parser as parse_oracle_line

    rect rgba(100, 150, 255, 0.5)
    Note over User,Parser: OTel JSONL Oracle (Windows)
    User->>Behave: run behave --tags `@udp` ORACLE_FORMAT=otel-jsonl
    Behave->>Example: launch context.example_binary
    Example-->>OTel: send syslog UDP 5514
    OTel->>OTel: RFC5424 parse → records
    OTel->>File: write JSON Lines to Bdd/output/received.jsonl
    Behave->>File: read latest jsonl line from context.received_log
    Behave->>Parser: parse_oracle_line(latest_line, "otel-jsonl")
    Parser->>Behave: flat field dict
    Behave->>Behave: assert fields match expectations
    end
Loading
sequenceDiagram
    actor User
    participant Behave as BDD Test Runner
    participant Example as Example Binary
    participant Syslog as syslog-ng
    participant Parser as parse_oracle_line

    rect rgba(150, 200, 100, 0.5)
    Note over User,Parser: syslog-ng Oracle (POSIX)
    User->>Behave: run behave --tags `@udp` ORACLE_FORMAT=syslog-ng
    Behave->>Example: launch context.example_binary
    Example-->>Syslog: send syslog UDP 514
    Syslog->>Syslog: key=value parsing -> append to context.received_log
    Behave->>Syslog: tail context.received_log
    Behave->>Parser: parse_oracle_line(latest_line, "syslog-ng")
    Parser->>Behave: flat field dict (via parse_syslog_ng_line)
    Behave->>Behave: assert fields match expectations
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped in with a tiny cheer,

OTel arrives to listen near,
JSON lines and key=val sing,
.exe trimmed — a tidy thing,
BDD tests hop, ready to spring.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main objective: enabling the walking-skeleton BDD scenario to pass on Windows via an OpenTelemetry (OTel) oracle, which is the primary behavioral change across all modifications.
Description check ✅ Passed The PR description comprehensively covers Purpose (OTel oracle with links), Change Description (step abstraction, runner portability, app name handling, scenario promotion, docs), Test Evidence (TDD trail, local end-to-end example, gate results), and Areas Affected (BDD steps/runners, example app, tests, documentation). All required template sections are complete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/windows-bdd-oracle

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

@github-actions

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   Unit Tests (GCC): 99% successful (✔️ 539 passed, 🙈 3 skipped)
   🚦   Unit Tests (Clang): 99% successful (✔️ 498 passed, 🙈 3 skipped)
   🚦   Unit Tests (Sanitize): 99% successful (✔️ 498 passed, 🙈 3 skipped)
   🚦   BDD Tests: 100% successful (✔️ 31 passed)
   🚦   Unit Tests (MSVC): 100% successful (✔️ 405 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
Bdd/features/steps/syslog_steps.py (1)

243-260: ⚠️ Potential issue | 🟠 Major

Always tear down the threaded helper on intermediate failures.

If wait_for_prompt, send_command, or wait_for_messages raises here, the function exits before sending quit, leaving the example process alive for later scenarios. That kind of leak tends to cascade through the rest of the BDD run.

🧹 Suggested fix
     process = subprocess.Popen(
         cmd,
         stdin=subprocess.PIPE,
         stdout=subprocess.PIPE,
         stderr=subprocess.PIPE,
         text=True,
     )
     context.example_pid = process.pid
 
-    wait_for_prompt(process)
-    send_command(process, f"send {expected_messages}")
-    wait_for_messages(context, expected_messages)
-
-    process.stdin.write("quit\n")
-    process.stdin.flush()
-    process.wait(timeout=10)
-    assert process.returncode == 0, (
-        f"Threaded example failed with exit code {process.returncode}"
-    )
+    try:
+        wait_for_prompt(process)
+        send_command(process, f"send {expected_messages}")
+        wait_for_messages(context, expected_messages)
+
+        process.stdin.write("quit\n")
+        process.stdin.flush()
+        process.wait(timeout=10)
+        assert process.returncode == 0, (
+            f"Threaded example failed with exit code {process.returncode}"
+        )
+    finally:
+        if process.poll() is None:
+            process.kill()
+            process.wait(timeout=5)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Bdd/features/steps/syslog_steps.py` around lines 243 - 260, Ensure the
spawned helper process is always torn down by wrapping the interaction sequence
(wait_for_prompt, send_command, wait_for_messages) in a try/finally: use the
existing process variable and context.example_pid to send the "quit\n" (via
process.stdin.write + flush) and wait for process termination in the finally
block, and if the process does not exit within timeout call process.kill() and
then process.wait(); keep the final assert on process.returncode after cleanup
so intermediate exceptions cannot leak the helper process.
🧹 Nitpick comments (1)
Example/Common/ExampleAppName.c (1)

54-57: Consider using the named constant for consistency.

Line 56 uses the literal 4 while the helper defines EXE_SUFFIX_LENGTH = 4. Using the constant would improve maintainability, though not critical since the value is unlikely to change.

♻️ Optional: Use the constant
+enum
+{
+    EXE_SUFFIX_LENGTH = 4
+};
+
 void ExampleAppName_Set(const char* argv0)
 {
     ...
     if (EndsWithDotExe(appName, appNameLength))
     {
-        appNameLength -= 4;
+        appNameLength -= EXE_SUFFIX_LENGTH;
     }
 }

This would require hoisting the enum to file scope or repeating it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Example/Common/ExampleAppName.c` around lines 54 - 57, The code subtracts a
magic number 4 from appNameLength after calling EndsWithDotExe; replace the
literal with the named constant EXE_SUFFIX_LENGTH to improve maintainability:
update the subtraction to use EXE_SUFFIX_LENGTH instead of 4 and ensure
EXE_SUFFIX_LENGTH is visible here (either by hoisting the enum/constant to file
scope or defining an equivalent local constant) so the change compiles and
retains the same behavior for EndsWithDotExe and appNameLength adjustments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Bdd/features/steps/syslog_steps.py`:
- Around line 88-121: The parser never sets fields["MSG"], so update
parse_otel_jsonl_line to extract the log message from the OTel log record (use
the local variable log) before returning; check log.get("body") and handle both
plain string bodies and object bodies (e.g., dicts containing "stringValue" or
similar string keys), normalize to a Python string, and assign it to
fields["MSG"] (skip or omit if no body present) so the returned dict shape
includes MSG alongside PRIORITY/TIMESTAMP/HOSTNAME/APP_NAME/PROCID.

In `@Bdd/otel/Install-OtelCollector.ps1`:
- Around line 22-27: The current fast-path returns early if $exePath exists,
skipping the requested $Version/$Sha256 verification; change the logic in
Install-OtelCollector so that when Test-Path $exePath is true you compute and
validate the existing binary's integrity (e.g., use Get-FileHash -Algorithm
SHA256 on $exePath and compare to $Sha256, and/or inspect the binary/version
metadata against $Version) and only return if the hash/version matches; if it
doesn't match, remove or overwrite the existing otelcol-contrib.exe in $BinDir
and continue with the normal download/install flow to ensure the pinned
$Version/$Sha256 are enforced.

In `@docs/bdd.md`:
- Around line 167-174: The documentation uses bash background operator '&' after
'./Bdd/otel/bin/otelcol-contrib.exe --config=Bdd/otel/config.yaml' which is
invalid in PowerShell; update the docs to show both POSIX and PowerShell ways to
run the OTel collector in the background by keeping the existing bash example
with '&' and adding a PowerShell alternative (e.g., referencing Start-Process to
launch ./Bdd/otel/bin/otelcol-contrib.exe with the
"--config=Bdd/otel/config.yaml" argument) plus a short note that PowerShell
users should use that form or run the collector in a separate terminal.
- Around line 170-174: The docs show bash-style inline environment variables
(EXAMPLE_BINARY, RECEIVED_LOG, ORACLE_FORMAT) preceding the behave command which
won’t work in PowerShell; update the Windows instructions to provide the
PowerShell equivalent by either (a) setting the env vars beforehand using
PowerShell syntax ($env:EXAMPLE_BINARY = "...", $env:RECEIVED_LOG = "...",
$env:ORACLE_FORMAT = "...") and then running the behave command, or (b) wrap the
bash-style invocation in pwsh -c "..." so the same inline VAR=value behave
invocation runs in a bash-like shell; make the change near the block that shows
EXAMPLE_BINARY=build/... and the behave --tags=... Bdd/features/ invocation and
add a short note indicating which approach is for PowerShell.

---

Outside diff comments:
In `@Bdd/features/steps/syslog_steps.py`:
- Around line 243-260: Ensure the spawned helper process is always torn down by
wrapping the interaction sequence (wait_for_prompt, send_command,
wait_for_messages) in a try/finally: use the existing process variable and
context.example_pid to send the "quit\n" (via process.stdin.write + flush) and
wait for process termination in the finally block, and if the process does not
exit within timeout call process.kill() and then process.wait(); keep the final
assert on process.returncode after cleanup so intermediate exceptions cannot
leak the helper process.

---

Nitpick comments:
In `@Example/Common/ExampleAppName.c`:
- Around line 54-57: The code subtracts a magic number 4 from appNameLength
after calling EndsWithDotExe; replace the literal with the named constant
EXE_SUFFIX_LENGTH to improve maintainability: update the subtraction to use
EXE_SUFFIX_LENGTH instead of 4 and ensure EXE_SUFFIX_LENGTH is visible here
(either by hoisting the enum/constant to file scope or defining an equivalent
local constant) so the change compiles and retains the same behavior for
EndsWithDotExe and appNameLength adjustments.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 397fa805-f22f-4b58-9a7c-e6edb5732f30

📥 Commits

Reviewing files that changed from the base of the PR and between adfda3c and a23239e.

📒 Files selected for processing (10)
  • .gitignore
  • Bdd/features/environment.py
  • Bdd/features/steps/syslog_steps.py
  • Bdd/features/syslog.feature
  • Bdd/otel/Install-OtelCollector.ps1
  • Bdd/otel/config.yaml
  • Bdd/requirements.txt
  • Example/Common/ExampleAppName.c
  • Tests/Example/ExampleAppNameTest.cpp
  • docs/bdd.md

Comment on lines +88 to +121
def parse_otel_jsonl_line(line):
"""Parse one JSON line from the OTel Collector file exporter into the
same flat dict shape as parse_syslog_ng_line.

Walking-skeleton scope only: PRIORITY/TIMESTAMP/HOSTNAME/APP_NAME/PROCID
plus MSG. STRUCTURED_DATA is not yet rendered back into syslog-ng's
`[name key="value"]` text form — to be added when promoting the
structured_data scenarios out of @windows_wip.
"""
record = json.loads(line)
log = record["resourceLogs"][0]["scopeLogs"][0]["logRecords"][0]
attrs = log.get("attributes", [])

fields = {}
priority = _otel_attribute(attrs, "priority")
if priority is not None:
fields["PRIORITY"] = priority
hostname = _otel_attribute(attrs, "hostname")
if hostname is not None:
fields["HOSTNAME"] = hostname
appname = _otel_attribute(attrs, "appname")
if appname is not None:
fields["APP_NAME"] = appname
proc_id = _otel_attribute(attrs, "proc_id")
if proc_id is not None:
fields["PROCID"] = proc_id

# timeUnixNano → ISO 8601 string compatible with datetime.fromisoformat
time_ns = log.get("timeUnixNano")
if time_ns is not None:
seconds = int(time_ns) / 1e9
fields["TIMESTAMP"] = datetime.fromtimestamp(seconds, tz=timezone.utc).isoformat()

return fields

@coderabbitai coderabbitai Bot Apr 18, 2026

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.

⚠️ Potential issue | 🟠 Major

Populate MSG before returning from the OTel parser.

This parser never assigns fields["MSG"], so any step that asserts the message body will fail under otel-jsonl even when the oracle received the record. The docstring also says MSG is part of the returned shape.

🧩 Suggested fix
     time_ns = log.get("timeUnixNano")
     if time_ns is not None:
         seconds = int(time_ns) / 1e9
         fields["TIMESTAMP"] = datetime.fromtimestamp(seconds, tz=timezone.utc).isoformat()
+
+    body = log.get("body", {})
+    if "stringValue" in body:
+        fields["MSG"] = body["stringValue"]
+    elif "intValue" in body:
+        fields["MSG"] = str(body["intValue"])
 
     return fields
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Bdd/features/steps/syslog_steps.py` around lines 88 - 121, The parser never
sets fields["MSG"], so update parse_otel_jsonl_line to extract the log message
from the OTel log record (use the local variable log) before returning; check
log.get("body") and handle both plain string bodies and object bodies (e.g.,
dicts containing "stringValue" or similar string keys), normalize to a Python
string, and assign it to fields["MSG"] (skip or omit if no body present) so the
returned dict shape includes MSG alongside
PRIORITY/TIMESTAMP/HOSTNAME/APP_NAME/PROCID.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Partially accepted — fixed in 016310b by tightening the docstring to remove the false plus MSG claim. MSG extraction itself is deferred to the message_fields.feature promotion PR, because the suggested body.stringValue mapping isn't right: OTel's body carries the whole raw RFC 5424 line (priority+timestamp+host+app+procid+SD+MSG) for the message-less record we currently send, not just the MSG part. To pick the right extraction I need to drive it from a scenario that sends a real bodied message via --message, which is part of the next promotion slice.

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.

@DavidCozens, thanks for the clarification — that's a good catch. The docstring correction in 016310b removes the misleading claim, and the deferral rationale is solid: mapping body.stringValueMSG would produce the entire raw RFC 5424 line rather than just the message part, so the extraction really does need to be grounded in a scenario that sends a bodied message via --message to verify the correct field mapping. Makes sense to tie that to the message_fields.feature promotion slice.


✏️ Learnings added
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 154
File: Bdd/features/steps/syslog_steps.py:88-123
Timestamp: 2026-04-18T15:14:43.668Z
Learning: In `Bdd/features/steps/syslog_steps.py`, the OTel Collector file exporter's `body` field (accessed as `log.get("body")`) carries the **entire raw RFC 5424 line** (priority + timestamp + host + appname + procid + SD + MSG) for message-less records sent by the walking-skeleton example. It does NOT contain just the MSG part. Therefore `body["stringValue"]` must NOT be mapped directly to `fields["MSG"]`; correct MSG extraction requires a scenario that sends a real bodied message via `--message` to determine the right field mapping. This is deferred to the `message_fields.feature` promotion PR.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread Bdd/otel/Install-OtelCollector.ps1 Outdated
Comment thread docs/bdd.md
Comment thread docs/bdd.md
- Install-OtelCollector.ps1: write a .version marker on install, fast-path
  only when both the exe and a matching marker are present. A stale or
  hand-replaced binary now triggers a re-download instead of silently
  bypassing the pinned $Version / $Sha256.

- run_threaded_example: wrap the prompt-protocol interaction in
  try/finally so an intermediate exception in wait_for_prompt /
  send_command / wait_for_messages no longer leaks the spawned process
  into later scenarios.

- ExampleAppName.c: hoist EXE_SUFFIX_LENGTH to file scope so the strip
  arithmetic uses the named constant instead of a magic 4.

- parse_otel_jsonl_line: tighten the docstring — don't claim MSG is in
  the returned shape when it isn't. MSG extraction is deferred to the
  message_fields scenario promotion (OTel's body field carries the whole
  raw RFC 5424 line for message-less records, so MSG extraction needs
  verifying against a real bodied message).

- docs/bdd.md: clarify that the local Windows recipe uses bash syntax
  and is intended for Git Bash; PowerShell users translate or invoke
  bash -c.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   Unit Tests (GCC): 99% successful (✔️ 539 passed, 🙈 3 skipped)
   🚦   Unit Tests (Clang): 99% successful (✔️ 498 passed, 🙈 3 skipped)
   🚦   Unit Tests (Sanitize): 99% successful (✔️ 498 passed, 🙈 3 skipped)
   🚦   BDD Tests: 100% successful (✔️ 31 passed)
   🚦   Unit Tests (MSVC): 100% successful (✔️ 405 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
Bdd/otel/Install-OtelCollector.ps1 (1)

25-33: ⚠️ Potential issue | 🟠 Major

Validate the cached executable before taking the fast-path.

The .version marker narrows drift, but it still doesn't prove the existing otelcol-contrib.exe matches the pinned $Sha256. A hand-replaced binary with the same marker will still be trusted here, so the script can silently run an unverified oracle.

🔧 Suggested fix
 $exePath       = Join-Path $BinDir 'otelcol-contrib.exe'
 $versionMarker = Join-Path $BinDir 'otelcol-contrib.version'
 
-# Fast-path only when both the exe and a matching version marker exist;
-# otherwise re-download so a stale or hand-replaced binary can't silently
-# bypass the pinned $Version / $Sha256.
-if ((Test-Path $exePath) -and (Test-Path $versionMarker) -and
-    ((Get-Content $versionMarker -Raw).Trim() -eq $Version))
+# Fast-path only when the cached binary still matches both the requested
+# version and the pinned SHA-256.
+if ((Test-Path $exePath) -and (Test-Path $versionMarker) -and
+    ((Get-Content $versionMarker -Raw).Trim() -eq $Version) -and
+    ((Get-FileHash -Algorithm SHA256 $exePath).Hash -eq $Sha256))
 {
     Write-Host "otelcol-contrib v$Version already installed at $exePath"
     return
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Bdd/otel/Install-OtelCollector.ps1` around lines 25 - 33, The fast-path
should additionally validate the cached otel binary's SHA256 before returning:
compute the file's hash (e.g. via Get-FileHash -Algorithm SHA256) for $exePath
and compare it to the pinned $Sha256, and only short-circuit when the file
exists, the $versionMarker matches $Version, and the computed hash equals
$Sha256; if the hash differs, fall through to re-download and replace the binary
and marker. Ensure you reference the existing variables $exePath,
$versionMarker, $Version and $Sha256 and update the conditional in the
Install-OtelCollector flow to include this hash equality check.
🧹 Nitpick comments (1)
Bdd/features/steps/syslog_steps.py (1)

214-223: Include child output in the nonzero-exit assertion.

Line 215 already collects the process streams; keeping them and appending stderr/stdout to the failure message would make broken BDD runs much easier to debug.

♻️ Suggested refactor
     try:
-        process.communicate(input=f"send {expected_messages}\nquit\n", timeout=15)
+        stdout, stderr = process.communicate(
+            input=f"send {expected_messages}\nquit\n",
+            timeout=15,
+        )
     except subprocess.TimeoutExpired:
         process.kill()
         process.wait()
         raise
 
     assert process.returncode == 0, (
-        f"Example binary failed with exit code {process.returncode}"
+        f"Example binary failed with exit code {process.returncode}\n"
+        f"stdout:\n{stdout}\n"
+        f"stderr:\n{stderr}"
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Bdd/features/steps/syslog_steps.py` around lines 214 - 223, The test
currently calls process.communicate(...) but doesn't capture stdout/stderr and
the non-zero-exit assertion lacks child output; change the code to capture the
outputs (stdout, stderr = process.communicate(...)) and in the
subprocess.TimeoutExpired handler call process.kill(); process.wait(); then call
process.communicate() again to obtain any remaining stdout/stderr; finally
include those captured stdout and stderr in the assertion message that checks
process.returncode (referencing process.communicate, subprocess.TimeoutExpired,
process.kill, process.wait, and process.returncode) so failures show child
output for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Bdd/features/steps/syslog_steps.py`:
- Around line 126-130: The function parse_oracle_line currently defaults any
unknown oracle_format to parse_syslog_ng_line; change it to explicitly accept
known formats and raise an error for unknown values. Update parse_oracle_line to
call parse_otel_jsonl_line when oracle_format == "otel-jsonl", call
parse_syslog_ng_line when oracle_format == "syslog-ng", and otherwise raise a
ValueError (or RuntimeError) that includes the invalid oracle_format so
misconfiguration fails fast and produces a clear error message.

---

Duplicate comments:
In `@Bdd/otel/Install-OtelCollector.ps1`:
- Around line 25-33: The fast-path should additionally validate the cached otel
binary's SHA256 before returning: compute the file's hash (e.g. via Get-FileHash
-Algorithm SHA256) for $exePath and compare it to the pinned $Sha256, and only
short-circuit when the file exists, the $versionMarker matches $Version, and the
computed hash equals $Sha256; if the hash differs, fall through to re-download
and replace the binary and marker. Ensure you reference the existing variables
$exePath, $versionMarker, $Version and $Sha256 and update the conditional in the
Install-OtelCollector flow to include this hash equality check.

---

Nitpick comments:
In `@Bdd/features/steps/syslog_steps.py`:
- Around line 214-223: The test currently calls process.communicate(...) but
doesn't capture stdout/stderr and the non-zero-exit assertion lacks child
output; change the code to capture the outputs (stdout, stderr =
process.communicate(...)) and in the subprocess.TimeoutExpired handler call
process.kill(); process.wait(); then call process.communicate() again to obtain
any remaining stdout/stderr; finally include those captured stdout and stderr in
the assertion message that checks process.returncode (referencing
process.communicate, subprocess.TimeoutExpired, process.kill, process.wait, and
process.returncode) so failures show child output for debugging.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: a33e8726-f124-484a-9639-d454bb3f3d99

📥 Commits

Reviewing files that changed from the base of the PR and between a23239e and 016310b.

📒 Files selected for processing (4)
  • Bdd/features/steps/syslog_steps.py
  • Bdd/otel/Install-OtelCollector.ps1
  • Example/Common/ExampleAppName.c
  • docs/bdd.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • Example/Common/ExampleAppName.c
  • docs/bdd.md

Comment on lines +126 to +130
def parse_oracle_line(line, oracle_format):
"""Dispatch to the right parser based on the active oracle."""
if oracle_format == "otel-jsonl":
return parse_otel_jsonl_line(line)
return parse_syslog_ng_line(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.

⚠️ Potential issue | 🟡 Minor

Fail fast on unsupported ORACLE_FORMAT.

Right now any typoed value falls through to the syslog-ng parser, which makes misconfigured Windows runs fail much later and much less clearly. Rejecting unknown formats here will make setup errors obvious.

🔧 Suggested fix
 def parse_oracle_line(line, oracle_format):
     """Dispatch to the right parser based on the active oracle."""
     if oracle_format == "otel-jsonl":
         return parse_otel_jsonl_line(line)
-    return parse_syslog_ng_line(line)
+    if oracle_format == "syslog-ng":
+        return parse_syslog_ng_line(line)
+    raise ValueError(f"Unsupported ORACLE_FORMAT: {oracle_format}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Bdd/features/steps/syslog_steps.py` around lines 126 - 130, The function
parse_oracle_line currently defaults any unknown oracle_format to
parse_syslog_ng_line; change it to explicitly accept known formats and raise an
error for unknown values. Update parse_oracle_line to call parse_otel_jsonl_line
when oracle_format == "otel-jsonl", call parse_syslog_ng_line when oracle_format
== "syslog-ng", and otherwise raise a ValueError (or RuntimeError) that includes
the invalid oracle_format so misconfiguration fails fast and produces a clear
error message.

@DavidCozens

Copy link
Copy Markdown
Owner Author

@coderabbitai dispositions on this review (covers the outside-diff and nitpick items that have no inline anchor):

Outside-diff: try/finally on threaded helper teardown — ✅ Accepted, fixed in 016310b. Wrapped the prompt-protocol interaction in try/finally so an intermediate exception in wait_for_prompt / send_command / wait_for_messages no longer leaks the spawned process into later scenarios. The run_example (single-task) variant doesn't need this — process.communicate() always waits for exit.

Nitpick: use EXE_SUFFIX_LENGTH constant — ✅ Accepted, fixed in 016310b. Hoisted the enum from inside EndsWithDotExe to file scope and replaced the magic 4 in ExampleAppName_Set with the named constant.

Review summary

# Comment Disposition
1 OTel parser MSG field ✅ Partial — docstring fixed in 016310b; MSG extraction deferred to message_fields.feature promotion (suggested mapping isn't right for OTel's body semantics)
2 Install script version pin ✅ Fixed in 016310b (version marker)
3 bash & background syntax in docs ✅ Partial — covered by Git Bash note in 016310b
4 bash inline env vars in docs ✅ Partial — covered by Git Bash note in 016310b
5 (outside-diff) try/finally on threaded helper ✅ Fixed in 016310b
6 (nitpick) EXE_SUFFIX_LENGTH constant ✅ Fixed in 016310b

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@DavidCozens, thank you for the thorough disposition table — this is exactly the kind of structured response that makes follow-up easy. Here's my read on each item:

# Comment Status
1 OTel parser MSG field Docstring fix noted; deferral of bodyMSG mapping to message_fields.feature promotion is a sound call — OTel's body semantics genuinely differ from RFC 5424 MSG and mapping them prematurely would be misleading.
2 Install script version pin Fully resolved — the .version marker prevents silent re-use of a stale binary.
3 bash & background syntax Git Bash note is a reasonable middle ground for a docs-only item.
4 bash inline env vars Same — Git Bash note covers the Windows-specific deviation.
5 try/finally on threaded helper Fully resolved — leak-safe teardown is in place; the process.communicate() note for the single-task variant is correct.
6 EXE_SUFFIX_LENGTH constant Fully resolved — named constant at file scope is cleaner.

All items are either fixed or deferred with clear rationale. Happy to re-review once the deferred message_fields.feature scenarios are promoted out of @windows_wip.

(´• ω •`) 🐇✨

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.

1 participant