From f707de50c4695f8d09bb91216749a283cf067282 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 6 Jun 2026 15:03:05 +0000 Subject: [PATCH 1/3] feat: S14.10 worked custom SD + send-custom command (cross-platform) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add BddTargetCustomSd (Bdd/Targets/Common) — a minimal worked custom SD-ELEMENT emitting [example@32473 detail="..."], the detail value containing ", \ and ] so the upcoming oracle round-trip BDD can prove the library applies RFC 5424 PARAM-VALUE escaping. Wire a 'send-custom' interactive command that emits it via SolidSyslog_LogWithSd, so all four BDD targets (Linux/Windows/FreeRtos/ FreeRtosLwip) expose it from the one shared Common runner. Registered in every target build + BddTargetTests. Unit tests: the SD's escaped output and the command dispatch/count. Co-Authored-By: Claude Opus 4.8 (1M context) --- Bdd/Targets/CMakeLists.txt | 2 + Bdd/Targets/Common/BddTargetCustomSd.c | 27 +++++++++++++ Bdd/Targets/Common/BddTargetCustomSd.h | 19 +++++++++ Bdd/Targets/Common/BddTargetInteractive.c | 20 +++++++++- Bdd/Targets/FreeRtos/CMakeLists.txt | 1 + Bdd/Targets/FreeRtosLwip/CMakeLists.txt | 1 + Tests/Bdd/Targets/BddTargetCustomSdTest.cpp | 39 +++++++++++++++++++ .../Bdd/Targets/BddTargetInteractiveTest.cpp | 7 ++++ Tests/Bdd/Targets/CMakeLists.txt | 2 + 9 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 Bdd/Targets/Common/BddTargetCustomSd.c create mode 100644 Bdd/Targets/Common/BddTargetCustomSd.h create mode 100644 Tests/Bdd/Targets/BddTargetCustomSdTest.cpp diff --git a/Bdd/Targets/CMakeLists.txt b/Bdd/Targets/CMakeLists.txt index f48e7070..6a0dcd18 100644 --- a/Bdd/Targets/CMakeLists.txt +++ b/Bdd/Targets/CMakeLists.txt @@ -19,6 +19,7 @@ if(SOLIDSYSLOG_POSIX) Linux/BddTargetUdpConfig.c Common/BddTargetAppName.c Common/BddTargetErrorText.c + Common/BddTargetCustomSd.c Common/BddTargetInteractive.c Common/BddTargetIps.c Common/BddTargetLanguage.c @@ -54,6 +55,7 @@ elseif(SOLIDSYSLOG_WINSOCK AND HAVE_WINDOWS_PLATFORM) Windows/BddTargetWindowsCommandLine.c Common/BddTargetAppName.c Common/BddTargetErrorText.c + Common/BddTargetCustomSd.c Common/BddTargetInteractive.c Common/BddTargetIps.c Common/BddTargetLanguage.c diff --git a/Bdd/Targets/Common/BddTargetCustomSd.c b/Bdd/Targets/Common/BddTargetCustomSd.c new file mode 100644 index 00000000..53a7ffac --- /dev/null +++ b/Bdd/Targets/Common/BddTargetCustomSd.c @@ -0,0 +1,27 @@ +#include "BddTargetCustomSd.h" + +#include "SolidSyslogSdElement.h" +#include "SolidSyslogSdValue.h" +#include "SolidSyslogStructuredDataDefinition.h" + +/* IANA-reserved "example" Private Enterprise Number — safe for documentation. */ +enum +{ + EXAMPLE_ENTERPRISE_NUMBER = 32473U +}; + +static void CustomSd_Format(struct SolidSyslogStructuredData* base, struct SolidSyslogSdElement* element) +{ + (void) base; /* Stateless: this example emits a fixed element. */ + + SolidSyslogSdElement_Begin(element, "example", EXAMPLE_ENTERPRISE_NUMBER); + SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "detail"), "a\"b\\c]d"); + SolidSyslogSdElement_End(element); +} + +static struct SolidSyslogStructuredData customSd = {CustomSd_Format}; + +struct SolidSyslogStructuredData* BddTargetCustomSd_Get(void) +{ + return &customSd; +} diff --git a/Bdd/Targets/Common/BddTargetCustomSd.h b/Bdd/Targets/Common/BddTargetCustomSd.h new file mode 100644 index 00000000..646d1d55 --- /dev/null +++ b/Bdd/Targets/Common/BddTargetCustomSd.h @@ -0,0 +1,19 @@ +#ifndef BDDTARGETCUSTOMSD_H +#define BDDTARGETCUSTOMSD_H + +#include "ExternC.h" + +EXTERN_C_BEGIN + + struct SolidSyslogStructuredData; + + /* The worked custom SD-ELEMENT for the integrator guide + * (docs/structured-data.md). Emits [example@32473 detail="..."]. The detail + * value deliberately contains ", \ and ] so the oracle round-trip BDD can + * prove the library applies RFC 5424 PARAM-VALUE escaping. Stateless + * singleton — handed to SolidSyslog_LogWithSd by the `send-custom` command. */ + struct SolidSyslogStructuredData* BddTargetCustomSd_Get(void); + +EXTERN_C_END + +#endif /* BDDTARGETCUSTOMSD_H */ diff --git a/Bdd/Targets/Common/BddTargetInteractive.c b/Bdd/Targets/Common/BddTargetInteractive.c index bf6b48df..e215998e 100644 --- a/Bdd/Targets/Common/BddTargetInteractive.c +++ b/Bdd/Targets/Common/BddTargetInteractive.c @@ -5,6 +5,7 @@ #include #include +#include "BddTargetCustomSd.h" #include "SolidSyslog.h" #include "SolidSyslogTunables.h" @@ -74,6 +75,19 @@ static void HandleSend(struct SolidSyslog* handle, const char* args, const struc printf("Sent %d message%s\n", count, (count == 1) ? "" : "s"); } +static void HandleSendCustom(struct SolidSyslog* handle, const char* args, const struct SolidSyslogMessage* message) +{ + int count = ParseCount(args); + struct SolidSyslogStructuredData* sd[] = {BddTargetCustomSd_Get()}; + + for (int i = 0; i < count; i++) + { + SolidSyslog_LogWithSd(handle, message, sd, 1); + } + + printf("Sent %d custom message%s\n", count, (count == 1) ? "" : "s"); +} + static void HandleSet(const char* args, BddTargetInteractiveSetHandler onSet) { char name[MAX_LINE_LENGTH]; @@ -120,7 +134,11 @@ void BddTargetInteractive_Run( } const char* args = NULL; - if (MatchCommand(line, "send", &args)) + if (MatchCommand(line, "send-custom", &args)) + { + HandleSendCustom(handle, args, message); + } + else if (MatchCommand(line, "send", &args)) { HandleSend(handle, args, message); } diff --git a/Bdd/Targets/FreeRtos/CMakeLists.txt b/Bdd/Targets/FreeRtos/CMakeLists.txt index 8b0b4b59..79fd4edb 100644 --- a/Bdd/Targets/FreeRtos/CMakeLists.txt +++ b/Bdd/Targets/FreeRtos/CMakeLists.txt @@ -287,6 +287,7 @@ add_executable(SolidSyslogBddTarget ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetCustomSd.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c diff --git a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt index 302b8cd5..ff0041c5 100644 --- a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt +++ b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt @@ -253,6 +253,7 @@ add_executable(SolidSyslogBddTargetLwip ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFatFsMount.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetCustomSd.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c diff --git a/Tests/Bdd/Targets/BddTargetCustomSdTest.cpp b/Tests/Bdd/Targets/BddTargetCustomSdTest.cpp new file mode 100644 index 00000000..f0ffcfe3 --- /dev/null +++ b/Tests/Bdd/Targets/BddTargetCustomSdTest.cpp @@ -0,0 +1,39 @@ +#include "BddTargetCustomSd.h" +#include "SolidSyslogFormatter.h" +#include "SolidSyslogSdElement.h" +#include "SolidSyslogSdElementPrivate.h" +#include "SolidSyslogStructuredData.h" + +#include "CppUTest/TestHarness.h" + +enum +{ + FORMATTER_BUFFER_SIZE = 128 +}; + +// clang-format off +TEST_GROUP(BddTargetCustomSd) +{ + SolidSyslogFormatterStorage storage[SOLIDSYSLOG_FORMATTER_STORAGE_SIZE(FORMATTER_BUFFER_SIZE)]; + struct SolidSyslogFormatter* formatter = nullptr; + struct SolidSyslogSdElement element{}; + + void setup() override + { + formatter = SolidSyslogFormatter_Create(storage, FORMATTER_BUFFER_SIZE); + SolidSyslogSdElement_FromFormatter(&element, formatter); + } + + [[nodiscard]] const char* formatted() const + { + return SolidSyslogFormatter_AsFormattedBuffer(formatter); + } +}; + +// clang-format on + +TEST(BddTargetCustomSd, EmitsExampleElementWithLibraryEscapedValue) +{ + SolidSyslogStructuredData_Format(BddTargetCustomSd_Get(), &element); + STRCMP_EQUAL("[example@32473 detail=\"a\\\"b\\\\c\\]d\"]", formatted()); +} diff --git a/Tests/Bdd/Targets/BddTargetInteractiveTest.cpp b/Tests/Bdd/Targets/BddTargetInteractiveTest.cpp index baa195d4..0cce3e3d 100644 --- a/Tests/Bdd/Targets/BddTargetInteractiveTest.cpp +++ b/Tests/Bdd/Targets/BddTargetInteractiveTest.cpp @@ -167,6 +167,13 @@ TEST(BddTargetInteractive, SetCommandPrintsInvalidOnHandlerFailure) CHECK(captured.find("set: invalid") != std::string::npos); } +TEST(BddTargetInteractive, SendCustomCommandIsDispatchedAndCounted) +{ + std::string captured = RunCapturingStdout("send-custom 2\nquit\n", RecordSet); + + CHECK(captured.find("Sent 2 custom messages") != std::string::npos); +} + /* Test list (ZOMBIES order): * Z [x] SetHandlerNotCalledWithQuitOnly * O [x] SetCommandCallsHandlerOnce diff --git a/Tests/Bdd/Targets/CMakeLists.txt b/Tests/Bdd/Targets/CMakeLists.txt index 62203af2..90212f67 100644 --- a/Tests/Bdd/Targets/CMakeLists.txt +++ b/Tests/Bdd/Targets/CMakeLists.txt @@ -1,6 +1,7 @@ # Platform-agnostic tests always included. set(BDD_TARGET_TEST_SOURCES BddTargetAppNameTest.cpp + BddTargetCustomSdTest.cpp BddTargetErrorTextTest.cpp BddTargetIpsTest.cpp BddTargetLanguageTest.cpp @@ -9,6 +10,7 @@ set(BDD_TARGET_TEST_SOURCES set(BDD_TARGET_PRODUCTION_SOURCES ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetAppName.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetCustomSd.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c From baf5c6130ba96c531552c3a93c7409bffc0b29d3 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 6 Jun 2026 15:17:18 +0000 Subject: [PATCH 2/3] docs: S14.10 custom-SD integrator guide + round-trip feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Author docs/structured-data.md — the custom-SD integrator guide: the two-type writer model (SdElement + SdValue), implementing a Format vtable with the embed-base downcast for stateful data, SD-IDs / enterprise numbers, registering via Config.Sd[] and SolidSyslog_LogWithSd (with the reentrancy note), and what the library owns vs the integrator. Written around the real BddTargetCustomSd. Simplify the worked custom SD to detail="Hello World" (per review — a clear example, not muddied by escaped data). Add custom_sd.feature + a send-custom/detail step pair (run_example gains a command arg) proving the custom element round-trips through the oracle. Per David: the dedicated escape round-trip oracle BDD is dropped — value escaping is already covered by the SdValue unit tests, and the syslog-ng/OTEL oracles re-render an escaped value differently, so an oracle assertion there tests the oracle, not the library. Co-Authored-By: Claude Opus 4.8 (1M context) --- Bdd/Targets/Common/BddTargetCustomSd.c | 2 +- Bdd/Targets/Common/BddTargetCustomSd.h | 7 +- Bdd/features/custom_sd.feature | 10 ++ Bdd/features/steps/syslog_steps.py | 24 +++- Tests/Bdd/Targets/BddTargetCustomSdTest.cpp | 4 +- docs/structured-data.md | 151 ++++++++++++++++++++ 6 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 Bdd/features/custom_sd.feature create mode 100644 docs/structured-data.md diff --git a/Bdd/Targets/Common/BddTargetCustomSd.c b/Bdd/Targets/Common/BddTargetCustomSd.c index 53a7ffac..7ee4120a 100644 --- a/Bdd/Targets/Common/BddTargetCustomSd.c +++ b/Bdd/Targets/Common/BddTargetCustomSd.c @@ -15,7 +15,7 @@ static void CustomSd_Format(struct SolidSyslogStructuredData* base, struct Solid (void) base; /* Stateless: this example emits a fixed element. */ SolidSyslogSdElement_Begin(element, "example", EXAMPLE_ENTERPRISE_NUMBER); - SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "detail"), "a\"b\\c]d"); + SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "detail"), "Hello World"); SolidSyslogSdElement_End(element); } diff --git a/Bdd/Targets/Common/BddTargetCustomSd.h b/Bdd/Targets/Common/BddTargetCustomSd.h index 646d1d55..0ba094bd 100644 --- a/Bdd/Targets/Common/BddTargetCustomSd.h +++ b/Bdd/Targets/Common/BddTargetCustomSd.h @@ -8,10 +8,9 @@ EXTERN_C_BEGIN struct SolidSyslogStructuredData; /* The worked custom SD-ELEMENT for the integrator guide - * (docs/structured-data.md). Emits [example@32473 detail="..."]. The detail - * value deliberately contains ", \ and ] so the oracle round-trip BDD can - * prove the library applies RFC 5424 PARAM-VALUE escaping. Stateless - * singleton — handed to SolidSyslog_LogWithSd by the `send-custom` command. */ + * (docs/structured-data.md). Emits [example@32473 detail="Hello World"]. + * Stateless singleton — handed to SolidSyslog_LogWithSd by the + * `send-custom` interactive command. */ struct SolidSyslogStructuredData* BddTargetCustomSd_Get(void); EXTERN_C_END diff --git a/Bdd/features/custom_sd.feature b/Bdd/features/custom_sd.feature new file mode 100644 index 00000000..773eb468 --- /dev/null +++ b/Bdd/features/custom_sd.feature @@ -0,0 +1,10 @@ +@udp +Feature: Custom structured data — caller-supplied SD-ELEMENT + An integrator can attach a custom SD-ELEMENT to a single log call via + SolidSyslog_LogWithSd. The library frames it per RFC 5424 §6.3 and the + element reaches the receiver alongside the per-instance SDs. + + Scenario: A custom SD-ELEMENT reaches the oracle + Given the syslog oracle is running + When the BDD target sends a custom syslog message + Then the structured data contains detail "Hello World" diff --git a/Bdd/features/steps/syslog_steps.py b/Bdd/features/steps/syslog_steps.py index 4d3a935b..a7b5afea 100644 --- a/Bdd/features/steps/syslog_steps.py +++ b/Bdd/features/steps/syslog_steps.py @@ -404,7 +404,7 @@ def wait_for_messages(context, expected_messages): context.message_count = len(context.all_lines) -def run_example(context, extra_args=None, expected_messages=1): +def run_example(context, extra_args=None, expected_messages=1, command="send"): """Run the BDD target via the prompt protocol. Every supported runner ships a single buffered binary now — Linux @@ -433,7 +433,7 @@ def run_example(context, extra_args=None, expected_messages=1): try: wait_for_prompt(process) apply_extra_args(context, process, extra_args) - send_command(process, f"send {expected_messages}") + send_command(process, f"{command} {expected_messages}") wait_for_messages(context, expected_messages) returncode = stop_example_process(process, context.target) @@ -804,6 +804,11 @@ def step_bdd_target_sends_message(context): run_example(context) +@when("the BDD target sends a custom syslog message") +def step_bdd_target_sends_custom_message(context): + run_example(context, command="send-custom") + + @when("the BDD target sends a syslog message with transport {transport}") def step_bdd_target_sends_with_transport(context, transport): run_example(context, ["--transport", transport]) @@ -1116,6 +1121,21 @@ def step_check_language(context, value): ) +@then('the structured data contains detail "{value}"') +def step_check_detail(context, value): + sd = context.fields.get("STRUCTURED_DATA", "") + # Escape-aware like the language matcher (RFC 5424 §6.3.3), so the assertion + # holds whether the oracle re-renders the value escaped or decoded. + match = re.search(r'detail="((?:\\.|[^"])*)"', sd) + assert match, ( + f"No detail found in structured data: {sd}" + ) + actual = re.sub(r"\\(.)", r"\1", match.group(1)) + assert actual == value, ( + f"Expected detail {value}, got {actual}" + ) + + @then("the structured data contains sysUpTime as a decimal integer") def step_check_sys_up_time_shape(context): sd = context.fields.get("STRUCTURED_DATA", "") diff --git a/Tests/Bdd/Targets/BddTargetCustomSdTest.cpp b/Tests/Bdd/Targets/BddTargetCustomSdTest.cpp index f0ffcfe3..678c10a6 100644 --- a/Tests/Bdd/Targets/BddTargetCustomSdTest.cpp +++ b/Tests/Bdd/Targets/BddTargetCustomSdTest.cpp @@ -32,8 +32,8 @@ TEST_GROUP(BddTargetCustomSd) // clang-format on -TEST(BddTargetCustomSd, EmitsExampleElementWithLibraryEscapedValue) +TEST(BddTargetCustomSd, EmitsTheExampleElement) { SolidSyslogStructuredData_Format(BddTargetCustomSd_Get(), &element); - STRCMP_EQUAL("[example@32473 detail=\"a\\\"b\\\\c\\]d\"]", formatted()); + STRCMP_EQUAL("[example@32473 detail=\"Hello World\"]", formatted()); } diff --git a/docs/structured-data.md b/docs/structured-data.md new file mode 100644 index 00000000..7cd17148 --- /dev/null +++ b/docs/structured-data.md @@ -0,0 +1,151 @@ +# Authoring custom structured data + +RFC 5424 lets a message carry **structured data** — one or more `SD-ELEMENT`s, each a +named bag of `PARAM="value"` pairs: + +``` +[example@32473 detail="Hello World"] +``` + +SolidSyslog ships the three standard elements (`meta`, `timeQuality`, `origin`). This guide +is for adding **your own** element — an access-control event, a device reading, a config +change — with the library's safe authoring API. + +You write only the *content*. The library owns the *framing*: the brackets, the `SD-ID` +spelling, the `PARAM-NAME="..."` punctuation, and the RFC 5424 §6.3.3 value escaping. A +custom element therefore cannot produce malformed structured data, and a value can never +break out of its quotes — whatever bytes you hand it stay inside the `"..."`. + +The complete worked example below lives at +[`Bdd/Targets/Common/BddTargetCustomSd.c`](../Bdd/Targets/Common/BddTargetCustomSd.c). + +## The two writer types + +Authoring uses two opaque, stack-transient writers — you never touch a raw buffer: + +| Type | You get it from | What it does | +|---|---|---| +| `SolidSyslogSdElement` | handed to your `Format` callback | Opens/closes one element: `_Begin(name, enterprise)`, `_Param(name)`, `_End`. Owns the `SD-ID` and `PARAM-NAME` syntax. | +| `SolidSyslogSdValue` | `SolidSyslogSdElement_Param(...)` returns one | Writes one `PARAM` value: `_String`, `_BoundedString`, `_Uint32`. Applies the §6.3.3 escaping. | + +A value producer is only ever handed a `SolidSyslogSdValue*` — it physically cannot open a +parameter or reach the framing. + +## Implementing a custom SD + +A custom SD is a `SolidSyslogStructuredData` — a one-function vtable. Implement `Format`, +which the library calls with an `SD-ELEMENT` writer when it builds a message: + +```c +#include "SolidSyslogSdElement.h" +#include "SolidSyslogSdValue.h" +#include "SolidSyslogStructuredDataDefinition.h" + +static void ExampleSd_Format(struct SolidSyslogStructuredData* base, struct SolidSyslogSdElement* element) +{ + (void) base; /* stateless here — see "Carrying data" below */ + + SolidSyslogSdElement_Begin(element, "example", 32473U); + SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "detail"), "Hello World"); + SolidSyslogSdElement_End(element); +} + +static struct SolidSyslogStructuredData exampleSd = {ExampleSd_Format}; +``` + +`Begin` → `Param`/value → `End`. Emit as many parameters as you like between `Begin` and +`End`; emit more than one element by repeating the `Begin…End` cycle. + +### Carrying instance or per-call data + +`Format` receives the SD object itself as `base`, so a stateful SD reads its data through +`base`. Embed the vtable as the first member and downcast: + +```c +struct ExampleSd +{ + struct SolidSyslogStructuredData base; /* must be first */ + const struct Event* event; /* whatever your element reports */ +}; + +static void ExampleSd_Format(struct SolidSyslogStructuredData* base, struct SolidSyslogSdElement* element) +{ + const struct ExampleSd* self = (const struct ExampleSd*) base; + SolidSyslogSdElement_Begin(element, "example", 32473U); + SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "user"), self->event->User); + SolidSyslogSdElement_End(element); +} +``` + +The library never allocates your SD — it lives in your storage (static, stack, or your own +pool). It only needs to stay valid for the duration of the log call. + +## SD-IDs and enterprise numbers + +`_Begin(name, enterpriseNumber)` builds the `SD-ID`: + +- **Enterprise number `0`** → an IANA-registered name, emitted verbatim: `[meta …]`. +- **Non-zero** → a private `name@number`: `[example@32473 …]`. The number is your + IANA Private Enterprise Number (PEN); `32473` is the reserved example/documentation PEN. + +Register for a PEN through IANA before shipping a private `SD-ID`; until then `32473` is the +correct placeholder. + +Keep your `SD-ID` and `PARAM-NAME`s within RFC 5424 §6.3.2 — printable US-ASCII excluding +`=`, `]`, `"`, and space. You write these as compile-time constants, so this is yours to get +right. The library bounds each name to 32 bytes and a `NULL` name suppresses the element (or +skips that parameter), but an otherwise-valid name is emitted as written. + +## Registering your SD + +There are two ways to attach a custom SD, and they compose — per-instance elements emit +first, then per-message ones. + +**Per-instance — on every message.** Put it in the config `Sd[]` array at create time: + +```c +struct SolidSyslogStructuredData* sds[] = {metaSd, &exampleSd}; +struct SolidSyslogConfig config = { /* … */ .Sd = sds, .SdCount = 2 }; +``` + +**Per-message — on one call.** Pass it to `SolidSyslog_LogWithSd`, which attaches the array +to just that message after the per-instance set: + +```c +struct SolidSyslogStructuredData* sds[] = {&exampleSd}; +SolidSyslog_LogWithSd(handle, &message, sds, 1); +``` + +`SolidSyslog_Log(handle, &message)` is exactly `SolidSyslog_LogWithSd(handle, &message, NULL, 0)`. +A `NULL` array entry is skipped, so you can leave conditional elements out without a +placeholder. + +### Reentrancy + +A stateful SD reads its data through `base` while `Format` runs. Because a log call formats +synchronously, the object only has to be valid across the call — but **don't share one +mutable SD object across threads that log concurrently**: if one thread repoints +`self->event` while another is mid-`Format`, the second thread emits the wrong data. Give +each call site its own SD instance, or keep the object immutable. A stateless SD (like the +first example) is never affected. + +## Values and escaping + +Write values with `SolidSyslogSdValue`: + +- `_String(value, source)` — a NUL-terminated string. +- `_BoundedString(value, source, maxLength)` — at most `maxLength` bytes. +- `_Uint32(value, number)` — decimal digits. + +The library applies the RFC 5424 §6.3.3 escaping for you (`"`, `\`, and `]` are +backslash-escaped) and validates UTF-8 — you pass the raw value and the receiver gets it +back unchanged. Output is bounded by the message buffer, so a value can never overrun it. + +## What the library owns, and what you own + +| The library owns | You own | +|---|---| +| Brackets, `SD-ID`, `PARAM-NAME="..."` punctuation | The element and parameter *names* | +| §6.3.3 value escaping + UTF-8 validation | The parameter *values* | +| Name length bound + `NULL`-name suppression | Valid name characters (§6.3.2) | +| Buffer bounding | Registering the SD (`Config.Sd[]` or `LogWithSd`), and its storage/lifetime | From b208d914a13d679b3a1e9b0c0211b192ed6bc1e3 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 6 Jun 2026 15:18:47 +0000 Subject: [PATCH 3/3] docs: S14.10 DEVLOG entry --- DEVLOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/DEVLOG.md b/DEVLOG.md index ced6197b..d06daf6a 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -14712,3 +14712,41 @@ SolidSyslogMessage"; the per-message Open questions); it was the never-decompose ### Open questions - None outstanding. + +## 2026-06-06 — S14.10: custom-SD integrator guide + worked example (E14 close) + +The user-facing payoff of E14: document and demonstrate authoring a custom SD with the new +safe API. + +### Decisions +- **`docs/structured-data.md`** — the custom-SD integrator guide: two-type writer model + (SdElement frames + names, SdValue escapes values), implementing a `Format` vtable with the + embed-base downcast for stateful data, SD-IDs / enterprise numbers (`@32473`), registering + per-instance (`Config.Sd[]`) **and** per-message (`SolidSyslog_LogWithSd`), the reentrancy + note, and a library-owns-vs-you-own table. Written around the real worked example. +- **Worked custom SD is cross-platform, not Linux-only** (David's steer): `BddTargetCustomSd` + + a `send-custom` command live in `Bdd/Targets/Common`, so all four targets + (Linux/Windows/FreeRtos/FreeRtosLwip) expose it from the one shared `BddTargetInteractive` + runner. `send-custom` emits the element via `SolidSyslog_LogWithSd` — exercising S14.09's + new API end-to-end. +- **Kept the example simple** (David's steer): `detail="Hello World"`, not a value full of + escapables. A clear example beats one muddied by escaped data. +- **Escape round-trip oracle BDD dropped** (David's call): escaping is already covered by the + SdValue unit tests, and syslog-ng vs OTEL re-render an *escaped* value differently — so an + oracle assertion there would be testing the oracle's quirks, not the library. (`Hello World` + round-trips identically on both, so the one `custom_sd.feature` scenario is clean and + cross-platform.) Updates #549's acceptance criteria. +- **Doc accuracy:** `_Begin`/`_Param` only suppress on a NULL name and bound length to 32; + they do not validate the name charset at format time (`=`/`]`/`"` pass through). The guide + says valid names are the integrator's responsibility — not an overstated "library skips + invalid names". + +### Checks +- BddTargetTests 68/68 (custom-SD escaped... → simple element; send-custom dispatch). Linux + target links; `send-custom` smoke-tested (prints "Sent 1 custom message"). clang-format clean. +- BDD target code is Tier-3 (outside the cppcheck-misra file set), so no suppression work. + +### Pending → WSL +- The `custom_sd.feature` oracle round-trip is **authored but not yet run** — `behave` needs + the docker/syslog-ng oracle, which runs from WSL, not the devcontainer. Next step: pull the + branch in WSL, run the `@udp` `custom_sd` scenario, confirm `detail "Hello World"` round-trips.