Logs
Send structured logs from your device to RelayX. Each call mirrors to the local console for development and publishes a copy to the backend, where the App SDK can stream live logs or query history.
Overview
The log module exposes three methods, one per severity level: info, warn, and error. Logging is fire-and-forget. The methods are synchronous, return nothing, and never throw on transport errors. When the device is offline, log entries buffer and flush on reconnect.
Telemetry vs Logs vs Events
All three are fire-and-forget, but they describe different things:
| Telemetry | Logs | Events | |
|---|---|---|---|
| What it captures | Numeric measurements (temperature, current) | Free-form messages about what the device is doing | Discrete moments (door opened, boot complete) |
| Schema | Validated against your device schema | None. Any string. | None. You define the name and payload. |
| Levels | None | info, warn, error | None |
| Typical use | Charts and alerts | Diagnostics, debugging, audit trail | Triggering app-side workflows |
If you would write it to stdout for a human to read later, it is a log.
API
info() / warn() / error()
Each level is its own method. Pass any number of arguments. They are formatted into a single string and sent as one message.
- JavaScript
- Python
- C++
device.log.info(...args)device.log.warn(...args)device.log.error(...args)| Parameter | Type | Description |
|---|---|---|
...args | any | Strings, numbers, booleans, null, undefined, plain objects, arrays, Date, Error. |
Returns nothing. Methods are synchronous, do not return a promise.
Throws ValidationError if any top-level argument is a Function, Symbol, BigInt, Map, Set, or class instance other than Date or Error. Validation is shallow, nested values are not walked.
device.log.info(*args)device.log.warn(*args)device.log.error(*args)| Parameter | Type | Description |
|---|---|---|
*args | Any | str, int, float, bool, None, dict, list, datetime, Exception. |
Returns None. Methods are regular def, not async def.
Throws ValidationError if any top-level argument is a callable, set, or other unsupported type. Validation is shallow.
device->log.info(const char* fmt, ...) → voiddevice->log.warn(const char* fmt, ...) → voiddevice->log.error(const char* fmt, ...) → void| Parameter | Type | Description |
|---|---|---|
fmt | const char* | printf-style format string. |
... | varargs | Format arguments. |
Returns nothing. The methods never return errors, the transport publisher absorbs failures.
Unlike telemetry.publish() and event.send(), log methods do not return a promise. Do not await them. This keeps logging fast inside hot paths and tight loops.
Send a Log
The most common pattern is a one-liner with a message and some context.
- JavaScript
- Python
- C++
device.log.info("Boot complete", { firmware: "1.2.0", uptimeMs: 3200 });
device.log.warn("Retrying transport publish");
device.log.error("Sensor read failed", err);
Arguments can be any mix of strings, numbers, booleans, plain objects, arrays, Date, or Error. They are joined with a single space when sent. Error instances render as Name: message followed by the stack trace. Objects and arrays are JSON-stringified.
device.log.info("Boot complete", {"firmware": "1.2.0", "uptime_ms": 3200})
device.log.warn("Retrying transport publish")
device.log.error("Sensor read failed", exc)
Arguments can be any mix of str, int, float, bool, None, dict, list, datetime, or Exception. They are joined with a single space when sent. Exception instances render as Name: message followed by the traceback. Dicts and lists are JSON-serialized.
device->log.info("Boot complete: firmware=%s uptime_ms=%d", "1.2.0", 3200);
device->log.warn("Retrying transport publish");
device->log.error("Sensor read failed: %s", esp_err_to_name(err));
The format string and arguments follow printf rules. The formatted string is truncated at RELAY_LOG_ENTRY_MAX_LEN bytes (default 256).
Console Mirroring
Every log call also writes to the local console before publishing. This guarantees you see the message during development even if the device is offline or transport publish fails.
| Level | JavaScript | Python | C++ |
|---|---|---|---|
info | console.info | print(..., file=sys.stdout) | ESP_LOGI |
warn | console.warn | print(..., file=sys.stderr) | ESP_LOGW |
error | console.error | print(..., file=sys.stderr) | ESP_LOGE |
The C++ runtime uses the tag relay-log for ESP-IDF console output.
Buffering and Flushing
- JavaScript
- Python
- C++
JavaScript and Python share the same buffering policy. Log entries are pushed onto a single in-memory buffer shared across all three levels. The buffer flushes when one of two conditions is met:
| Trigger | Threshold |
|---|---|
| Entry count | 15 entries |
| Time | 5 seconds since the first entry into an empty buffer |
Whichever fires first wins. Subsequent enqueues do not reset the timer, so a steady trickle of logs flushes on the 5-second cadence rather than waiting indefinitely.
JavaScript and Python share the same buffering policy. Log entries are pushed onto a single in-memory buffer shared across all three levels. The buffer flushes when one of two conditions is met:
| Trigger | Threshold |
|---|---|
| Entry count | 15 entries |
| Time | 5 seconds since the first entry into an empty buffer |
Whichever fires first wins. Subsequent enqueues do not reset the timer, so a steady trickle of logs flushes on the 5-second cadence rather than waiting indefinitely.
The C++ log module does not maintain its own buffer. Each call hands the formatted entry directly to the transport's shared async publisher queue, which is the same queue used by telemetry and events. Ordering, retry, and offline behavior are handled there.
There is no log-side flush interval or threshold to tune. See the transport configuration for queue sizing.
Offline Behavior
Logs sent while the device is disconnected are held in the transport's offline buffer and flushed in order once the connection is restored.
Buffered logs are stored in memory only. If the device process restarts while offline, buffered logs are lost.
Graceful Shutdown
- JavaScript
- Python
- C++
device.disconnect() drains the log buffer and waits for every in-flight publish to settle before tearing down the transport. No log entry is silently dropped on graceful disconnect.
await device.log.error("Shutting down due to fatal error", err);
await device.disconnect();
device.disconnect() drains the log buffer and awaits every in-flight publish before tearing down the transport. No log entry is silently dropped on graceful disconnect.
device.log.error("Shutting down due to fatal error", exc)
await device.disconnect()
Graceful shutdown is handled by the transport-level publisher queue. Logs enqueued before relay_device_disconnect() are drained as part of the transport teardown.
Validation Errors
Argument validation runs before the console mirror or the publish, so an invalid argument raises immediately and nothing is logged.
- JavaScript
- Python
- C++
// Throws ValidationError. Functions are not loggable.
device.log.info("handler", () => {});
// Works. Errors render with stack.
device.log.error("Sensor read failed", new Error("EIO"));
# Raises ValidationError. Sets are not loggable.
device.log.info("tags", {"a", "b"})
# Works. Exceptions render with traceback.
try:
risky()
except Exception as exc:
device.log.error("Sensor read failed", exc)
C++ uses printf-style formatting and does not perform per-argument type validation. A vsnprintf failure produces an empty string and the entry is still enqueued.
Reading Logs from the App SDK
The App SDK provides matching log.stream(), log.off(), and log.history() methods to consume the logs your devices produce. See the App SDK Logs reference for details.