|
| 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) |
0 commit comments