Skip to content

Commit d14764e

Browse files
DavidCozensclaude
andcommitted
feat: send the first record — passthrough buffer over UDP
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) <noreply@anthropic.com>
1 parent c1b59ec commit d14764e

8 files changed

Lines changed: 140 additions & 42 deletions

File tree

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ It builds on a baseline that simulates the sort of device you might be adding th
1010
measures itself: see [docs/baseline.md](docs/baseline.md) for what the baseline is, how the
1111
figures are made, and how to run it.
1212

13-
## This stage — Logger created
13+
## This stage — First record
1414

15-
The logger exists, with nothing wired into it. Both collaborators that decide where a record goes
16-
are absent, so the handler from the previous stage reports them and the run report carries the
17-
faults — which is the point of creating it empty first.
15+
A UDP sender over lwIP, with a passthrough buffer in front of it: `Log` sends inline on the calling
16+
task, so there is no queue and nothing to drain. The collector receives a valid RFC 5424 record
17+
with timestamp, hostname, app-name and procid all left as `-`, which the RFC allows and a
18+
collector accepts.
1819

1920
<!-- STAGE-COST:START (generated by scripts/gen-cost-table.py — do not edit by hand) -->
2021

21-
**Cost above baseline: Flash +1,036 B, RAM +180 B.**
22+
**Cost above baseline: Flash +4,716 B, RAM +372 B.**
2223

2324
<!-- STAGE-COST:END -->
2425

@@ -35,6 +36,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage.
3536
| Linked | the core library and lwIP raw-mode networking, linked but not yet called | +0 | +0 |
3637
| Error handler | a fault inside the logger reaches the console instead of being silent | +404 | +8 |
3738
| Logger created | the logger object, reporting exactly what is still missing from it | +1,036 | +180 |
39+
| First record | a valid RFC 5424 record on the wire, over UDP | +4,716 | +372 |
3840

3941
*Deltas are bytes above the baseline, which is itself Flash 350,308 B, RAM 111,192 B.*
4042

app/main.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,14 @@ static void HarnessTask(void* parameters)
6666
bool logIdle = LogTask_WaitIdle(2000U);
6767
bool serviceIdle = ServiceTask_WaitIdle(2000U);
6868

69+
/* Emitted before the figures are taken: the record goes out inline on the
70+
* log task's stack, so its stack figure only means anything afterwards. */
71+
bool logged = LogTask_EmitOnce(5000U);
72+
(void) printf("[device] first record logged: %s\n", logged ? "yes" : "FAILED");
73+
6974
(void) Measure_Report();
7075

71-
bool ready = simReady && logIdle && serviceIdle;
76+
bool ready = simReady && logIdle && serviceIdle && logged;
7277
(void) printf("[device] %s\n", ready ? "ready" : "FAILED");
7378
SemihostingExit(ready ? 0 : 1);
7479
}
@@ -98,8 +103,6 @@ int main(void)
98103
/* Before the first _Create — see SyslogErrorHandler.h for why that matters. */
99104
SyslogErrorHandler_Install();
100105

101-
Syslog_Start();
102-
103106
/* lwIP tcpip thread + core-lock mutex + mbox. Pre-scheduler safe. */
104107
tcpip_init(NULL, NULL);
105108

@@ -111,6 +114,11 @@ int main(void)
111114
SemihostingExit(1);
112115
}
113116

