From 8a2988cb50dcdb5d430936b1d67dbcd7998600f0 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Tue, 16 Sep 2025 10:10:23 +0200 Subject: [PATCH] feat: logging service --- docs/LoggingService.md | 238 ++++++++++++++++++++++ examples/logging_example/main.cpp | 45 ++++ examples/neopattern/NeoPatternService.cpp | 3 +- examples/neopattern/main.cpp | 3 +- examples/neopixel/NeoPixelService.cpp | 4 +- examples/neopixel/main.cpp | 3 +- examples/relay/RelayService.cpp | 12 +- examples/relay/RelayService.h | 4 +- examples/relay/main.cpp | 5 +- examples/static_web/main.cpp | 5 +- include/spore/services/LoggingService.h | 72 +++++++ platformio.ini | 2 +- src/spore/Spore.cpp | 45 ++-- src/spore/core/ApiServer.cpp | 5 +- src/spore/core/ClusterManager.cpp | 30 ++- src/spore/core/NetworkManager.cpp | 31 ++- src/spore/core/TaskManager.cpp | 28 ++- src/spore/services/LoggingService.cpp | 215 +++++++++++++++++++ src/spore/services/NodeService.cpp | 22 +- src/spore/services/StaticFileService.cpp | 5 +- 20 files changed, 676 insertions(+), 101 deletions(-) create mode 100644 docs/LoggingService.md create mode 100644 examples/logging_example/main.cpp create mode 100644 include/spore/services/LoggingService.h create mode 100644 src/spore/services/LoggingService.cpp diff --git a/docs/LoggingService.md b/docs/LoggingService.md new file mode 100644 index 0000000..e9521e5 --- /dev/null +++ b/docs/LoggingService.md @@ -0,0 +1,238 @@ +# LoggingService - Centralized Event-Based Logging + +The LoggingService provides a centralized, event-driven logging system for the SPORE framework. It allows components to log messages through the event system, enabling flexible log routing and filtering. + +## Features + +- **Event-Driven Architecture**: Uses the SPORE event system for decoupled logging +- **Multiple Log Levels**: DEBUG, INFO, WARN, ERROR with configurable filtering +- **Multiple Destinations**: Serial output (extensible) +- **Component-Based Logging**: Tag messages with component names +- **API Configuration**: Configure logging via HTTP API endpoints +- **Easy Integration**: Simple macros and functions for logging + +## Usage + +### Basic Logging + +```cpp +#include "spore/services/LoggingService.h" + +// Using convenience macros (requires NodeContext) +LOG_DEBUG(ctx, "ComponentName", "Debug message"); +LOG_INFO(ctx, "ComponentName", "Info message"); +LOG_WARN(ctx, "ComponentName", "Warning message"); +LOG_ERROR(ctx, "ComponentName", "Error message"); + +// Using global functions +logDebug(ctx, "ComponentName", "Debug message"); +logInfo(ctx, "ComponentName", "Info message"); +logWarn(ctx, "ComponentName", "Warning message"); +logError(ctx, "ComponentName", "Error message"); +``` + +### Event System Integration + +The logging system uses the following events: + +- `log/debug` - Debug level messages +- `log/info` - Info level messages +- `log/warn` - Warning level messages +- `log/error` - Error level messages +- `log/serial` - Messages routed to serial output + +You can subscribe to these events for custom log handling: + +```cpp +// Subscribe to all debug messages +ctx.on("log/debug", [](void* data) { + LogData* logData = static_cast(data); + // Custom handling for debug messages + delete logData; +}); + +// Subscribe to serial log output +ctx.on("log/serial", [](void* data) { + LogData* logData = static_cast(data); + // Custom serial formatting + Serial.println("CUSTOM: " + logData->message); + delete logData; +}); +``` + +### LoggingService Class + +The LoggingService class provides programmatic control over logging: + +```cpp +LoggingService logger(ctx, apiServer); + +// Configure logging +logger.setLogLevel(LogLevel::DEBUG); +logger.enableSerialLogging(true); + +// Direct logging +logger.debug("MyComponent", "Debug message"); +logger.info("MyComponent", "Info message"); +logger.warn("MyComponent", "Warning message"); +logger.error("MyComponent", "Error message"); +``` + +## API Endpoints + +The LoggingService provides HTTP API endpoints for configuration with proper parameter validation: + +### Get Log Level +``` +GET /api/logging/level +``` +Returns current log level as JSON. + +**Response:** +```json +{ + "level": "INFO" +} +``` + +### Set Log Level +``` +POST /api/logging/level +Content-Type: application/x-www-form-urlencoded + +level=DEBUG +``` +Sets the log level with parameter validation. + +**Parameters:** +- `level` (required): One of `DEBUG`, `INFO`, `WARN`, `ERROR` + +**Success Response:** +```json +{ + "success": true, + "level": "DEBUG" +} +``` + +**Error Response:** +```json +{ + "error": "Invalid log level. Must be one of: DEBUG, INFO, WARN, ERROR" +} +``` + +### Get Configuration +``` +GET /api/logging/config +``` +Returns complete logging configuration. + +**Response:** +```json +{ + "level": "INFO", + "serialEnabled": true +} +``` + +## Log Levels + +- **DEBUG**: Detailed information for debugging +- **INFO**: General information about system operation +- **WARN**: Warning messages for potentially harmful situations +- **ERROR**: Error messages for serious problems + +Only messages at or above the current log level are processed. + +## Log Format + +Log messages are formatted as: +``` +[timestamp] [level] [component] message +``` + +Example: +``` +[12345] [INFO] [Spore] Framework setup complete +[12350] [DEBUG] [Network] WiFi connection established +[12355] [WARN] [Cluster] Node timeout detected +[12360] [ERROR] [API] Failed to start server +``` + +## Integration with SPORE + +The LoggingService is automatically registered as a core service in the SPORE framework. It's initialized before other services to ensure logging is available throughout the system. + +## Example + +See `examples/logging_example/main.cpp` for a complete example demonstrating the logging system. + +## Extending the Logging System + +### Adding New Log Destinations + +To add new log destinations (e.g., network logging, database logging): + +1. Subscribe to the appropriate log events: +```cpp +ctx.on("log/info", [](void* data) { + LogData* logData = static_cast(data); + // Send to your custom destination + sendToNetwork(logData->message); + delete logData; +}); +``` + +2. Or create a custom LoggingService subclass: +```cpp +class CustomLoggingService : public LoggingService { +public: + CustomLoggingService(NodeContext& ctx, ApiServer& apiServer) + : LoggingService(ctx, apiServer) { + // Register custom event handlers + ctx.on("log/custom", [this](void* data) { + this->handleCustomLog(data); + }); + } + +private: + void handleCustomLog(void* data) { + // Custom log handling + } +}; +``` + +### Custom Log Formatting + +Override the `formatLogMessage` method in a custom LoggingService to change log formatting: + +```cpp +String CustomLoggingService::formatLogMessage(const LogData& logData) { + // Custom formatting + return "CUSTOM[" + logData.component + "] " + logData.message; +} +``` + +## Performance Considerations + +- Log messages are processed asynchronously through the event system +- Memory is allocated for each log message (LogData struct) +- Consider log level filtering to reduce overhead in production + +## Troubleshooting + +### Logs Not Appearing +1. Check that LoggingService is registered +2. Verify log level is set appropriately +3. Ensure serial logging is enabled +4. Check Serial.begin() is called before logging + +### Memory Issues +1. Reduce log frequency +2. Use higher log levels to filter messages + +### Custom Event Handlers Not Working +1. Ensure event handlers are registered before logging starts +2. Check that LogData is properly deleted in custom handlers +3. Verify event names match exactly diff --git a/examples/logging_example/main.cpp b/examples/logging_example/main.cpp new file mode 100644 index 0000000..ffbd03b --- /dev/null +++ b/examples/logging_example/main.cpp @@ -0,0 +1,45 @@ +#include "spore/Spore.h" +#include "spore/services/LoggingService.h" +#include + +Spore spore; + +void setup() { + // Initialize Spore framework + spore.setup(); + + // Start the framework + spore.begin(); + + // Demonstrate different logging levels + LOG_INFO(spore.ctx, "Example", "Logging example started"); + LOG_DEBUG(spore.ctx, "Example", "This is a debug message"); + LOG_WARN(spore.ctx, "Example", "This is a warning message"); + LOG_ERROR(spore.ctx, "Example", "This is an error message"); + + // Demonstrate logging with different components + LOG_INFO(spore.ctx, "Network", "WiFi connection established"); + LOG_INFO(spore.ctx, "Cluster", "Node discovered: esp-123456"); + LOG_INFO(spore.ctx, "API", "Server started on port 80"); + + // Demonstrate event-based logging + LogData* logData = new LogData(LogLevel::INFO, "Example", "Event-based logging demonstration"); + spore.ctx.fire("log/serial", logData); + + // Show that all logging now goes through the centralized system + LOG_INFO(spore.ctx, "Example", "All Serial.println calls have been replaced with event-based logging!"); +} + +void loop() { + // Run the Spore framework + spore.loop(); + + // Log some periodic information + static unsigned long lastLog = 0; + if (millis() - lastLog > 5000) { + LOG_DEBUG(spore.ctx, "Example", "System running - Free heap: " + String(ESP.getFreeHeap())); + lastLog = millis(); + } + + delay(100); +} diff --git a/examples/neopattern/NeoPatternService.cpp b/examples/neopattern/NeoPatternService.cpp index 155d36f..585ccc8 100644 --- a/examples/neopattern/NeoPatternService.cpp +++ b/examples/neopattern/NeoPatternService.cpp @@ -82,7 +82,8 @@ void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) { if (request->hasParam("brightness", true)) { int b = request->getParam("brightness", true)->value().toInt(); - if (b < 0) b = 0; if (b > 255) b = 255; + if (b < 0) b = 0; + if (b > 255) b = 255; setBrightness((uint8_t)b); } diff --git a/examples/neopattern/main.cpp b/examples/neopattern/main.cpp index a936aac..c48206d 100644 --- a/examples/neopattern/main.cpp +++ b/examples/neopattern/main.cpp @@ -1,5 +1,6 @@ #include #include "spore/Spore.h" +#include "spore/services/LoggingService.h" #include "NeoPatternService.h" #ifndef LED_STRIP_PIN @@ -36,7 +37,7 @@ void setup() { // Start the API server and complete initialization spore.begin(); - Serial.println("[Main] NeoPattern service registered and ready!"); + LOG_INFO(spore.getContext(), "Main", "NeoPattern service registered and ready!"); } void loop() { diff --git a/examples/neopixel/NeoPixelService.cpp b/examples/neopixel/NeoPixelService.cpp index 7798c03..162f296 100644 --- a/examples/neopixel/NeoPixelService.cpp +++ b/examples/neopixel/NeoPixelService.cpp @@ -1,5 +1,6 @@ #include "NeoPixelService.h" #include "spore/core/ApiServer.h" +#include "spore/services/LoggingService.h" // Wheel helper: map 0-255 to RGB rainbow static uint32_t colorWheel(Adafruit_NeoPixel& strip, uint8_t pos) { @@ -104,7 +105,8 @@ void NeoPixelService::handleControlRequest(AsyncWebServerRequest* request) { if (request->hasParam("brightness", true)) { int b = request->getParam("brightness", true)->value().toInt(); - if (b < 0) b = 0; if (b > 255) b = 255; + if (b < 0) b = 0; + if (b > 255) b = 255; setBrightness((uint8_t)b); } diff --git a/examples/neopixel/main.cpp b/examples/neopixel/main.cpp index a22650f..43531fa 100644 --- a/examples/neopixel/main.cpp +++ b/examples/neopixel/main.cpp @@ -1,5 +1,6 @@ #include #include "spore/Spore.h" +#include "spore/services/LoggingService.h" #include "NeoPixelService.h" #ifndef NEOPIXEL_PIN @@ -36,7 +37,7 @@ void setup() { // Start the API server and complete initialization spore.begin(); - Serial.println("[Main] NeoPixel service registered and ready!"); + LOG_INFO(spore.getContext(), "Main", "NeoPixel service registered and ready!"); } void loop() { diff --git a/examples/relay/RelayService.cpp b/examples/relay/RelayService.cpp index ef0eb99..f9fbf78 100644 --- a/examples/relay/RelayService.cpp +++ b/examples/relay/RelayService.cpp @@ -1,8 +1,9 @@ #include "RelayService.h" #include "spore/core/ApiServer.h" +#include "spore/services/LoggingService.h" -RelayService::RelayService(TaskManager& taskMgr, int pin) - : taskManager(taskMgr), relayPin(pin), relayOn(false) { +RelayService::RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin) + : ctx(ctx), taskManager(taskMgr), relayPin(pin), relayOn(false) { pinMode(relayPin, OUTPUT); // Many relay modules are active LOW. Start in OFF state (relay de-energized). digitalWrite(relayPin, HIGH); @@ -64,13 +65,13 @@ void RelayService::turnOn() { relayOn = true; // Active LOW relay digitalWrite(relayPin, LOW); - Serial.println("[RelayService] Relay ON"); + LOG_INFO(ctx, "RelayService", "Relay ON"); } void RelayService::turnOff() { relayOn = false; digitalWrite(relayPin, HIGH); - Serial.println("[RelayService] Relay OFF"); + LOG_INFO(ctx, "RelayService", "Relay OFF"); } void RelayService::toggle() { @@ -83,7 +84,6 @@ void RelayService::toggle() { void RelayService::registerTasks() { taskManager.registerTask("relay_status_print", 5000, [this]() { - Serial.printf("[RelayService] Status - pin: %d, state: %s\n", - relayPin, relayOn ? "ON" : "OFF"); + LOG_INFO(ctx, "RelayService", "Status - pin: " + String(relayPin) + ", state: " + (relayOn ? "ON" : "OFF")); }); } diff --git a/examples/relay/RelayService.h b/examples/relay/RelayService.h index 9bc2de4..4b62848 100644 --- a/examples/relay/RelayService.h +++ b/examples/relay/RelayService.h @@ -1,11 +1,12 @@ #pragma once #include "spore/Service.h" #include "spore/core/TaskManager.h" +#include "spore/core/NodeContext.h" #include class RelayService : public Service { public: - RelayService(TaskManager& taskMgr, int pin); + RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin); void registerEndpoints(ApiServer& api) override; const char* getName() const override { return "Relay"; } @@ -16,6 +17,7 @@ public: private: void registerTasks(); + NodeContext& ctx; TaskManager& taskManager; int relayPin; bool relayOn; diff --git a/examples/relay/main.cpp b/examples/relay/main.cpp index 3675d4c..abb9991 100644 --- a/examples/relay/main.cpp +++ b/examples/relay/main.cpp @@ -1,5 +1,6 @@ #include #include "spore/Spore.h" +#include "spore/services/LoggingService.h" #include "RelayService.h" // Choose a default relay pin. For ESP-01 this is GPIO0. Adjust as needed for your board. @@ -22,13 +23,13 @@ void setup() { spore.setup(); // Create and add custom service - relayService = new RelayService(spore.getTaskManager(), RELAY_PIN); + relayService = new RelayService(spore.getContext(), spore.getTaskManager(), RELAY_PIN); spore.addService(relayService); // Start the API server and complete initialization spore.begin(); - Serial.println("[Main] Relay service registered and ready!"); + LOG_INFO(spore.getContext(), "Main", "Relay service registered and ready!"); } void loop() { diff --git a/examples/static_web/main.cpp b/examples/static_web/main.cpp index 011669b..20518da 100644 --- a/examples/static_web/main.cpp +++ b/examples/static_web/main.cpp @@ -1,5 +1,6 @@ #include #include "spore/Spore.h" +#include "spore/services/LoggingService.h" // Create Spore instance Spore spore; @@ -11,8 +12,8 @@ void setup() { // Start the framework (this will start the API server with static file serving) spore.begin(); - Serial.println("Static web server started!"); - Serial.println("Visit http:/// to see the web interface"); + LOG_INFO(spore.getContext(), "Example", "Static web server started!"); + LOG_INFO(spore.getContext(), "Example", "Visit http:/// to see the web interface"); } void loop() { diff --git a/include/spore/services/LoggingService.h b/include/spore/services/LoggingService.h new file mode 100644 index 0000000..963b382 --- /dev/null +++ b/include/spore/services/LoggingService.h @@ -0,0 +1,72 @@ +#pragma once + +#include "spore/Service.h" +#include "spore/core/NodeContext.h" +#include "spore/core/ApiServer.h" +#include + +enum class LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3 +}; + +struct LogData { + LogLevel level; + String component; + String message; + unsigned long timestamp; + + LogData(LogLevel lvl, const String& comp, const String& msg) + : level(lvl), component(comp), message(msg), timestamp(millis()) {} +}; + +class LoggingService : public Service { +public: + LoggingService(NodeContext& ctx, ApiServer& apiServer); + virtual ~LoggingService() = default; + + // Service interface + void registerEndpoints(ApiServer& api) override; + const char* getName() const override { return "logging"; } + + // Logging methods + void log(LogLevel level, const String& component, const String& message); + void debug(const String& component, const String& message); + void info(const String& component, const String& message); + void warn(const String& component, const String& message); + void error(const String& component, const String& message); + + // Configuration + void setLogLevel(LogLevel level); + void enableSerialLogging(bool enable); + +private: + NodeContext& ctx; + ApiServer& apiServer; + LogLevel currentLogLevel; + bool serialLoggingEnabled; + + void setupEventHandlers(); + void handleSerialLog(void* data); + String formatLogMessage(const LogData& logData); + String logLevelToString(LogLevel level); + + // API endpoint handlers + void handleGetLogLevel(AsyncWebServerRequest* request); + void handleSetLogLevel(AsyncWebServerRequest* request); + void handleGetConfig(AsyncWebServerRequest* request); +}; + +// Global logging functions that use the event system directly +void logDebug(NodeContext& ctx, const String& component, const String& message); +void logInfo(NodeContext& ctx, const String& component, const String& message); +void logWarn(NodeContext& ctx, const String& component, const String& message); +void logError(NodeContext& ctx, const String& component, const String& message); + +// Convenience macros for easy logging (requires ctx to be available) +#define LOG_DEBUG(ctx, component, message) logDebug(ctx, component, message) +#define LOG_INFO(ctx, component, message) logInfo(ctx, component, message) +#define LOG_WARN(ctx, component, message) logWarn(ctx, component, message) +#define LOG_ERROR(ctx, component, message) logError(ctx, component, message) diff --git a/platformio.ini b/platformio.ini index 0dc7fa9..ce386f1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,7 +9,7 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = esp01_1m +;default_envs = esp01_1m src_dir = . [common] diff --git a/src/spore/Spore.cpp b/src/spore/Spore.cpp index 99b0136..c27c719 100644 --- a/src/spore/Spore.cpp +++ b/src/spore/Spore.cpp @@ -4,6 +4,7 @@ #include "spore/services/ClusterService.h" #include "spore/services/TaskService.h" #include "spore/services/StaticFileService.h" +#include "spore/services/LoggingService.h" #include Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager), @@ -23,12 +24,12 @@ Spore::~Spore() { void Spore::setup() { if (initialized) { - Serial.println("[Spore] Already initialized, skipping setup"); + LOG_INFO(ctx, "Spore", "Already initialized, skipping setup"); return; } Serial.begin(115200); - Serial.println("[Spore] Starting Spore framework..."); + LOG_INFO(ctx, "Spore", "Starting Spore framework..."); // Initialize core components initializeCore(); @@ -37,21 +38,21 @@ void Spore::setup() { registerCoreServices(); initialized = true; - Serial.println("[Spore] Framework setup complete - call begin() to start API server"); + LOG_INFO(ctx, "Spore", "Framework setup complete - call begin() to start API server"); } void Spore::begin() { if (!initialized) { - Serial.println("[Spore] Framework not initialized, call setup() first"); + LOG_ERROR(ctx, "Spore", "Framework not initialized, call setup() first"); return; } if (apiServerStarted) { - Serial.println("[Spore] API server already started"); + LOG_WARN(ctx, "Spore", "API server already started"); return; } - Serial.println("[Spore] Starting API server..."); + LOG_INFO(ctx, "Spore", "Starting API server..."); // Start API server startApiServer(); @@ -59,12 +60,12 @@ void Spore::begin() { // Print initial task status taskManager.printTaskStatus(); - Serial.println("[Spore] Framework ready!"); + LOG_INFO(ctx, "Spore", "Framework ready!"); } void Spore::loop() { if (!initialized) { - Serial.println("[Spore] Framework not initialized, call setup() first"); + LOG_ERROR(ctx, "Spore", "Framework not initialized, call setup() first"); return; } @@ -74,7 +75,7 @@ void Spore::loop() { void Spore::addService(std::shared_ptr service) { if (!service) { - Serial.println("[Spore] Warning: Attempted to add null service"); + LOG_WARN(ctx, "Spore", "Attempted to add null service"); return; } @@ -83,15 +84,15 @@ void Spore::addService(std::shared_ptr service) { if (apiServerStarted) { // If API server is already started, register the service immediately apiServer.addService(*service); - Serial.printf("[Spore] Added service '%s' to running API server\n", service->getName()); + LOG_INFO(ctx, "Spore", "Added service '" + String(service->getName()) + "' to running API server"); } else { - Serial.printf("[Spore] Registered service '%s' (will be added to API server when begin() is called)\n", service->getName()); + LOG_INFO(ctx, "Spore", "Registered service '" + String(service->getName()) + "' (will be added to API server when begin() is called)"); } } void Spore::addService(Service* service) { if (!service) { - Serial.println("[Spore] Warning: Attempted to add null service"); + LOG_WARN(ctx, "Spore", "Attempted to add null service"); return; } @@ -101,7 +102,7 @@ void Spore::addService(Service* service) { void Spore::initializeCore() { - Serial.println("[Spore] Initializing core components..."); + LOG_INFO(ctx, "Spore", "Initializing core components..."); // Setup WiFi first network.setupWiFi(); @@ -109,11 +110,14 @@ void Spore::initializeCore() { // Initialize task manager taskManager.initialize(); - Serial.println("[Spore] Core components initialized"); + LOG_INFO(ctx, "Spore", "Core components initialized"); } void Spore::registerCoreServices() { - Serial.println("[Spore] Registering core services..."); + LOG_INFO(ctx, "Spore", "Registering core services..."); + + // Create logging service first (other services may use it) + auto loggingService = std::make_shared(ctx, apiServer); // Create core services auto nodeService = std::make_shared(ctx, apiServer); @@ -123,28 +127,29 @@ void Spore::registerCoreServices() { auto staticFileService = std::make_shared(ctx, apiServer); // Add to services list + services.push_back(loggingService); services.push_back(nodeService); services.push_back(networkService); services.push_back(clusterService); services.push_back(taskService); services.push_back(staticFileService); - Serial.println("[Spore] Core services registered"); + LOG_INFO(ctx, "Spore", "Core services registered"); } void Spore::startApiServer() { if (apiServerStarted) { - Serial.println("[Spore] API server already started"); + LOG_WARN(ctx, "Spore", "API server already started"); return; } - Serial.println("[Spore] Starting API server..."); + LOG_INFO(ctx, "Spore", "Starting API server..."); // Register all services with API server for (auto& service : services) { if (service) { apiServer.addService(*service); - Serial.printf("[Spore] Added service '%s' to API server\n", service->getName()); + LOG_INFO(ctx, "Spore", "Added service '" + String(service->getName()) + "' to API server"); } } @@ -152,5 +157,5 @@ void Spore::startApiServer() { apiServer.begin(); apiServerStarted = true; - Serial.println("[Spore] API server started"); + LOG_INFO(ctx, "Spore", "API server started"); } diff --git a/src/spore/core/ApiServer.cpp b/src/spore/core/ApiServer.cpp index 2f5c49c..9e27352 100644 --- a/src/spore/core/ApiServer.cpp +++ b/src/spore/core/ApiServer.cpp @@ -1,5 +1,6 @@ #include "spore/core/ApiServer.h" #include "spore/Service.h" +#include "spore/services/LoggingService.h" #include const char* ApiServer::methodToStr(int method) { @@ -77,14 +78,14 @@ void ApiServer::addEndpoint(const String& uri, int method, std::functionbeginPacket("255.255.255.255", ctx.config.udp_port); ctx.udp->write(ClusterProtocol::DISCOVERY_MSG); ctx.udp->endPacket(); @@ -36,14 +37,14 @@ void ClusterManager::listenForDiscovery() { if (len > 0) { incoming[len] = 0; } - //Serial.printf("[UDP] Packet received: %s\n", incoming); + //LOG_DEBUG(ctx, "UDP", "Packet received: " + String(incoming)); if (strcmp(incoming, ClusterProtocol::DISCOVERY_MSG) == 0) { - //Serial.printf("[UDP] Discovery request from: %s\n", ctx.udp->remoteIP().toString().c_str()); + //LOG_DEBUG(ctx, "UDP", "Discovery request from: " + ctx.udp->remoteIP().toString()); ctx.udp->beginPacket(ctx.udp->remoteIP(), ctx.config.udp_port); String response = String(ClusterProtocol::RESPONSE_MSG) + ":" + ctx.hostname; ctx.udp->write(response.c_str()); ctx.udp->endPacket(); - //Serial.printf("[UDP] Sent response with hostname: %s\n", ctx.hostname.c_str()); + //LOG_DEBUG(ctx, "UDP", "Sent response with hostname: " + ctx.hostname); } else if (strncmp(incoming, ClusterProtocol::RESPONSE_MSG, strlen(ClusterProtocol::RESPONSE_MSG)) == 0) { char* hostPtr = incoming + strlen(ClusterProtocol::RESPONSE_MSG) + 1; String nodeHost = String(hostPtr); @@ -72,16 +73,13 @@ void ClusterManager::addOrUpdateNode(const String& nodeHost, IPAddress nodeIP) { newNode.lastSeen = millis(); updateNodeStatus(newNode, newNode.lastSeen, ctx.config.node_inactive_threshold_ms, ctx.config.node_dead_threshold_ms); memberList[nodeHost] = newNode; - Serial.printf("[Cluster] Added node: %s @ %s | Status: %s | last update: 0\n", - nodeHost.c_str(), - newNode.ip.toString().c_str(), - statusToStr(newNode.status)); + LOG_INFO(ctx, "Cluster", "Added node: " + nodeHost + " @ " + newNode.ip.toString() + " | Status: " + statusToStr(newNode.status) + " | last update: 0"); //fetchNodeInfo(nodeIP); // Do not fetch here, handled by periodic task } void ClusterManager::fetchNodeInfo(const IPAddress& ip) { if(ip == ctx.localIP) { - Serial.println("[Cluster] Skipping fetch for local node"); + LOG_DEBUG(ctx, "Cluster", "Skipping fetch for local node"); return; } unsigned long requestStart = millis(); @@ -135,13 +133,13 @@ void ClusterManager::fetchNodeInfo(const IPAddress& ip) { node.labels[k] = v; } } - Serial.printf("[Cluster] Fetched info for node: %s @ %s\n", node.hostname.c_str(), ip.toString().c_str()); + LOG_DEBUG(ctx, "Cluster", "Fetched info for node: " + node.hostname + " @ " + ip.toString()); break; } } } } else { - Serial.printf("[Cluster] Failed to fetch info for node @ %s, HTTP code: %d\n", ip.toString().c_str(), httpCode); + LOG_ERROR(ctx, "Cluster", "Failed to fetch info for node @ " + ip.toString() + ", HTTP code: " + String(httpCode)); } http.end(); } @@ -185,7 +183,7 @@ void ClusterManager::removeDeadNodes() { for (auto it = memberList.begin(); it != memberList.end(); ) { unsigned long diff = now - it->second.lastSeen; if (it->second.status == NodeInfo::DEAD && diff > ctx.config.node_dead_threshold_ms) { - Serial.printf("[Cluster] Removing node: %s\n", it->second.hostname.c_str()); + LOG_INFO(ctx, "Cluster", "Removing node: " + it->second.hostname); it = memberList.erase(it); } else { ++it; @@ -196,13 +194,13 @@ void ClusterManager::removeDeadNodes() { void ClusterManager::printMemberList() { auto& memberList = *ctx.memberList; if (memberList.empty()) { - Serial.println("[Cluster] Member List: empty"); + LOG_INFO(ctx, "Cluster", "Member List: empty"); return; } - Serial.println("[Cluster] Member List:"); + LOG_INFO(ctx, "Cluster", "Member List:"); for (const auto& pair : memberList) { const NodeInfo& node = pair.second; - Serial.printf(" %s @ %s | Status: %s | last seen: %lu\n", node.hostname.c_str(), node.ip.toString().c_str(), statusToStr(node.status), millis() - node.lastSeen); + LOG_INFO(ctx, "Cluster", " " + node.hostname + " @ " + node.ip.toString() + " | Status: " + statusToStr(node.status) + " | last seen: " + String(millis() - node.lastSeen)); } } diff --git a/src/spore/core/NetworkManager.cpp b/src/spore/core/NetworkManager.cpp index 8a1f66d..356cf5a 100644 --- a/src/spore/core/NetworkManager.cpp +++ b/src/spore/core/NetworkManager.cpp @@ -1,26 +1,27 @@ #include "spore/core/NetworkManager.h" +#include "spore/services/LoggingService.h" // SSID and password are now configured via Config class void NetworkManager::scanWifi() { if (!isScanning) { isScanning = true; - Serial.println("[WiFi] Starting WiFi scan..."); + LOG_INFO(ctx, "WiFi", "Starting WiFi scan..."); // Start async WiFi scan WiFi.scanNetworksAsync([this](int networksFound) { - Serial.printf("[WiFi] Scan completed, found %d networks\n", networksFound); + LOG_INFO(ctx, "WiFi", "Scan completed, found " + String(networksFound) + " networks"); this->processAccessPoints(); this->isScanning = false; }, true); } else { - Serial.println("[WiFi] Scan already in progress..."); + LOG_WARN(ctx, "WiFi", "Scan already in progress..."); } } void NetworkManager::processAccessPoints() { int numNetworks = WiFi.scanComplete(); if (numNetworks <= 0) { - Serial.println("[WiFi] No networks found or scan not complete"); + LOG_WARN(ctx, "WiFi", "No networks found or scan not complete"); return; } @@ -43,8 +44,7 @@ void NetworkManager::processAccessPoints() { accessPoints.push_back(ap); - Serial.printf("[WiFi] Found network %d: %s, Ch: %d, RSSI: %d\n", - i + 1, ap.ssid.c_str(), ap.channel, ap.rssi); + LOG_DEBUG(ctx, "WiFi", "Found network " + String(i + 1) + ": " + ap.ssid + ", Ch: " + String(ap.channel) + ", RSSI: " + String(ap.rssi)); } // Free the memory used by the scan @@ -77,31 +77,26 @@ void NetworkManager::setHostnameFromMac() { void NetworkManager::setupWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ctx.config.wifi_ssid.c_str(), ctx.config.wifi_password.c_str()); - Serial.println("[WiFi] Connecting to AP..."); + LOG_INFO(ctx, "WiFi", "Connecting to AP..."); unsigned long startAttemptTime = millis(); while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < ctx.config.wifi_connect_timeout_ms) { delay(ctx.config.wifi_retry_delay_ms); - Serial.print("."); + // Progress dots handled by delay, no logging needed } if (WiFi.status() == WL_CONNECTED) { - Serial.println(); - Serial.print("[WiFi] Connected to AP, IP: "); - Serial.println(WiFi.localIP()); + LOG_INFO(ctx, "WiFi", "Connected to AP, IP: " + WiFi.localIP().toString()); } else { - Serial.println(); - Serial.println("[WiFi] Failed to connect to AP. Creating AP..."); + LOG_WARN(ctx, "WiFi", "Failed to connect to AP. Creating AP..."); WiFi.mode(WIFI_AP); WiFi.softAP(ctx.config.wifi_ssid.c_str(), ctx.config.wifi_password.c_str()); - Serial.print("[WiFi] AP created, IP: "); - Serial.println(WiFi.softAPIP()); + LOG_INFO(ctx, "WiFi", "AP created, IP: " + WiFi.softAPIP().toString()); } setHostnameFromMac(); ctx.udp->begin(ctx.config.udp_port); ctx.localIP = WiFi.localIP(); ctx.hostname = WiFi.hostname(); - Serial.print("[WiFi] Hostname set to: "); - Serial.println(ctx.hostname); - Serial.printf("[WiFi] UDP listening on port %d\n", ctx.config.udp_port); + LOG_INFO(ctx, "WiFi", "Hostname set to: " + ctx.hostname); + LOG_INFO(ctx, "WiFi", "UDP listening on port " + String(ctx.config.udp_port)); // Populate self NodeInfo ctx.self.hostname = ctx.hostname; diff --git a/src/spore/core/TaskManager.cpp b/src/spore/core/TaskManager.cpp index 9f5a117..9726e06 100644 --- a/src/spore/core/TaskManager.cpp +++ b/src/spore/core/TaskManager.cpp @@ -1,4 +1,5 @@ #include "spore/core/TaskManager.h" +#include "spore/services/LoggingService.h" #include TaskManager::TaskManager(NodeContext& ctx) : ctx(ctx) {} @@ -31,9 +32,9 @@ void TaskManager::enableTask(const std::string& name) { int idx = findTaskIndex(name); if (idx >= 0) { taskDefinitions[idx].enabled = true; - Serial.printf("[TaskManager] Enabled task: %s\n", name.c_str()); + LOG_INFO(ctx, "TaskManager", "Enabled task: " + String(name.c_str())); } else { - Serial.printf("[TaskManager] Warning: Task not found: %s\n", name.c_str()); + LOG_WARN(ctx, "TaskManager", "Task not found: " + String(name.c_str())); } } @@ -41,9 +42,9 @@ void TaskManager::disableTask(const std::string& name) { int idx = findTaskIndex(name); if (idx >= 0) { taskDefinitions[idx].enabled = false; - Serial.printf("[TaskManager] Disabled task: %s\n", name.c_str()); + LOG_INFO(ctx, "TaskManager", "Disabled task: " + String(name.c_str())); } else { - Serial.printf("[TaskManager] Warning: Task not found: %s\n", name.c_str()); + LOG_WARN(ctx, "TaskManager", "Task not found: " + String(name.c_str())); } } @@ -51,9 +52,9 @@ void TaskManager::setTaskInterval(const std::string& name, unsigned long interva int idx = findTaskIndex(name); if (idx >= 0) { taskDefinitions[idx].interval = interval; - Serial.printf("[TaskManager] Set interval for task %s: %lu ms\n", name.c_str(), interval); + LOG_INFO(ctx, "TaskManager", "Set interval for task " + String(name.c_str()) + ": " + String(interval) + " ms"); } else { - Serial.printf("[TaskManager] Warning: Task not found: %s\n", name.c_str()); + LOG_WARN(ctx, "TaskManager", "Task not found: " + String(name.c_str())); } } @@ -84,27 +85,24 @@ void TaskManager::enableAllTasks() { for (auto& taskDef : taskDefinitions) { taskDef.enabled = true; } - Serial.println("[TaskManager] Enabled all tasks"); + LOG_INFO(ctx, "TaskManager", "Enabled all tasks"); } void TaskManager::disableAllTasks() { for (auto& taskDef : taskDefinitions) { taskDef.enabled = false; } - Serial.println("[TaskManager] Disabled all tasks"); + LOG_INFO(ctx, "TaskManager", "Disabled all tasks"); } void TaskManager::printTaskStatus() const { - Serial.println("\n[TaskManager] Task Status:"); - Serial.println("=========================="); + LOG_INFO(ctx, "TaskManager", "\nTask Status:"); + LOG_INFO(ctx, "TaskManager", "=========================="); for (const auto& taskDef : taskDefinitions) { - Serial.printf(" %s: %s (interval: %lu ms)\n", - taskDef.name.c_str(), - taskDef.enabled ? "ENABLED" : "DISABLED", - taskDef.interval); + LOG_INFO(ctx, "TaskManager", " " + String(taskDef.name.c_str()) + ": " + (taskDef.enabled ? "ENABLED" : "DISABLED") + " (interval: " + String(taskDef.interval) + " ms)"); } - Serial.println("==========================\n"); + LOG_INFO(ctx, "TaskManager", "==========================\n"); } void TaskManager::execute() { diff --git a/src/spore/services/LoggingService.cpp b/src/spore/services/LoggingService.cpp new file mode 100644 index 0000000..a7aaabc --- /dev/null +++ b/src/spore/services/LoggingService.cpp @@ -0,0 +1,215 @@ +#include "spore/services/LoggingService.h" +#include + +LoggingService::LoggingService(NodeContext& ctx, ApiServer& apiServer) + : ctx(ctx), apiServer(apiServer), currentLogLevel(LogLevel::INFO), + serialLoggingEnabled(true) { + setupEventHandlers(); +} + +void LoggingService::setupEventHandlers() { + // Register event handlers for different log destinations + ctx.on("log/serial", [this](void* data) { + this->handleSerialLog(data); + }); + + // Register for all log events + ctx.on("log/debug", [this](void* data) { + LogData* logData = static_cast(data); + if (logData->level >= currentLogLevel) { + this->log(logData->level, logData->component, logData->message); + } + delete logData; + }); + + ctx.on("log/info", [this](void* data) { + LogData* logData = static_cast(data); + if (logData->level >= currentLogLevel) { + this->log(logData->level, logData->component, logData->message); + } + delete logData; + }); + + ctx.on("log/warn", [this](void* data) { + LogData* logData = static_cast(data); + if (logData->level >= currentLogLevel) { + this->log(logData->level, logData->component, logData->message); + } + delete logData; + }); + + ctx.on("log/error", [this](void* data) { + LogData* logData = static_cast(data); + if (logData->level >= currentLogLevel) { + this->log(logData->level, logData->component, logData->message); + } + delete logData; + }); +} + +void LoggingService::registerEndpoints(ApiServer& api) { + LOG_INFO(ctx, "LoggingService", "Registering logging API endpoints"); + + // Register API endpoints for logging configuration + api.addEndpoint("/api/logging/level", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleGetLogLevel(request); }, + std::vector{}); + LOG_DEBUG(ctx, "LoggingService", "Registered GET /api/logging/level"); + + api.addEndpoint("/api/logging/level", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleSetLogLevel(request); }, + std::vector{ + ParamSpec{String("level"), true, String("body"), String("string"), + std::vector{"DEBUG", "INFO", "WARN", "ERROR"}, String("INFO")} + }); + LOG_DEBUG(ctx, "LoggingService", "Registered POST /api/logging/level with level parameter"); + + api.addEndpoint("/api/logging/config", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleGetConfig(request); }, + std::vector{}); + LOG_DEBUG(ctx, "LoggingService", "Registered GET /api/logging/config"); + + LOG_INFO(ctx, "LoggingService", "All logging API endpoints registered successfully"); +} + +void LoggingService::log(LogLevel level, const String& component, const String& message) { + if (level < currentLogLevel) { + return; // Skip logging if below current level + } + + LogData* logData = new LogData(level, component, message); + + // Fire events for different log destinations + if (serialLoggingEnabled) { + ctx.fire("log/serial", logData); + } +} + +void LoggingService::debug(const String& component, const String& message) { + LogData* logData = new LogData(LogLevel::DEBUG, component, message); + ctx.fire("log/debug", logData); +} + +void LoggingService::info(const String& component, const String& message) { + LogData* logData = new LogData(LogLevel::INFO, component, message); + ctx.fire("log/info", logData); +} + +void LoggingService::warn(const String& component, const String& message) { + LogData* logData = new LogData(LogLevel::WARN, component, message); + ctx.fire("log/warn", logData); +} + +void LoggingService::error(const String& component, const String& message) { + LogData* logData = new LogData(LogLevel::ERROR, component, message); + ctx.fire("log/error", logData); +} + +void LoggingService::setLogLevel(LogLevel level) { + currentLogLevel = level; + info("LoggingService", "Log level set to " + logLevelToString(level)); +} + +void LoggingService::enableSerialLogging(bool enable) { + serialLoggingEnabled = enable; + info("LoggingService", "Serial logging " + String(enable ? "enabled" : "disabled")); +} + + +void LoggingService::handleSerialLog(void* data) { + LogData* logData = static_cast(data); + String formattedMessage = formatLogMessage(*logData); + Serial.println(formattedMessage); +} + + +String LoggingService::formatLogMessage(const LogData& logData) { + String timestamp = String(logData.timestamp); + String level = logLevelToString(logData.level); + return "[" + timestamp + "] [" + level + "] [" + logData.component + "] " + logData.message; +} + +String LoggingService::logLevelToString(LogLevel level) { + switch (level) { + case LogLevel::DEBUG: return "DEBUG"; + case LogLevel::INFO: return "INFO"; + case LogLevel::WARN: return "WARN"; + case LogLevel::ERROR: return "ERROR"; + default: return "UNKNOWN"; + } +} + +// Global logging functions that use the event system directly +void logDebug(NodeContext& ctx, const String& component, const String& message) { + LogData* logData = new LogData(LogLevel::DEBUG, component, message); + ctx.fire("log/debug", logData); +} + +void logInfo(NodeContext& ctx, const String& component, const String& message) { + LogData* logData = new LogData(LogLevel::INFO, component, message); + ctx.fire("log/info", logData); +} + +void logWarn(NodeContext& ctx, const String& component, const String& message) { + LogData* logData = new LogData(LogLevel::WARN, component, message); + ctx.fire("log/warn", logData); +} + +void logError(NodeContext& ctx, const String& component, const String& message) { + LogData* logData = new LogData(LogLevel::ERROR, component, message); + ctx.fire("log/error", logData); +} + +// API endpoint handlers +void LoggingService::handleGetLogLevel(AsyncWebServerRequest* request) { + LOG_DEBUG(ctx, "LoggingService", "handleGetLogLevel called"); + String response = "{\"level\":\"" + logLevelToString(currentLogLevel) + "\"}"; + LOG_DEBUG(ctx, "LoggingService", "Sending response: " + response); + request->send(200, "application/json", response); +} + +void LoggingService::handleSetLogLevel(AsyncWebServerRequest* request) { + LOG_DEBUG(ctx, "LoggingService", "handleSetLogLevel called"); + LOG_DEBUG(ctx, "LoggingService", "Request method: " + String(request->method())); + LOG_DEBUG(ctx, "LoggingService", "Request URL: " + request->url()); + LOG_DEBUG(ctx, "LoggingService", "Content type: " + request->contentType()); + + if (request->hasParam("level", true)) { + String levelStr = request->getParam("level", true)->value(); + LOG_DEBUG(ctx, "LoggingService", "Level parameter found: '" + levelStr + "'"); + + if (levelStr == "DEBUG") { + LOG_DEBUG(ctx, "LoggingService", "Setting log level to DEBUG"); + setLogLevel(LogLevel::DEBUG); + } else if (levelStr == "INFO") { + LOG_DEBUG(ctx, "LoggingService", "Setting log level to INFO"); + setLogLevel(LogLevel::INFO); + } else if (levelStr == "WARN") { + LOG_DEBUG(ctx, "LoggingService", "Setting log level to WARN"); + setLogLevel(LogLevel::WARN); + } else if (levelStr == "ERROR") { + LOG_DEBUG(ctx, "LoggingService", "Setting log level to ERROR"); + setLogLevel(LogLevel::ERROR); + } else { + LOG_ERROR(ctx, "LoggingService", "Invalid log level received: '" + levelStr + "'"); + request->send(400, "application/json", "{\"error\":\"Invalid log level. Must be one of: DEBUG, INFO, WARN, ERROR\"}"); + return; + } + LOG_INFO(ctx, "LoggingService", "Log level successfully set to " + logLevelToString(currentLogLevel)); + request->send(200, "application/json", "{\"success\":true, \"level\":\"" + logLevelToString(currentLogLevel) + "\"}"); + } else { + LOG_ERROR(ctx, "LoggingService", "Missing required parameter: level"); + request->send(400, "application/json", "{\"error\":\"Missing required parameter: level\"}"); + } +} + +void LoggingService::handleGetConfig(AsyncWebServerRequest* request) { + LOG_DEBUG(ctx, "LoggingService", "handleGetConfig called"); + JsonDocument doc; + doc["level"] = logLevelToString(currentLogLevel); + doc["serialEnabled"] = serialLoggingEnabled; + String response; + serializeJson(doc, response); + LOG_DEBUG(ctx, "LoggingService", "Sending config response: " + response); + request->send(200, "application/json", response); +} diff --git a/src/spore/services/NodeService.cpp b/src/spore/services/NodeService.cpp index 1b3f4bf..42798bf 100644 --- a/src/spore/services/NodeService.cpp +++ b/src/spore/services/NodeService.cpp @@ -1,5 +1,6 @@ #include "spore/services/NodeService.h" #include "spore/core/ApiServer.h" +#include "spore/services/LoggingService.h" NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {} @@ -65,8 +66,8 @@ void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) { success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}"); response->addHeader("Connection", "close"); request->send(response); - request->onDisconnect([]() { - Serial.println("[API] Restart device"); + request->onDisconnect([this]() { + LOG_INFO(ctx, "API", "Restart device"); delay(10); ESP.restart(); }); @@ -75,11 +76,10 @@ void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) { void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) { if (!index) { - Serial.print("[OTA] Update Start "); - Serial.println(filename); + LOG_INFO(ctx, "OTA", "Update Start " + String(filename)); Update.runAsync(true); if(!Update.begin(request->contentLength(), U_FLASH)) { - Serial.println("[OTA] Update failed: not enough space"); + LOG_ERROR(ctx, "OTA", "Update failed: not enough space"); Update.printError(Serial); AsyncWebServerResponse* response = request->beginResponse(500, "application/json", "{\"status\": \"FAIL\"}"); @@ -96,14 +96,12 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin if (final) { if (Update.end(true)) { if(Update.isFinished()) { - Serial.print("[OTA] Update Success with "); - Serial.print(index + len); - Serial.println("B"); + LOG_INFO(ctx, "OTA", "Update Success with " + String(index + len) + "B"); } else { - Serial.println("[OTA] Update not finished"); + LOG_WARN(ctx, "OTA", "Update not finished"); } } else { - Serial.print("[OTA] Update failed: "); + LOG_ERROR(ctx, "OTA", "Update failed: " + String(Update.getError())); Update.printError(Serial); } } @@ -114,8 +112,8 @@ void NodeService::handleRestartRequest(AsyncWebServerRequest* request) { "{\"status\": \"restarting\"}"); response->addHeader("Connection", "close"); request->send(response); - request->onDisconnect([]() { - Serial.println("[API] Restart device"); + request->onDisconnect([this]() { + LOG_INFO(ctx, "API", "Restart device"); delay(10); ESP.restart(); }); diff --git a/src/spore/services/StaticFileService.cpp b/src/spore/services/StaticFileService.cpp index 5fbf133..415d40b 100644 --- a/src/spore/services/StaticFileService.cpp +++ b/src/spore/services/StaticFileService.cpp @@ -1,4 +1,5 @@ #include "spore/services/StaticFileService.h" +#include "spore/services/LoggingService.h" #include const String StaticFileService::name = "StaticFileService"; @@ -10,10 +11,10 @@ StaticFileService::StaticFileService(NodeContext& ctx, ApiServer& apiServer) void StaticFileService::registerEndpoints(ApiServer& api) { // Initialize LittleFS if (!LittleFS.begin()) { - Serial.println("[StaticFileService] LittleFS Mount Failed"); + LOG_ERROR(ctx, "StaticFileService", "LittleFS Mount Failed"); return; } - Serial.println("[StaticFileService] LittleFS mounted successfully"); + LOG_INFO(ctx, "StaticFileService", "LittleFS mounted successfully"); // Root endpoint - serve index.html api.addEndpoint("/", HTTP_GET,