Skip to content

Commit e6bc53a

Browse files
DavidCozensclaude
andcommitted
feat: send over TCP instead of UDP
A StreamSender over an lwIP TCP stream replaces the UDP sender, and the collector moves to 5601. Records are framed by octet count (RFC 6587), which is what a receiver expects on a stream transport. Flash +7,348 B (+544 on the previous stage) RAM +7,664 B (+180) Log stack +672 B (unchanged) Service +896 B (-56) TCP before TLS is deliberate. It is the smaller step — a stream, a connect and a framing rule, with no certificates in the picture — and it is what a later store-and-forward stage will spool onto. It also completes what the sequenceId started: the transport now detects loss where it happens, while the sequence only reveals it afterwards. The store is still the Null object, so a record whose send fails is reported but not kept. NullStore_Write rejects the record and NullStore_IsTransient says so, which is Service's cue to fall through to a direct send whose result it discards; the delivery-failed event still reaches the handler. The device therefore learns that delivery is failing without yet being able to do anything about it. The flash is small because the baseline already holds a real mTLS session, so lwIP's TCP code was in the image before this stage asked for it. What is charged here is the stream and the sender in front of it, and the 180 bytes of RAM are those two objects in SolidSyslog's static pool. The log stack does not move at all: a task that calls Log is unaffected by the transport underneath it. The service seam's high-water actually fell 56 bytes — this stream's send path measures shallower than the datagram's — but the seam keeps its allocation, so nothing is given back here. The stream takes a Sleep callback because a connect is not instantaneous and the library will not pick a blocking primitive on your behalf; one vTaskDelay is the whole of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c7abcb8 commit e6bc53a

5 files changed

Lines changed: 84 additions & 51 deletions

File tree

README.md

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,49 @@ 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 — Buffered
13+
## This stage — TCP
1414

15-
A `SolidSyslogCircularBuffer` between `SolidSyslog_Log` and the sender, drained by a service task
16-
calling `SolidSyslog_Service`. `SolidSyslog_Log` formats, enqueues and returns; the service task
17-
does the I/O.
15+
UDP to TCP, by putting a `SolidSyslogStreamSender` over an lwIP TCP stream. The network
16+
retransmits rather than dropping, and a send fails when the collector is gone instead of succeeding
17+
into a void. Records are framed by octet count per RFC 6587, which is what a receiver expects on a
18+
stream transport.
1819

1920
```c
20-
static uint8_t s_ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(SYSLOG_BUFFER_RECORDS)];
21-
22-
.Buffer = SolidSyslogCircularBuffer_Create(SolidSyslogFreeRtosMutex_Create(), s_ring, sizeof(s_ring)),
21+
struct SolidSyslogLwipRawTcpStreamConfig tcpConfig = {.Sleep = SyslogSleep};
22+
23+
struct SolidSyslogStreamSenderConfig senderConfig = {
24+
.Resolver = SolidSyslogLwipRawResolver_Create(),
25+
.Stream = SolidSyslogLwipRawTcpStream_Create(&tcpConfig),
26+
.Address = SolidSyslogLwipRawAddress_Create(),
27+
.Endpoint = CollectorEndpoint,
28+
};
29+
struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig);
2330
```
2431

25-
This separates logging an event from sending it. `SolidSyslog_Log` becomes safe to call from any
26-
number of tasks, and cheap enough to call from the place the event actually happens rather than
27-
from somewhere convenient later. Nothing that logs waits on the network.
32+
Taken with the sequence number, this completes the loss story: the transport detects loss where it
33+
happens, and the sequence reveals afterwards anything the transport could not. It is also what
34+
makes the delivery-failed and delivery-restored events from the error-handler stage meaningful.
35+
36+
The store is still the Null object, so a record whose send fails is reported but not kept. The
37+
device learns that delivery is failing without yet being able to do anything about it; retaining
38+
the record is the store stage's job.
2839

29-
**A whole record now passes through both seams,** and the depth follows it. The log task formats
30-
one; the service task drains one and sends it. Sized at the RTOS floor, the service task did not
31-
merely trip its overflow hook — it locked the CPU up, because a frame that large clears the guard
32-
band entirely rather than growing into it. Both seams now hold a record and their onward call, and
33-
both are tightened against measured high-water marks at the end.
40+
TCP before TLS is deliberate. It is the smaller step — a stream, a connect and a framing rule, with
41+
no certificates in the picture — and it is what a later store-and-forward stage will spool onto.
3442

