Skip to content

Commit 81fad45

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,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>
1 parent 9c080cf commit 81fad45

9 files changed

Lines changed: 172 additions & 62 deletions

File tree

README.md

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,45 +10,57 @@ 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-
Create the logger with both collaborators absent, deliberately, and read what the handler prints.
15+
The simplest configuration that sends a syslog message. A `SolidSyslogUdpSender` over lwIP's raw
16+
API, with a `SolidSyslogPassthroughBuffer` in front of it, so `SolidSyslog_Log` formats the record
17+
and hands it straight to the sender on the calling task — no queue, no background drain, nothing
18+
to service.
1619

1720
```c
18-
struct SolidSyslogConfig config = {
19-
.Buffer = NULL,
20-
.Sender = NULL,
21+
struct SolidSyslogUdpSenderConfig senderConfig = {
22+
.Resolver = SolidSyslogLwipRawResolver_Create(),
23+
.Datagram = SolidSyslogLwipRawDatagram_Create(),
24+
.Address = SolidSyslogLwipRawAddress_Create(),
25+
.Endpoint = CollectorEndpoint,
2126
};
27+
struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig);
2228

23-
struct SolidSyslog* logger = SolidSyslog_Create(&config);
29+
struct SolidSyslogConfig config = {
30+
.Buffer = SolidSyslogPassthroughBuffer_Create(sender),
31+
.Sender = sender,
32+
.Store = SolidSyslogNullStore_Get(),
33+
};
2434
```
2535

26-
No `_Create` fails or returns `NULL` — a missing collaborator is substituted with its Null object
27-
and reported — so the only evidence is what the handler says:
36+
What arrives is a valid RFC 5424 record any collector will parse:
2837

2938
```text
30-
[syslog] CRITICAL SolidSyslog bad-config (detail 1)
31-
[syslog] CRITICAL SolidSyslog bad-config (detail 2)
32-
[syslog] CRITICAL SolidSyslog bad-config (detail 3)
39+
<134>1 - - - - BOOT - device started
3340
```
3441

35-
Three, for the buffer, the sender and the store. Each names the collaborator in `Detail`, as a
36-
value of the emitting class's own error enum.
42+
Timestamp, hostname, app-name and process-id are the RFC's nil value. The record is valid without
43+
them; filling them in is the next stage. The three bad-config reports from the previous stage are
44+
gone, which is the other half of that stage's point.
3745

38-
The order matters. Wire everything at once and see nothing, and you cannot tell a working logger
39-
from a silent one. Seeing the faults first, then watching them go quiet as each collaborator
40-
arrives, is the difference between believing it works and knowing.
46+
**The record is built on the stack of whichever task calls `SolidSyslog_Log`.** Its size follows
47+
`SOLIDSYSLOG_MAX_MESSAGE_SIZE`, so the logging task needs room for the record and the send beneath
48+
it — here the task was at the RTOS floor and had to grow. It is sized generously for now and
49+
tightened against measured high-water marks at the end.
4150

42-
A convention worth adopting now: `NULL` as a parameter means "not supplied" and is reported, while
43-
a collaborator you have deliberately done without is passed as its Null object. The library
44-
distinguishes the two, and so should anyone reading the wiring later.
51+
Two details are worth getting right. Every lwIP raw call has to happen on the thread that owns the
52+
lwIP core; `lwipopts.h` sets `LWIP_TCPIP_CORE_LOCKING`, so taking the core lock in the caller's own
53+
task is simpler than posting to the tcpip mailbox and is unconditionally synchronous, which the
54+
marshal contract requires. And the collector address is a numeric literal, which keeps the resolver
55+
numeric-only — no DNS, so no `LWIP_DNS` and no DNS resolver component compiled in.
4556

46-
**When you need it.** As a step rather than a destination. It costs one build to prove the handler
47-
is connected and the library is reachable, before anything can be blamed on the network.
57+
**When you need it.** Every device needs this much. The question is whether UDP is enough: it drops
58+
records silently, and anyone on the path can read them. If either matters, treat UDP as a stepping
59+
stone to the TCP and TLS stages.
4860

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

