Skip to content

Commit 0d7c593

Browse files
DavidCozensclaude
andcommitted
feat: fill in the RFC 5424 header fields
TIMESTAMP, HOSTNAME and APP-NAME come from what the device already has — the wall clock it acquired at boot, the address on its interface, and its own name. PROCID stays nil, because a bare-metal image has no process to identify. <134>1 2026-08-15T13:27:08.310000Z 10.0.2.15 solid-syslog-example - BOOT - device started Flash +5,116 B (+392 on the previous stage) RAM +1,912 B (unchanged) 392 bytes is what a record that says when it happened and which device it came from costs over one that says neither. That is the difference between a log line and evidence, and it is worth knowing the price of separately. Two things the adapters have to get right. The timestamp struct is zeroed before use, so a clock that cannot answer leaves Month == 0, fails the library's validation, and is emitted as the nil value rather than as a wrong time. And the hostname is read under the lwIP core lock, with ip4addr_ntoa_r rather than ip4addr_ntoa — the latter shares one static buffer across callers. RFC 5424 section 6.2.4 allows an address where a device has no resolvable name, which is this device exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 81fad45 commit 0d7c593

8 files changed

Lines changed: 144 additions & 53 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ add_executable(baseline
131131
${APP_DIR}/storage/diskio.c
132132
${APP_DIR}/storage/SemihostingDisk.c
133133
${APP_DIR}/syslog/Syslog.c
134+
${APP_DIR}/syslog/SyslogFields.c
134135
${APP_DIR}/syslog/SyslogErrorHandler.c
135136
$<TARGET_OBJECTS:baseline_upstream>
136137
)
@@ -152,7 +153,7 @@ target_include_directories(baseline PRIVATE
152153
${APP_DIR}/net/smsc9220
153154
${APP_DIR}/platform # CmsdkUart.h, SemihostingExit.h, SemihostingIo.h
154155
${APP_DIR}/storage # SemihostingDisk.h
155-
${APP_DIR}/syslog # Syslog.h, SyslogErrorHandler.h
156+
${APP_DIR}/syslog # this device's SolidSyslog wiring
156157
${FREERTOS_KERNEL_PATH}/include
157158
${FREERTOS_PORT_DIR}
158159
${LWIP_DIR}/src/include

README.md

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,57 +10,43 @@ 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 — First record
13+
## This stage — Header fields
1414

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.
15+
The record so far carries no timestamp and no device name. Fill the RFC 5424 header fields from
16+
what the device already has: the clock it acquired at boot, the address on its interface, and its
17+
own name.
1918

2019
```c
21-
struct SolidSyslogUdpSenderConfig senderConfig = {
22-
.Resolver = SolidSyslogLwipRawResolver_Create(),
23-
.Datagram = SolidSyslogLwipRawDatagram_Create(),
24-
.Address = SolidSyslogLwipRawAddress_Create(),
25-
.Endpoint = CollectorEndpoint,
26-
};
27-
struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig);
28-
2920
struct SolidSyslogConfig config = {
30-
.Buffer = SolidSyslogPassthroughBuffer_Create(sender),
31-
.Sender = sender,
32-
.Store = SolidSyslogNullStore_Get(),
21+
/* ... */
22+
.Clock = SyslogFields_Clock,
23+
.GetHostname = SyslogFields_Hostname,
24+
.GetAppName = SyslogFields_AppName,
3325
};
3426
```
3527

36-
What arrives is a valid RFC 5424 record any collector will parse:
37-
3828
```text
39-
<134>1 - - - - BOOT - device started
29+
<134>1 2026-08-15T13:27:08.310000Z 10.0.2.15 solid-syslog-example - BOOT - device started
4030
```
4131

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.
32+
PROCID stays nil, because a bare-metal image has no process to identify, and so does
33+
STRUCTURED-DATA until the next stage.
4534

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.
35+
Two things the adapters have to get right. The timestamp struct is zeroed before it is filled, so a
36+
clock that cannot answer leaves `Month == 0`, fails the library's validation, and is emitted as the
37+
nil value rather than as a wrong time. And the hostname is read under the lwIP core lock, with
38+
`ip4addr_ntoa_r` rather than `ip4addr_ntoa`the latter shares one static buffer across callers.
5039

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.
40+
Where a device has no resolvable name, RFC 5424 section 6.2.4 allows its address in the HOSTNAME
41+
field instead, which is this device exactly.
5642

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.
43+
**When you need it.** As soon as more than one device reports to the collector, or a record's time
44+
will be relied on. Everything the later stages add — a sequence number, the clock's quality, the
45+
device's own identity — builds on these fields rather than replacing them.
6046

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

63-
**Cost above baseline: Flash +4,724 B, RAM +1,912 B.**
49+
**Cost above baseline: Flash +5,116 B, RAM +1,912 B.**
6450

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

@@ -78,6 +64,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage.
7864
| Error handler | a fault inside the logger reaches the console instead of being silent | +404 | +8 |
7965
| Logger created | the logger object, reporting exactly what is still missing from it | +1,052 | +184 |
8066
| First record | a valid RFC 5424 record on the wire, over UDP | +4,724 | +1,912 |
67+
| Header fields | a timestamped record naming the device, instead of nil values | +5,116 | +1,912 |
8168

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

app/syslog/Syslog.c

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22
*
33
* The smallest wiring that delivers: a UDP sender over lwIP, with a passthrough
44
* 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. */
5+
* — no queue, no background drain, nothing to service. */
106

117
#include "Syslog.h"
128

@@ -20,6 +16,7 @@
2016
#include "SolidSyslogNullStore.h"
2117
#include "SolidSyslogPassthroughBuffer.h"
2218
#include "SolidSyslogUdpSender.h"
19+
#include "SyslogFields.h"
2320

2421
#include "lwip/tcpip.h"
2522

@@ -79,6 +76,10 @@ void Syslog_Start(void)
7976
/* No store-and-forward here. The Null object rather than NULL is how
8077
* that is said out loud — NULL is reported as a fault. */
8178
.Store = SolidSyslogNullStore_Get(),
79+
/* PROCID stays unset — a bare-metal image has no process. */
80+
.Clock = SyslogFields_Clock,
81+
.GetHostname = SyslogFields_Hostname,
82+
.GetAppName = SyslogFields_AppName,
8283
};
8384