35-
The mutex is what makes the enqueue and drain sides safe on different tasks, and it comes from the
36-
RTOS — which is what brings the `FreeRtos` platform into the build. A single-task device injects
37-
`SolidSyslogNullMutex_Get()` instead and pays nothing.
43+
The stream takes a `Sleep` callback because a connect is not instantaneous and the library will not
44+
pick a blocking primitive on your behalf; one `vTaskDelay` is the whole of it.
3845

39-
`SolidSyslog_Service` returns a status a device wanting more sophisticated scheduling can drive
40-
from. A loop with a delay is the simplest model that works.
46+
**When you need it.** If the device must know that delivery is failing — to raise an alarm, to fall
47+
back, to start storing. Over UDP it never finds out.
4148

42-
**When you need it.** Once logging and sending are decoupled, the buffer has to absorb however many
43-
events can be logged before it is next serviced. It also makes logging from multiple tasks safe.
49+
> RFC 6587 is Historic, and the IESG recommends TLS over plain TCP for new deployments. Plain TCP
50+
> is here for collectors you do not control, and as the step a later storage stage will spool onto,
51+
> before cryptography arrives.
4452
4553
<!-- STAGE-COST:START (generated by scripts/gen-cost-table.py — do not edit by hand) -->
4654

47-
**Cost above baseline: Flash +6,804 B, RAM +7,484 B.**
55+
**Cost above baseline: Flash +7,348 B, RAM +7,664 B.**
4856

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

@@ -65,6 +73,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage.
6573
| Header fields | a timestamped record naming the device, instead of nil values | +5,108 | +1,908 |
6674
| Sequence numbers | every record numbered, so a gap in the sequence is visible | +6,052 | +1,972 |
6775
| Buffered | logging that returns immediately, with the send moved off the logging task | +6,804 | +7,484 |
76+
| TCP | records the network retransmits instead of dropping, and a send that fails when the collector is gone | +7,348 | +7,664 |
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: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* See Syslog.h.
22
*
3-
* A UDP sender over lwIP behind a circular buffer: Log enqueues and returns, and
3+
* A TCP stream over lwIP behind a circular buffer: Log enqueues and returns, and
44
* the service task drains and sends. The mutex is what makes those two sides
55
* safe on different tasks.
66
*
@@ -15,17 +15,20 @@
1515
#include "SolidSyslogEndpointHost.h"
1616
#include "SolidSyslogFreeRtosMutex.h"
1717
#include "SolidSyslogLwipRawAddress.h"
18-
#include "SolidSyslogLwipRawDatagram.h"
1918
#include "SolidSyslogLwipRawMarshal.h"
2019
#include "SolidSyslogLwipRawResolver.h"
20+
#include "SolidSyslogLwipRawTcpStream.h"
2121
#include "SolidSyslogMetaSd.h"
2222
#include "SolidSyslogNullStore.h"
2323
#include "SolidSyslogStdAtomicCounter.h"
24-
#include "SolidSyslogUdpSender.h"
24+
#include "SolidSyslogStreamSender.h"
2525
#include "SyslogFields.h"
2626

2727
#include "lwip/tcpip.h"
2828

29+
#include "FreeRTOS.h"
30+
#include "task.h"
31+
2932
#include <stddef.h>
3033
#include <stdint.h>
3134
#include <string.h>
@@ -34,7 +37,7 @@
3437
* the resolver numeric-only — no DNS, so no LWIP_DNS and no DNS resolver
3538
* component to compile. */
3639
#define SYSLOG_COLLECTOR_HOST "10.0.2.2"
37-
#define SYSLOG_COLLECTOR_PORT ((uint16_t) 5514U)
40+
#define SYSLOG_COLLECTOR_PORT ((uint16_t) 5601U)
3841

3942
/* Depth enough to absorb a burst while the sender is busy, without sizing for a
4043
* backlog the store is there to hold. */
@@ -46,7 +49,13 @@ static uint8_t s_ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(SYSLOG_BUFFER_RECOR
4649
/* The logger reads these on every record, so they outlive Syslog_Start. */
4750
static struct SolidSyslogStructuredData* s_sd[1];
4851

49-
/* Every lwIP Raw call the datagram makes has to happen on the thread that owns
52+
/* Bounds the connect spin so it yields instead of busy-waiting. */
53+
static void SyslogSleep(int milliseconds)
54+
{
55+
vTaskDelay(pdMS_TO_TICKS(milliseconds));
56+
}
57+
58+
/* Every lwIP Raw call the stream makes has to happen on the thread that owns
5059
* the lwIP core. lwipopts.h sets LWIP_TCPIP_CORE_LOCKING, so taking the core
5160
* lock in the caller's own task is simpler and cheaper than posting to the tcpip
5261
* mailbox — and unconditionally synchronous, which the marshal contract
@@ -73,16 +82,17 @@ void Syslog_Start(void)
7382
{
7483
SolidSyslogLwipRaw_SetMarshal(LwipCoreLockMarshal);
7584

76-
/* A numeric resolver to parse the literal, a datagram for the socket, and an
77-
* address slot for the resolver to write into. No EndpointVersion — this
78-
* collector never moves, so the sender resolves once and pins it. */
79-
struct SolidSyslogUdpSenderConfig senderConfig = {
85+
struct SolidSyslogLwipRawTcpStreamConfig tcpConfig = {.Sleep = SyslogSleep};
86+
87+
/* No EndpointVersion — this collector never moves, so the sender resolves
88+
* once and pins it. */
89+
struct SolidSyslogStreamSenderConfig senderConfig = {
8090
.Resolver = SolidSyslogLwipRawResolver_Create(),
81-
.Datagram = SolidSyslogLwipRawDatagram_Create(),
91+
.Stream = SolidSyslogLwipRawTcpStream_Create(&tcpConfig),
8292
.Address = SolidSyslogLwipRawAddress_Create(),
8393
.Endpoint = CollectorEndpoint,
8494
};
85-
struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig);
95+
struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig);
8696

