diff --git a/.github/workflows/arduino-lint.yml b/.github/workflows/arduino-lint.yml index ff7b55f..63b60dd 100644 --- a/.github/workflows/arduino-lint.yml +++ b/.github/workflows/arduino-lint.yml @@ -1,7 +1,9 @@ -on: pull_request +on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: arduino/arduino-lint-action@v1 + with: + library-manager: update \ No newline at end of file diff --git a/README.md b/README.md index e918ac4..d3459fd 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,110 @@ -# AdvancedLogger +![Apparently this is what AI thinks about this library](logo.png) -[![Release](https://img.shields.io/github/v/release/jibrilsharafi/AdvancedLogger)](#release) +# AdvancedLogger +[![Release](https://img.shields.io/github/v/release/jibrilsharafi/AdvancedLogger)](https://github.com/jibrilsharafi/AdvancedLogger/releases/latest) [![PlatformIO Registry](https://badges.registry.platformio.org/packages/jijio/library/AdvancedLogger.svg)](https://registry.platformio.org/libraries/jijio/AdvancedLogger) - [![arduino-library-badge](https://www.ardu-badge.com/badge/AdvancedLogger.svg?)](https://www.ardu-badge.com/AdvancedLogger) [![ESP32](https://img.shields.io/badge/ESP-32S3-000000.svg?longCache=true&style=flat&colorA=CC101F)](https://www.espressif.com/en/products/socs/esp32-S3) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/jibrilsharafi/AdvancedLogger/blob/master/LICENSE) + [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/jibrilsharafi) A **simple** logging library capable of **saving logs to memory** and with a **comprehensive format** capable of including all the accessory information to each message. -Usage: +## Installing + +The library is available on the [PlatformIO registry](https://registry.platformio.org/libraries/jijio/AdvancedLogger), and can be installed by adding the following line to the `platformio.ini` file: + +```ini +lib_deps = jijio/AdvancedLogger +``` + +Moreover, the library is also available on the [Arduino Library Manager](https://www.arduinolibraries.info/libraries/advanced-logger), and can be installed by searching for `AdvancedLogger` in the Library Manager. + +Alternatively, the library can be installed manually by downloading the latest release from the [releases page](https://github.com/jibrilsharafi/AdvancedLogger/releases). + +**No external dependencies are required**, as the library uses only the standard libraries in the Arduino Framework. + +The library so far has been successfully tested on the following microcontrollers: +- ESP32S3 + +## Usage +### Basic +The simplest way to use the library is the following: ```cpp +#include + AdvancedLogger logger; -... -logger.begin(); -... -logger.log("This is an info message!", "main::setup", ADVANCEDLOGGER_INFO); -delay(1000); -logger.log("This is an error message!!", "main::loop", ADVANCEDLOGGER_ERROR); + +void setup() { + ... + logger.begin(); + ... +} + +void loop() { + ... + logger.info("This is an info message!", "main::setup"); + delay(1000); + logger.error("This is an error message!! Random value: %d", "main::loop", random(0, 100)); + ... +} ``` -Output (both in the Serial and in the log file in memory): +Output (both in the Serial and in the log file in the SPIFFS memory): + ```cpp -[2024-03-23 09:44:10] [1450 ms] [INFO] [Core 1] [main::setup] This is an info message! -[2024-03-23 09:44:12] [3250 ms] [ERROR] [Core 1] [main::loop] This is an error message!! +[2024-03-23 09:44:10] [1 450 ms] [INFO ] [Core 1] [main::setup] This is an info message! +[2024-03-23 09:44:11] [2 459 ms] [ERROR ] [Core 1] [main::loop] This is an error message!! Random value: 42 ``` -For a detailed example, see the [basicUsage](examples/basicUsage/basicUsage.cpp) and [basicServer](examples/basicServer/basicServer.cpp) in the examples folder. - -## Why? -There is no way of sugar-coat it: *every well-developed project will eventually need a proper logging* in place to make sure that not only everything is fully monitored, but also that everything monitored is later accessible for debugging. -The development of [EnergyMe](https://github.com/jibrilsharafi/EnergyMe-Home) (my open-source energy monitoring device) quickly led to the necessity of having such feature. Nonetheless, I quickly found out that no repositories on GitHub where available capable of fully covering all of my [Requirements](#requirements). -### Requirements -Does it exist a C++ module for the ESP32 (the projects I am currently working on use this microcontroller) that allows logging with the following features: -- **Format**: a comprehensive format capable of including all the accessory information to each print message. Something like: `[2024-03-13 09:44:10] [1313 ms] [INFO] [Core 1] [main::setup] Setting up ADE7953...`, which indeed explains very clearly the nature and context of the message. This type of logging is the norm in more complex systems and environments. -- **Saving to memory**: developing a complex logging system is useless if it can only be accessed in real time. The ability of saving each message in chronological order in a long time storage medium is crucial to unlock the full potential of it. -- **Ease of use**: the module should comprise of only a few functions, such as the core one `log(...)` and the accessory ones needed to set the print and save levels to adapt to each use case. -### Don't reinvent the wheel -Some existing libraries are: - -| Name | Owner | Description | Format | Saving | Ease of use | -| :------------------------------------------------------------------------------------------------: | :-----------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------: | :----: | :----: | :---------: | -| **[elog](https://github.com/x821938/elog)** | [x821938](https://github.com/x821938) | Fast and efficient logging to multiple outputs, with many configuration settings. | ❕ | ✔ | ✔ | -| **[ESP.multiLogger](https://github.com/voelkerb/ESP.multiLogger)** | [voelkerb](https://github.com/voelkerb) | Simple logging to multiple outputs. | ❌ | ✔ | ✔ | -| **[logging](https://github.com/esp32m/logging)** | [esp32m](https://github.com/esp32m) | Context-based logging, multiple appenders, buffers and queues. | ❌ | ✔ | ❕ | -| **[MycilaLogger](https://github.com/mathieucarbou/MycilaLogger)** | [mathieucarbou](https://github.com/mathieucarbou) | Simple and efficient. A wrapper around the standard `buffer.print()`. | ❌ | ❌ | ✔ | -| **[esp_log.h](https://github.com/espressif/esp-idf/blob/master/components/log/include/esp_log.h)** | [espressif](https://github.com/espressif) | Native method implemented by espressif. [Documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/log.html). | ❌ | ❌ | ❌ | - -Many other similar libraries exists, but it is clear that **none of the reported libraries** (and of the others available on *GitHub*) **fully match my requirements**. -## How? -The rule is one: *keep it simple*. -I am in no way a advanced coder, and as such I tend to structure and write code to be as streamlined and simple as possible. So, no memory optimization or any of the pointer-magic stuff in C++. *Not my kink*. -### Format -**The format** of the message should **include any information** that could store **any value**. As such, everything related to the time should be logged, along with information about the location of the code in which the log takes place, as well as having different logging levels. -We can set a custom format by using the function `sprintf` and passing the following as third argument: -- `[%s] [%lu ms] [%s] [Core %d] [%s] %s` → `[2024-03-13 09:44:10] [1313 ms] [INFO] [Core 1] [main::setup] Setting up ADE7953...` -which will represent the following parts: -- `[%s]` → `[2024-03-13 09:44:10]`: human-readable ISO8601 format in the format `%Y-%m-%d %H:%M:%S`. Indeed, the time must be set externally via other libraries. -- `[%lu ms]` → `[1313 ms]`: time passed since device boot, in *ms*. Crucial to precisely evaluate the time taken to perform any action. -- `[%s]` → `[INFO]`: the log level, which could be one of the following 5 levels: *DEBUG, INFO, WARNING, ERROR, FATAL*. -- `[Core %d]` → `[Core 1]`: core in which the operation takes place. Useful for multi-core devices (such as many ESP32 variations). -- `[%s]` → `[main::setup]`: the current function, or in general any name to identify a particular section of the code. -- `%s` → `Setting up ADE7953...`: the actual message that contains all the info needed to be logged. -### Saving to memory -Saving should be quite straightforward: just copy any message that is sent to the serial to a log file, appending it to the end with a newline. It has to be noted that a memory clearing has to be set in place in order to avoid filling all the SPIFFS memory of the device, making it slow and unresponsive. -### Ease of use -Last but not least, the use should be as simple as possible: you just need to create `AdvancedLogger advancedLogger;` and simply use `advancedLogger.log("Setting up ADE7953...", "main::setup", INFO);` - -## Dependencies -This project has no external dependencies, and uses only the standard libraries. - -## What's next? -- [x] **Customizable paths**: allow to set a custom path when creating the AdvancedLogger object. +### Advanced +The library provides the following public methods: +- `begin()`: initializes the logger, creating the log file and loading the configuration. +- Logging methods. All of them have the same structure, where the first argument is the message to be logged, and the second argument is the function name. The message can be formatted using the `printf` syntax. + - `verbose(const char *format, const char *function = "functionName", ...)` + - `debug(const char *format, const char *function = "functionName", ...)` + - `info(const char *format, const char *function = "functionName", ...)` + - `warning(const char *format, const char *function = "functionName", ...)` + - `error(const char *format, const char *function = "functionName", ...)` + - `fatal(const char *format, const char *function = "functionName", ...)` +- `setPrintLevel(LogLevel logLevel)` and `setSaveLevel(LogLevel logLevel)`: set the log level for printing and saving respectively. The log level can be one of the following (The default log level is **INFO**, and the default save level is WARNING): + - `LogLevel::VERBOSE` + - `LogLevel::DEBUG` + - `LogLevel::INFO` + - `LogLevel::WARNING` + - `LogLevel::ERROR` + - `LogLevel::FATAL` +- `getPrintLevel()` and `getSaveLevel()`: get the log level for printing and saving respectively, as a LogLevel enum. To be used in conjunction with the `logLevelToString` method. +- `logLevelToString(LogLevel logLevel, bool trim = true)`: convert a log level from the LogLevel enum to a String. +- `setMaxLogLines(int maxLogLines)`: set the maximum number of log lines. The default value is 1000. +- `getLogLines()`: get the number of log lines. +- `clearLog()`: clear the log. +- `dump(Stream& stream)`: dump the log to a stream, such as the Serial or an opened file. +- `setDefaultConfig()`: set the default configuration. + +For a detailed example, see the [basicUsage](examples/basicUsage/basicUsage.ino) and [basicServer](examples/basicServer/basicServer.ino) in the examples folder. + +## Contributing +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcome. + +For more information regarding the necessity of this library, see the [WHY.md](WHY.md) file. + +### What's next? - [x] **Automatic log clearing**: if the free memory is less than a certain threshold, the oldest logs should be deleted, keeping the memory usage under control. - [ ] **Log to SD card**: the ability to log to an external SD card would be a great addition, as it would allow to store a much larger amount of logs. -- [x] **Dump to serial**: implement a function that dumps the entire log to the serial, so that it can be accessed in real time. -- [x] **Remove ArduinoJson dependency**: the library is used only for the configuration file, and as such it could be removed by implementing a simpler configuration in .txt format. -- [x] **Dump to serial**: implement a function that dumps the entire log to the serial, so that it can be accessed in real time. - [x] **Remove ArduinoJson dependency**: the library is used only for the configuration file, and as such it could be removed by implementing a simpler configuration in .txt format. - [ ] **Upgrade to LittleFS**: the SPIFFS library is deprecated, and as such it should be replaced with the LittleFS library. Some effort has been made in this direction, but the nested folder approach gave some problems. -- [ ] **Test other microcontrollers**: the library is currently tested only on the ESP32, but it should be tested on other microcontrollers to ensure compatibility. +- [ ] **Test other microcontrollers**: the library is currently tested only on the ESP32, but it should be tested on other microcontrollers to ensure compatibility. The *ESP8266* has been tested, but full compatibility has not been achieved yet. - [ ] **MQTT integration**: the ability to send logs to an MQTT server would be a great addition, as it would allow to monitor the device remotely. -- [x] ~~**Consistent spacing**: the spacing between the different parts of the log should be consistent, to make it easier to read.~~ Not needed, as the format is already quite clear. -- [ ] **Buffered logging**: the ability to buffer the logs and send them in chunks would be a great addition, as it would allow to save power and reduce the number of writes to the memory. This has been tested, but the results did not show any significant improvement in speed. \ No newline at end of file +- [x] **Consistent spacing**: the spacing between the different parts of the log should be consistent, to make it easier to read. +- [ ] **Buffered logging**: the ability to buffer the logs and send them in chunks would be a great addition, as it would allow to save power and reduce the number of writes to the memory. This has been tested, but the results did not show any significant improvement in speed. +- [x] **New APIs**: the logging APIs should be updated to be more consistent with the usual logging APIs, such as log.debug(...), log.info(...), log.error(...), etc. +- [x] **Customizable paths**: allow to set a custom path when creating the AdvancedLogger object, to allow for different log files. +- [x] **Custom timestamp format**: the ability to set a custom timestamp format would be a great addition, as it would allow to adapt the logs to different needs and regions. +- [x] **Dump to any stream**: the ability to dump the logs to any stream would be a great addition, as it would allow to send the logs to different destinations, such as a file or a serial port. + +## Licensing + +The code in this project is licensed under MIT license. See the [LICENSE](LICENSE) file for more information. \ No newline at end of file diff --git a/examples/basicServer/basicServer.ino b/examples/basicServer/basicServer.ino index eeb0c8f..7240a8c 100644 --- a/examples/basicServer/basicServer.ino +++ b/examples/basicServer/basicServer.ino @@ -1,61 +1,52 @@ /* - * File: basicServer.cpp + * File: basicServer.ino * -------------------- * This file provides a simple example to show how to use the AdvancedLogger library. * * Author: Jibril Sharafi, @jibrilsharafi - * Date: 07/04/2024 + * Created: 21/03/2024 + * Last modified: 22/05/2024 * GitHub repository: https://github.com/jibrilsharafi/AdvancedLogger * * This library is licensed under the MIT License. See the LICENSE file for more information. * - * This example is a simple web server that serves a webpage with a button - * that sends the user to the /log page, where the logs are displayed, and - * another button that sends the user to the /config page, where the configuration - * is displayed. - * - * Remember to change the ssid and password to your own. - * - * Tested only on the ESP32. For other boards, you may need to change the - * WebServer library. - * - * Possible logging levels: - * - ADVANCEDLOGGER_VERBOSE - * - ADVANCEDLOGGER_DEBUG - * - ADVANCEDLOGGER_INFO - * - ADVANCEDLOGGER_WARNING - * - ADVANCEDLOGGER_ERROR - * - ADVANCEDLOGGER_FATAL + * This example covers the addition of a simple web server to the basicUsage, which allows + * the user to explore the log and configuration files remotely. + * + * All the other advanced usage features are reported in the basicUsage example. */ + #include #include #include #include +#include #include "AdvancedLogger.h" -String customLogPath = "/customPath/log.txt"; -String customConfigPath = "/customPath/config.txt"; - -AdvancedLogger logger(customLogPath.c_str(), customConfigPath.c_str()); // Leave empty for default paths +const char *customLogPath = "/customPath/log.txt"; +const char *customConfigPath = "/customPath/config.txt"; +const char *customTimestampFormat = "%Y-%m-%d %H:%M:%S"; +AdvancedLogger logger( + customLogPath, + customConfigPath, + customTimestampFormat); AsyncWebServer server(80); -String printLevel; -String saveLevel; - -long lastMillisLogDump = 0; -const long intervalLogDump = 10000; - -long lastMillisLogClear = 0; -const long intervalLogClear = 30000; - -int maxLogLines = 100; // Low value for testing purposes +const int timeZone = 0; // UTC. In milliseconds +const int daylightOffset = 0; // No daylight saving time. In milliseconds +const char *ntpServer1 = "pool.ntp.org"; +const char *ntpServer2 = "time.nist.gov"; +const char *ntpServer3 = "time.windows.com"; // **** CHANGE THESE TO YOUR SSID AND PASSWORD **** const char *ssid = "YOUR_SSID"; const char *password = "YOUR_PASSWORD"; +long lastMillisLogClear = 0; +const long intervalLogClear = 30000; + void setup() { // Initialize Serial and SPIFFS (mandatory for the AdvancedLogger library) @@ -69,78 +60,61 @@ void setup() logger.begin(); - // Setting the print and save levels is not mandatory. - // If you don't set them, the default levels are first taken - // from the SPIFFS file, and if it doesn't exist, the default - // levels are used (DEBUG for print and INFO for save). - logger.setPrintLevel(ADVANCEDLOGGER_DEBUG); - logger.setSaveLevel(ADVANCEDLOGGER_INFO); - // Set the maximum number of log lines before the log is cleared - // If you don't set this, the default is used - logger.setMaxLogLines(maxLogLines); - logger.log("AdvancedLogger setup done!", "basicServer::setup", ADVANCEDLOGGER_INFO); - + logger.debug("AdvancedLogger setup done!", "basicServer::setup"); + // Connect to WiFi // -------------------- + // Connect to the specified SSID WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { delay(1000); - logger.log("Connecting to WiFi..", "basicServer::setup", ADVANCEDLOGGER_INFO); + logger.info("Connecting to WiFi... SSID: %s | Password: %s", "basicServer::setup", ssid, password); } - logger.log(("IP address: " + WiFi.localIP().toString()).c_str(), "basicServer::setup", ADVANCEDLOGGER_INFO); + + logger.info(("IP address: " + WiFi.localIP().toString()).c_str(), "basicServer::setup"); + + configTime(timeZone, daylightOffset, ntpServer1, ntpServer2, ntpServer3); // Serve a simple webpage with a button that sends the user to the page /log and /config // -------------------- server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/html", "

