Skip to content

Commit a1c3352

Browse files
DavidCozensclaude
andcommitted
feat: spool records to a file store with a CRC-16 at rest
A SolidSyslogBlockStore over a FileBlockDevice over the library's FatFs port, replacing the Null store. The service task drains the ring into the store and sends from there, so a failed send costs a retry rather than the record: the audit trail survives an outage instead of ending at it. Flash +11,584 B (+3,956 on the previous stage) RAM +9,128 B (+1,436) Log stack +672 B (unchanged) Service +960 B (+64) The log stack does not move: storing happens on the service task, and a task that calls Log still knows nothing about what happens after it returns. The service task's own high-water rises 64 bytes, which its existing allocation absorbs. The static RAM is pool allocation, not buffers. The block size is file capacity — nothing holds a block in memory, so the store costs its handles rather than its capacity. CRC-16 detects corruption, not tampering: it catches a truncated write or bit-rot, and anyone who can edit a stored record can recompute it. It establishes that a record came back the way it went in, which is the prerequisite for spooling at all. Making stored records tamper-evident, and then unreadable, are later stages. Three decisions come with the store — capacity, what happens when it fills, and whether to be warned before that point. This device stores four blocks, one file per block, syslog00.log upward on the volume it already mounts, and discards the oldest when full. SolidSyslog::FatFs is a header-configured upstream, so it is named in SOLIDSYSLOG_PLATFORMS and linked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d871a51 commit a1c3352

6 files changed

Lines changed: 85 additions & 47 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ set(LWIP_CONTRIB_FREERTOS_DIR "${LWIP_DIR}/contrib/ports/freertos")
3939
# link target — only the header-configured packs below do.
4040
# https://docs.cososo.co.uk/solid-syslog/getting-started/#path-a--cmake-consumer
4141
# Pinned to a commit until there is a release tag to pin to.
42-
set(SOLIDSYSLOG_PLATFORMS "LwipRaw;StdAtomic;FreeRtos" CACHE STRING "" FORCE)
42+
set(SOLIDSYSLOG_PLATFORMS "LwipRaw;StdAtomic;FreeRtos;FatFs" CACHE STRING "" FORCE)
4343

