Skip to content

Commit a551645

Browse files
authored
feat: logging (#6)
1 parent 92a9ea3 commit a551645

15 files changed

Lines changed: 645 additions & 48 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ add_custom_target(git_sha_header ALL
6464
# bm_sbc_core – shared stack runtime library (section 3.1)
6565
# ---------------------------------------------------------------------------
6666
add_library(bm_sbc_core STATIC
67+
src/core/bm_log.c
6768
src/core/runtime.cpp
6869
src/core/app_runner.cpp
6970
src/core/pcap_file_sink.cpp

apps/example/app_main.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
///
44
/// Demonstrates the setup()/loop() app contract.
55

6-
#include <cstdio>
6+
#include "bm_log.h"
77

88
void setup(void) {
9-
printf("example app: setup\n");
9+
bm_log_info("example app: setup");
1010
}
1111

1212
void loop(void) {
1313
// Placeholder – application logic goes here.
1414
}
15-

apps/multinode/app_main.cpp

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
/// NEIGHBOR_UP — emitted when a peer is discovered
1111
/// NEIGHBOR_DOWN — emitted when a peer goes offline
1212
/// PUBSUB_RX — emitted when a pub/sub message arrives from a remote node
13-
/// 🏓 — emitted by bm_core/bcmp/ping.c when a ping reply arrives
13+
/// bcmp_seq= — emitted by bm_core/bcmp/ping.c when a ping reply arrives
1414

1515
#include <cinttypes>
1616
#include <cstdio>
1717
#include <cstring>
1818
#include <ctime>
1919

20+
#include "bm_log.h"
21+
2022
// Headers without C++ guards must be wrapped so their symbols have C linkage.
2123
// util.h is included here first so its include guard fires before
2224
// messages/neighbors.h pulls it in outside any extern "C" block.
@@ -51,23 +53,21 @@ static bool s_actions_done = false;
5153
// ---------------------------------------------------------------------------
5254

5355
static void on_neighbor(bool discovered, BcmpNeighbor *neighbor) {
54-
printf("[%016" PRIx64 "] NEIGHBOR_%s node=%016" PRIx64 " port=%u\n",
55-
node_id(),
56-
discovered ? "UP" : "DOWN",
57-
neighbor->node_id,
58-
(unsigned)neighbor->port);
59-
fflush(stdout);
56+
bm_log_info("[%016" PRIx64 "] NEIGHBOR_%s node=%016" PRIx64 " port=%u",
57+
node_id(),
58+
discovered ? "UP" : "DOWN",
59+
neighbor->node_id,
60+
(unsigned)neighbor->port);
6061
}
6162

6263
static void on_pubsub(uint64_t src_node_id, const char *topic,
6364
uint16_t topic_len, const uint8_t *data,
6465
uint16_t data_len, uint8_t /*type*/, uint8_t /*version*/) {
65-
printf("[%016" PRIx64 "] PUBSUB_RX from=%016" PRIx64
66-
" topic=%.*s data=%.*s\n",
67-
node_id(), src_node_id,
68-
(int)topic_len, topic,
69-
(int)data_len, reinterpret_cast<const char *>(data));
70-
fflush(stdout);
66+
bm_log_info("[%016" PRIx64 "] PUBSUB_RX from=%016" PRIx64
67+
" topic=%.*s data=%.*s",
68+
node_id(), src_node_id,
69+
(int)topic_len, topic,
70+
(int)data_len, reinterpret_cast<const char *>(data));
7171
}
7272

7373
// ---------------------------------------------------------------------------
@@ -77,8 +77,7 @@ static void on_pubsub(uint64_t src_node_id, const char *topic,
7777
void setup(void) {
7878
bcmp_neighbor_register_discovery_callback(on_neighbor);
7979
bm_sub(k_topic, on_pubsub);
80-
printf("[%016" PRIx64 "] multinode app: setup\n", node_id());
81-
fflush(stdout);
80+
bm_log_info("[%016" PRIx64 "] multinode app: setup", node_id());
8281
}
8382

8483
void loop(void) {
@@ -100,7 +99,7 @@ void loop(void) {
10099
s_actions_done = true;
101100

102101
// Send a multicast ping — bm_core handles the echo request/reply cycle and
103-
// logs the reply line (🏓 ... bcmp_seq=...) via bm_debug/printf.
102+
// logs the reply line (bcmp_seq=...) via bm_debug/printf.
104103
bcmp_send_ping_request(0, &multicast_global_addr, nullptr, 0);
105104

106105
// Publish a test message on the shared topic. Remote peers that subscribed
@@ -109,7 +108,5 @@ void loop(void) {
109108
static_cast<uint16_t>(strlen(k_payload)),
110109
0, BM_COMMON_PUB_SUB_VERSION);
111110

112-
printf("[%016" PRIx64 "] multinode app: ping + pub sent\n", node_id());
113-
fflush(stdout);
111+
bm_log_info("[%016" PRIx64 "] multinode app: ping + pub sent", node_id());
114112
}
115-

deploy/logrotate.d/bm_sbc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/var/log/bm_sbc/*.log {
2+
daily
3+
rotate 7
4+
compress
5+
delaycompress
6+
missingok
7+
notifempty
8+
copytruncate
9+
maxsize 10M
10+
}

deploy/tmpfiles.d/bm_sbc.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
d /var/log/bm_sbc 0755 root root -

logging-plan.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Logging Plan for bm_sbc
2+
3+
## Context
4+
5+
All bm_sbc logging is currently `printf()` via a `bm_debug()` macro, writing to stdout only. There is no file output, no log rotation, no severity levels, and no structured format. Multiple bm_sbc processes running simultaneously produce interleaved output with no way to separate them unless the caller redirects each to a different file. The goal is proper Linux system logging that works with standard sysadmin tooling.
6+
7+
## OpenTelemetry Evaluation
8+
9+
**Conclusion: adopt OTEL log data model format, do not use OTEL SDK.**
10+
11+
The opentelemetry-cpp SDK is inappropriate for the Pi Zero 2W:
12+
- Pulls protobuf + gRPC/HTTP + abseil (~15 transitive deps, 20-40MB binary bloat)
13+
- Thread pools and batching exporters consume meaningful CPU on the single-core BCM2710A1
14+
- Massive CMake/cross-compile burden for zero runtime benefit at this scale
15+
16+
Instead: emit logs in an OTEL-compatible semi-structured text format. An OTEL Collector with a `filelog` receiver can ingest these logs via regex if OTEL infrastructure is ever deployed. Zero runtime cost — just `snprintf`.
17+
18+
## Log Format
19+
20+
Semi-structured, one line per entry, human-readable and grep-friendly:
21+
22+
```
23+
2024-01-15T10:30:45.123456Z INFO [multinode node=0x0123456789abcdef] stack initialized
24+
2024-01-15T10:30:45.234567Z WARN [multinode node=0x0123456789abcdef] vpd_send: flood peer 3 failed errno=111
25+
```
26+
27+
Layout: `<ISO-8601 UTC timestamp>Z <SEVERITY 5-char padded> [<app_name> node=<0x hex16>] <message>`
28+
29+
Severity levels (mapping to OTEL SeverityNumber ranges):
30+
31+
| Level | Name | Use |
32+
|-------|-------|-----|
33+
| 0 | TRACE | Protocol frame dumps |
34+
| 1 | DEBUG | Neighbor events, pubsub, all existing bm_debug calls |
35+
| 2 | INFO | Startup, shutdown, configuration |
36+
| 3 | WARN | Recoverable errors, truncation warnings |
37+
| 4 | ERROR | Unrecoverable errors, init failures |
38+
| 5 | FATAL | Process-terminating errors |
39+
40+
Rationale for semi-structured over JSON: human-readable when tailing, parseable by grep/awk/OTEL filelog receiver, lower CPU than JSON serialization. JSON can be added later as `--log-format json`.
41+
42+
## Log Destination: Direct File I/O
43+
44+
**Decision: write directly to files in `/var/log/bm_sbc/`, not syslog.**
45+
46+
Why not syslog: `openlog()` ident is limited/static, rsyslog per-process routing needs system config, syslog strips structured fields.
47+
48+
Why direct files: each process gets a unique file (no collision), full format control, logrotate works natively, simple SIGHUP handler for rotation.
49+
50+
### File naming
51+
52+
```
53+
/var/log/bm_sbc/<app_name>_<node_id_hex16>.log
54+
```
55+
56+
Examples: `multinode_0000000000000001.log`, `example_00000000deadbeef.log`
57+
58+
Each process has a unique (app_name, node_id) pair.
59+
60+
### stdout preservation
61+
62+
- Also log to stdout when `BM_SBC_LOG_STDOUT=1` env var is set, or when stdout is a TTY (development)
63+
- Production (non-TTY): file only by default
64+
65+
## Logging API
66+
67+
### New files: `src/core/bm_log.h` and `src/core/bm_log.c`
68+
69+
C-compatible API (bm_core is pure C):
70+
71+
```c
72+
typedef enum {
73+
BM_LOG_TRACE = 0, BM_LOG_DEBUG = 1, BM_LOG_INFO = 2,
74+
BM_LOG_WARN = 3, BM_LOG_ERROR = 4, BM_LOG_FATAL = 5,
75+
} BmLogLevel;
76+
77+
int bm_log_init(const char *app_name, uint64_t node_id,
78+
const char *log_dir, bool also_stdout);
79+
void bm_log_set_level(BmLogLevel level);
80+
void bm_log(BmLogLevel level, const char *fmt, ...) __attribute__((format(printf,2,3)));
81+
void bm_log_reopen(void); // called by SIGHUP handler
82+
void bm_log_shutdown(void);
83+
```
84+
85+
Convenience macros: `bm_log_trace(...)`, `bm_log_debug(...)`, `bm_log_info(...)`, `bm_log_warn(...)`, `bm_log_error(...)`, `bm_log_fatal(...)`.
86+
87+
Compile-time gate: `BM_LOG_MIN_LEVEL` strips lower-severity calls from release builds.
88+
89+
### bm_debug backward compatibility
90+
91+
Single-line change in `src/core/bm_config.h`:
92+
93+
```c
94+
// Before:
95+
#define bm_debug(format, ...) printf(format, ##__VA_ARGS__)
96+
// After:
97+
#include "bm_log.h"
98+
#define bm_debug(format, ...) bm_log_debug(format, ##__VA_ARGS__)
99+
```
100+
101+
This routes all ~225 bm_core `bm_debug()` calls through the new system at DEBUG severity, with zero changes to the bm_core submodule.
102+
103+
## Implementation Details
104+
105+
### bm_log.c internals (~200 lines)
106+
107+
- **Thread safety**: single `pthread_mutex_t` serializes writes. Lock duration ~5-10us (snprintf + fwrite + fflush). At <100 lines/sec, contention is negligible.
108+
- **Timestamp**: `clock_gettime(CLOCK_REALTIME)` (vDSO on Pi, no syscall overhead) + `strftime` + manual microsecond append.
109+
- **Buffer**: stack-local `char buf[1024]` per call. No heap allocation on log path. Messages >~900 chars truncated with `...`.
110+
- **File I/O**: `fwrite()` + `fflush()` per line for immediate visibility.
111+
- **SIGHUP**: handler sets `volatile sig_atomic_t` flag; checked inside mutex section of `bm_log()` to close/reopen file. Async-signal-safe.
112+
- **Pre-init fallback**: if `bm_log()` called before `bm_log_init()`, falls back to `fprintf(stderr, ...)` with no structured prefix. Handles early CLI parsing errors.
113+
- **Directory creation**: `bm_log_init()` attempts `mkdir(log_dir, 0755)` as fallback (may fail without root, in which case logs go to stdout-only with a stderr warning).
114+
115+
## CLI Changes to `runtime.cpp`
116+
117+
New flags:
118+
119+
| Flag | Default | Description |
120+
|------|---------|-------------|
121+
| `--log-dir <path>` | `/var/log/bm_sbc` | Log file directory |
122+
| `--log-level <level>` | `info` | Minimum: trace/debug/info/warn/error/fatal |
123+
| `--log-stdout` | auto (tty=yes) | Also log to stdout |
124+
125+
Env var overrides: `BM_SBC_LOG_DIR`, `BM_SBC_LOG_LEVEL`, `BM_SBC_LOG_STDOUT`.
126+
127+
Init ordering: parse CLI -> `bm_log_init()` -> register SIGHUP -> rest of stack init.
128+
129+
Pre-init `fprintf(stderr, ...)` calls for CLI parsing errors remain as-is (process terminates immediately on those errors; stderr is the correct Unix convention destination).
130+
131+
## Log Rotation
132+
133+
### `deploy/logrotate.d/bm_sbc`
134+
135+
```
136+
/var/log/bm_sbc/*.log {
137+
daily
138+
rotate 7
139+
compress
140+
delaycompress
141+
missingok
142+
notifempty
143+
copytruncate
144+
maxsize 10M
145+
}
146+
```
147+
148+
`copytruncate` works even without SIGHUP. The SIGHUP handler is belt-and-suspenders for users preferring `create` + `postrotate`.
149+
150+
### Directory provisioning: `deploy/tmpfiles.d/bm_sbc.conf`
151+
152+
```
153+
d /var/log/bm_sbc 0755 root root -
154+
```
155+
156+
## Test Script Compatibility
157+
158+
The test script (`scripts/multinode_test.sh`) uses `grep -qF` (fixed-string substring match) on captured stdout. Since the new system only **prepends** a structured prefix to each line, all existing patterns (`"multinode app: setup"`, `"bcmp_seq="`, `"PUBSUB_RX from="`, `"NEIGHBOR_UP node="`, `"vpd: peer count 16 exceeds cap 15"`) remain valid substrings.
159+
160+
Only change: set `BM_SBC_LOG_STDOUT=1` in `start_node()` so test processes also write to stdout (which gets captured to the log file via redirection).
161+
162+
## Files to Create
163+
164+
| File | Purpose |
165+
|------|---------|
166+
| `src/core/bm_log.h` | Public logging API header |
167+
| `src/core/bm_log.c` | Logging implementation |
168+
| `deploy/logrotate.d/bm_sbc` | logrotate config |
169+
| `deploy/tmpfiles.d/bm_sbc.conf` | systemd-tmpfiles log dir creation |
170+
171+
## Files to Modify
172+
173+
| File | Change |
174+
|------|--------|
175+
| `src/core/bm_config.h:10` | Redefine `bm_debug` to route through `bm_log_debug` |
176+
| `src/core/runtime.cpp` | Add `--log-dir/level/stdout` flags, call `bm_log_init()`, register SIGHUP, migrate post-init log calls |
177+
| `src/core/main.cpp` | Add `bm_log_shutdown()` call |
178+
| `src/transports/uart_l2/uart_l2_transport.cpp` | Replace `fprintf(stderr, ...)` with `bm_log_error()`/`bm_log_warn()` |
179+
| `apps/multinode/app_main.cpp` | Replace `printf()`+`fflush()` with `bm_log_info()`/`bm_log_debug()` |
180+
| `apps/example/app_main.cpp` | Replace `printf()` with `bm_log_info()` |
181+
| `CMakeLists.txt:60-71` | Add `src/core/bm_log.c` to `bm_sbc_core` sources |
182+
| `scripts/multinode_test.sh:41-43` | Add `BM_SBC_LOG_STDOUT=1` env var |
183+
184+
## Implementation Sequence
185+
186+
1. Create `bm_log.h` + `bm_log.c`, add to CMakeLists.txt
187+
2. Update `bm_config.h` to redirect `bm_debug` (functionally a no-op until init is called — falls through to stderr fallback)
188+
3. Update `runtime.cpp` — CLI flags, `bm_log_init()`, SIGHUP handler
189+
4. Migrate app files (multinode, example, uart_l2_transport)
190+
5. Update test script with `BM_SBC_LOG_STDOUT=1`
191+
6. Add deploy configs (logrotate, tmpfiles.d)
192+
193+
## Verification
194+
195+
1. `cmake --preset example && cmake --build --preset example` — builds cleanly
196+
2. `cmake -B build -S . -DBUILD_ALL_APPS=ON && cmake --build build` — all apps build
197+
3. Run `./scripts/multinode_test.sh` — all tests pass
198+
4. Manual: run a process, verify log file appears in `/var/log/bm_sbc/` (or `--log-dir /tmp/test_logs`)
199+
5. Manual: run two processes with different node IDs, verify separate log files
200+
6. Manual: verify log lines match the documented format
201+
7. Manual: `kill -HUP <pid>`, verify log file is reopened (rotate test)

scripts/gateway_loopback_test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ start_node() {
4949
local log="$1"; shift
5050
# Launch in its own process group to avoid stray SIGINT.
5151
perl -MPOSIX -e 'setpgrp(0,0); exec @ARGV' -- \
52-
"$BINARY" "$@" >"$log" 2>&1 &
52+
env BM_SBC_LOG_STDOUT=1 BM_SBC_LOG_LEVEL=debug "$BINARY" --log-dir "$WORK/logs" "$@" >"$log" 2>&1 &
5353
echo $!
5454
}
5555

scripts/multinode_test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ check() {
3939
# start_node <log-file> [binary-args...] — launches in background, prints PID
4040
start_node() {
4141
local log="$1"; shift
42-
"$BINARY" "$@" >"$log" 2>&1 &
42+
BM_SBC_LOG_STDOUT=1 BM_SBC_LOG_LEVEL=debug "$BINARY" --log-dir "$WORK/logs" "$@" >"$log" 2>&1 &
4343
echo $!
4444
}
4545

src/core/bm_config.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
extern const char *bm_sbc_app_name_runtime;
88
#define bm_app_name bm_sbc_app_name_runtime
99

10-
#define bm_debug(format, ...) printf(format, ##__VA_ARGS__)
10+
#include "bm_log.h"
11+
#define bm_debug(format, ...) bm_log_debug(format, ##__VA_ARGS__)
1112

1213
// ---------------------------------------------------------------------------
1314
// Compile-time device identity constants used by device_init() in runtime.cpp.

0 commit comments

Comments
 (0)