8485
s_logger = SolidSyslog_Create(&config);

app/syslog/SyslogFields.c

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* See SyslogFields.h. */
2+
3+
#include "SyslogFields.h"
4+
5+
#include "DeviceClock.h"
6+
7+
#include "SolidSyslogHeaderField.h"
8+
#include "SolidSyslogTimestamp.h"
9+
10+
#include "lwip/ip4_addr.h"
11+
#include "lwip/netif.h"
12+
#include "lwip/tcpip.h"
13+
14+
#include <stdint.h>
15+
#include <string.h>
16+
#include <time.h>
17+
18+
#define SYSLOG_APP_NAME "solid-syslog-example"
19+
20+
void SyslogFields_Clock(struct SolidSyslogTimestamp* timestamp)
21+
{
22+
struct tm utc;
23+
uint32_t microseconds = 0U;
24+
25+
/* Zeroed means "no usable time": Month == 0 fails the library's validation
26+
* and the field is emitted as the RFC 5424 nil value. */
27+
(void) memset(timestamp, 0, sizeof(*timestamp));
28+
29+
if (DeviceClock_Now(&utc, &microseconds))
30+
{
31+
timestamp->Year = (uint16_t) (utc.tm_year + 1900);
32+
timestamp->Month = (uint8_t) (utc.tm_mon + 1);
33+
timestamp->Day = (uint8_t) utc.tm_mday;
34+
timestamp->Hour = (uint8_t) utc.tm_hour;
35+
timestamp->Minute = (uint8_t) utc.tm_min;
36+
timestamp->Second = (uint8_t) utc.tm_sec;
37+
timestamp->Microsecond = microseconds;
38+
timestamp->UtcOffsetMinutes = 0;
39+
}
40+
}
41+
42+
void SyslogFields_Hostname(struct SolidSyslogHeaderField* field, void* context)
43+
{
44+
(void) context;
45+
46+
char address[IP4ADDR_STRLEN_MAX] = {0};
47+
48+
/* netif state belongs to the lwIP core, so read and format under its lock.
49+
* ip4addr_ntoa_r, not ip4addr_ntoa: the latter shares one static buffer. */
50+
LOCK_TCPIP_CORE();
51+
if (netif_default != NULL)
52+
{
53+
(void) ip4addr_ntoa_r(netif_ip4_addr(netif_default), address, (int) sizeof(address));
54+
}
55+
UNLOCK_TCPIP_CORE();
56+
57+
if (address[0] != '\0')
58+
{
59+
SolidSyslogHeaderField_PrintUsAscii(field, address, strlen(address));
60+
}
61+
}
62+
63+
void SyslogFields_AppName(struct SolidSyslogHeaderField* field, void* context)
64+
{
65+
(void) context;
66+
67+
SolidSyslogHeaderField_PrintUsAscii(field, SYSLOG_APP_NAME, strlen(SYSLOG_APP_NAME));
68+
}