"); }); - server.serveStatic("/log", SPIFFS, customLogPath.c_str()); - server.serveStatic("/config", SPIFFS, customConfigPath.c_str()); + + server.serveStatic("/log", SPIFFS, customLogPath); + server.serveStatic("/config", SPIFFS, customConfigPath); + server.onNotFound([](AsyncWebServerRequest *request) { request->send(404, "text/plain", "Not found"); }); server.begin(); - logger.log("Server started!", "basicServer::setup", ADVANCEDLOGGER_INFO); + + logger.info("Server started!", "basicServer::setup"); - lastMillisLogDump = millis(); - lastMillisLogClear = millis(); - logger.log("Setup done!", "basicServer::setup", ADVANCEDLOGGER_INFO); + logger.info("Setup done!", "basicServer::setup"); } void loop() { - logger.log("This is a debug message!", "basicServer::loop", ADVANCEDLOGGER_DEBUG); + logger.debug("This is a debug message!", "basicServer::loop"); delay(500); - logger.log("This is an info message!!", "basicServer::loop", ADVANCEDLOGGER_INFO); + logger.info("This is an info message!!", "basicServer::loop"); delay(500); - logger.log("This is a warning message!!!", "basicServer::loop", ADVANCEDLOGGER_WARNING); + logger.warning("This is a warning message!!!", "basicServer::loop"); delay(500); - logger.log("This is a error message!!!!", "basicServer::loop", ADVANCEDLOGGER_ERROR); + logger.error("This is a error message!!!!", "basicServer::loop"); delay(500); - logger.log("This is a fatal message!!!!!", "basicServer::loop", ADVANCEDLOGGER_FATAL); + logger.fatal("This is a fatal message!!!!!", "basicServer::loop"); delay(500); - logger.logOnly("This is an info message (logOnly)!!", "basicServer::loop", ADVANCEDLOGGER_INFO); - delay(1000); - - printLevel = logger.getPrintLevel(); - saveLevel = logger.getSaveLevel(); - - if (millis() - lastMillisLogDump > intervalLogDump) - { - logger.dumpToSerial(); + logger.info("This is an info message!!", "basicServer::loop", true); + delay(1000);; - lastMillisLogDump = millis(); - } - if (millis() - lastMillisLogClear > intervalLogClear) { - logger.log( - ("Current number of log lines: " + String(logger.getLogLines())).c_str(), - "basicServer::loop", - ADVANCEDLOGGER_INFO - ); + logger.info("Current number of log lines: %d", "basicServer::loop", logger.getLogLines()); logger.clearLog(); - logger.setDefaultLogLevels(); - logger.log("Log cleared!", "basicServer::loop", ADVANCEDLOGGER_WARNING); + logger.setDefaultConfig(); + logger.warning("Log cleared!", "basicServer::loop"); lastMillisLogClear = millis(); } diff --git a/examples/basicUsage/basicUsage.ino b/examples/basicUsage/basicUsage.ino index 24b937a..dc99c49 100644 --- a/examples/basicUsage/basicUsage.ino +++ b/examples/basicUsage/basicUsage.ino @@ -1,60 +1,60 @@ /* - * File: basicUsage.cpp + * File: basicUsage.ino * -------------------- * This file provides a simple example to show how to use the AdvancedLogger library. * * Author: Jibril Sharafi, @jibrilsharafi - * Date: 12/05/2024 + * Created: 21/03/2024 + * Last modified: 22/05/2024 * GitHub repository: https://github.com/jibrilsharafi/AdvancedLogger * * This library is licensed under the MIT License. See the LICENSE file for more information. * - * The AdvancedLogger library provides advanced logging for the ESP32. - * It allows you to log messages to the console and to a file on the SPIFFS. - * You can set the print level and save level, and the library will only log - * messages accordingly. - * - * Possible logging levels: - * - ADVANCEDLOGGER_VERBOSE - * - ADVANCEDLOGGER_DEBUG - * - ADVANCEDLOGGER_INFO - * - ADVANCEDLOGGER_WARNING - * - ADVANCEDLOGGER_ERROR - * - ADVANCEDLOGGER_FATAL + * This example covers the basic usage of the AdvancedLogger library: + * - Initializing the logger + * - Setting the print and save levels + * - Setting the maximum number of log lines before the log is cleared + * - Logging messages + * - Dumping the log + * - Clearing the log + * - Getting the current print and save levels + * - Getting the current number of log lines + * - Setting the default configuration */ + #include #include #include "AdvancedLogger.h" -String customLogPath = "/customPath/log.txt"; -String customConfigPath = "/customPath/config.txt"; +const char *customLogPath = "/customPath/log.txt"; +const char *customConfigPath = "/customPath/config.txt"; +// For more info on formatting, see https://www.cplusplus.com/reference/ctime/strftime/ +const char *customTimestampFormat = "%Y-%m-%d %H:%M:%S"; -AdvancedLogger logger(customLogPath.c_str(), customConfigPath.c_str()); // Leave empty for default paths -String customLogPath = "/customPath/log.txt"; -String customConfigPath = "/customPath/config.txt"; +AdvancedLogger logger( + customLogPath, + customConfigPath, + customTimestampFormat); +// If you don't want to set custom paths and timestamp format, you can +// just use the default constructor: +// AdvancedLogger logger; -AdvancedLogger logger(customLogPath.c_str(), customConfigPath.c_str()); // Leave empty for default paths +// Set the custom print and save levels +LogLevel printLevel = LogLevel::INFO; +LogLevel saveLevel = LogLevel::WARNING; -String printLevel; -String saveLevel; +// Set the maximum number of log lines before the log is cleared +int maxLogLines = 100; // Low value for testing purposes +// Variables for logging and clearing the log long lastMillisLogDump = 0; const long intervalLogDump = 10000; +const char *logDumpPath = "/logDump.txt"; long lastMillisLogClear = 0; const long intervalLogClear = 30000; -int maxLogLines = 10; // Low value for testing purposes - -long lastMillisLogDump = 0; -const long intervalLogDump = 10000; - -long lastMillisLogClear = 0; -const long intervalLogClear = 30000; - -int maxLogLines = 10; // Low value for testing purposes - void setup() { // Initialize Serial and SPIFFS (mandatory for the AdvancedLogger library) @@ -66,81 +66,83 @@ void setup() Serial.println("An Error has occurred while mounting SPIFFS"); } + // Initialize the logger logger.begin(); - // Setting the print and save levels is not mandatory. - // If you don't set them, the default levels are first taken - // from the SPIFFS file, and if it doesn't exist, the default - // levels are used (DEBUG for print and INFO for save). - logger.setPrintLevel(ADVANCEDLOGGER_DEBUG); - logger.setSaveLevel(ADVANCEDLOGGER_INFO); - // Set the maximum number of log lines before the log is cleared - // If you don't set this, the default is used - logger.setMaxLogLines(maxLogLines); - logger.log("AdvancedLogger setup done!", "basicUsage::setup", ADVANCEDLOGGER_INFO); - // Set the maximum number of log lines before the log is cleared - // If you don't set this, the default is used + // Setting the print and save levels (optional) + logger.setPrintLevel(printLevel); + logger.setSaveLevel(saveLevel); + + // Set the maximum number of log lines before the log is cleared (optional) logger.setMaxLogLines(maxLogLines); - logger.log("AdvancedLogger setup done!", "basicUsage::setup", ADVANCEDLOGGER_INFO); + logger.debug("AdvancedLogger setup done!", "basicUsage::setup"); + lastMillisLogDump = millis(); lastMillisLogClear = millis(); - lastMillisLogDump = millis(); - lastMillisLogClear = millis(); - logger.log("Setup done!", "basicUsage::setup", ADVANCEDLOGGER_INFO); + + logger.info("Setup done!", "basicUsage::setup"); } void loop() { - logger.log("This is a debug message!", "basicServer::loop", ADVANCEDLOGGER_DEBUG); + logger.verbose("This is a verbose message", "basicUsage::loop"); delay(500); - logger.log("This is an info message!!", "basicServer::loop", ADVANCEDLOGGER_INFO); + logger.debug("This is a debug message!", "basicUsage::loop"); delay(500); - logger.log("This is a warning message!!!", "basicServer::loop", ADVANCEDLOGGER_WARNING); + logger.info("This is an info message!!", "basicUsage::loop"); delay(500); - logger.log("This is a error message!!!!", "basicServer::loop", ADVANCEDLOGGER_ERROR); + logger.warning("This is a warning message!!!", "basicUsage::loop"); delay(500); - logger.log("This is a fatal message!!!!!", "basicServer::loop", ADVANCEDLOGGER_FATAL); + logger.error("This is a error message!!!!", "basicUsage::loop"); + delay(500); + logger.fatal("This is a fatal message!!!!!", "basicUsage::loop"); delay(500); - logger.logOnly("This is an info message (logOnly)!!", "basicServer::loop", ADVANCEDLOGGER_INFO); - delay(1000); - printLevel = logger.getPrintLevel(); - saveLevel = logger.getSaveLevel(); + logger.info("Testing printf functionality: %d, %f, %s", "basicUsage::loop", 1, 2.0, "three"); + delay(500); + + // Get the current print and save levels + String printLevel = logger.logLevelToString(logger.getPrintLevel()); + String saveLevel = logger.logLevelToString(logger.getSaveLevel()); if (millis() - lastMillisLogDump > intervalLogDump) { - logger.dumpToSerial(); + // Print the current number of log lines + logger.info("Current number of log lines: %d", "basicUsage::loop", logger.getLogLines()); + + // Dump the log to Serial + logger.info("Dumping log to Serial...", "basicUsage::loop"); + logger.dump(Serial); + logger.info("Log dumped!", "basicUsage::loop"); + + // Dump the log to another file + logger.info("Dumping log to file...", "basicUsage::loop"); + File tempFile = SPIFFS.open(logDumpPath, "w"); + logger.dump(tempFile); + tempFile.close(); + logger.info("Log dumped!", "basicUsage::loop"); + + // Ensure the log has been dumped correctly + logger.info("Printing the temporary log dump file...", "basicUsage::loop"); + tempFile = SPIFFS.open(logDumpPath, "r"); + while (tempFile.available()) + { + Serial.write(tempFile.read()); + } + tempFile.close(); + logger.info("Log dump file printed!", "basicUsage::loop"); lastMillisLogDump = millis(); } - - if (millis() - lastMillisLogClear > intervalLogClear) - if (millis() - lastMillisLogDump > intervalLogDump) - { - logger.dumpToSerial(); - lastMillisLogDump = millis(); - } - if (millis() - lastMillisLogClear > intervalLogClear) { - logger.log( - ("Current number of log lines: " + String(logger.getLogLines())).c_str(), - "basicServer::loop", - ADVANCEDLOGGER_INFO - ); - logger.log( - ("Current number of log lines: " + String(logger.getLogLines())).c_str(), - "basicServer::loop", - ADVANCEDLOGGER_INFO - ); + // Clear the log and set the default configuration logger.clearLog(); - logger.setDefaultLogLevels(); - logger.log("Log cleared!", "basicServer::loop", ADVANCEDLOGGER_WARNING); + logger.setDefaultConfig(); - lastMillisLogClear = millis(); - logger.log("Log cleared!", "basicServer::loop", ADVANCEDLOGGER_WARNING); + logger.info("Log cleared and default configuration set!", "basicUsage::loop"); lastMillisLogClear = millis(); } diff --git a/keywords.txt b/keywords.txt index dc4ec31..3d4591d 100644 --- a/keywords.txt +++ b/keywords.txt @@ -10,8 +10,12 @@ AdvancedLogger KEYWORD1 #################################################################################################### # AdvancedLogger functions and methods #################################################################################################### -log KEYWORD2 -logOnly KEYWORD2 +verbose KEYWORD2 +debug KEYWORD2 +info KEYWORD2 +warning KEYWORD2 +error KEYWORD2 +fatal KEYWORD2 setPrintLevel KEYWORD2 setSaveLevel KEYWORD2 getPrintLevel KEYWORD2 @@ -25,16 +29,9 @@ dumpToSerial KEYWORD2 #################################################################################################### # AdvancedLogger constants #################################################################################################### -ADVANCEDLOGGER_VERBOSE LITERAL1 -ADVANCEDLOGGER_DEBUG LITERAL1 -ADVANCEDLOGGER_INFO LITERAL1 -ADVANCEDLOGGER_WARNING LITERAL1 -ADVANCEDLOGGER_ERROR LITERAL1 -ADVANCEDLOGGER_FATAL LITERAL1 -ADVANCEDLOGGER_DEFAULT_PRINT_LEVEL LITERAL1 -ADVANCEDLOGGER_DEFAULT_SAVE_LEVEL LITERAL1 -ADVANCEDLOGGER_DEFAULT_MAX_LOG_LINES LITERAL1 -ADVANCEDLOGGER_TIMESTAMP_FORMAT LITERAL1 -ADVANCEDLOGGER_FORMAT LITERAL1 -ADVANCEDLOGGER_LOG_PATH LITERAL1 -ADVANCEDLOGGER_CONFIG_PATH LITERAL1 \ No newline at end of file +LogLevel::VERBOSE KEYWORD3 +LogLevel::DEBUG KEYWORD3 +LogLevel::INFO KEYWORD3 +LogLevel::WARNING KEYWORD3 +LogLevel::ERROR KEYWORD3 +LogLevel::FATAL KEYWORD3 \ No newline at end of file diff --git a/library.json b/library.json index 9665c26..76c474f 100644 --- a/library.json +++ b/library.json @@ -15,7 +15,7 @@ } ], "dependencies": {}, - "version": "1.1.4", + "version": "1.2.0", "frameworks": "arduino", "platforms": "*" } \ No newline at end of file diff --git a/library.properties b/library.properties index fd695f3..8250b63 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=AdvancedLogger -version=1.1.4 +version=1.2.0 author=Jibril Sharafi maintainer=Jibril Sharafi sentence=Library for simple logging to memory with comprehensive format. diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..b218423 Binary files /dev/null and b/logo.png differ diff --git a/src/AdvancedLogger.cpp b/src/AdvancedLogger.cpp index b6ee81d..839858b 100644 --- a/src/AdvancedLogger.cpp +++ b/src/AdvancedLogger.cpp @@ -1,141 +1,342 @@ #include "AdvancedLogger.h" -AdvancedLogger::AdvancedLogger(const char *logFilePath, const char *configFilePath) - : _logFilePath(logFilePath), _configFilePath(configFilePath) +// Macros +#define PROCESS_ARGS(format, function) \ + char _message[MAX_LOG_LENGTH]; \ + va_list args; \ + va_start(args, function); \ + vsnprintf(_message, sizeof(_message), format, args); \ + va_end(args); + +/** + * @brief Constructs a new AdvancedLogger object. + * + * This constructor initializes the AdvancedLogger object with the provided + * log file path, config file path, and timestamp format. If the provided paths + * or format are invalid, it uses the default values and logs a warning. + * + * @param logFilePath Path to the log file. If invalid, the default path will be used. + * @param configFilePath Path to the config file. If invalid, the default path will be used. + * @param timestampFormat Format for timestamps in the log. If invalid, the default format will be used. + */ +AdvancedLogger::AdvancedLogger( + const char *logFilePath = DEFAULT_LOG_PATH, + const char *configFilePath = DEFAULT_CONFIG_PATH, + const char *timestampFormat = DEFAULT_TIMESTAMP_FORMAT) + : _logFilePath(logFilePath), + _configFilePath(configFilePath), + _timestampFormat(timestampFormat) { - _printLevel = ADVANCEDLOGGER_DEFAULT_PRINT_LEVEL; - _saveLevel = ADVANCEDLOGGER_DEFAULT_SAVE_LEVEL; - _maxLogLines = ADVANCEDLOGGER_DEFAULT_MAX_LOG_LINES; + if (!_isValidPath(_logFilePath.c_str()) || !_isValidPath(_configFilePath.c_str())) + { + log_w( + "Invalid path for log %s or config file %s, using default paths: %s and %s", + _logFilePath.c_str(), + _configFilePath.c_str(), + DEFAULT_LOG_PATH, + DEFAULT_CONFIG_PATH); + + _logFilePath = DEFAULT_LOG_PATH; + _configFilePath = DEFAULT_CONFIG_PATH; + _invalidPath = true; + } - _logLines = 0; + if (!_isValidTimestampFormat(_timestampFormat)) + { + log_w( + "Invalid timestamp format %s, using default format: %s", + _timestampFormat, + DEFAULT_TIMESTAMP_FORMAT); + + _timestampFormat = DEFAULT_TIMESTAMP_FORMAT; + _invalidTimestampFormat = true; + } } +/** + * @brief Initializes the AdvancedLogger object. + * + * Initializes the AdvancedLogger object by setting the configuration + * from the SPIFFS filesystem. If the configuration file is not found, + * the default configuration is used. + * + */ void AdvancedLogger::begin() { - log("Initializing AdvancedLogger...", "AdvancedLogger::begin", ADVANCEDLOGGER_DEBUG); + debug("AdvancedLogger initializing...", "AdvancedLogger::begin"); if (!_setConfigFromSpiffs()) { - setDefaultLogLevels(); + log_w("Failed to set config from filesystem, using default config"); + setDefaultConfig(); } _logLines = getLogLines(); - log("AdvancedLogger initialized", "AdvancedLogger::begin", ADVANCEDLOGGER_DEBUG); + if (_invalidPath) + { + warning("Invalid path for log or config file, using default paths", "AdvancedLogger::begin"); + } + if (_invalidTimestampFormat) + { + warning("Invalid timestamp format, using default format", "AdvancedLogger::begin"); + } + + debug("AdvancedLogger initialized", "AdvancedLogger::begin"); +} + +/** + * @brief Logs a verbose message. + * + * This method logs a verbose message with the provided format and function name. + * + * @param format Format of the message. + * @param function Name of the function where the message is logged. + * @param ... Arguments to be formatted into the message using the printf format. +*/ +void AdvancedLogger::verbose(const char *format, const char *function = "unknown", ...) +{ + PROCESS_ARGS(format, function); + _log(_message, function, LogLevel::VERBOSE); +} + +/** + * @brief Logs a debug message. + * + * This method logs a debug message with the provided format and function name. + * + * @param format Format of the message. + * @param function Name of the function where the message is logged. + * @param ... Arguments to be formatted into the message using the printf format. +*/ +void AdvancedLogger::debug(const char *format, const char *function = "unknown", ...) +{ + PROCESS_ARGS(format, function); + _log(_message, function, LogLevel::DEBUG); +} + +/** + * @brief Logs an info message. + * + * This method logs an info message with the provided format and function name. + * + * @param format Format of the message. + * @param function Name of the function where the message is logged. + * @param ... Arguments to be formatted into the message using the printf format. +*/ +void AdvancedLogger::info(const char *format, const char *function = "unknown", ...) +{ + PROCESS_ARGS(format, function); + _log(_message, function, LogLevel::INFO); +} + +/** + * @brief Logs a warning message. + * + * This method logs a warning message with the provided format and function name. + * + * @param format Format of the message. + * @param function Name of the function where the message is logged. + * @param ... Arguments to be formatted into the message using the printf format. +*/ +void AdvancedLogger::warning(const char *format, const char *function = "unknown", ...) +{ + PROCESS_ARGS(format, function); + _log(_message, function, LogLevel::WARNING); +} + +/** + * @brief Logs an error message. + * + * This method logs an error message with the provided format and function name. + * + * @param format Format of the message. + * @param function Name of the function where the message is logged. + * @param ... Arguments to be formatted into the message using the printf format. +*/ +void AdvancedLogger::error(const char *format, const char *function = "unknown", ...) +{ + PROCESS_ARGS(format, function); + _log(_message, function, LogLevel::ERROR); +} + +/** + * @brief Logs a fatal message. + * + * This method logs a fatal message with the provided format and function name. + * + * @param format Format of the message. + * @param function Name of the function where the message is logged. + * @param ... Arguments to be formatted into the message using the printf format. +*/ +void AdvancedLogger::fatal(const char *format, const char *function = "unknown", ...) +{ + PROCESS_ARGS(format, function); + _log(_message, function, LogLevel::FATAL); } -void AdvancedLogger::log(const char *message, const char *function, int logLevel) +/** + * @brief Logs a message with a specific log level. + * + * This method logs a message with the provided format, function name, and log level. + * + * @param format Format of the message. + * @param function Name of the function where the message is logged. + * @param logLevel Log level of the message. + * @param ... Arguments to be formatted into the message using the printf format. +*/ +void AdvancedLogger::_log(const char *message, const char *function, LogLevel logLevel) { - logLevel = _saturateLogLevel(logLevel); - if (logLevel < _printLevel && logLevel < _saveLevel) + if (static_cast(logLevel) < static_cast(_printLevel) && (static_cast(logLevel) < static_cast(_saveLevel))) { + log_d("Message not logged due to log level too low"); return; } - char _message_formatted[26 + strlen(message) + strlen(function) + 70]; // 26 = length of timestamp, 70 = length of log level + char _messageFormatted[MAX_LOG_LENGTH]; snprintf( - _message_formatted, - sizeof(_message_formatted), - ADVANCEDLOGGER_FORMAT, + _messageFormatted, + sizeof(_messageFormatted), + LOG_FORMAT, _getTimestamp().c_str(), - millis(), - _logLevelToString(logLevel).c_str(), - xPortGetCoreID(), + _formatMillis(millis()).c_str(), + logLevelToString(logLevel, false).c_str(), + CORE_ID, function, message); - if (logLevel >= _printLevel) - { - Serial.println(_message_formatted); - } + Serial.println(_messageFormatted); if (logLevel >= _saveLevel) { - _save(_message_formatted); + _save(_messageFormatted); if (_logLines >= _maxLogLines) { - _logLines = 0; clearLog(); - log( - ("Log cleared due to max log lines (" + String(_maxLogLines) + ") reached").c_str(), - "AdvancedLogger::log", - ADVANCEDLOGGER_WARNING); } } } -void AdvancedLogger::logOnly(const char *message, const char *function, int logLevel) +/** + * @brief Logs a message with a specific log level and prints it. + * + * This method logs a message with the provided format, function name, and log level. + * It also prints the message to the Serial monitor. + * + * @param format Format of the message. + * @param function Name of the function where the message is logged. + * @param logLevel Log level of the message. + * @param ... Arguments to be formatted into the message using the printf format. +*/ +void AdvancedLogger::_logPrint(const char *format, const char *function, LogLevel logLevel, ...) { - logLevel = _saturateLogLevel(logLevel); - if (logLevel < _printLevel) + if (static_cast(logLevel) < static_cast(_printLevel) && (static_cast(logLevel) < static_cast(_saveLevel))) { + log_d("Message not logged due to log level too low"); return; } - char _message_formatted[26 + strlen(message) + strlen(function) + 70]; // 26 = length of timestamp, 70 = length of log level + PROCESS_ARGS(format, logLevel); + char logMessage[MAX_LOG_LENGTH + 200]; snprintf( - _message_formatted, - sizeof(_message_formatted), - ADVANCEDLOGGER_FORMAT, + logMessage, + sizeof(logMessage), + LOG_FORMAT, _getTimestamp().c_str(), - millis(), - _logLevelToString(logLevel).c_str(), - xPortGetCoreID(), + _formatMillis(millis()).c_str(), + logLevelToString(logLevel, false).c_str(), + CORE_ID, function, - message); + _message); - if (logLevel >= _printLevel) - { - Serial.println(_message_formatted); - } + Serial.println(logMessage); } -void AdvancedLogger::setPrintLevel(int level) +/** + * @brief Sets the print level. + * + * This method sets the print level to the provided log level. + * + * @param logLevel Log level to set. +*/ +void AdvancedLogger::setPrintLevel(LogLevel logLevel) { - log( - ("Setting print level to " + String(level)).c_str(), - "AdvancedLogger::setPrintLevel", - ADVANCEDLOGGER_INFO); - _printLevel = _saturateLogLevel(level); + debug("Setting print level to %s", logLevelToString(logLevel).c_str(), "AdvancedLogger::setPrintLevel"); + _printLevel = logLevel; _saveConfigToSpiffs(); } -void AdvancedLogger::setSaveLevel(int level) +/** + * @brief Sets the save level. + * + * This method sets the save level to the provided log level. + * + * @param logLevel Log level to set. +*/ +void AdvancedLogger::setSaveLevel(LogLevel logLevel) { - log( - ("Setting save level to " + String(level)).c_str(), - "AdvancedLogger::setSaveLevel", - ADVANCEDLOGGER_INFO); - _saveLevel = _saturateLogLevel(level); + debug("Setting save level to %s", logLevelToString(logLevel).c_str(), "AdvancedLogger::setSaveLevel"); + _saveLevel = logLevel; _saveConfigToSpiffs(); } -String AdvancedLogger::getPrintLevel() +/** + * @brief Gets the print level. + * + * This method returns the current print level. + * + * @return LogLevel Current print level. +*/ +LogLevel AdvancedLogger::getPrintLevel() { - return _logLevelToString(_printLevel); + return _printLevel; } -String AdvancedLogger::getSaveLevel() +/** + * @brief Gets the save level. + * + * This method returns the current save level. + * + * @return LogLevel Current save level. +*/ +LogLevel AdvancedLogger::getSaveLevel() { - return _logLevelToString(_saveLevel); + return _saveLevel; } -void AdvancedLogger::setDefaultLogLevels() +/** + * @brief Sets the configuration to default. + * + * This method sets the configuration to the default values. +*/ +void AdvancedLogger::setDefaultConfig() { - setPrintLevel(ADVANCEDLOGGER_DEFAULT_PRINT_LEVEL); - setSaveLevel(ADVANCEDLOGGER_DEFAULT_SAVE_LEVEL); - setMaxLogLines(ADVANCEDLOGGER_DEFAULT_MAX_LOG_LINES); + debug("Setting config to default...", "AdvancedLogger::setDefaultConfig"); + + setPrintLevel(DEFAULT_PRINT_LEVEL); + setSaveLevel(DEFAULT_SAVE_LEVEL); + setMaxLogLines(DEFAULT_MAX_LOG_LINES); - log("Log levels set to default", "AdvancedLogger::setDefaultLogLevels", ADVANCEDLOGGER_INFO); + debug("Config set to default", "AdvancedLogger::setDefaultConfig"); } +/** + * @brief Sets the maximum number of log lines. + * + * This method sets the maximum number of log lines to the provided value. + * + * @param maxLogLines Maximum number of log lines. +*/ bool AdvancedLogger::_setConfigFromSpiffs() { + debug("Setting config from filesystem...", "AdvancedLogger::_setConfigFromSpiffs"); + File _file = SPIFFS.open(_configFilePath, "r"); if (!_file) { - log("Failed to open config file for reading", "AdvancedLogger::_setConfigFromSpiffs", ADVANCEDLOGGER_ERROR); + LOG_E("Failed to open config file for reading"); + _logPrint("Failed to open config file", "AdvancedLogger::_setConfigFromSpiffs", LogLevel::ERROR); return false; } @@ -145,14 +346,15 @@ bool AdvancedLogger::_setConfigFromSpiffs() int separatorPosition = line.indexOf('='); String key = line.substring(0, separatorPosition); String value = line.substring(separatorPosition + 1); + value.trim(); if (key == "printLevel") { - setPrintLevel(value.toInt()); + setPrintLevel(_charToLogLevel(value.c_str())); } else if (key == "saveLevel") { - setSaveLevel(value.toInt()); + setSaveLevel(_charToLogLevel(value.c_str())); } else if (key == "maxLogLines") { @@ -161,43 +363,66 @@ bool AdvancedLogger::_setConfigFromSpiffs() } _file.close(); - log("Log levels set from SPIFFS", "AdvancedLogger::_setConfigFromSpiffs", ADVANCEDLOGGER_DEBUG); + + debug("Config set from filesystem", "AdvancedLogger::_setConfigFromSpiffs"); return true; } +/** + * @brief Saves the configuration to the SPIFFS filesystem. + * + * This method saves the configuration to the SPIFFS filesystem. +*/ void AdvancedLogger::_saveConfigToSpiffs() { + debug("Saving config to filesystem...", "AdvancedLogger::_saveConfigToSpiffs"); File _file = SPIFFS.open(_configFilePath, "w"); if (!_file) { - log("Failed to open config file for writing", "AdvancedLogger::_saveConfigToSpiffs", ADVANCEDLOGGER_ERROR); + LOG_E("Failed to open config file for writing"); + _logPrint("Failed to open config file", "AdvancedLogger::_saveConfigToSpiffs", LogLevel::ERROR); return; } - _file.println(String("printLevel=") + String(_printLevel)); - _file.println(String("saveLevel=") + String(_saveLevel)); + _file.println(String("printLevel=") + logLevelToString(_printLevel)); + _file.println(String("saveLevel=") + logLevelToString(_saveLevel)); _file.println(String("maxLogLines=") + String(_maxLogLines)); _file.close(); - log("Log levels saved to SPIFFS", "AdvancedLogger::_saveConfigToSpiffs", ADVANCEDLOGGER_DEBUG); + + debug("Config saved to filesystem", "AdvancedLogger::_saveConfigToSpiffs"); } -void AdvancedLogger::setMaxLogLines(int maxLines) +/** + * @brief Sets the maximum number of log lines. + * + * This method sets the maximum number of log lines to the provided value. + * + * @param maxLogLines Maximum number of log lines. +*/ +void AdvancedLogger::setMaxLogLines(int maxLogLines) { - log( - ("Setting max log lines to " + String(maxLines)).c_str(), - "AdvancedLogger::setMaxLogLines", - ADVANCEDLOGGER_INFO); - _maxLogLines = maxLines; + debug( + ("Setting max log lines to " + String(maxLogLines)).c_str(), + "AdvancedLogger::setMaxLogLines"); + _maxLogLines = maxLogLines; _saveConfigToSpiffs(); } +/** + * @brief Gets the number of log lines. + * + * This method returns the number of log lines in the log file. + * + * @return int Number of log lines. +*/ int AdvancedLogger::getLogLines() { File _file = SPIFFS.open(_logFilePath, "r"); if (!_file) { - logOnly("Failed to open log file", "AdvancedLogger::getLogLines", ADVANCEDLOGGER_ERROR); - return -1; + LOG_E("Failed to open log file for reading"); + _logPrint("Failed to open log file", "AdvancedLogger::getLogLines", LogLevel::ERROR); + return 0; } int lines = 0; @@ -212,102 +437,250 @@ int AdvancedLogger::getLogLines() return lines; } +/** + * @brief Clears the log. + * + * This method clears the log file. +*/ void AdvancedLogger::clearLog() { - logOnly("Clearing log", "AdvancedLogger::clearLog", ADVANCEDLOGGER_WARNING); - SPIFFS.remove(_logFilePath); File _file = SPIFFS.open(_logFilePath, "w"); if (!_file) { - logOnly("Failed to open log file", "AdvancedLogger::clearLog", ADVANCEDLOGGER_ERROR); + LOG_E("Failed to open log file for writing"); + _logPrint("Failed to open log file", "AdvancedLogger::clearLog", LogLevel::ERROR); return; } + _file.print(""); _file.close(); - log("Log cleared", "AdvancedLogger::clearLog", ADVANCEDLOGGER_WARNING); + _logLines = 0; + _logPrint("Log cleared", "AdvancedLogger::clearLog", LogLevel::INFO); } +/** + * @brief Saves a message to the log file. + * + * This method saves a message to the log file. + * + * @param messageFormatted Formatted message to save. +*/ void AdvancedLogger::_save(const char *messageFormatted) { File _file = SPIFFS.open(_logFilePath, "a"); - if (_file) + if (!_file) { - _file.println(messageFormatted); - _file.close(); - _logLines++; + LOG_E("Failed to open log file for writing"); + _logPrint("Failed to open log file", "AdvancedLogger::_save", LogLevel::ERROR); + return; } else { - logOnly("Failed to open log file", "AdvancedLogger::_save", ADVANCEDLOGGER_ERROR); + _file.println(messageFormatted); + _file.close(); + _logLines++; } } -void AdvancedLogger::dumpToSerial() +/** + * @brief Dumps the log to a Stream. + * + * Dump the log to a Stream, such as Serial or an opened file. + * + * @param stream Stream to dump the log to. +*/ +void AdvancedLogger::dump(Stream &stream) { - logOnly( - "Dumping log to Serial", - "AdvancedLogger::dumpToSerial", - ADVANCEDLOGGER_INFO); - - for (int i = 0; i < 2 * 50; i++) - Serial.print("_"); - Serial.println(); + debug("Dumping log to Stream...", "AdvancedLogger::dump"); File _file = SPIFFS.open(_logFilePath, "r"); if (!_file) { - logOnly("Failed to open log file", "AdvancedLogger::dumpToSerial", ADVANCEDLOGGER_ERROR); + LOG_E("Failed to open log file for reading"); + _logPrint("Failed to open log file", "AdvancedLogger::dump", LogLevel::ERROR); return; } + while (_file.available()) { - Serial.write(_file.read()); - Serial.flush(); + stream.write(_file.read()); } + stream.flush(); _file.close(); - for (int i = 0; i < 2 * 50; i++) - Serial.print("_"); - Serial.println(); - - logOnly( - "Log dumped to Serial", - "AdvancedLogger::dumpToSerial", - ADVANCEDLOGGER_INFO); + debug("Log dumped to Stream", "AdvancedLogger::dump"); } -String AdvancedLogger::_logLevelToString(int logLevel) +/** + * @brief Converts a log level to a string. + * + * This method converts a log level to a string. + * + * @param logLevel Log level to convert. + * @param trim Whether to trim the string. + * @return String String representation of the log level. +*/ +String AdvancedLogger::logLevelToString(LogLevel logLevel, bool trim) { + String logLevelStr; + switch (logLevel) { - case ADVANCEDLOGGER_VERBOSE: - return "VERBOSE"; - case ADVANCEDLOGGER_DEBUG: - return "DEBUG"; - case ADVANCEDLOGGER_INFO: - return "INFO"; - case ADVANCEDLOGGER_WARNING: - return "WARNING"; - case ADVANCEDLOGGER_ERROR: - return "ERROR"; - case ADVANCEDLOGGER_FATAL: - return "FATAL"; + case LogLevel::DEBUG: + logLevelStr = "DEBUG "; + break; + case LogLevel::INFO: + logLevelStr = "INFO "; + break; + case LogLevel::WARNING: + logLevelStr = "WARNING"; + break; + case LogLevel::ERROR: + logLevelStr = "ERROR "; + break; + case LogLevel::FATAL: + logLevelStr = "FATAL "; + break; default: - return "UNKNOWN"; + log_w("Unknown log level %d", static_cast(logLevel)); + logLevelStr = "UNKNOWN"; + break; + } + + if (trim) + { + logLevelStr.trim(); } + + return logLevelStr; } -int AdvancedLogger::_saturateLogLevel(int logLevel) +/** + * @brief Converts a character to a log level. + * + * This method converts a character to a log level. + * + * @param logLevelChar Character to convert. + * @return LogLevel Log level. +*/ +LogLevel AdvancedLogger::_charToLogLevel(const char *logLevelChar) { - return min(max(logLevel, ADVANCEDLOGGER_VERBOSE), ADVANCEDLOGGER_FATAL); + if (strcmp(logLevelChar, "DEBUG") == 0) + return LogLevel::DEBUG; + else if (strcmp(logLevelChar, "INFO") == 0) + return LogLevel::INFO; + else if (strcmp(logLevelChar, "WARNING") == 0) + return LogLevel::WARNING; + else if (strcmp(logLevelChar, "ERROR") == 0) + return LogLevel::ERROR; + else if (strcmp(logLevelChar, "FATAL") == 0) + return LogLevel::FATAL; + else + log_w( + "Unknown log level %s, using default log level %s", + logLevelChar, + logLevelToString(DEFAULT_PRINT_LEVEL).c_str()); + return DEFAULT_PRINT_LEVEL; } +/** + * @brief Gets the timestamp. + * + * This method gets the timestamp. + * + * @return String Timestamp. +*/ String AdvancedLogger::_getTimestamp() { - struct tm *_timeinfo; - char _timestamp[26]; + char _timestamp[1024]; - long _time = static_cast(time(nullptr)); - _timeinfo = localtime(&_time); - strftime(_timestamp, sizeof(_timestamp), ADVANCEDLOGGER_TIMESTAMP_FORMAT, _timeinfo); + time_t _time = time(nullptr); + struct tm _timeinfo = *localtime(&_time); + strftime(_timestamp, sizeof(_timestamp), _timestampFormat, &_timeinfo); return String(_timestamp); } + +/** + * @brief Checks if a path is valid. + * + * This method checks if a path is valid. + * + * @param path Path to check. + * @return bool Whether the path is valid. +*/ +bool AdvancedLogger::_isValidPath(const char *path) +{ + const char *invalidChars = "<>:\"\\|?*"; + const char *invalidStartChars = ". "; + const char *invalidEndChars = " ."; + const int filesystemMaxPathLength = 255; + + for (size_t i = 0; i < strlen(invalidChars); i++) + { + if (strchr(path, invalidChars[i]) != nullptr) + { + return false; + } + } + + for (size_t i = 0; i < strlen(invalidStartChars); i++) + { + if (path[0] == invalidStartChars[i]) + { + return false; + } + } + + for (size_t i = 0; i < strlen(invalidEndChars); i++) + { + if (path[strlen(path) - 1] == invalidEndChars[i]) + { + return false; + } + } + + if (strlen(path) > filesystemMaxPathLength) + { + return false; + } + + return true; +} + +/** + * @brief Checks if a timestamp format is valid. + * + * This method checks if a timestamp format is valid. + * + * @param format Timestamp format to check. + * @return bool Whether the timestamp format is valid. +*/ +bool AdvancedLogger::_isValidTimestampFormat(const char *format) +{ + if (_getTimestamp().length() > 0) + { + return true; + } + else + { + return false; + } +} + +/** + * @brief Formats milliseconds. + * + * This method formats milliseconds. + * + * @param millis Milliseconds to format. + * @return std::string Formatted milliseconds. +*/ +std::string AdvancedLogger::_formatMillis(unsigned long millis) { + std::string str = std::to_string(millis); + int n = str.length(); + int insertPosition = n - 3; + while (insertPosition > 0) { + str.insert(insertPosition, " "); + insertPosition -= 3; + } + return str; +} \ No newline at end of file diff --git a/src/AdvancedLogger.h b/src/AdvancedLogger.h index 8915825..cad853a 100644 --- a/src/AdvancedLogger.h +++ b/src/AdvancedLogger.h @@ -5,83 +5,111 @@ * advanced logging for the ESP32. * * Author: Jibril Sharafi, @jibrilsharafi - * Date: 12/05/2024 + * Date: 22/05/2024 * GitHub repository: https://github.com/jibrilsharafi/AdvancedLogger - * Version: 1.1.4 + * Version: 1.2.0 * * This library is licensed under the MIT License. See the LICENSE file for more information. * - * This library provides advanced logging for the ESP32. - * It allows you to log messages to the console and to a file on the SPIFFS. - * You can set the print level and save level, and the library will only log - * messages accordingly. -*/ + * This library provides advanced logging capabilities for the ESP32, allowing you to log messages + * to the console and to a file on the SPIFFS. + */ #ifndef ADVANCEDLOGGER_H #define ADVANCEDLOGGER_H -#define ADVANCEDLOGGER_VERBOSE 0 -#define ADVANCEDLOGGER_DEBUG 1 -#define ADVANCEDLOGGER_INFO 2 -#define ADVANCEDLOGGER_WARNING 3 -#define ADVANCEDLOGGER_ERROR 4 -#define ADVANCEDLOGGER_FATAL 5 +#include +#include -#define ADVANCEDLOGGER_DEFAULT_PRINT_LEVEL 1 // 1 = DEBUG -#define ADVANCEDLOGGER_DEFAULT_SAVE_LEVEL 2 // 2 = INFO -#define ADVANCEDLOGGER_DEFAULT_MAX_LOG_LINES 1000 // 1000 lines before the log is cleared +#define CORE_ID xPortGetCoreID() +#define LOG_D(format, ...) log_d(format, ##__VA_ARGS__) +#define LOG_I(format, ...) log_i(format, ##__VA_ARGS__) +#define LOG_W(format, ...) log_w(format, ##__VA_ARGS__) +#define LOG_E(format, ...) log_e(format, ##__VA_ARGS__) + +enum class LogLevel : int { + VERBOSE = 0, + DEBUG = 1, + INFO = 2, + WARNING = 3, + ERROR = 4, + FATAL = 5 +}; -#define ADVANCEDLOGGER_TIMESTAMP_FORMAT "%Y-%m-%d %H:%M:%S" -#define ADVANCEDLOGGER_FORMAT "[%s] [%lu ms] [%s] [Core %d] [%s] %s" // [TIME] [MICROS us] [LOG_LEVEL] [Core CORE] [FUNCTION] MESSAGE +constexpr const LogLevel DEFAULT_PRINT_LEVEL = LogLevel::INFO; +constexpr const LogLevel DEFAULT_SAVE_LEVEL = LogLevel::WARNING; -#define ADVANCEDLOGGER_LOG_PATH "/AdvancedLogger/log.txt" -#define ADVANCEDLOGGER_CONFIG_PATH "/AdvancedLogger/config.txt" +constexpr int MAX_LOG_LENGTH = 1024; -#include +constexpr const char* DEFAULT_LOG_PATH = "/AdvancedLogger/log.txt"; +constexpr const char* DEFAULT_CONFIG_PATH = "/AdvancedLogger/config.txt"; -#include +constexpr int DEFAULT_MAX_LOG_LINES = 1000; + +constexpr const char* DEFAULT_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"; + +constexpr const char* LOG_FORMAT = "[%s] [%s ms] [%s] [Core %d] [%s] %s"; // [TIME] [MILLIS ms] [LOG_LEVEL] [Core CORE] [FUNCTION] MESSAGE class AdvancedLogger { public: - AdvancedLogger(const char *logFilePath = ADVANCEDLOGGER_LOG_PATH, const char *configFilePath = ADVANCEDLOGGER_CONFIG_PATH); + AdvancedLogger( + const char *logFilePath, + const char *configFilePath, + const char *timestampFormat); void begin(); - void log(const char *message, const char *function, int logLevel); - void logOnly(const char *message, const char *function, int logLevel); - void setPrintLevel(int printLevel); - void setSaveLevel(int saveLevel); + void verbose(const char *format, const char *function, ...); + void debug(const char *format, const char *function, ...); + void info(const char *format, const char *function, ...); + void warning(const char *format, const char *function, ...); + void error(const char *format, const char *function, ...); + void fatal(const char *format, const char *function, ...); + + void setPrintLevel(LogLevel logLevel); + void setSaveLevel(LogLevel logLevel); - String getPrintLevel(); - String getSaveLevel(); + LogLevel getPrintLevel(); + LogLevel getSaveLevel(); - void setDefaultLogLevels(); + void setDefaultConfig(); - void setMaxLogLines(int maxLines); + void setMaxLogLines(int maxLogLines); int getLogLines(); void clearLog(); - void dumpToSerial(); + void dump(Stream& stream); + + String logLevelToString(LogLevel logLevel, bool trim = true); private: - String _logFilePath; - String _configFilePath; - - int _printLevel; - int _saveLevel; + String _logFilePath = DEFAULT_LOG_PATH; + String _configFilePath = DEFAULT_CONFIG_PATH; - int _maxLogLines; - int _logLines; - + LogLevel _printLevel = DEFAULT_PRINT_LEVEL; + LogLevel _saveLevel = DEFAULT_SAVE_LEVEL; + + int _maxLogLines = DEFAULT_MAX_LOG_LINES; + int _logLines = 0; + + void _log(const char *format, const char *function, LogLevel logLevel); + void _logPrint(const char *format, const char *function, LogLevel logLevel, ...); void _save(const char *messageFormatted); bool _setConfigFromSpiffs(); void _saveConfigToSpiffs(); - String _logLevelToString(int logLevel); - int _saturateLogLevel(int logLevel); + LogLevel _charToLogLevel(const char *logLevelStr); + const char *_timestampFormat = DEFAULT_TIMESTAMP_FORMAT; String _getTimestamp(); + + bool _invalidPath = false; + bool _invalidTimestampFormat = false; + bool _isValidPath(const char *path); + bool _isValidTimestampFormat(const char *format); + + std::string _formatMillis(unsigned long millis); }; #endif \ No newline at end of file diff --git a/why.md b/why.md new file mode 100644 index 0000000..abb5c07 --- /dev/null +++ b/why.md @@ -0,0 +1,45 @@ + +## Why? +There is no way of sugar-coat it: *every well-developed project will eventually need a proper logging* in place to make sure that not only everything is fully monitored, but also that everything monitored is later accessible for debugging. +The development of [EnergyMe](https://github.com/jibrilsharafi/EnergyMe-Home) (my open-source energy monitoring device) quickly led to the necessity of having such feature. Nonetheless, I quickly found out that no repositories on GitHub where available capable of fully covering all of my [Requirements](#requirements). + +### Requirements +Does it exist a C++ module for the ESP32 (the projects I am currently working on use this microcontroller) that allows logging with the following features: +- **Format**: a comprehensive format capable of including all the accessory information to each print message. Something like: `[2024-03-13 09:44:10] [1 313 ms] [INFO ] [Core 1] [main::setup] Setting up ADE7953...`, which indeed explains very clearly the nature and context of the message. This type of logging is the norm in more complex systems and environments. +- **Saving to memory**: developing a complex logging system is useless if it can only be accessed in real time. The ability of saving each message in chronological order in a long time storage medium is crucial to unlock the full potential of it. +- **Ease of use**: the module should comprise of only a few functions, such as the core one `info(...)` and the accessory ones needed to set the print and save levels to adapt to each use case. + +### Don't reinvent the wheel +Some existing libraries are: + +| Name | Owner | Description | Format | Saving | Ease of use | +| :------------------------------------------------------------------------------------------------: | :-----------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------: | :----: | :----: | :---------: | +| **[elog](https://github.com/x821938/elog)** | [x821938](https://github.com/x821938) | Fast and efficient logging to multiple outputs, with many configuration settings. | ❕ | ✔ | ✔ | +| **[ESP.multiLogger](https://github.com/voelkerb/ESP.multiLogger)** | [voelkerb](https://github.com/voelkerb) | Simple logging to multiple outputs. | ❌ | ✔ | ✔ | +| **[logging](https://github.com/esp32m/logging)** | [esp32m](https://github.com/esp32m) | Context-based logging, multiple appenders, buffers and queues. | ❌ | ✔ | ❕ | +| **[MycilaLogger](https://github.com/mathieucarbou/MycilaLogger)** | [mathieucarbou](https://github.com/mathieucarbou) | Simple and efficient. A wrapper around the standard `buffer.print()`. | ❌ | ❌ | ✔ | +| **[esp_log.h](https://github.com/espressif/esp-idf/blob/master/components/log/include/esp_log.h)** | [espressif](https://github.com/espressif) | Native method implemented by espressif. [Documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/log.html). | ❕ | ❌ | ❕ | + +Many other similar libraries exists, but it is clear that **none of the reported libraries** (and of the others available on *GitHub*) **fully match my requirements**. + +## How? +The rule is one: *keep it simple*. +I am in no way a advanced coder, and as such I tend to structure and write code to be as streamlined and simple as possible. So, no memory optimization or any of the pointer-magic stuff in C++. *Not my kink*. + +### Format +**The format** of the message should **include any information** that could store **any value**. As such, everything related to the time should be logged, along with information about the location of the code in which the log takes place, as well as having different logging levels. +We can set a custom format by using the function `sprintf` and passing the following as third argument: +- `[%s] [%lu ms] [%s] [Core %d] [%s] %s` → `[2024-03-13 09:44:10] [1313 ms] [INFO] [Core 1] [main::setup] Setting up ADE7953...` +which will represent the following parts: +- `[%s]` → `[2024-03-13 09:44:10]`: human-readable ISO8601 format in the format `%Y-%m-%d %H:%M:%S`. Indeed, the time must be set externally via other libraries. +- `[%lu ms]` → `[1313 ms]`: time passed since device boot, in *ms*. Crucial to precisely evaluate the time taken to perform any action. +- `[%s]` → `[INFO]`: the log level, which could be one of the following 5 levels: *DEBUG, INFO, WARNING, ERROR, FATAL*. +- `[Core %d]` → `[Core 1]`: core in which the operation takes place. Useful for multi-core devices (such as many ESP32 variations). +- `[%s]` → `[main::setup]`: the current function, or in general any name to identify a particular section of the code. +- `%s` → `Setting up ADE7953...`: the actual message that contains all the info needed to be logged. + +### Saving to memory +Saving should be quite straightforward: just copy any message that is sent to the serial to a log file, appending it to the end with a newline. Whenever too many lines are present, the oldest ones should be deleted to keep the memory usage under control. + +### Ease of use +Last but not least, the use should be as simple as possible: you just need to create `AdvancedLogger advancedLogger;` and simply use `advancedLogger.info("Setting up ADE7953...", "main::setup");`. Other (more complex) functions and settings should be available, but not necessary for the basic use. As someone once said: *Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away*.