diff --git a/README.md b/README.md index bfd1f02..72e7a0f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![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. +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. Easily integrated into any project, the **callback function** allows for a wide range of applications, from sending logs to a server to displaying them on a screen. ## Installing @@ -31,6 +31,7 @@ Alternatively, the library can be installed manually by downloading the latest r The library so far has been successfully tested on the following microcontrollers: - ESP32S3 +- ESP-WROVER ## Usage @@ -92,6 +93,15 @@ The library provides the following public methods: - `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. +- `setCallback(LogCallback callback)`: Register a callback function that will be called whenever a log message is generated. The callback receives the following parameters: + - `timestamp`: Current formatted timestamp + - `millisEsp`: System uptime in milliseconds + - `level`: Log level as a lowercase string + - `coreId`: The core ID that generated the log + - `function`: Name of the function that generated the log + - `message`: The actual log message + +Example of using the callback: For a detailed example, see the [basicUsage](examples/basicUsage/basicUsage.ino) and [basicServer](examples/basicServer/basicServer.ino) in the examples folder. @@ -104,11 +114,11 @@ For more information regarding the necessity of this library, see the [WHY.md](W ### 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] **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] **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. 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] **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. - [ ] **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. diff --git a/examples/callbackHttpMqtt/callbackHttpMqtt.ino b/examples/callbackHttpMqtt/callbackHttpMqtt.ino new file mode 100644 index 0000000..14c0907 --- /dev/null +++ b/examples/callbackHttpMqtt/callbackHttpMqtt.ino @@ -0,0 +1,236 @@ +/* + * File: callbackHttpMqtt.ino + * ---------------------------- + * This example demonstrates integrating AdvancedLogger with MQTT and HTTP logging. + * It shows how to: + * - Forward logs to an HTTP endpoint + * - Send logs to a local MQTT broker + * - Track logging performance metrics + * - Handle network reconnections + * - Format logs as JSON + * + * Author: Jibril Sharafi, @jibrilsharafi + * 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 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" + +// HTTP configuration +const String serverEndpoint = "YOUR_IP"; // **** CHANGE THIS TO YOUR SERVER **** +HTTPClient http; + +// MQTT configuration +const char* mqttServer = "YOUR_BROKER"; // **** CHANGE THIS TO YOUR BROKER **** +const unsigned int mqttPort = 1883; +const char* mainTopic = "advancedlogger/log"; +const unsigned int bufferSize = 1024; + +// **** CHANGE THESE TO YOUR SSID AND PASSWORD **** +const char *ssid = "SSID"; +const char *password = "PASSWORD"; + +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); + +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"; + +unsigned long lastMillisLogClear = 0; +unsigned long intervalLogClear = 60000; +unsigned int maxLogLines = 100; // Low value for testing purposes + +// Add after WiFi client +WiFiClient espClient; +PubSubClient mqttClient(espClient); + +// Modified callback function to make the JSON in the callback faster +char jsonBuffer[512]; // Pre-allocated buffer +String cachedDeviceId; +String cachedTopicPrefix; + +// Get device ID from MAC +String getDeviceId() { + return String((uint32_t)ESP.getEfuseMac(), HEX); +} + +/* +This callback function will be called by the AdvancedLogger +whenever a log is generated. It will pass the log information, +then the function will decide what to do with it (eg. based on +the level, it may decide to send it to an HTTP endpoint or to +set a flag). + +In this example, the function will: +- Format the log as JSON +- Send the log to an HTTP endpoint +- Publish the log to an MQTT topic + +The function will also measure the time taken to format the JSON, +send the HTTP request, and publish the MQTT message. +*/ +void callback( + const char* timestamp, + unsigned long millisEsp, + const char* level, + unsigned int coreId, + const char* function, + const char* message +) { + if (WiFi.status() != WL_CONNECTED) return; + + unsigned long startJson = micros(); + + snprintf(jsonBuffer, sizeof(jsonBuffer), + "{\"timestamp\":\"%s\"," + "\"millis\":%lu," + "\"level\":\"%s\"," + "\"core\":%u," + "\"function\":\"%s\"," + "\"message\":\"%s\"}", + timestamp, + millisEsp, + level, + coreId, + function, + message); + + unsigned long jsonTime = micros() - startJson; + + // HTTP POST + unsigned long startHttp = micros(); + HTTPClient http; + http.begin(serverEndpoint); + http.addHeader("Content-Type", "application/json"); + int httpCode = http.POST(jsonBuffer); + if (httpCode != HTTP_CODE_OK) { + Serial.printf("HTTP POST failed: %s\n", http.errorToString(httpCode).c_str()); + } + http.end(); + unsigned long httpTime = micros() - startHttp; + + // MQTT Publish + unsigned long startMqtt = micros(); + if (mqttClient.connected()) { + if (cachedDeviceId.isEmpty()) { + cachedDeviceId = getDeviceId(); + cachedTopicPrefix = String(mainTopic) + "/" + cachedDeviceId + "/log/"; + } + + String topic = cachedTopicPrefix + String(level); + + if (!mqttClient.publish(topic.c_str(), jsonBuffer)) { + Serial.printf("MQTT publish failed to %s. Error: %d\n", + topic.c_str(), mqttClient.state()); + } + } + unsigned long mqttTime = micros() - startMqtt; + + Serial.printf("Durations - JSON: %lu µs, HTTP: %lu µs, MQTT: %lu µs\n", + jsonTime, httpTime, mqttTime); +} + +void reconnectMQTT() { + while (!mqttClient.connected()) { + String clientId = "ESP32Client-" + getDeviceId(); + if (mqttClient.connect(clientId.c_str())) { + logger.info("MQTT Connected", "reconnectMQTT"); + } else { + logger.error("MQTT Connection failed, rc=%d", "reconnectMQTT", mqttClient.state()); + } + } +} + +void setup() +{ + // Initialize Serial and SPIFFS (mandatory for the AdvancedLogger library) + // -------------------- + Serial.begin(115200); + + if (!SPIFFS.begin(true)) // Setting to true will format the SPIFFS if mounting fails + { + Serial.println("An Error has occurred while mounting SPIFFS"); + } + + logger.begin(); + logger.setMaxLogLines(maxLogLines); + logger.setCallback(callback); + + 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.info("Connecting to WiFi... SSID: %s | Password: %s", "basicServer::setup", ssid, password); + } + + logger.info(("IP address: " + WiFi.localIP().toString()).c_str(), "basicServer::setup"); + + mqttClient.setServer(mqttServer, mqttPort); + mqttClient.setBufferSize(bufferSize); // Raise the buffer size as the standard one is only 256 bytes + reconnectMQTT(); + + configTime(timeZone, daylightOffset, ntpServer1, ntpServer2, ntpServer3); + + logger.info("Server started!", "basicServer::setup"); + + logger.info("Setup done!", "basicServer::setup"); +} + +void loop() +{ + if (!mqttClient.connected()) { + reconnectMQTT(); + } + mqttClient.loop(); + + // Test a burst of messages to see the performance + for (int i = 0; i < 10; i++) { + logger.verbose("[BURST] This is a verbose message", "basicServer::loop"); + logger.debug("[BURST] This is a debug message!", "basicServer::loop"); + logger.info("[BURST] This is an info message!!", "basicServer::loop"); + logger.warning("[BURST] This is a warning message!!!", "basicServer::loop"); + logger.error("[BURST] This is a error message!!!!", "basicServer::loop"); + logger.fatal("[BURST] This is a fatal message!!!!!", "basicServer::loop"); + } + + logger.debug("This is a debug message!", "basicServer::loop"); + delay(500); + logger.info("This is an info message!!", "basicServer::loop"); + delay(500); + logger.warning("This is a warning message!!!", "basicServer::loop"); + delay(500); + logger.error("This is a error message!!!!", "basicServer::loop"); + delay(500); + logger.fatal("This is a fatal message!!!!!", "basicServer::loop"); + delay(500); + logger.info("This is an info message!!", "basicServer::loop", true); + delay(1000); +} \ No newline at end of file diff --git a/library.json b/library.json index 8ab7b35..ee5ec58 100644 --- a/library.json +++ b/library.json @@ -15,7 +15,7 @@ } ], "dependencies": {}, - "version": "1.2.2", + "version": "1.3.0", "frameworks": "arduino", "platforms": "*" } \ No newline at end of file diff --git a/library.properties b/library.properties index 49c981e..2078f09 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=AdvancedLogger -version=1.2.2 +version=1.3.0 author=Jibril Sharafi maintainer=Jibril Sharafi sentence=Library for simple logging to memory with comprehensive format. diff --git a/src/AdvancedLogger.cpp b/src/AdvancedLogger.cpp index 424859c..36bf915 100644 --- a/src/AdvancedLogger.cpp +++ b/src/AdvancedLogger.cpp @@ -20,16 +20,16 @@ * @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) + const char *logFilePath, + const char *configFilePath, + const char *timestampFormat) : _logFilePath(logFilePath), _configFilePath(configFilePath), _timestampFormat(timestampFormat) { if (!_isValidPath(_logFilePath.c_str()) || !_isValidPath(_configFilePath.c_str())) { - log_w( + Serial.printf( "Invalid path for log %s or config file %s, using default paths: %s and %s", _logFilePath.c_str(), _configFilePath.c_str(), @@ -43,7 +43,7 @@ AdvancedLogger::AdvancedLogger( if (!_isValidTimestampFormat(_timestampFormat)) { - log_w( + Serial.printf( "Invalid timestamp format %s, using default format: %s", _timestampFormat, DEFAULT_TIMESTAMP_FORMAT); @@ -67,7 +67,7 @@ void AdvancedLogger::begin() if (!_setConfigFromSpiffs()) { - log_w("Failed to set config from filesystem, using default config"); + Serial.printf("Failed to set config from filesystem, using default config"); setDefaultConfig(); } _logLines = getLogLines(); @@ -186,11 +186,7 @@ void AdvancedLogger::fatal(const char *format, const char *function = "unknown", */ void AdvancedLogger::_log(const char *message, const char *function, LogLevel logLevel) { - 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; - } + if ((logLevel < _printLevel) && (logLevel < _saveLevel)) return; char _messageFormatted[MAX_LOG_LENGTH]; @@ -200,7 +196,7 @@ void AdvancedLogger::_log(const char *message, const char *function, LogLevel lo LOG_FORMAT, _getTimestamp().c_str(), _formatMillis(millis()).c_str(), - logLevelToString(logLevel, false).c_str(), + logLevelToString(logLevel, false), CORE_ID, function, message); @@ -210,10 +206,17 @@ void AdvancedLogger::_log(const char *message, const char *function, LogLevel lo if (logLevel >= _saveLevel) { _save(_messageFormatted); - if (_logLines >= _maxLogLines) - { - clearLogKeepLatestXPercent(); - } + } + + if (_callback) { + _callback( + _getTimestamp().c_str(), + millis(), + logLevelToStringLower(logLevel), + CORE_ID, + function, + message + ); } } @@ -230,11 +233,7 @@ void AdvancedLogger::_log(const char *message, const char *function, LogLevel lo */ void AdvancedLogger::_logPrint(const char *format, const char *function, LogLevel logLevel, ...) { - 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; - } + if (logLevel < _printLevel && (logLevel < _saveLevel)) return; PROCESS_ARGS(format, logLevel); char logMessage[MAX_LOG_LENGTH + 200]; @@ -245,7 +244,7 @@ void AdvancedLogger::_logPrint(const char *format, const char *function, LogLeve LOG_FORMAT, _getTimestamp().c_str(), _formatMillis(millis()).c_str(), - logLevelToString(logLevel, false).c_str(), + logLevelToString(logLevel, false), CORE_ID, function, _message); @@ -262,7 +261,7 @@ void AdvancedLogger::_logPrint(const char *format, const char *function, LogLeve */ void AdvancedLogger::setPrintLevel(LogLevel logLevel) { - debug("Setting print level to %s", "AdvancedLogger::setPrintLevel", logLevelToString(logLevel).c_str()); + debug("Setting print level to %s", "AdvancedLogger::setPrintLevel", logLevelToString(logLevel)); _printLevel = logLevel; _saveConfigToSpiffs(); } @@ -276,7 +275,7 @@ void AdvancedLogger::setPrintLevel(LogLevel logLevel) */ void AdvancedLogger::setSaveLevel(LogLevel logLevel) { - debug("Setting save level to %s", "AdvancedLogger::setSaveLevel", logLevelToString(logLevel).c_str()); + debug("Setting save level to %s", "AdvancedLogger::setSaveLevel", logLevelToString(logLevel)); _saveLevel = logLevel; _saveConfigToSpiffs(); } @@ -335,13 +334,15 @@ bool AdvancedLogger::_setConfigFromSpiffs() File _file = SPIFFS.open(_configFilePath, "r"); if (!_file) { - LOG_E("Failed to open config file for reading"); + Serial.printf("Failed to open config file for reading"); _logPrint("Failed to open config file", "AdvancedLogger::_setConfigFromSpiffs", LogLevel::ERROR); return false; } - while (_file.available()) + int _loopCount = 0; + while (_file.available() && _loopCount < MAX_WHILE_LOOP_COUNT) { + _loopCount++; String line = _file.readStringUntil('\n'); int separatorPosition = line.indexOf('='); String key = line.substring(0, separatorPosition); @@ -379,7 +380,7 @@ void AdvancedLogger::_saveConfigToSpiffs() File _file = SPIFFS.open(_configFilePath, "w"); if (!_file) { - LOG_E("Failed to open config file for writing"); + Serial.printf("Failed to open config file for writing"); _logPrint("Failed to open config file", "AdvancedLogger::_saveConfigToSpiffs", LogLevel::ERROR); return; } @@ -420,14 +421,16 @@ int AdvancedLogger::getLogLines() File _file = SPIFFS.open(_logFilePath, "r"); if (!_file) { - LOG_E("Failed to open log file for reading"); + Serial.printf("Failed to open log file for reading"); _logPrint("Failed to open log file", "AdvancedLogger::getLogLines", LogLevel::ERROR); return 0; } int lines = 0; - while (_file.available()) + int _loopCount = 0; + while (_file.available() && _loopCount < MAX_WHILE_LOOP_COUNT) { + _loopCount++; if (_file.read() == '\n') { lines++; @@ -447,7 +450,7 @@ void AdvancedLogger::clearLog() File _file = SPIFFS.open(_logFilePath, "w"); if (!_file) { - LOG_E("Failed to open log file for writing"); + Serial.printf("Failed to open log file for writing"); _logPrint("Failed to open log file", "AdvancedLogger::clearLog", LogLevel::ERROR); return; } @@ -463,48 +466,58 @@ void AdvancedLogger::clearLog() * This method clears the log file but retains the latest X percent of log entries. * The default value is 10%. */ -void AdvancedLogger::clearLogKeepLatestXPercent(int percent) +void AdvancedLogger::clearLogKeepLatestXPercent(int percent) { - File _file = SPIFFS.open(_logFilePath, "r"); - if (!_file) - { - LOG_E("Failed to open log file for reading"); - _logPrint("Failed to open log file", "AdvancedLogger::clearLogKeepLatestXPercent", LogLevel::ERROR); + File sourceFile = SPIFFS.open(_logFilePath, "r"); + if (!sourceFile) { + _logPrint("Failed to open source file", "AdvancedLogger::clearLogKeepLatestXPercent", LogLevel::ERROR); return; } - std::vector lines; - while (_file.available()) - { - String line = _file.readStringUntil('\n'); - lines.push_back(line.c_str()); + // Count lines first + size_t totalLines = 0; + while (sourceFile.available() && totalLines < MAX_WHILE_LOOP_COUNT) { + if (sourceFile.readStringUntil('\n').length() > 0) totalLines++; } - _file.close(); + sourceFile.seek(0); - size_t totalLines = lines.size(); + // Calculate lines to keep/skip percent = min(max(percent, 0), 100); - size_t linesToKeep = totalLines / 100 * percent; + size_t linesToKeep = (totalLines * percent) / 100; + size_t linesToSkip = totalLines - linesToKeep; - _file = SPIFFS.open(_logFilePath, "w"); - if (!_file) - { - LOG_E("Failed to open log file for writing"); - _logPrint("Failed to open log file", "AdvancedLogger::clearLogKeepLatestXPercent", LogLevel::ERROR); + File tempFile = SPIFFS.open(_logFilePath + ".tmp", "w"); + if (!tempFile) { + _logPrint("Failed to create temp file", "AdvancedLogger::clearLogKeepLatestXPercent", LogLevel::ERROR); + sourceFile.close(); return; } - for (size_t i = totalLines - linesToKeep; i < totalLines; ++i) - { - _file.print(lines[i].c_str()); + // Skip lines by reading + for (size_t i = 0; i < linesToSkip && sourceFile.available(); i++) { + sourceFile.readStringUntil('\n'); } - _file.close(); + + // Direct copy of remaining lines + int _loopCount = 0; + while (sourceFile.available() && _loopCount < MAX_WHILE_LOOP_COUNT) { + String line = sourceFile.readStringUntil('\n'); + if (line.length() > 0) { + tempFile.print(line); + } + } + + sourceFile.close(); + tempFile.close(); + + SPIFFS.remove(_logFilePath); + SPIFFS.rename(_logFilePath + ".tmp", _logFilePath); _logLines = linesToKeep; - _logPrint("Log cleared but kept the latest 10%", "AdvancedLogger::clearLogKeepLatestXPercent", LogLevel::INFO); + _logPrint("Log cleared keeping latest entries", + "AdvancedLogger::clearLogKeepLatestXPercent", LogLevel::INFO); } -// ... - /** * @brief Saves a message to the log file. * @@ -517,7 +530,7 @@ void AdvancedLogger::_save(const char *messageFormatted) File _file = SPIFFS.open(_logFilePath, "a"); if (!_file) { - LOG_E("Failed to open log file for writing"); + Serial.printf("Failed to open log file for writing"); _logPrint("Failed to open log file", "AdvancedLogger::_save", LogLevel::ERROR); return; } @@ -527,6 +540,8 @@ void AdvancedLogger::_save(const char *messageFormatted) _file.close(); _logLines++; } + + if (_logLines >= _maxLogLines) clearLogKeepLatestXPercent(); } /** @@ -543,13 +558,15 @@ void AdvancedLogger::dump(Stream &stream) File _file = SPIFFS.open(_logFilePath, "r"); if (!_file) { - LOG_E("Failed to open log file for reading"); + Serial.printf("Failed to open log file for reading"); _logPrint("Failed to open log file", "AdvancedLogger::dump", LogLevel::ERROR); return; } - while (_file.available()) + int _loopCount = 0; + while (_file.available() && _loopCount < MAX_WHILE_LOOP_COUNT) { + _loopCount++; stream.write(_file.read()); } stream.flush(); @@ -558,50 +575,6 @@ void AdvancedLogger::dump(Stream &stream) debug("Log dumped to Stream", "AdvancedLogger::dump"); } -/** - * @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 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: - log_w("Unknown log level %d", static_cast(logLevel)); - logLevelStr = "UNKNOWN"; - break; - } - - if (trim) - { - logLevelStr.trim(); - } - - return logLevelStr; -} - /** * @brief Converts a character to a log level. * @@ -623,10 +596,10 @@ LogLevel AdvancedLogger::_charToLogLevel(const char *logLevelChar) else if (strcmp(logLevelChar, "FATAL") == 0) return LogLevel::FATAL; else - log_w( + Serial.printf( "Unknown log level %s, using default log level %s", logLevelChar, - logLevelToString(DEFAULT_PRINT_LEVEL).c_str()); + logLevelToString(DEFAULT_PRINT_LEVEL)); return DEFAULT_PRINT_LEVEL; } @@ -720,14 +693,17 @@ bool AdvancedLogger::_isValidTimestampFormat(const char *format) * This method formats milliseconds. * * @param millis Milliseconds to format. - * @return std::string Formatted milliseconds. + * @return String Formatted milliseconds. */ -std::string AdvancedLogger::_formatMillis(unsigned long millis) { - std::string str = std::to_string(millis); +String AdvancedLogger::_formatMillis(unsigned long millisToFormat) { + String str = String(millisToFormat); int n = str.length(); int insertPosition = n - 3; - while (insertPosition > 0) { - str.insert(insertPosition, " "); + + int _loopCount = 0; + while (insertPosition > 0 && _loopCount < MAX_WHILE_LOOP_COUNT) { + _loopCount++; + str = str.substring(0, insertPosition) + " " + str.substring(insertPosition); insertPosition -= 3; } return str; diff --git a/src/AdvancedLogger.h b/src/AdvancedLogger.h index 567e868..bf3e9ef 100644 --- a/src/AdvancedLogger.h +++ b/src/AdvancedLogger.h @@ -20,7 +20,6 @@ #include #include -#include #define CORE_ID xPortGetCoreID() #define LOG_D(format, ...) log_d(format, ##__VA_ARGS__) @@ -29,12 +28,12 @@ #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 + VERBOSE, + DEBUG, + INFO, + WARNING, + ERROR, + FATAL }; constexpr const LogLevel DEFAULT_PRINT_LEVEL = LogLevel::DEBUG; @@ -46,18 +45,29 @@ constexpr const char* DEFAULT_LOG_PATH = "/AdvancedLogger/log.txt"; constexpr const char* DEFAULT_CONFIG_PATH = "/AdvancedLogger/config.txt"; constexpr int DEFAULT_MAX_LOG_LINES = 1000; +constexpr int MAX_WHILE_LOOP_COUNT = 10000; 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 +using LogCallback = std::function; + + class AdvancedLogger { public: AdvancedLogger( - const char *logFilePath, - const char *configFilePath, - const char *timestampFormat); + const char *logFilePath = DEFAULT_LOG_PATH, + const char *configFilePath = DEFAULT_CONFIG_PATH, + const char *timestampFormat = DEFAULT_TIMESTAMP_FORMAT); void begin(); @@ -83,7 +93,33 @@ class AdvancedLogger void dump(Stream& stream); - String logLevelToString(LogLevel logLevel, bool trim = true); + static const char* logLevelToString(LogLevel level, bool trim = true) { + switch (level) { + case LogLevel::VERBOSE: return trim ? "VERBOSE" : "VERBOSE "; + case LogLevel::DEBUG: return trim ? "DEBUG" : "DEBUG "; + case LogLevel::INFO: return trim ? "INFO" : "INFO "; + case LogLevel::WARNING: return trim ? "WARNING" : "WARNING "; + case LogLevel::ERROR: return trim ? "ERROR" : "ERROR "; + case LogLevel::FATAL: return trim ? "FATAL" : "FATAL "; + default: return "UNKNOWN"; + } + } + + static const char* logLevelToStringLower(LogLevel level) { + switch (level) { + case LogLevel::VERBOSE: return "verbose"; + case LogLevel::DEBUG: return "debug"; + case LogLevel::INFO: return "info"; + case LogLevel::WARNING: return "warning"; + case LogLevel::ERROR: return "error"; + case LogLevel::FATAL: return "fatal"; + default: return "unknown"; + } + } + + void setCallback(LogCallback callback) { + _callback = callback; + } private: String _logFilePath = DEFAULT_LOG_PATH; @@ -111,7 +147,9 @@ class AdvancedLogger bool _isValidPath(const char *path); bool _isValidTimestampFormat(const char *format); - std::string _formatMillis(unsigned long millis); + String _formatMillis(unsigned long millis); + + LogCallback _callback = nullptr; }; #endif \ No newline at end of file