4444
include(FetchContent)
4545
FetchContent_Declare(SolidSyslog
@@ -167,7 +167,7 @@ target_include_directories(baseline PRIVATE
167167
# library, or context struct sizes diverge between consumer and library.
168168
target_compile_definitions(baseline PRIVATE MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER})
169169

170-
target_link_libraries(baseline PRIVATE mbedtls mbedx509 mbedcrypto SolidSyslog SolidSyslog::LwipRaw SolidSyslog::FreeRtos)
170+
target_link_libraries(baseline PRIVATE mbedtls mbedx509 mbedcrypto SolidSyslog SolidSyslog::LwipRaw SolidSyslog::FreeRtos SolidSyslog::FatFs)
171171

172172
target_link_options(baseline PRIVATE
173173
-mcpu=cortex-m3 -mthumb

README.md

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,51 @@ 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 — Time quality
13+
## This stage — File store
1414

15-
Add `SolidSyslogTimeQualitySd`, and give `MetaSd` an uptime source alongside its counter.
15+
Spool to a `SolidSyslogBlockStore` over a `SolidSyslogFileBlockDevice` over the library's FatFs
16+
port, replacing the Null store. The service task drains the ring into storage and sends from there,
17+
so a failed send costs a retry rather than the record: the audit trail survives an outage instead of
18+
ending at it.
1619

1720
```c
18-
struct SolidSyslogMetaSdConfig metaConfig = {
19-
.Counter = SolidSyslogStdAtomicCounter_Create(),
20-
.GetSysUpTime = SolidSyslogFreeRtos_GetSysUpTime, /* new */
21+
#define SYSLOG_STORE_PREFIX "syslog"
22+
#define SYSLOG_STORE_BLOCKS 4U
23+
24+
struct SolidSyslogBlockStoreConfig storeConfig = {
25+
.BlockDevice = SolidSyslogFileBlockDevice_Create(SolidSyslogFatFsFile_Create(), SYSLOG_STORE_PREFIX, 0U),
26+
.MaxBlocks = SYSLOG_STORE_BLOCKS,
27+
.DiscardPolicy = SOLIDSYSLOG_DISCARD_POLICY_OLDEST,
28+
.SecurityPolicy = SolidSyslogCrc16Policy_Create(),
2129
};
22-
sd[1] = SolidSyslogTimeQualitySd_Create(SyslogTimeQuality);
2330
```
2431

25-
```text
26-
... BOOT [meta sequenceId="1" sysUpTime="385"][timeQuality tzKnown="1" isSynced="0"] device started
27-
```
32+
Three decisions come with it: how much to store, which is capacity on the medium rather than RAM;
33+
what happens when it fills — discard oldest, discard newest, or halt; and whether to be warned
34+
before that point, via the capacity-threshold callback.
2835

29-
Time quality states how far the clock can be trusted, which matters when comparing events from
30-
different devices.
36+
This device stores four blocks, one file per block, `syslog00.log` upward on the volume it already
37+
mounts, and discards the oldest when full.
3138

32-
This device reads the host clock once at boot and then free-runs on the FreeRTOS tick, so `isSynced`
33-
is `0` and the callback writes no `syncAccuracy`. `tzKnown` is `1`; the device works in UTC
34-
throughout.
39+
The CRC-16 detects corruption, not tampering. It catches a truncated write or bit-rot; anyone who
40+
can edit a stored record can recompute it. It establishes that a record came back the way it went
41+
in, which is the prerequisite for spooling at all. Making stored records tamper-evident, and then
42+
unreadable, are later stages.
3543

36-
`sysUpTime` accompanies the sequence number. After a reboot the sequence restarts at one, and an
37-
uptime near zero distinguishes that from a counter wrap.
44+
Storing happens on the service task, so a task that calls `SolidSyslog_Log` still knows nothing
45+
about what happens after it returns and its stack does not move. The RAM is pool allocation and
46+
handles rather than buffers — nothing holds a block in memory, so the store costs its handles
47+
rather than its capacity.
3848

39-
The element lands before the store because store-and-forward breaks the assumption that a record
40-
reaches the collector shortly after it was raised. A record can arrive hours later, so the device
41-
states what its clock is worth first.
49+
`SolidSyslog::FatFs` is a header-configured upstream, so it is both named in
50+
`SOLIDSYSLOG_PLATFORMS` and linked.
4251

43-
**When you need it.** If events from this device will be ordered against events from others, or if a
44-
record's timestamp will be relied on after a delay.
52+
**When you need it.** If losing the records raised during an outage is not acceptable, or if they
53+
must survive a reboot.
4554

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

48-
**Cost above baseline: Flash +7,628 B, RAM +7,692 B.**
57+
**Cost above baseline: Flash +11,584 B, RAM +9,128 B.**
4958

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

@@ -68,6 +77,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage.
6877
| Buffered | logging that returns immediately, with the send moved off the logging task | +6,804 | +7,488 |
6978
| TCP | records the network retransmits instead of dropping, and a send that fails when the collector is gone | +7,336 | +7,668 |
7079
| Time quality | a timestamp the collector knows how far to trust, and an uptime that tells a reboot from a counter wrap | +7,628 | +7,692 |
80+
| File store | records that survive a failed send, spooled to disk with a checksum at rest | +11,584 | +9,128 |
7181

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

app/syslog/Syslog.c

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@
99

1010
#include "Syslog.h"
1111

12+
#include "SolidSyslogBlockStore.h"
1213
#include "SolidSyslogCircularBuffer.h"
1314
#include "SolidSyslogConfig.h"
15+
#include "SolidSyslogCrc16Policy.h"
1416
#include "SolidSyslogEndpoint.h"
1517
#include "SolidSyslogEndpointHost.h"
18+
#include "SolidSyslogFatFsFile.h"
19+
#include "SolidSyslogFileBlockDevice.h"
1620
#include "SolidSyslogFreeRtosMutex.h"
1721
#include "SolidSyslogFreeRtosSysUpTime.h"
1822
#include "SolidSyslogLwipRawAddress.h"
1923
#include "SolidSyslogLwipRawMarshal.h"
2024
#include "SolidSyslogLwipRawResolver.h"
2125
#include "SolidSyslogLwipRawTcpStream.h"
2226
#include "SolidSyslogMetaSd.h"
23-
#include "SolidSyslogNullStore.h"
2427
#include "SolidSyslogStdAtomicCounter.h"
2528
#include "SolidSyslogStreamSender.h"
2629
#include "SolidSyslogTimeQuality.h"
@@ -46,6 +49,10 @@
4649
* backlog the store is there to hold. */
4750
#define SYSLOG_BUFFER_RECORDS 8U
4851

52+
/* One "<prefix>NN.log" per block, on the volume the device already mounts. */
53+
#define SYSLOG_STORE_PREFIX "syslog"
54+
#define SYSLOG_STORE_BLOCKS 4U
55+
4956
static struct SolidSyslog* s_logger = NULL;
5057
static uint8_t s_ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(SYSLOG_BUFFER_RECORDS)];
5158

@@ -115,12 +122,19 @@ void Syslog_Start(void)
115122
s_sd[0] = SolidSyslogMetaSd_Create(&metaConfig);
116123
s_sd[1] = SolidSyslogTimeQualitySd_Create(SyslogTimeQuality);
117124

125+
/* Oldest discarded when the ceiling is reached: a device that cannot reach its
126+
* collector should keep the newest evidence, not stop logging. */
127+
struct SolidSyslogBlockStoreConfig storeConfig = {
128+
.BlockDevice = SolidSyslogFileBlockDevice_Create(SolidSyslogFatFsFile_Create(), SYSLOG_STORE_PREFIX, 0U),
129+
.MaxBlocks = SYSLOG_STORE_BLOCKS,
130+
.DiscardPolicy = SOLIDSYSLOG_DISCARD_POLICY_OLDEST,
131+
.SecurityPolicy = SolidSyslogCrc16Policy_Create(),
132+
};
133+
118134
struct SolidSyslogConfig config = {
119135
.Buffer = SolidSyslogCircularBuffer_Create(SolidSyslogFreeRtosMutex_Create(), s_ring, sizeof(s_ring)),
120136
.Sender = sender,
121-
/* No store-and-forward here. The Null object rather than NULL is how
122-
* that is said out loud — NULL is reported as a fault. */
123-
.Store = SolidSyslogNullStore_Get(),
137+
.Store = SolidSyslogBlockStore_Create(&storeConfig),
124138
/* PROCID stays unset — a bare-metal image has no process. */
125139
.Clock = SyslogFields_Clock,
126140
.GetHostname = SyslogFields_Hostname,

measurements/file-store.csv

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# file-store 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,361260
4+
flash_data,632
5+
static_bss,119688
6+
heap_used,4440
7+
mbedtls_peak,21332
8+
mbedtls_free,11436
9+
lwip_mem_free,7576
10+
lwip_pbufs_free,14
11+
stack_log,792
12+
stack_service,1012
13+
stack_harness,2848

measurements/stages.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ sequence-id Sequence numbers every record numbered, so a gap in the sequence is
1919
buffered Buffered logging that returns immediately, with the send moved off the logging task
2020
tcp TCP records the network retransmits instead of dropping, and a send that fails when the collector is gone
2121
time-quality Time quality a timestamp the collector knows how far to trust, and an uptime that tells a reboot from a counter wrap
22+
file-store File store records that survive a failed send, spooled to disk with a checksum at rest

run-report.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# solid-syslog-example — run (time-quality)
1+
# solid-syslog-example — run (file-store)
22

33
## Device (self-measured)
44

@@ -10,16 +10,16 @@
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,357440,349992,7448
14-
[report] flash_data,496,316,180
15-
[report] static_bss,118388,110876,7512
13+
[report] flash_text,361260,349992,11268
14+
[report] flash_data,632,316,316
15+
[report] static_bss,119688,110876,8812
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,21300,21332,-32
18+
[report] mbedtls_free,11468,11436,32
1919
[report] lwip_mem_free,7576,7576,0
20-
[report] lwip_pbufs_free,14,14,0
20+
[report] lwip_pbufs_free,13,14,-1
2121
[report] stack_log,792,120,672
22-
[report] stack_service,948,52,896
22+
[report] stack_service,1012,52,960
2323
[report] stack_harness,2848,2840,8
2424
[report] --- end ---
2525
[device] ready
@@ -29,7 +29,7 @@
2929

3030
```text
3131
text data bss dec hex filename
32-
357432 504 118388 476324 744a4 /w/build/baseline-cross/baseline.elf
32+
361252 640 119688 481580 7592c /w/build/baseline-cross/baseline.elf
3333
```
3434

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

4949
```text
50-
wire <134>1 2026-08-15T22:00:10.850000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="385"][timeQuality tzKnown="1" isSynced="0"] device started
51-
parsed PRIORITY=134 TIMESTAMP=2026-08-15T22:00:10+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="385"][timeQuality tzKnown="1" isSynced="0"] MSG=device started
50+
wire <134>1 2026-08-16T06:08:05.430000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="243"][timeQuality tzKnown="1" isSynced="0"] device started
51+
parsed PRIORITY=134 TIMESTAMP=2026-08-16T06:08:05+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="243"][timeQuality tzKnown="1" isSynced="0"] MSG=device started
5252
```
5353

54-
## Self-check (vs measurements/time-quality.csv)
54+
## Self-check (vs measurements/file-store.csv)
5555

5656
```text
57-
OK flash_text: 357440 (expected 357440, Δ0)
58-
OK flash_data: 496 (expected 496, Δ0)
59-
OK static_bss: 118388 (expected 118388, Δ0)
57+
OK flash_text: 361260 (expected 361260, Δ0)
58+
OK flash_data: 632 (expected 632, Δ0)
59+
OK static_bss: 119688 (expected 119688, Δ0)
6060
OK heap_used: 4440 (expected 4440, Δ0)
61-
OK mbedtls_peak: 21316 (expected 21288, Δ28)
62-
OK mbedtls_free: 11452 (expected 11480, Δ28)
61+
OK mbedtls_peak: 21300 (expected 21332, Δ32)
62+
OK mbedtls_free: 11468 (expected 11436, Δ32)
6363
OK lwip_mem_free: 7576 (expected 7576, Δ0)
64-
OK lwip_pbufs_free: 14 (expected 14, Δ0)
64+
OK lwip_pbufs_free: 13 (expected 14, Δ1)
6565
OK stack_log: 792 (expected 792, Δ0)
66-
OK stack_service: 948 (expected 948, Δ0)
66+
OK stack_service: 1012 (expected 1012, Δ0)
6767
OK stack_harness: 2848 (expected 2848, Δ0)
6868
```
6969

0 commit comments

Comments
 (0)