Skip to content

feat: send the first record — passthrough buffer over UDP - #52

Merged
DavidCozens merged 1 commit into
mainfrom
stage-04-udp
Aug 16, 2026
Merged

feat: send the first record — passthrough buffer over UDP#52
DavidCozens merged 1 commit into
mainfrom
stage-04-udp

Conversation

@DavidCozens

@DavidCozens DavidCozens commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

The simplest configuration that sends a syslog message. A SolidSyslogUdpSender over lwIP's raw
API, with a SolidSyslogPassthroughBuffer in front of it, so SolidSyslog_Log formats the record
and hands it straight to the sender on the calling task — no queue, no background drain, nothing
to service.

struct SolidSyslogUdpSenderConfig senderConfig = {
    .Resolver = SolidSyslogLwipRawResolver_Create(),
    .Datagram = SolidSyslogLwipRawDatagram_Create(),
    .Address  = SolidSyslogLwipRawAddress_Create(),
    .Endpoint = CollectorEndpoint,
};
struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig);

struct SolidSyslogConfig config = {
    .Buffer = SolidSyslogPassthroughBuffer_Create(sender),
    .Sender = sender,
    .Store  = SolidSyslogNullStore_Get(),
};

What the collector received:

<134>1 - - - - BOOT - device started

Timestamp, hostname, app-name and process-id are the RFC's nil value. The record is valid without
them, and syslog-ng parses it; filling them in is the next stage. The three bad-config reports from
the previous stage are gone, which is the other half of that stage's point.

The record is built on the stack of whichever task calls SolidSyslog_Log, sized by
SOLIDSYSLOG_MAX_MESSAGE_SIZE. The logging task was at the FreeRTOS floor, and the first record
overflowed it — the overflow hook caught it and named the task. It grows to four times the floor
here, which measures 1,024 bytes used against 2,048 allocated. The margin is deliberate: stack
allocations are tightened against measured high-water marks once every collaborator is in place.

Two details are worth getting right. Every lwIP raw call has to happen on the thread that owns the
lwIP core; lwipopts.h sets LWIP_TCPIP_CORE_LOCKING, so taking the core lock in the caller's own
task is simpler than posting to the tcpip mailbox and is unconditionally synchronous, which the
marshal contract requires. And the collector address is a numeric literal, which keeps the resolver
numeric-only — no DNS, so no LWIP_DNS and no DNS resolver component compiled in.

Syslog_Start also moves after tcpip_init, because the marshal it installs takes the lwIP core
lock and tcpip_init is what creates it.

Summary by CodeRabbit

  • New Features
    • Added end-to-end UDP syslog output for the first boot record.
    • Emits an informational BOOT message in RFC 5424 format to the configured collector.
    • Added support for stack usage during record formatting and transmission.
  • Documentation
    • Updated the README with setup requirements, networking behavior, address resolution limitations, and stage applicability.
    • Updated run reports with UDP results, parsed message details, and revised resource measurements.

@DavidCozens

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77c48213-88ad-48c7-8a4b-0efafedec690

📥 Commits

Reviewing files that changed from the base of the PR and between 9c080cf and 81fad45.

⛔ Files ignored due to path filters (2)
  • measurements/stages.tsv is excluded by !**/*.tsv
  • measurements/udp.csv is excluded by !**/*.csv
📒 Files selected for processing (7)
  • README.md
  • app/AppConfig.h
  • app/main.c
  • app/syslog/Syslog.c
  • app/tasks/LogTask.c
  • app/tasks/LogTask.h
  • run-report.md

📝 Walkthrough

Walkthrough

The application now initializes lwIP before Syslog, configures a synchronous UDP sender for 10.0.2.2:5514, emits a BOOT record, validates logging, and updates documentation and measured resource values.

Changes

UDP Syslog pipeline

Layer / File(s) Summary
UDP transport configuration
app/syslog/Syslog.c
Syslog_Start now configures lwIP core-lock marshaling, numeric endpoint resolution, UDP sending, passthrough buffering, and a null store.
Startup and record emission
app/AppConfig.h, app/main.c, app/tasks/LogTask.c, app/tasks/LogTask.h
The log task receives four times the minimum stack size. The harness initializes lwIP before Syslog, emits a BOOT record, and requires logging success.
Documentation and measurements
README.md, run-report.md
The first-record UDP stage, RFC 5424 output, requirements, resource cost, parsed datagram, and self-check values are documented.

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

Merge Risk: ⚪ Minimal · up to 81fad

This change sends the first formatted syslog record synchronously over UDP using the documented configuration, with no actionable merge-blocking risk remaining beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Main
  participant lwIP
  participant Syslog_Start
  participant LogTask
  participant UDP_Sender
  participant Collector
  Main->>lwIP: tcpip_init
  Main->>Syslog_Start: configure UDP Syslog
  LogTask->>UDP_Sender: SolidSyslog_Log(BOOT record)
  UDP_Sender->>Collector: send UDP datagram
  LogTask-->>Main: logging success
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies the main change: sending the first syslog record over UDP with a passthrough buffer.
Description check ✅ Passed The description clearly explains the capability, configuration, received record, stack impact, lwIP requirements, and startup ordering.
✨ 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 stage-04-udp

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

@coderabbitai

coderabbitai Bot commented Aug 15, 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.

@DavidCozens

Copy link
Copy Markdown
Contributor Author

@coderabbitai pause

Base automatically changed from stage-03-logger to main August 16, 2026 17:56
The smallest wiring that delivers anything: a UDP sender over lwIP's raw API,
with a passthrough buffer in front of it. Passthrough means Log sends inline on
the calling task — no queue, no background drain, nothing to service — so this is
the cheapest thing that can be called working.

  Flash     +4,724 B
  RAM       +1,912 B
  Log stack   +904 B

What the collector received:

  <134>1 - - - - BOOT - device started

Timestamp, hostname, app-name and procid are all NILVALUE. RFC 5424 defines one
for each, so the record is valid and syslog-ng parses it — filling them in is a
later stage with a cost of its own, and separating the two is what lets the cost
of each be seen.

The three bad-config reports from the previous stage are gone, which is the other
half of that stage's point.

Most of the RAM is stack. The record is built on the stack of whichever task
calls Log, sized by SOLIDSYSLOG_MAX_MESSAGE_SIZE, and the log seam was at the
FreeRTOS floor: the first record overflowed it and the overflow hook said so. It
grows to four times the floor here, which measures 1,024 bytes used against 2,048
allocated. That margin is deliberate for now and comes off at the end, once every
collaborator is in place and the high-water marks are worth trusting.

Two details worth knowing. Every lwIP raw call has to happen on the thread that
owns the lwIP core; lwipopts.h sets LWIP_TCPIP_CORE_LOCKING, so taking the core
lock in the caller's own task is simpler than posting to the tcpip mailbox and is
unconditionally synchronous, which the marshal contract requires. And the
collector address is a numeric literal, which keeps the resolver numeric-only —
no DNS, so no LWIP_DNS and no resolver component compiled in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DavidCozens
DavidCozens merged commit dc51231 into main Aug 16, 2026
@DavidCozens
DavidCozens deleted the stage-04-udp branch August 16, 2026 17:56
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

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