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
49 changes: 29 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,51 @@ 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 — Header fields
## This stage — Sequence numbers

The record so far carries no timestamp and no device name. Fill the RFC 5424 header fields from
what the device already has: the clock it acquired at boot, the address on its interface, and its
own name.
Add the first structured-data element, `SolidSyslogMetaSd`, carrying a sequence number. Elements
are supplied to the logger as an array and read on every record, so they must outlive the call that
creates the logger.

```c
static struct SolidSyslogStructuredData* sd[1];

struct SolidSyslogMetaSdConfig metaConfig = {
.Counter = SolidSyslogStdAtomicCounter_Create(),
};
sd[0] = SolidSyslogMetaSd_Create(&metaConfig);

struct SolidSyslogConfig config = {
/* ... */
.Clock = SyslogFields_Clock,
.GetHostname = SyslogFields_Hostname,
.GetAppName = SyslogFields_AppName,
.Sd = sd,
.SdCount = 1U,
};
```

```text
<134>1 2026-08-16T19:17:54.430000Z 10.0.2.15 solid-syslog-example - BOOT - device started
... BOOT [meta sequenceId="1"] device started
```

PROCID stays nil, because a bare-metal image has no process to identify, and so does
STRUCTURED-DATA until the next stage.
The sequence number is incremented once per record *formatted*, not once per record delivered. A
record that never arrives therefore leaves a gap in the sequence rather than no trace at all, which
is why it is worth adding before any buffering or storage that could drop one. Instrument first,
then introduce the failure mode.

Unlike a header field, an SD PARAM has no nil value: one that is unset is omitted entirely rather
than written as `-`.

Two things the adapters have to get right. The timestamp struct is zeroed before it is filled, 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.
The counter comes from `SolidSyslogStdAtomicCounter`. If your toolchain has no atomics, supply your
own to the contract `SolidSyslogAtomicCounter_Increment` states — and note that logging from more
than one task is what makes the atomic part of it necessary.

Where a device has no resolvable name, RFC 5424 section 6.2.4 allows its address in the HOSTNAME
field instead, which is this device exactly.
`StdAtomic` joins the platform list, and here that is all it is: naming it compiles its two sources
exactly as naming `LwipRaw` compiles its eleven.

**When you need it.** As soon as more than one device reports to the collector, or a record's time
will be relied on. Everything the later stages add — a sequence number, the clock's quality, the
device's own identity — builds on these fields rather than replacing them.
**When you need it.** If anyone needs to know that records have gone missing.

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

**Cost above baseline: Flash +5,108 B, RAM +1,908 B.**
**Cost above baseline: Flash +6,052 B, RAM +1,972 B.**

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

Expand All @@ -65,6 +73,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage.
| Logger created | the logger object, reporting exactly what is still missing from it | +1,060 | +184 |
| First record | a valid RFC 5424 record on the wire, over UDP | +4,724 | +1,908 |
| Header fields | a timestamped record naming the device, instead of nil values | +5,108 | +1,908 |
| Sequence numbers | every record numbered, so a gap in the sequence is visible | +6,052 | +1,972 |

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

Expand Down
17 changes: 16 additions & 1 deletion app/syslog/Syslog.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
*
* 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. */
* — no queue, no background drain, nothing to service.
*
* Unlike a header field, an SD PARAM has no NILVALUE: an unset one is omitted
* entirely rather than written as "-". */

#include "Syslog.h"

Expand All @@ -13,8 +16,10 @@
#include "SolidSyslogLwipRawDatagram.h"
#include "SolidSyslogLwipRawMarshal.h"
#include "SolidSyslogLwipRawResolver.h"
#include "SolidSyslogMetaSd.h"
#include "SolidSyslogNullStore.h"
#include "SolidSyslogPassthroughBuffer.h"
#include "SolidSyslogStdAtomicCounter.h"
#include "SolidSyslogUdpSender.h"
#include "SyslogFields.h"

Expand All @@ -32,6 +37,9 @@

static struct SolidSyslog* s_logger = NULL;

/* The logger reads these on every record, so they outlive Syslog_Start. */
static struct SolidSyslogStructuredData* s_sd[1];

/* 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
Expand Down Expand Up @@ -70,6 +78,11 @@ void Syslog_Start(void)
};
struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig);

/* One counter Increment per record formatted, so a record that never reaches
* the collector leaves a gap in the sequence rather than no trace at all. */
struct SolidSyslogMetaSdConfig metaConfig = {.Counter = SolidSyslogStdAtomicCounter_Create()};
s_sd[0] = SolidSyslogMetaSd_Create(&metaConfig);