117+
/* After tcpip_init, not before: the marshal Syslog_Start installs takes the
118+
* lwIP core lock, and tcpip_init is what creates it. Nothing is sent here —
119+
* the sender resolves and opens lazily on its first record. */
120+
Syslog_Start();
121+
114122
if (!LogTask_Create() || !ServiceTask_Create())
115123
{
116124
(void) printf("[device] FATAL: application task create failed\n");

app/syslog/Syslog.c

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,86 @@
1-
/* See Syslog.h. Created with nothing wired into it, on purpose: a missing
2-
* collaborator is substituted with its Null object and reported, and the run
3-
* report is where that shows. */
1+
/* See Syslog.h.
2+
*
3+
* The smallest wiring that delivers: a UDP sender over lwIP, with a passthrough
4+
* buffer in front of it. Passthrough means Log sends inline on the calling task
5+
* — no queue, no background drain, nothing to service.
6+
*
7+
* Timestamp, hostname, app-name and procid are left unset. RFC 5424 defines a
8+
* NILVALUE for each, so a record carrying "-" for them is valid and a collector
9+
* accepts it. */
410

511
#include "Syslog.h"
612

713
#include "SolidSyslogConfig.h"
14+
#include "SolidSyslogEndpoint.h"
15+
#include "SolidSyslogEndpointHost.h"
16+
#include "SolidSyslogLwipRawAddress.h"
17+
#include "SolidSyslogLwipRawDatagram.h"
18+
#include "SolidSyslogLwipRawMarshal.h"
19+
#include "SolidSyslogLwipRawResolver.h"
20+
#include "SolidSyslogNullStore.h"
21+
#include "SolidSyslogPassthroughBuffer.h"
22+
#include "SolidSyslogUdpSender.h"
23+
24+
#include "lwip/tcpip.h"
825

926
#include <stddef.h>
27+
#include <stdint.h>
28+
#include <string.h>
29+
30+
/* The collector, reached through QEMU's slirp gateway. A numeric literal keeps
31+
* the resolver numeric-only — no DNS, so no LWIP_DNS and no DNS resolver
32+
* component to compile. */
33+
#define SYSLOG_COLLECTOR_HOST "10.0.2.2"
34+
#define SYSLOG_COLLECTOR_PORT ((uint16_t) 5514U)
1035

1136
static struct SolidSyslog* s_logger = NULL;
1237

38+
/* Every lwIP Raw call the datagram makes has to happen on the thread that owns
39+
* the lwIP core. lwipopts.h sets LWIP_TCPIP_CORE_LOCKING, so taking the core
40+
* lock in the caller's own task is simpler and cheaper than posting to the tcpip
41+
* mailbox — and unconditionally synchronous, which the marshal contract
42+
* requires. The lock is recursive and these callbacks never re-marshal, so it
43+
* cannot deadlock against itself. */
44+
static void LwipCoreLockMarshal(SolidSyslogLwipRawCallback callback, void* context)
45+
{
46+
LOCK_TCPIP_CORE();
47+
callback(context);
48+
UNLOCK_TCPIP_CORE();
49+
}
50+
51+
/* Pulled by the sender when it connects, not on every send. Host is a bounded
52+
* sink rather than a raw buffer, so a destination cannot overrun the field. */
53+
static void CollectorEndpoint(struct SolidSyslogEndpoint* endpoint, void* context)
54+
{
55+
(void) context;
56+
57+
SolidSyslogEndpointHost_String(endpoint->Host, SYSLOG_COLLECTOR_HOST, strlen(SYSLOG_COLLECTOR_HOST));
58+
endpoint->Port = SYSLOG_COLLECTOR_PORT;
59+
}
60+
1361
void Syslog_Start(void)
1462
{
15-
/* Buffer and Sender decide where a record goes. NULL is "not supplied" and
16-
* is reported; a collaborator deliberately done without is passed as its
17-
* Null object instead, which is how the library tells the two apart. */
63+
SolidSyslogLwipRaw_SetMarshal(LwipCoreLockMarshal);
64+
65+
/* A numeric resolver to parse the literal, a datagram for the socket, and an
66+
* address slot for the resolver to write into. No EndpointVersion — this
67+
* collector never moves, so the sender resolves once and pins it. */
68+
struct SolidSyslogUdpSenderConfig senderConfig = {
69+
.Resolver = SolidSyslogLwipRawResolver_Create(),
70+
.Datagram = SolidSyslogLwipRawDatagram_Create(),
71+
.Address = SolidSyslogLwipRawAddress_Create(),
72+
.Endpoint = CollectorEndpoint,
73+
};
74+
struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig);
75+
1876
struct SolidSyslogConfig config = {
19-
.Buffer = NULL,
20-
.Sender = NULL,
77+
.Buffer = SolidSyslogPassthroughBuffer_Create(sender),
78+
.Sender = sender,
79+
/* No store-and-forward here. The Null object rather than NULL is how
80+
* that is said out loud — NULL is reported as a fault. */
81+
.Store = SolidSyslogNullStore_Get(),
2182
};
2283

23-
/* No null check — Create returns a shared null instance rather than NULL. */
2484
s_logger = SolidSyslog_Create(&config);
2585
}
2686

