ESP32 remote logging: ESP_LOG without a serial cable
Everything your firmware says, it says to the serial port. Once the device is installed, that is a port nobody is listening to. The log can go somewhere else as well, with one function.
The hook
Every ESP_LOGI, ESP_LOGW and ESP_LOGE ends in one function that prints the formatted line. ESP-IDF lets you replace it:
static vprintf_like_t previous;
static int to_both(const char *format, va_list args)
{
va_list copy;
va_copy(copy, args);
char line[192];
int length = vsnprintf(line, sizeof(line), format, copy);
va_end(copy);
if (length > 0) {
remote_log_queue(line, length); /* yours: must not block */
}
return previous(format, args); /* still goes to the serial port */
}
void app_main(void)
{
previous = esp_log_set_vprintf(to_both);
}
That is the whole mechanism. What you do with the line is where the work is.
Three ways to get it wrong
- Sending from inside the hook. The hook runs in whichever task logged, including the WiFi and TCP/IP tasks. A send that blocks there stalls the network stack, and a send that itself logs calls the hook again. Copy the line into a queue and return; send from a task of your own.
- Logging in the code that sends. If the sender logs an error when the connection drops, each failed send produces another line to send. Keep the sending path silent, or mark its lines so the hook skips them.
- A queue with no limit. When the network is down the lines pile up. Decide in advance how much memory the log may use and drop the oldest lines beyond that. A device that runs out of heap because it could not report something has the priorities backwards.
Where to send it
| Good for | What it leaves to you | |
|---|---|---|
| UDP to a syslog server | Devices on your own network | Nothing arrives from behind someone else's router unless you open a port; lines are lost silently; no encryption |
| MQTT topic | Fleets that already use a broker | A TLS session on the device, a broker, and something that stores and searches what arrives |
| HTTP POST in batches | Occasional uploads | A TLS handshake per batch, and the last lines before a crash are the ones that never get sent |
| A connection that is already open | Live tailing | Building that connection |
How Pinscope does it
The component installs the hook when it starts, copies each line into a 2 KB queue that also carries metrics, and sends from its own task over the connection it already holds. When the device is offline the newest lines wait in the queue and the oldest are dropped. On the other end the lines are tailed live, stored, and searchable by text and by time.
There is nothing to write: logging is on once you call pinscope_start(NULL), and a switch in menuconfig turns it off. Text that does not go through ESP_LOG can be sent with pinscope_log(text, length).
What to log, once someone can read it
- The firmware version and the reset reason, once, at start.
- State changes, not states: "valve opened", not "valve is open" every second.
- Numbers with their unit, so the line can be read without the source code.
- Nothing personal. A log that leaves the device is data you are now responsible for.
Try it
Three devices are free with every feature, and no card is needed. Create an account, or read the five-minute guide first.
More
- Remote access to an ESP32 without TLS: what it measured and what it costs
- Reaching an ESP32 web server from outside its network
- Updating ESP32 firmware over the internet, with rollback
- Crash backtraces from devices in the field
- Heap and stack monitoring in the field
- An ESP32 behind CGNAT, 4G or Starlink
- TLS memory usage on the ESP32, and how to reduce it