struct SolidSyslogConfig config = {
.Buffer = SolidSyslogPassthroughBuffer_Create(sender),
.Sender = sender,
Expand All @@ -80,6 +93,8 @@ void Syslog_Start(void)
.Clock = SyslogFields_Clock,
.GetHostname = SyslogFields_Hostname,
.GetAppName = SyslogFields_AppName,
.Sd = s_sd,
.SdCount = sizeof(s_sd) / sizeof(s_sd[0]),
};

s_logger = SolidSyslog_Create(&config);
Expand Down
2 changes: 1 addition & 1 deletion make/solidsyslog.mk
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# upstream option it needs.
# https://docs.cososo.co.uk/solid-syslog/getting-started/#path-b--non-cmake-integrator-the-manifest

SOLIDSYSLOG_PLATFORMS := LwipRaw
SOLIDSYSLOG_PLATFORMS := LwipRaw StdAtomic
include $(THIRD_PARTY)/solid-syslog/solidsyslog.mk

SOLIDSYSLOG_LIB := $(BUILD)/libSolidSyslog.a
Expand Down
13 changes: 13 additions & 0 deletions measurements/sequence-id.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# sequence-id 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,355704
flash_data,472
static_bss,112692
heap_used,4440
mbedtls_peak,21320
mbedtls_free,11448
lwip_mem_free,7576
lwip_pbufs_free,14
stack_log,1024
stack_service,52
stack_harness,2848
1 change: 1 addition & 0 deletions measurements/stages.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ error-handler Error handler a fault inside the logger reaches the console instea
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
header-fields Header fields a timestamped record naming the device, instead of nil values
sequence-id Sequence numbers every record numbered, so a gap in the sequence is visible
32 changes: 16 additions & 16 deletions run-report.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# solid-syslog-example — run (header-fields)
# solid-syslog-example — run (sequence-id)

## Device (self-measured)

Expand All @@ -10,12 +10,12 @@
[device] first record logged: yes
[report] --- SolidSyslog cost above baseline (simulated existing application) ---
[report] key,current,baseline,used_above_baseline
[report] flash_text,354784,349808,4976
[report] flash_data,448,316,132
[report] static_bss,112652,110876,1776
[report] flash_text,355704,349808,5896
[report] flash_data,472,316,156
[report] static_bss,112692,110876,1816
[report] heap_used,4440,4440,0
[report] mbedtls_peak,21292,21328,-36
[report] mbedtls_free,11476,11440,36
[report] mbedtls_peak,21352,21328,24
[report] mbedtls_free,11416,11440,-24
[report] lwip_mem_free,7576,7576,0
[report] lwip_pbufs_free,14,13,1
[report] stack_log,1024,120,904
Expand All @@ -29,7 +29,7 @@

```text
text data bss dec hex filename
354776 456 112652 467884 723ac /w/build/baseline.elf
355696 480 112692 468868 72784 /w/build/baseline.elf
```

## Listeners (proved before the device ran)
Expand All @@ -47,21 +47,21 @@
## Collector (syslog-ng) received

```text
wire <134>1 2026-08-16T19:19:27.850000Z 10.0.2.15 solid-syslog-example - BOOT - device started
parsed PRIORITY=134 TIMESTAMP=2026-08-16T19:19:27+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA= MSG=device started
wire <134>1 2026-08-16T19:22:20.850000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1"] device started
parsed PRIORITY=134 TIMESTAMP=2026-08-16T19:22:20+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1"] MSG=device started
```

## Self-check (vs measurements/header-fields.csv)
## Self-check (vs measurements/sequence-id.csv)

```text
OK flash_text: 354784 (expected 354784, Δ0)
OK flash_data: 448 (expected 448, Δ0)
OK static_bss: 112652 (expected 112652, Δ0)
OK flash_text: 355704 (expected 355704, Δ0)
OK flash_data: 472 (expected 472, Δ0)
OK static_bss: 112692 (expected 112692, Δ0)
OK heap_used: 4440 (expected 4440, Δ0)
OK mbedtls_peak: 21292 (expected 21288, Δ4)
OK mbedtls_free: 11476 (expected 11480, Δ4)
OK mbedtls_peak: 21352 (expected 21320, Δ32)
OK mbedtls_free: 11416 (expected 11448, Δ32)
OK lwip_mem_free: 7576 (expected 7576, Δ0)
OK lwip_pbufs_free: 14 (expected 13, Δ1)
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: 2848 (expected 2848, Δ0)
Expand Down