Skip to content

Commit 87039e5

Browse files
DavidCozensclaude
andcommitted
feat: number every record with a meta sequenceId
<134>1 ... solid-syslog-example - BOOT [meta sequenceId="1"] device started Flash +6,052 B (+944 on the previous stage) RAM +1,972 B (+64) The counter increments once per record *formatted*, not per record delivered. A record that never reaches the collector therefore leaves a gap in the sequence rather than no trace at all — which is the whole value of the field, and the reason it lands before the circular buffer rather than after it. Instrument first, then introduce the failure mode; the other order means asserting a drop you already caused. The first SD-ELEMENT also introduces the rule that governs the rest: unlike a header field, an SD PARAM has no NILVALUE, so an unset one is omitted entirely rather than written as "-". 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, because the fragment hands back sources rather than link targets. The counter is the real one rather than the Null fallback that reports 1 forever — the image retains its StdAtomicCounter symbols. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ad9b39b commit 87039e5

6 files changed

Lines changed: 76 additions & 38 deletions

File tree

README.md

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +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 — Header fields
13+
## This stage — Sequence numbers
1414

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.
15+
Add the first structured-data element, `SolidSyslogMetaSd`, carrying a sequence number. Elements
16+
are supplied to the logger as an array and read on every record, so they must outlive the call that
17+
creates the logger.
1818

1919
```c
20+
static struct SolidSyslogStructuredData* sd[1];
21+
22+
struct SolidSyslogMetaSdConfig metaConfig = {
23+
.Counter = SolidSyslogStdAtomicCounter_Create(),
24+
};
25+
sd[0] = SolidSyslogMetaSd_Create(&metaConfig);
26+
2027
struct SolidSyslogConfig config = {
2128
/* ... */
22-
.Clock = SyslogFields_Clock,
23-
.GetHostname = SyslogFields_Hostname,
24-
.GetAppName = SyslogFields_AppName,
29+
.Sd = sd,
30+
.SdCount = 1U,
2531
};
2632
```
2733

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

32-
PROCID stays nil, because a bare-metal image has no process to identify, and so does
33-
STRUCTURED-DATA until the next stage.
38+
The sequence number is incremented once per record *formatted*, not once per record delivered. A
39+
record that never arrives therefore leaves a gap in the sequence rather than no trace at all, which
40+
is why it is worth adding before any buffering or storage that could drop one. Instrument first,
41+
then introduce the failure mode.
42+
43+
Unlike a header field, an SD PARAM has no nil value: one that is unset is omitted entirely rather
44+
than written as `-`.
3445

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.
46+
The counter comes from `SolidSyslogStdAtomicCounter`. If your toolchain has no atomics, supply your
47+
own to the contract `SolidSyslogAtomicCounter_Increment` states — and note that logging from more
48+
than one task is what makes the atomic part of it necessary.
3949

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.
50+
`StdAtomic` joins the platform list, and here that is all it is: naming it compiles its two sources
51+
exactly as naming `LwipRaw` compiles its eleven.
4252

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.
53+
**When you need it.** If anyone needs to know that records have gone missing.
4654

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

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

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

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

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

app/syslog/Syslog.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
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. */
5+
* — no queue, no background drain, nothing to service.
6+
*
7+
* Unlike a header field, an SD PARAM has no NILVALUE: an unset one is omitted
8+
* entirely rather than written as "-". */
69

710
#include "Syslog.h"
811

@@ -13,8 +16,10 @@
1316
#include "SolidSyslogLwipRawDatagram.h"
1417
#include "SolidSyslogLwipRawMarshal.h"
1518
#include "SolidSyslogLwipRawResolver.h"
19+
#include "SolidSyslogMetaSd.h"
1620
#include "SolidSyslogNullStore.h"
1721
#include "SolidSyslogPassthroughBuffer.h"
22+
#include "SolidSyslogStdAtomicCounter.h"
1823
#include "SolidSyslogUdpSender.h"
1924
#include "SyslogFields.h"
2025

@@ -32,6 +37,9 @@
3237

3338
static struct SolidSyslog* s_logger = NULL;
3439