app/syslog/SyslogFields.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* The RFC 5424 header fields this device supplies: adapters between what the
2+
* device already has — a wall clock, an IP address, a name — and the shapes
3+
* SolidSyslog asks for. */
4+
#ifndef SYSLOG_FIELDS_H
5+
#define SYSLOG_FIELDS_H
6+
7+
struct SolidSyslogTimestamp;
8+
struct SolidSyslogHeaderField;
9+
10+
/** SolidSyslogClockFunction. */
11+
void SyslogFields_Clock(struct SolidSyslogTimestamp* timestamp);
12+
13+
/** HOSTNAME as the interface's IPv4 address — RFC 5424 section 6.2.4 allows an
14+
* address where a device has no resolvable name. */
15+
void SyslogFields_Hostname(struct SolidSyslogHeaderField* field, void* context);
16+
17+
/** APP-NAME, fixed for this firmware. */
18+
void SyslogFields_AppName(struct SolidSyslogHeaderField* field, void* context);
19+
20+
#endif /* SYSLOG_FIELDS_H */

measurements/header-fields.csv

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# header-fields 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,354976
4+
flash_data,448
5+
static_bss,112656
6+
heap_used,4440
7+
mbedtls_peak,21332
8+
mbedtls_free,11436
9+
lwip_mem_free,7576
10+
lwip_pbufs_free,13
11+
stack_log,1024
12+
stack_service,52
13+
stack_harness,2848

measurements/stages.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ linked Linked the core library and lwIP raw-mode networking, linked but not yet
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
1616
udp First record a valid RFC 5424 record on the wire, over UDP
17+
header-fields Header fields a timestamped record naming the device, instead of nil values

run-report.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# solid-syslog-example — run (udp)
1+
# solid-syslog-example — run (header-fields)
22

33
## Device (self-measured)
44

@@ -10,12 +10,12 @@
1010
[device] first record logged: yes
1111
[report] --- SolidSyslog cost above baseline (simulated existing application) ---
1212
[report] key,current,baseline,used_above_baseline
13-
[report] flash_text,354584,349992,4592
13+
[report] flash_text,354976,349992,4984
1414
[report] flash_data,448,316,132
1515
[report] static_bss,112656,110876,1780
1616
[report] heap_used,4440,4440,0
17-
[report] mbedtls_peak,21316,21332,-16
18-
[report] mbedtls_free,11452,11436,16
17+
[report] mbedtls_peak,21232,21332,-100
18+
[report] mbedtls_free,11536,11436,100
1919
[report] lwip_mem_free,7576,7576,0
2020
[report] lwip_pbufs_free,14,14,0
2121
[report] stack_log,1024,120,904
@@ -29,7 +29,7 @@
2929

3030
```text
3131
text data bss dec hex filename
32-
354576 456 112656 467688 722e8 /w/build/baseline-cross/baseline.elf
32+
354968 456 112656 468080 72470 /w/build/baseline-cross/baseline.elf
3333
```
3434

3535
## Listeners (proved before the device ran)
@@ -47,21 +47,21 @@
4747
## Collector (syslog-ng) received
4848

4949
```text
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
50+
wire <134>1 2026-08-15T13:29:17.850000Z 10.0.2.15 solid-syslog-example - BOOT - device started
51+
parsed PRIORITY=134 TIMESTAMP=2026-08-15T13:29:17+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA= MSG=device started
5252
```
5353

54-
## Self-check (vs measurements/udp.csv)
54+
## Self-check (vs measurements/header-fields.csv)
5555

5656
```text
57-
OK flash_text: 354584 (expected 354584, Δ0)
57+
OK flash_text: 354976 (expected 354976, Δ0)
5858
OK flash_data: 448 (expected 448, Δ0)
5959
OK static_bss: 112656 (expected 112656, Δ0)
6060
OK heap_used: 4440 (expected 4440, Δ0)
61-
OK mbedtls_peak: 21316 (expected 21236, Δ80)
62-
OK mbedtls_free: 11452 (expected 11532, Δ80)
61+
OK mbedtls_peak: 21232 (expected 21332, Δ100)
62+
OK mbedtls_free: 11536 (expected 11436, Δ100)
6363
OK lwip_mem_free: 7576 (expected 7576, Δ0)
64-
OK lwip_pbufs_free: 14 (expected 14, Δ0)
64+
OK lwip_pbufs_free: 14 (expected 13, Δ1)
6565
OK stack_log: 1024 (expected 1024, Δ0)
6666
OK stack_service: 52 (expected 52, Δ0)
6767
OK stack_harness: 2848 (expected 2848, Δ0)

0 commit comments

Comments
 (0)