Skip to content
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,53 @@
clock: they do, and until the device has been told the date the table keeps accumulating
rather than being dropped — a counter over a longer interval is still true, unlike a stale
duration.
- Offline buffer grown from 8 to 16 slots in both examples, and documented how to size
`slots`: `sentry_storage_nvs()` already accepted any count from 1 to `SENTRY_NVS_MAX_SLOTS`
(64), a smaller firmware should pass a smaller one. Still one shared ring across every
envelope type with no priority tier — the fix for a high-volume category evicting
something that mattered more is a transport that keeps delivering, not triage over whose
envelope keeps its slot.
- Logs: `sentry_log()` / `sentry::log()`, a fixed-size ring of `SENTRY_MICRO_MAX_LOGS`
console lines emitted as a `log` envelope item. Unlike a metric, each line remembers
whatever trace was active when it was recorded rather than whatever is active at flush
time — a line written during a real operation stays attached to it, the same way a
breadcrumb would. A line recorded while idle is still held and sent, just without that
attachment; logging the console is the point even when nothing else is going on.
- Each log line tracks its own truncation from `vsnprintf()`'s actual return value rather
than predicting it at compile time — exact instead of a conservative worst-case bound, and
it works for a runtime format string too. Surfaced as a persistent
`sentry_logs_truncated_count()` and a per-line `t7d` attribute, present only when true.
`sentry_logs_dropped_count()` persists since `sentry_init()` rather than resetting every
flush, matching `sentry_metrics_dropped_count()`'s own contract.
- A full ring of realistic-length log lines did not fit in
`SENTRY_MICRO_ENVELOPE_BUFFER_BYTES`, even before the truncation attribute existed —
`flush_logs()` would have silently refused to send and left the ring stuck indefinitely.
Fixed by abbreviating attribute keys (`truncated` → `t7d`, `device_id` → `d_id`), making
both keys — and the `attributes` object itself — conditional, and lowering
`SENTRY_MICRO_MAX_LOGS` from 8 to 6.
- `wifi_basic` now traces the WiFi connect attempt and a deliberate demo crash
(`-D SENTRY_DEMO_CRASH=1`), with `sentry_log()` calls riding both: one line recorded
before any trace exists this boot carries no `trace_id`, one recorded during the
wifi-connect transaction does. The crash-demo transaction is deliberately never
finished — the trace it leaves active is what `sentry_event_attach_coredump()` joins the
recovered crash event to on the next boot.
- `wifi_basic`'s periodic flush in `loop()` was gated on `sentry_buffered_count() > 0`,
which only tracks the offline retry buffer. Metrics and logs accumulate independently of
that buffer and were never actually being flushed in the common case of nothing ever
landing in it — a pre-existing gap since the buffering example was written, unnoticed
until logs needed the ring to ever go out. Flush is no longer gated on it.
- Holding a live `sentry::Transaction` open across a WiFi/TLS send overflows Arduino's
default 8 KB loop task stack, confirmed on real hardware.
`-D CONFIG_ARDUINO_LOOP_STACK_SIZE=<n>` looks like the fix but does nothing: `sdkconfig.h`
redefines that macro unconditionally after the command line and wins. `wifi_basic` now
overrides Arduino-ESP32's own `getArduinoLoopTaskStackSize()` `weak` hook instead, raising
the stack to 16 KB.
- `SENTRY_MICRO_LOGS_ENABLED=0` and `SENTRY_MICRO_METRICS_ENABLED=0` remove the log ring and
metrics table entirely, the same pattern `SENTRY_MICRO_WIFI_TLS=0` already uses for the
TLS branch. Unlike a transaction's spans, both tables are permanent `g_state` fields —
a metric or a log line has to survive across flushes rather than living on a caller's
stack for one operation — so they cost RAM whether or not firmware ever calls them, with
no way before this to get it back. Measured on `esp32dev`: 832 B RAM / 1,496 B flash for
logs, 272 B RAM / 1,140 B flash for metrics, on a build that never calls either. Disabled
functions are not declared at all rather than becoming no-ops, so a build that turns a
feature off and still calls it fails to compile instead of silently doing nothing.
82 changes: 81 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ matters more here than on a desktop: the most valuable event this SDK produces
of the crash that just happened — is built at boot, *before* the radio has associated.