app/tasks/LogTask.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
#include "LogTask.h"
44

5+
#include "Syslog.h"
6+
7+
#include "SolidSyslog.h"
8+
#include "SolidSyslogPrival.h"
9+
510
#include "AppConfig.h"
611

712
#include "semphr.h"
@@ -26,7 +31,16 @@ static void LogTask_Entry(void* parameters)
2631
{
2732
if (xSemaphoreTake(s_emitRequested, portMAX_DELAY) == pdTRUE)
2833
{
29-
/* Nothing to say: this device has no logger. */
34+
const struct SolidSyslogMessage message = {
35+
.Facility = SOLIDSYSLOG_FACILITY_LOCAL0,
36+
.Severity = SOLIDSYSLOG_SEVERITY_INFORMATIONAL,
37+
.MessageId = "BOOT",
38+
.Msg = "device started",
39+
};
40+
41+
/* Sends inline on this stack and returns once it is done. */
42+
SolidSyslog_Log(Syslog_Handle(), &message);
43+
3044
(void) xSemaphoreGive(s_emitDone);
3145
}
3246
}

app/tasks/LogTask.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ extern "C"
2222
* high-water mark reflects something real. */
2323
bool LogTask_WaitIdle(uint32_t timeoutMs);
2424

25-
/* Emit one record and wait for it to finish. Nothing to emit yet. */
25+
/* Emit one record and wait for it to finish. */
2626
bool LogTask_EmitOnce(uint32_t timeoutMs);
2727

2828
#ifdef __cplusplus
2929
}
3030
#endif
3131

3232
#endif /* APP_TASKS_LOG_TASK_H */
33+

measurements/stages.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ Baseline Baseline a device that already networks, stores, and holds an mTLS sess
1313
linked Linked the core library and lwIP raw-mode networking, linked but not yet called
1414
error-handler Error handler a fault inside the logger reaches the console instead of being silent
1515
logger Logger created the logger object, reporting exactly what is still missing from it
16+
udp First record a valid RFC 5424 record on the wire, over UDP

measurements/udp.csv

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# udp figures (bytes) — captured by scripts/run.sh (CAPTURE=1).
2+
# The device reads measurements/Baseline.csv as its frozen baseline and reports current-minus-Baseline.
3+
flash_text,354576
4+
flash_data,448
5+
static_bss,111116
6+
heap_used,4440
7+
mbedtls_peak,21292
8+
mbedtls_free,11476
9+
lwip_mem_free,7576
10+
lwip_pbufs_free,13
11+
stack_log,136
12+
stack_service,52
13+
stack_harness,2848

run-report.md

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
1-
# solid-syslog-example — run (logger)
1+
# solid-syslog-example — run (udp)
22

33
## Device (self-measured)
44

