Logger in Pico-Framework
Logging is essential in embedded systems for understanding what your application is doing, especially when something goes wrong. The Pico-Framework includes a built-in Logger
utility that supports both console output and persistent storage using the active StorageManager
.
This guide explains:
- Why structured logging matters
- How the
Logger
works - How to log to SD card or internal flash
- Log levels and filtering
- Real usage examples
Purpose and Design Goals
Embedded developers often rely on printf()
for debug output, but it has limitations:
- No timestamps
- No severity levels
- No persistent storage (logs are lost on reset)
The Logger
solves these problems with:
- Timestamps formatted as
YYYY-MM-DD HH:MM:SS
- Log levels: INFO, WARN, ERROR
- Optional file logging via
StorageManager
- Unified logging API throughout your application
Basic Usage
You can start using the logger without configuration. It will write to stdout:
Logger::info("System initialized");
Logger::warn("Low battery detected");
Logger::error("Sensor read failed");
Sample output:
[2025-05-03 12:45:17] [INFO] System initialized
[2025-05-03 12:45:42] [WARN] Low battery detected
[2025-05-03 12:46:00] [ERROR] Sensor read failed
Enabling File Logging
To log messages to a file (e.g., /log.txt
on SD card or flash), enable file logging and ensure a StorageManager
is registered:
Logger::enableFileLogging("/log.txt");
Now all log messages will be appended to the file as well as printed to stdout.
Note: Logging to file uses the same file API provided by StorageManager
, so it works with both FatFs
and LittleFs
.
Setting Log Level
By default, the log level is INFO
, which means warn()
and error()
will also appear.
You can raise the minimum log level to filter out less important messages:
Logger::setMinLogLevel(LOG_WARN); // Now info messages will be suppressed
This is useful for production builds where you may want to omit low-priority logs.
Integration with StorageManager
Logger uses the active StorageManager
to append log entries to a file. This means:
- Logs are preserved across reboots
- You can download logs via a file browser route
- You can store logs on SD or flash depending on your configuration
Example:
AppContext::register<StorageManager>(new FatFsStorageManager());
Logger::enableFileLogging("/log.txt");
Logger::info("Log system started");
The call to appendToFile()
is safe and thread-aware, using FreeRTOS synchronization under the hood.
Thread Safety and Performance
Logger is designed to be safe in multi-task environments. However:
- Log messages are formatted in a temporary buffer (256 bytes max)
- Logging should not be used inside tight loops or ISRs
- File logging may fail silently if the storage backend is not ready or mounted
To optimize performance:
- Use
LOG_WARN
level or higher in production - Avoid verbose logging in real-time sections
Adding Logger to Your Own Classes
While Logger
is a singleton-style utility class, you can wrap it for clarity:
class MyService {
public:
void init() {
Logger::info("MyService initialized");
}
};
No additional setup is required beyond calling the static methods.
Summary
Feature | Supported |
---|---|
Timestamps | Yes |
Log levels | Yes |
Output to stdout | Yes |
Output to file | Yes |
Thread-safe | Yes |
Custom levels | No |
Dynamic format | No |
Logger provides a structured, consistent, and portable way to record operational messages across your embedded system. It integrates with the rest of the Pico-Framework seamlessly, with no external dependencies.
Example: Full Setup
AppContext::register<StorageManager>(new LittleFsStorageManager());
Logger::enableFileLogging("/system.log");
Logger::setMinLogLevel(LOG_INFO);
Logger::info("App started");
Logger::warn("Config file not found, using defaults");
Logger::error("SD card not mounted");
Log Rotation (Manual)
The built-in logger does not automatically rotate files, but you can implement manual rotation in your controller logic:
void rotateLogIfNeeded() {
auto* storage = AppContext::get<StorageManager>();
size_t logSize = storage->getFileSize("/system.log");
if (logSize > 50 * 1024) { // 50 KB threshold
storage->removeFile("/system.old.log");
storage->renameFile("/system.log", "/system.old.log");
Logger::enableFileLogging("/system.log"); // Reopen new log file
Logger::info("Log rotated");
}
}
You can run this periodically using a FrameworkTask::runEvery()
or in a background poll loop.
Handling Logging Failures and Buffer Overflow
Logger uses a 256-byte internal buffer to format messages. If your log message (including timestamp and level) exceeds this length, it may be truncated.
Best Practices:
- Avoid excessive log data in a single line
- Format JSON or structured data externally before logging
- Don't rely on Logger output inside tight loops or ISR contexts
File logging may fail silently if the storage is unmounted or full. To monitor logging health:
if (!Logger::isFileLoggingEnabled()) {
Logger::warn("Logging fallback to console only");
}
Web UI Log Viewer Integration
You can add a basic file viewer route to your app for remote log access:
router.addRoute("GET", "/logs", [](HttpRequest& req, HttpResponse& res, const RouteMatch&) {
std::vector<uint8_t> content;
auto* storage = AppContext::get<StorageManager>();
if (storage->readFile("/system.log", content)) {
res.send(content, "text/plain");
} else {
res.send("Log file not found", "text/plain", 404);
}
});
For a web-friendly UI, render it with line breaks and scrollable view:
<pre style="white-space: pre-wrap; max-height: 500px; overflow-y: auto;">
[Log content goes here]
</pre>
This allows developers and users to inspect logs without a terminal connection.
Notes
- Manual log rotation can be added using file rename + size check
- Max buffer is 256 bytes — long messages may be truncated
- Logger is best used in main loop and controller context, not ISRs
- File logging depends on mounted storage and can fall back silently
- Web UI integration gives insight without serial access