```cpp
sentry_enable_buffering(sentry_storage_nvs(8)); // or storage_fs(), below
sentry_enable_buffering(sentry_storage_nvs(16)); // or storage_fs(), below
...
void loop() {
if (sentry_buffered_count() > 0) sentry_flush(2); // on an interval, not every pass
Expand All @@ -407,6 +407,17 @@ space, and the filesystem backend never calls `begin()` — a reporter that refo
partition of user data to report a crash would be worse than the crash. Everything is
confined to a `sentry` namespace / `/sentry` directory.

**Sizing `slots`** is a flash budget question, not a correctness one: `sentry_storage_nvs()`
accepts any count from 1 up to `SENTRY_NVS_MAX_SLOTS` (64) and rejects anything outside that
range rather than clamping it. An envelope runs roughly 1 KB, so slots × 1 KB is what you are
spending against whatever partition you gave the buffer — the stock 20 KB `nvs` partition
makes 16 a comfortable ceiling before NVS has no room left for anything else you keep there.
Weigh that against how long the device is realistically offline at a stretch: more slots
survive a longer outage, at the cost of the flash they occupy whether or not they are ever
used. This is one ring shared by every envelope type with no priority between them, so a
size chosen too small is what an unrelated high-volume category — Application Metrics
today, see below — can evict a crash report from.

Writing your own is five functions (`write`, `read`, `erase`, `load_meta`, `save_meta`) —
the same vtable pattern as transports, which is what lets the ring logic be host-tested
against a plain array.
Expand Down Expand Up @@ -747,6 +758,14 @@ reports it.
Integers only, because printf's float support is an opt-in linker flag on this target that
firmware routinely leaves off.

**The table costs 272 bytes of permanent RAM whether or not you ever call these** — unlike a
transaction's spans, a metric has to survive across flushes rather than living on a caller's
stack for one operation, so it is a permanent `g_state` field the same way the log ring
below is. `SENTRY_MICRO_METRICS_ENABLED=0` removes it, along with `sentry_metric_count()` /
`sentry_metric_gauge()` / `sentry_metrics_dropped_count()` — see
[Logs](#logs-a-continuous-console-correlated-by-trace) for the measured table; the two
toggles are independent and combine.

### What it costs

| | |
Expand Down Expand Up @@ -776,6 +795,67 @@ the device the time, which on a BLE-only device may never happen at all on a giv
cycle. `sentry-sample_rand` is parsed from `baggage` and carried for later use, but the
device honours the caller's sampling decision rather than making its own.

### Logs: a continuous console, correlated by trace

A deployed device's console is the one thing you most want and cannot have — it is a cable
you are not attached to. `sentry_log()` mirrors it:

```cpp
sentry::log(SENTRY_LEVEL_WARNING, "WiFi reconnect attempt %u", attempt);
```

**Recording does not send, the same as a metric** — it writes into a fixed ring and rides
the next `sentry_flush()`. Unlike a metric, each line remembers whatever trace was active
when it was *recorded*, not whatever happens to be active when the ring is flushed later —
the same way a breadcrumb attaches to what the device was actually doing, rather than to
nothing (or something unrelated) by the time the batch goes out. A line recorded while idle
is still held and sent, just without that attachment: logging the console is the point even
when nothing else is going on.

The message is formatted printf-style into a fixed `SENTRY_MICRO_LOG_BODY_LEN`-byte buffer
(81 bytes by default, a conventional terminal line width) and truncated to fit rather than
dropped — a shortened line you can still read beats losing it entirely. Truncation is
computed from `vsnprintf()`'s own return value, not predicted at compile time, and reported
two ways: `sentry_logs_truncated_count()` since init, and a per-line `t7d` attribute
(present only when true) once the line reaches Sentry.

The ring holds `SENTRY_MICRO_MAX_LOGS` lines (6 by default) and evicts the oldest once
full — unlike the metrics table, there is no running total to protect here, so the newest
line displacing the old one is the right trade for a continuous stream.
`sentry_logs_dropped_count()` reports how many were evicted before they were ever sent.

### What it costs

| | |
| --- | --- |
| Permanent RAM | **832 B** at the defaults (`sentry_log_ring_t`: 6 × 136-byte entries) |
| Flash, always linked | ~1.5 KB — `flush_logs()` runs on every `sentry_flush()`, whether or not the firmware ever calls `sentry_log()` |

Unlike a transaction, this is not opt-in by usage: the ring is a permanent `g_state` field,
because a log line — like a metric — has to survive across flushes rather than living on a
caller's stack for one operation. `SENTRY_MICRO_LOGS_ENABLED=0` removes it entirely:

```ini
build_flags = -D SENTRY_MICRO_LOGS_ENABLED=0
```

Measured on `esp32dev`, a build of `wifi_basic` that never calls `sentry_log()`, with and
without:

| | Enabled (default) | `SENTRY_MICRO_LOGS_ENABLED=0` | Saved |
| --- | --- | --- | --- |
| Flash | 941,557 B | 940,061 B | 1,496 B |
| RAM | 49,924 B | 49,092 B | 832 B |

`sentry_log()`, `sentry_logs_dropped_count()` and `sentry_logs_truncated_count()` are not
declared at all when disabled, the same as `set_ca_cert()` under `SENTRY_MICRO_WIFI_TLS=0`
above — a build that turns logs off and still tries to call one fails to compile rather than
silently doing nothing.

`SENTRY_MICRO_METRICS_ENABLED=0` does the same for Application Metrics (272 B RAM, ~1.1 KB
flash on the same build), and the two toggles combine: **1,104 B RAM and 3,056 B flash**
saved with both off.

## Writing a transport

Everything Sentry-specific has already happened by the time a transport is called: it gets a
Expand Down
Loading
Loading