40+
/* The logger reads these on every record, so they outlive Syslog_Start. */
41+
static struct SolidSyslogStructuredData* s_sd[1];
42+
3543
/* Every lwIP Raw call the datagram makes has to happen on the thread that owns
3644
* the lwIP core. lwipopts.h sets LWIP_TCPIP_CORE_LOCKING, so taking the core
3745
* lock in the caller's own task is simpler and cheaper than posting to the tcpip
@@ -70,6 +78,11 @@ void Syslog_Start(void)
7078
};
7179
struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig);
7280

81+
/* One counter Increment per record formatted, so a record that never reaches
82+
* the collector leaves a gap in the sequence rather than no trace at all. */
83+
struct SolidSyslogMetaSdConfig metaConfig = {.Counter = SolidSyslogStdAtomicCounter_Create()};
84+
s_sd[0] = SolidSyslogMetaSd_Create(&metaConfig);
85+
7386
struct SolidSyslogConfig config = {
7487
.Buffer = SolidSyslogPassthroughBuffer_Create(sender),
7588
.Sender = sender,
@@ -80,6 +93,8 @@ void Syslog_Start(void)
8093
.Clock = SyslogFields_Clock,
8194
.GetHostname = SyslogFields_Hostname,
8295
.GetAppName = SyslogFields_AppName,
96+
.Sd = s_sd,
97+
.SdCount = sizeof(s_sd) / sizeof(s_sd[0]),
8398
};
8499

85100
s_logger = SolidSyslog_Create(&config);

make/solidsyslog.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# upstream option it needs.
44
# https://docs.cososo.co.uk/solid-syslog/getting-started/#path-b--non-cmake-integrator-the-manifest
55

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

99
SOLIDSYSLOG_LIB := $(BUILD)/libSolidSyslog.a

measurements/sequence-id.csv

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# sequence-id 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,355704
4+
flash_data,472
5+
static_bss,112692
6+
heap_used,4440
7+
mbedtls_peak,21320
8+
mbedtls_free,11448
9+
lwip_mem_free,7576
10+
lwip_pbufs_free,14
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
@@ -15,3 +15,4 @@ error-handler Error handler a fault inside the logger reaches the console instea
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
1717
header-fields Header fields a timestamped record naming the device, instead of nil values
18+
sequence-id Sequence numbers every record numbered, so a gap in the sequence is visible

run-report.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# solid-syslog-example — run (header-fields)
1+
# solid-syslog-example — run (sequence-id)
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,354784,349808,4976
14-
[report] flash_data,448,316,132
15-
[report] static_bss,112652,110876,1776
13+
[report] flash_text,355704,349808,5896
14+
[report] flash_data,472,316,156
15+
[report] static_bss,112692,110876,1816
1616
[report] heap_used,4440,4440,0
17-
[report] mbedtls_peak,21292,21328,-36
18-
[report] mbedtls_free,11476,11440,36
17+
[report] mbedtls_peak,21352,21328,24
18+
[report] mbedtls_free,11416,11440,-24
1919
[report] lwip_mem_free,7576,7576,0
2020
[report] lwip_pbufs_free,14,13,1
2121
[report] stack_log,1024,120,904
@@ -29,7 +29,7 @@
2929

3030
```text
3131
text data bss dec hex filename
32-
354776 456 112652 467884 723ac /w/build/baseline.elf
32+
355696 480 112692 468868 72784 /w/build/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 2026-08-16T19:19:27.850000Z 10.0.2.15 solid-syslog-example - BOOT - device started
51-
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
50+
wire <134>1 2026-08-16T19:22:20.850000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1"] device started
51+
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
5252
```
5353

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

5656
```text
57-
OK flash_text: 354784 (expected 354784, Δ0)
58-
OK flash_data: 448 (expected 448, Δ0)
59-
OK static_bss: 112652 (expected 112652, Δ0)
57+
OK flash_text: 355704 (expected 355704, Δ0)
58+
OK flash_data: 472 (expected 472, Δ0)
59+
OK static_bss: 112692 (expected 112692, Δ0)
6060
OK heap_used: 4440 (expected 4440, Δ0)
61-
OK mbedtls_peak: 21292 (expected 21288, Δ4)
62-
OK mbedtls_free: 11476 (expected 11480, Δ4)
61+
OK mbedtls_peak: 21352 (expected 21320, Δ32)
62+
OK mbedtls_free: 11416 (expected 11448, Δ32)
6363
OK lwip_mem_free: 7576 (expected 7576, Δ0)
64-
OK lwip_pbufs_free: 14 (expected 13, Δ1)
64+
OK lwip_pbufs_free: 14 (expected 14, Δ0)
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)