Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 35 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,57 @@ 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

Create the logger with both collaborators absent, deliberately, and read what the handler prints.
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.

```c
struct SolidSyslogConfig config = {
.Buffer = NULL,
.Sender = NULL,
struct SolidSyslogUdpSenderConfig senderConfig = {
.Resolver = SolidSyslogLwipRawResolver_Create(),
.Datagram = SolidSyslogLwipRawDatagram_Create(),
.Address = SolidSyslogLwipRawAddress_Create(),
.Endpoint = CollectorEndpoint,
};
struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig);

struct SolidSyslog* logger = SolidSyslog_Create(&config);
struct SolidSyslogConfig config = {
.Buffer = SolidSyslogPassthroughBuffer_Create(sender),
.Sender = sender,
.Store = SolidSyslogNullStore_Get(),
};
```

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

```text
[syslog] CRITICAL SolidSyslog bad-config (detail 1)
[syslog] CRITICAL SolidSyslog bad-config (detail 2)
[syslog] CRITICAL SolidSyslog bad-config (detail 3)
<134>1 - - - - BOOT - device started
```

Three, for the buffer, the sender and the store. Each names the collaborator in `Detail`, as a
value of the emitting class's own error enum.
Timestamp, hostname, app-name and process-id are the RFC's nil value. The record is valid without
them; 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 order matters. Wire everything at once and see nothing, and you cannot tell a working logger
from a silent one. Seeing the faults first, then watching them go quiet as each collaborator
arrives, is the difference between believing it works and knowing.
**The record is built on the stack of whichever task calls `SolidSyslog_Log`.** Its size follows
`SOLIDSYSLOG_MAX_MESSAGE_SIZE`, so the logging task needs room for the record and the send beneath
it — here the task was at the RTOS floor and had to grow. It is sized generously for now and
tightened against measured high-water marks at the end.

A convention worth adopting now: `NULL` as a parameter means "not supplied" and is reported, while
a collaborator you have deliberately done without is passed as its Null object. The library
distinguishes the two, and so should anyone reading the wiring later.
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.

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

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

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

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

Expand All @@ -65,6 +77,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,052 | +184 |
| First record | a valid RFC 5424 record on the wire, over UDP | +4,724 | +1,912 |

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

Expand Down
10 changes: 6 additions & 4 deletions app/AppConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
/* CMSDK UART0 on the mps2-an385, surfaced by QEMU over -serial stdio. */
#define DEVICE_UART0_BASE ((uintptr_t) 0x40004000U)

/* Both seams are idle, so both get the FreeRTOS floor — twice what an idle seam
* measures is well below it. Whatever deepens one grows it there. The reported
* figure is high-water usage, which does not depend on the allocation. */
#define LOG_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE)
/* The log seam formats the record on its own stack, so it holds
* SOLIDSYSLOG_MAX_MESSAGE_SIZE and the send beneath it and no longer fits the
* FreeRTOS floor. The service seam is still idle and keeps it. Both are sized
* generously here and tightened against measured high-water marks once the
* pipeline is complete. */
#define LOG_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 4U)
#define SERVICE_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE)
#define LOG_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)
#define SERVICE_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)
Expand Down
14 changes: 11 additions & 3 deletions app/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);

Expand All @@ -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");
Expand Down
78 changes: 69 additions & 9 deletions app/syslog/Syslog.c
Original file line number Diff line number Diff line change
@@ -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 <stddef.h>
#include <stdint.h>
#include <string.h>

/* 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);
}

Expand Down
16 changes: 15 additions & 1 deletion app/tasks/LogTask.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

#include "LogTask.h"

#include "Syslog.h"

#include "SolidSyslog.h"
#include "SolidSyslogPrival.h"

#include "AppConfig.h"

#include "semphr.h"
Expand All @@ -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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/tasks/LogTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions measurements/stages.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions measurements/udp.csv
Original file line number Diff line number Diff line change
@@ -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,354584
flash_data,448
static_bss,112656
heap_used,4440
mbedtls_peak,21236
mbedtls_free,11532
lwip_mem_free,7576
lwip_pbufs_free,14
stack_log,1024
stack_service,52
stack_harness,2848
43 changes: 21 additions & 22 deletions run-report.md
Original file line number Diff line number Diff line change
@@ -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,350976,349992,984
[report] flash_data,384,316,68
[report] static_bss,110992,110876,116
[report] flash_text,354584,349992,4592
[report] flash_data,448,316,132
[report] static_bss,112656,110876,1780
[report] heap_used,4440,4440,0
[report] mbedtls_peak,21276,21332,-56
[report] mbedtls_free,11492,11436,56
[report] mbedtls_peak,21316,21332,-16
[report] mbedtls_free,11452,11436,16
[report] lwip_mem_free,7576,7576,0
[report] lwip_pbufs_free,14,14,0
[report] stack_log,120,120,0
[report] stack_log,1024,120,904
[report] stack_service,52,52,0
[report] stack_harness,2840,2840,0
[report] stack_harness,2848,2840,8
[report] --- end ---
[device] ready
```
Expand All @@ -31,7 +29,7 @@

```text
text data bss dec hex filename
350968 392 110992 462352 70e10 /w/build/baseline-cross/baseline.elf
354576 456 112656 467688 722e8 /w/build/baseline-cross/baseline.elf
```

## Listeners (proved before the device ran)
Expand All @@ -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-08-15T13:02:30+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: 350976 (expected 350976, Δ0)
OK flash_data: 384 (expected 384, Δ0)
OK static_bss: 110992 (expected 110992, Δ0)
OK flash_text: 354584 (expected 354584, Δ0)
OK flash_data: 448 (expected 448, Δ0)
OK static_bss: 112656 (expected 112656, Δ0)
OK heap_used: 4440 (expected 4440, Δ0)
OK mbedtls_peak: 21276 (expected 21300, Δ24)
OK mbedtls_free: 11492 (expected 11468, Δ24)
OK mbedtls_peak: 21316 (expected 21236, Δ80)
OK mbedtls_free: 11452 (expected 11532, Δ80)
OK lwip_mem_free: 7576 (expected 7576, Δ0)
OK lwip_pbufs_free: 14 (expected 13, Δ1)
OK stack_log: 120 (expected 120, Δ0)
OK lwip_pbufs_free: 14 (expected 14, Δ0)
OK stack_log: 1024 (expected 1024, Δ0)
OK stack_service: 52 (expected 52, Δ0)
OK stack_harness: 2840 (expected 2840, Δ0)
OK stack_harness: 2848 (expected 2848, Δ0)
```

**RESULT: PASS**
Loading