55
```text
66
[device] solid-syslog-example (FreeRTOS + lwIP + mbedTLS + FatFs)
7-
[syslog] CRITICAL SolidSyslog bad-config (detail 1)
8-
[syslog] CRITICAL SolidSyslog bad-config (detail 2)
9-
[syslog] CRITICAL SolidSyslog bad-config (detail 3)
107
[device] starting simulated existing application...
118
[sim] broker session to 10.0.2.2:8883: TLSv1.3, TLS1-3-CHACHA20-POLY1305-SHA256
129
[device] sim app (lwIP up, FatFs mounted, broker session held over mTLS): ready
10+
[device] first record logged: yes
1311
[report] --- SolidSyslog cost above baseline (simulated existing application) ---
1412
[report] key,current,baseline,used_above_baseline
15-
[report] flash_text,350960,349992,968
16-
[report] flash_data,384,316,68
17-
[report] static_bss,110988,110876,112
13+
[report] flash_text,354576,349992,4584
14+
[report] flash_data,448,316,132
15+
[report] static_bss,111116,110876,240
1816
[report] heap_used,4440,4440,0
19-
[report] mbedtls_peak,21228,21332,-104
20-
[report] mbedtls_free,11540,11436,104
17+
[report] mbedtls_peak,21336,21332,4
18+
[report] mbedtls_free,11432,11436,-4
2119
[report] lwip_mem_free,7576,7576,0
22-
[report] lwip_pbufs_free,13,14,-1
23-
[report] stack_log,120,120,0
20+
[report] lwip_pbufs_free,14,14,0
21+
[report] stack_log,136,120,16
2422
[report] stack_service,52,52,0
25-
[report] stack_harness,2840,2840,0
23+
[report] stack_harness,2848,2840,8
2624
[report] --- end ---
2725
[device] ready
2826
```
@@ -31,7 +29,7 @@
3129

3230
```text
3331
text data bss dec hex filename
34-
350952 392 110988 462332 70dfc /w/build/baseline-cross/baseline.elf
32+
354568 456 111116 466140 71cdc /w/build/baseline-cross/baseline.elf
3533
```
3634

3735
## Listeners (proved before the device ran)
@@ -49,23 +47,24 @@
4947
## Collector (syslog-ng) received
5048

5149
```text
52-
(nothing — this device sends no records yet)
50+
wire <134>1 - - - - BOOT - device started
51+
parsed PRIORITY=134 TIMESTAMP=2026-07-29T07:13:03+00:00 HOSTNAME=localhost APP_NAME= PROCID= MSGID=BOOT STRUCTURED_DATA= MSG=device started
5352
```
5453

55-
## Self-check (vs measurements/logger.csv)
54+
## Self-check (vs measurements/udp.csv)
5655

5756
```text
58-
OK flash_text: 350960 (expected 350960, Δ0)
59-
OK flash_data: 384 (expected 384, Δ0)
60-
OK static_bss: 110988 (expected 110988, Δ0)
57+
OK flash_text: 354576 (expected 354576, Δ0)
58+
OK flash_data: 448 (expected 448, Δ0)
59+
OK static_bss: 111116 (expected 111116, Δ0)
6160
OK heap_used: 4440 (expected 4440, Δ0)
62-
OK mbedtls_peak: 21228 (expected 21192, Δ36)
63-
OK mbedtls_free: 11540 (expected 11576, Δ36)
61+
OK mbedtls_peak: 21336 (expected 21292, Δ44)
62+
OK mbedtls_free: 11432 (expected 11476, Δ44)
6463
OK lwip_mem_free: 7576 (expected 7576, Δ0)
65-
OK lwip_pbufs_free: 13 (expected 13, Δ0)
66-
OK stack_log: 120 (expected 120, Δ0)
64+
OK lwip_pbufs_free: 14 (expected 13, Δ1)
65+
OK stack_log: 136 (expected 136, Δ0)
6766
OK stack_service: 52 (expected 52, Δ0)
68-
OK stack_harness: 2840 (expected 2840, Δ0)
67+
OK stack_harness: 2848 (expected 2848, Δ0)
6968
```
7069

7170
**RESULT: PASS**

0 commit comments

Comments
 (0)