51-
**Cost above baseline: Flash +1,052 B, RAM +184 B.**
63+
**Cost above baseline: Flash +4,724 B, RAM +1,912 B.**
5264

5365
<!-- STAGE-COST:END -->
5466

@@ -65,6 +77,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage.
6577
| Linked | the core library and lwIP raw-mode networking, linked but not yet called | +0 | +0 |
6678
| Error handler | a fault inside the logger reaches the console instead of being silent | +404 | +8 |
6779
| Logger created | the logger object, reporting exactly what is still missing from it | +1,052 | +184 |
80+
| First record | a valid RFC 5424 record on the wire, over UDP | +4,724 | +1,912 |
6881

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

app/AppConfig.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
/* CMSDK UART0 on the mps2-an385, surfaced by QEMU over -serial stdio. */
1111
#define DEVICE_UART0_BASE ((uintptr_t) 0x40004000U)
1212

13-
/* Both seams are idle, so both get the FreeRTOS floor — twice what an idle seam
14-
* measures is well below it. Whatever deepens one grows it there. The reported
15-
* figure is high-water usage, which does not depend on the allocation. */
16-
#define LOG_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE)
13+
/* The log seam formats the record on its own stack, so it holds
14+
* SOLIDSYSLOG_MAX_MESSAGE_SIZE and the send beneath it and no longer fits the
15+
* FreeRTOS floor. The service seam is still idle and keeps it. Both are sized
16+
* generously here and tightened against measured high-water marks once the
17+
* pipeline is complete. */
18+
#define LOG_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 4U)
1719
#define SERVICE_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE)
1820
#define LOG_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)
1921
#define SERVICE_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ 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

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,354584
4+
flash_data,448
5+
static_bss,112656
6+
heap_used,4440
7+
mbedtls_peak,21236
8+
mbedtls_free,11532
9+
lwip_mem_free,7576
10+
lwip_pbufs_free,14
11+
stack_log,1024
12+
stack_service,52
13+
stack_harness,2848

run-report.md

Lines changed: 21 additions & 22 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,350976,349992,984
16-
[report] flash_data,384,316,68
17-
[report] static_bss,110992,110876,116
13+
[report] flash_text,354584,349992,4592
14+
[report] flash_data,448,316,132
15+
[report] static_bss,112656,110876,1780
1816
[report] heap_used,4440,4440,0
19-
[report] mbedtls_peak,21276,21332,-56
20-
[report] mbedtls_free,11492,11436,56
17+
[report] mbedtls_peak,21316,21332,-16
18+
[report] mbedtls_free,11452,11436,16
2119
[report] lwip_mem_free,7576,7576,0
2220
[report] lwip_pbufs_free,14,14,0
23-
[report] stack_log,120,120,0
21+
[report] stack_log,1024,120,904
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-
350968 392 110992 462352 70e10 /w/build/baseline-cross/baseline.elf
32+
354576 456 112656 467688 722e8 /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-08-15T13:02:30+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: 350976 (expected 350976, Δ0)
59-
OK flash_data: 384 (expected 384, Δ0)
60-
OK static_bss: 110992 (expected 110992, Δ0)
57+
OK flash_text: 354584 (expected 354584, Δ0)
58+
OK flash_data: 448 (expected 448, Δ0)
59+
OK static_bss: 112656 (expected 112656, Δ0)
6160
OK heap_used: 4440 (expected 4440, Δ0)
62-
OK mbedtls_peak: 21276 (expected 21300, Δ24)
63-
OK mbedtls_free: 11492 (expected 11468, Δ24)
61+
OK mbedtls_peak: 21316 (expected 21236, Δ80)
62+
OK mbedtls_free: 11452 (expected 11532, Δ80)
6463
OK lwip_mem_free: 7576 (expected 7576, Δ0)
65-
OK lwip_pbufs_free: 14 (expected 13, Δ1)
66-
OK stack_log: 120 (expected 120, Δ0)
64+
OK lwip_pbufs_free: 14 (expected 14, Δ0)
65+
OK stack_log: 1024 (expected 1024, Δ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)