8797
/* One counter Increment per record formatted, so a record that never reaches
8898
* the collector leaves a gap in the sequence rather than no trace at all. */

measurements/stages.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ 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
1818
sequence-id Sequence numbers every record numbered, so a gap in the sequence is visible
1919
buffered Buffered logging that returns immediately, with the send moved off the logging task
20+
tcp TCP records the network retransmits instead of dropping, and a send that fails when the collector is gone

measurements/tcp.csv

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# tcp 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,356984
4+
flash_data,488
5+
static_bss,118368
6+
heap_used,4440
7+
mbedtls_peak,21288
8+
mbedtls_free,11480
9+
lwip_mem_free,7576
10+
lwip_pbufs_free,13
11+
stack_log,792
12+
stack_service,948
13+
stack_harness,2848

run-report.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# solid-syslog-example — run (buffered)
1+
# solid-syslog-example — run (tcp)
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,356440,349808,6632
13+
[report] flash_text,356984,349808,7176
1414
[report] flash_data,488,316,172
15-
[report] static_bss,118188,110876,7312
15+
[report] static_bss,118368,110876,7492
1616
[report] heap_used,4440,4440,0
17-
[report] mbedtls_peak,21236,21328,-92
18-
[report] mbedtls_free,11532,11440,92
17+
[report] mbedtls_peak,21328,21328,0
18+
[report] mbedtls_free,11440,11440,0
1919
[report] lwip_mem_free,7576,7576,0
20-
[report] lwip_pbufs_free,13,13,0
20+
[report] lwip_pbufs_free,14,13,1
2121
[report] stack_log,792,120,672
22-
[report] stack_service,1004,52,952
22+
[report] stack_service,948,52,896
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-
356432 496 118188 475116 73fec /w/build/baseline.elf
32+
356976 496 118368 475840 742c0 /w/build/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-16T19:29:06.310000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1"] device started
51-
parsed PRIORITY=134 TIMESTAMP=2026-08-16T19:29:06+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1"] MSG=device started
50+
wire <134>1 2026-08-16T19:31:24.850000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1"] device started
51+
parsed PRIORITY=134 TIMESTAMP=2026-08-16T19:31:24+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/buffered.csv)
54+
## Self-check (vs measurements/tcp.csv)
5555

5656
```text
57-
OK flash_text: 356440 (expected 356440, Δ0)
57+
OK flash_text: 356984 (expected 356984, Δ0)
5858
OK flash_data: 488 (expected 488, Δ0)
59-
OK static_bss: 118188 (expected 118188, Δ0)
59+
OK static_bss: 118368 (expected 118368, Δ0)
6060
OK heap_used: 4440 (expected 4440, Δ0)
61-
OK mbedtls_peak: 21236 (expected 21332, Δ96)
62-
OK mbedtls_free: 11532 (expected 11436, Δ96)
61+
OK mbedtls_peak: 21328 (expected 21288, Δ40)
62+
OK mbedtls_free: 11440 (expected 11480, Δ40)
6363
OK lwip_mem_free: 7576 (expected 7576, Δ0)
64-
OK lwip_pbufs_free: 13 (expected 13, Δ0)
64+
OK lwip_pbufs_free: 14 (expected 13, Δ1)
6565
OK stack_log: 792 (expected 792, Δ0)
66-
OK stack_service: 1004 (expected 1004, Δ0)
66+
OK stack_service: 948 (expected 948, Δ0)
6767
OK stack_harness: 2848 (expected 2848, Δ0)
6868
```
6969

0 commit comments

Comments
 (0)