From 8761d434c482036771c006fd58a743de109783fa Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 28 Jul 2026 23:24:45 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20send=20the=20first=20record=20=E2=80=94?= =?UTF-8?q?=20passthrough=20buffer=20over=20UDP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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,716 B RAM +372 B Log stack +16 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. 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) --- README.md | 12 ++++--- app/main.c | 14 ++++++-- app/syslog/Syslog.c | 78 ++++++++++++++++++++++++++++++++++++----- app/tasks/LogTask.c | 16 ++++++++- app/tasks/LogTask.h | 3 +- measurements/stages.tsv | 1 + measurements/udp.csv | 13 +++++++ run-report.md | 45 ++++++++++++------------ 8 files changed, 140 insertions(+), 42 deletions(-) create mode 100644 measurements/udp.csv diff --git a/README.md b/README.md index 43eb43e..e0f6328 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,16 @@ It builds on a baseline that simulates the sort of device you might be adding th measures itself: see [docs/baseline.md](docs/baseline.md) for what the baseline is, how the figures are made, and how to run it. -## This stage — Logger created +## This stage — First record -The logger exists, with nothing wired into it. Both collaborators that decide where a record goes -are absent, so the handler from the previous stage reports them and the run report carries the -faults — which is the point of creating it empty first. +A UDP sender over lwIP, with a passthrough buffer in front of it: `Log` sends inline on the calling +task, so there is no queue and nothing to drain. The collector receives a valid RFC 5424 record +with timestamp, hostname, app-name and procid all left as `-`, which the RFC allows and a +collector accepts. -**Cost above baseline: Flash +1,036 B, RAM +180 B.** +**Cost above baseline: Flash +4,716 B, RAM +372 B.** @@ -35,6 +36,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage. | Linked | the core library and lwIP raw-mode networking, linked but not yet called | +0 | +0 | | Error handler | a fault inside the logger reaches the console instead of being silent | +404 | +8 | | Logger created | the logger object, reporting exactly what is still missing from it | +1,036 | +180 | +| First record | a valid RFC 5424 record on the wire, over UDP | +4,716 | +372 | *Deltas are bytes above the baseline, which is itself Flash 350,308 B, RAM 111,192 B.* diff --git a/app/main.c b/app/main.c index e64d325..25fa459 100644 --- a/app/main.c +++ b/app/main.c @@ -66,9 +66,14 @@ static void HarnessTask(void* parameters) bool logIdle = LogTask_WaitIdle(2000U); bool serviceIdle = ServiceTask_WaitIdle(2000U); + /* Emitted before the figures are taken: the record goes out inline on the + * log task's stack, so its stack figure only means anything afterwards. */ + bool logged = LogTask_EmitOnce(5000U); + (void) printf("[device] first record logged: %s\n", logged ? "yes" : "FAILED"); + (void) Measure_Report(); - bool ready = simReady && logIdle && serviceIdle; + bool ready = simReady && logIdle && serviceIdle && logged; (void) printf("[device] %s\n", ready ? "ready" : "FAILED"); SemihostingExit(ready ? 0 : 1); } @@ -98,8 +103,6 @@ int main(void) /* Before the first _Create — see SyslogErrorHandler.h for why that matters. */ SyslogErrorHandler_Install(); - Syslog_Start(); - /* lwIP tcpip thread + core-lock mutex + mbox. Pre-scheduler safe. */ tcpip_init(NULL, NULL); @@ -111,6 +114,11 @@ int main(void) SemihostingExit(1); } + /* After tcpip_init, not before: the marshal Syslog_Start installs takes the + * lwIP core lock, and tcpip_init is what creates it. Nothing is sent here — + * the sender resolves and opens lazily on its first record. */ + Syslog_Start(); + if (!LogTask_Create() || !ServiceTask_Create()) { (void) printf("[device] FATAL: application task create failed\n"); diff --git a/app/syslog/Syslog.c b/app/syslog/Syslog.c index ffad356..d4a4733 100644 --- a/app/syslog/Syslog.c +++ b/app/syslog/Syslog.c @@ -1,26 +1,86 @@ -/* See Syslog.h. Created with nothing wired into it, on purpose: a missing - * collaborator is substituted with its Null object and reported, and the run - * report is where that shows. */ +/* See Syslog.h. + * + * The smallest wiring that delivers: a UDP sender over lwIP, 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. + * + * Timestamp, hostname, app-name and procid are left unset. RFC 5424 defines a + * NILVALUE for each, so a record carrying "-" for them is valid and a collector + * accepts it. */ #include "Syslog.h" #include "SolidSyslogConfig.h" +#include "SolidSyslogEndpoint.h" +#include "SolidSyslogEndpointHost.h" +#include "SolidSyslogLwipRawAddress.h" +#include "SolidSyslogLwipRawDatagram.h" +#include "SolidSyslogLwipRawMarshal.h" +#include "SolidSyslogLwipRawResolver.h" +#include "SolidSyslogNullStore.h" +#include "SolidSyslogPassthroughBuffer.h" +#include "SolidSyslogUdpSender.h" + +#include "lwip/tcpip.h" #include +#include +#include + +/* The collector, reached through QEMU's slirp gateway. A numeric literal keeps + * the resolver numeric-only — no DNS, so no LWIP_DNS and no DNS resolver + * component to compile. */ +#define SYSLOG_COLLECTOR_HOST "10.0.2.2" +#define SYSLOG_COLLECTOR_PORT ((uint16_t) 5514U) static struct SolidSyslog* s_logger = NULL; +/* Every lwIP Raw call the datagram makes 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 and cheaper than posting to the tcpip + * mailbox — and unconditionally synchronous, which the marshal contract + * requires. The lock is recursive and these callbacks never re-marshal, so it + * cannot deadlock against itself. */ +static void LwipCoreLockMarshal(SolidSyslogLwipRawCallback callback, void* context) +{ + LOCK_TCPIP_CORE(); + callback(context); + UNLOCK_TCPIP_CORE(); +} + +/* Pulled by the sender when it connects, not on every send. Host is a bounded + * sink rather than a raw buffer, so a destination cannot overrun the field. */ +static void CollectorEndpoint(struct SolidSyslogEndpoint* endpoint, void* context) +{ + (void) context; + + SolidSyslogEndpointHost_String(endpoint->Host, SYSLOG_COLLECTOR_HOST, strlen(SYSLOG_COLLECTOR_HOST)); + endpoint->Port = SYSLOG_COLLECTOR_PORT; +} + void Syslog_Start(void) { - /* Buffer and Sender decide where a record goes. NULL is "not supplied" and - * is reported; a collaborator deliberately done without is passed as its - * Null object instead, which is how the library tells the two apart. */ + SolidSyslogLwipRaw_SetMarshal(LwipCoreLockMarshal); + + /* A numeric resolver to parse the literal, a datagram for the socket, and an + * address slot for the resolver to write into. No EndpointVersion — this + * collector never moves, so the sender resolves once and pins it. */ + struct SolidSyslogUdpSenderConfig senderConfig = { + .Resolver = SolidSyslogLwipRawResolver_Create(), + .Datagram = SolidSyslogLwipRawDatagram_Create(), + .Address = SolidSyslogLwipRawAddress_Create(), + .Endpoint = CollectorEndpoint, + }; + struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig); + struct SolidSyslogConfig config = { - .Buffer = NULL, - .Sender = NULL, + .Buffer = SolidSyslogPassthroughBuffer_Create(sender), + .Sender = sender, + /* No store-and-forward here. The Null object rather than NULL is how + * that is said out loud — NULL is reported as a fault. */ + .Store = SolidSyslogNullStore_Get(), }; - /* No null check — Create returns a shared null instance rather than NULL. */ s_logger = SolidSyslog_Create(&config); } diff --git a/app/tasks/LogTask.c b/app/tasks/LogTask.c index 9837a13..aaf27ca 100644 --- a/app/tasks/LogTask.c +++ b/app/tasks/LogTask.c @@ -2,6 +2,11 @@ #include "LogTask.h" +#include "Syslog.h" + +#include "SolidSyslog.h" +#include "SolidSyslogPrival.h" + #include "AppConfig.h" #include "semphr.h" @@ -26,7 +31,16 @@ static void LogTask_Entry(void* parameters) { if (xSemaphoreTake(s_emitRequested, portMAX_DELAY) == pdTRUE) { - /* Nothing to say: this device has no logger. */ + const struct SolidSyslogMessage message = { + .Facility = SOLIDSYSLOG_FACILITY_LOCAL0, + .Severity = SOLIDSYSLOG_SEVERITY_INFORMATIONAL, + .MessageId = "BOOT", + .Msg = "device started", + }; + + /* Sends inline on this stack and returns once it is done. */ + SolidSyslog_Log(Syslog_Handle(), &message); + (void) xSemaphoreGive(s_emitDone); } } diff --git a/app/tasks/LogTask.h b/app/tasks/LogTask.h index 09fa38c..a822609 100644 --- a/app/tasks/LogTask.h +++ b/app/tasks/LogTask.h @@ -22,7 +22,7 @@ extern "C" * high-water mark reflects something real. */ bool LogTask_WaitIdle(uint32_t timeoutMs); - /* Emit one record and wait for it to finish. Nothing to emit yet. */ + /* Emit one record and wait for it to finish. */ bool LogTask_EmitOnce(uint32_t timeoutMs); #ifdef __cplusplus @@ -30,3 +30,4 @@ extern "C" #endif #endif /* APP_TASKS_LOG_TASK_H */ + diff --git a/measurements/stages.tsv b/measurements/stages.tsv index c9b1a5c..79fc61c 100644 --- a/measurements/stages.tsv +++ b/measurements/stages.tsv @@ -13,3 +13,4 @@ Baseline Baseline a device that already networks, stores, and holds an mTLS sess linked Linked the core library and lwIP raw-mode networking, linked but not yet called error-handler Error handler a fault inside the logger reaches the console instead of being silent logger Logger created the logger object, reporting exactly what is still missing from it +udp First record a valid RFC 5424 record on the wire, over UDP diff --git a/measurements/udp.csv b/measurements/udp.csv new file mode 100644 index 0000000..e1931b3 --- /dev/null +++ b/measurements/udp.csv @@ -0,0 +1,13 @@ +# udp figures (bytes) — captured by scripts/run.sh (CAPTURE=1). +# The device reads measurements/Baseline.csv as its frozen baseline and reports current-minus-Baseline. +flash_text,354576 +flash_data,448 +static_bss,111116 +heap_used,4440 +mbedtls_peak,21292 +mbedtls_free,11476 +lwip_mem_free,7576 +lwip_pbufs_free,13 +stack_log,136 +stack_service,52 +stack_harness,2848 diff --git a/run-report.md b/run-report.md index b989132..b7076a2 100644 --- a/run-report.md +++ b/run-report.md @@ -1,28 +1,26 @@ -# solid-syslog-example — run (logger) +# solid-syslog-example — run (udp) ## Device (self-measured) ```text [device] solid-syslog-example (FreeRTOS + lwIP + mbedTLS + FatFs) -[syslog] CRITICAL SolidSyslog bad-config (detail 1) -[syslog] CRITICAL SolidSyslog bad-config (detail 2) -[syslog] CRITICAL SolidSyslog bad-config (detail 3) [device] starting simulated existing application... [sim] broker session to 10.0.2.2:8883: TLSv1.3, TLS1-3-CHACHA20-POLY1305-SHA256 [device] sim app (lwIP up, FatFs mounted, broker session held over mTLS): ready +[device] first record logged: yes [report] --- SolidSyslog cost above baseline (simulated existing application) --- [report] key,current,baseline,used_above_baseline -[report] flash_text,350960,349992,968 -[report] flash_data,384,316,68 -[report] static_bss,110988,110876,112 +[report] flash_text,354576,349992,4584 +[report] flash_data,448,316,132 +[report] static_bss,111116,110876,240 [report] heap_used,4440,4440,0 -[report] mbedtls_peak,21228,21332,-104 -[report] mbedtls_free,11540,11436,104 +[report] mbedtls_peak,21336,21332,4 +[report] mbedtls_free,11432,11436,-4 [report] lwip_mem_free,7576,7576,0 -[report] lwip_pbufs_free,13,14,-1 -[report] stack_log,120,120,0 +[report] lwip_pbufs_free,14,14,0 +[report] stack_log,136,120,16 [report] stack_service,52,52,0 -[report] stack_harness,2840,2840,0 +[report] stack_harness,2848,2840,8 [report] --- end --- [device] ready ``` @@ -31,7 +29,7 @@ ```text text data bss dec hex filename - 350952 392 110988 462332 70dfc /w/build/baseline-cross/baseline.elf + 354568 456 111116 466140 71cdc /w/build/baseline-cross/baseline.elf ``` ## Listeners (proved before the device ran) @@ -49,23 +47,24 @@ ## Collector (syslog-ng) received ```text -(nothing — this device sends no records yet) +wire <134>1 - - - - BOOT - device started +parsed PRIORITY=134 TIMESTAMP=2026-07-29T07:13:03+00:00 HOSTNAME=localhost APP_NAME= PROCID= MSGID=BOOT STRUCTURED_DATA= MSG=device started ``` -## Self-check (vs measurements/logger.csv) +## Self-check (vs measurements/udp.csv) ```text - OK flash_text: 350960 (expected 350960, Δ0) - OK flash_data: 384 (expected 384, Δ0) - OK static_bss: 110988 (expected 110988, Δ0) + OK flash_text: 354576 (expected 354576, Δ0) + OK flash_data: 448 (expected 448, Δ0) + OK static_bss: 111116 (expected 111116, Δ0) OK heap_used: 4440 (expected 4440, Δ0) - OK mbedtls_peak: 21228 (expected 21192, Δ36) - OK mbedtls_free: 11540 (expected 11576, Δ36) + OK mbedtls_peak: 21336 (expected 21292, Δ44) + OK mbedtls_free: 11432 (expected 11476, Δ44) OK lwip_mem_free: 7576 (expected 7576, Δ0) - OK lwip_pbufs_free: 13 (expected 13, Δ0) - OK stack_log: 120 (expected 120, Δ0) + OK lwip_pbufs_free: 14 (expected 13, Δ1) + OK stack_log: 136 (expected 136, Δ0) OK stack_service: 52 (expected 52, Δ0) - OK stack_harness: 2840 (expected 2840, Δ0) + OK stack_harness: 2848 (expected 2848, Δ0) ``` **RESULT: PASS**