diff --git a/include/Configuration.h b/include/Configuration.h index bfbf26574..3b99c38bb 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -178,7 +178,7 @@ struct CONFIG_T { struct { bool Enabled; char Hostname[SYSLOG_MAX_HOSTNAME_STRLEN + 1]; - uint32_t Port; + uint16_t Port; } Syslog; struct { diff --git a/include/SyslogLogger.h b/include/SyslogLogger.h new file mode 100644 index 000000000..a6982e7bd --- /dev/null +++ b/include/SyslogLogger.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include +#include + +class SyslogLogger { +public: + SyslogLogger(); + void init(Scheduler& scheduler); + void updateSettings(const String&& hostname); + void write(const uint8_t *buffer, size_t size); + +private: + void loop(); + void disable(); + void enable(); + bool resolveAndStart(); + bool isResolved() const { + return _address != INADDR_NONE; + } + + Task _loopTask; + std::mutex _mutex; + WiFiUDP _udp; + IPAddress _address; + String _syslog_hostname; + String _proc_id; + String _header; + uint16_t _port; + bool _enabled; +}; + +extern SyslogLogger Syslog; diff --git a/src/MessageOutput.cpp b/src/MessageOutput.cpp index 04e9ddd44..9db788506 100644 --- a/src/MessageOutput.cpp +++ b/src/MessageOutput.cpp @@ -4,6 +4,7 @@ */ #include #include "MessageOutput.h" +#include "SyslogLogger.h" MessageOutputClass MessageOutput; @@ -102,12 +103,14 @@ void MessageOutputClass::loop() if (!_ws) { while (!_lines.empty()) { + Syslog.write(_lines.front().data(), _lines.front().size()); _lines.pop(); // do not hog memory } return; } while (!_lines.empty() && _ws->availableForWriteAll()) { + Syslog.write(_lines.front().data(), _lines.front().size()); _ws->textAll(std::make_shared(std::move(_lines.front()))); _lines.pop(); } diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index c104fca2e..a81b907c3 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -5,6 +5,7 @@ #include "NetworkSettings.h" #include "Configuration.h" #include "MessageOutput.h" +#include "SyslogLogger.h" #include "PinMapping.h" #include "Utils.h" #include "SPIPortManager.h" @@ -53,6 +54,8 @@ void NetworkSettingsClass::init(Scheduler& scheduler) scheduler.addTask(_loopTask); _loopTask.enable(); + + Syslog.init(scheduler); } void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info) @@ -294,6 +297,8 @@ void NetworkSettingsClass::applyConfig() } MessageOutput.println("done"); setStaticIp(); + + Syslog.updateSettings(getHostname()); } void NetworkSettingsClass::setHostname() diff --git a/src/SyslogLogger.cpp b/src/SyslogLogger.cpp new file mode 100644 index 000000000..00c50a1ad --- /dev/null +++ b/src/SyslogLogger.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include +#include +#include "defaults.h" +#include "SyslogLogger.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "NetworkSettings.h" + +SyslogLogger::SyslogLogger() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&SyslogLogger::loop, this)) +{ +} + +void SyslogLogger::init(Scheduler& scheduler) +{ + // PROCID change indicates a restart. + _proc_id = String(esp_random(), HEX); + + scheduler.addTask(_loopTask); + _loopTask.enable(); +} + +void SyslogLogger::updateSettings(const String&& hostname) +{ + auto& config = Configuration.get().Syslog; + + // Disable logger while it is reconfigured. + disable(); + + if (!config.Enabled) { + MessageOutput.println("[SyslogLogger] Syslog not enabled"); + return; + } + + _port = config.Port; + _syslog_hostname = config.Hostname; + if (_syslog_hostname.isEmpty()) { + MessageOutput.println("[SyslogLogger] Hostname not configured"); + return; + } + + MessageOutput.printf("[SyslogLogger] Logging to %s!\r\n", _syslog_hostname.c_str()); + + _header = "<7>1 - "; // RFC5424: Facility KERNEL, severity DEBUG, version 1, NIL timestamp. + _header += hostname; + _header += " OpenDTU "; + _header += _proc_id; + // NIL values for message id and structured // data; utf-8 BOM. + _header += " - - \xEF\xBB\xBF"; + + // Enable logger. + enable(); +} + +void SyslogLogger::write(const uint8_t *buffer, size_t size) +{ + std::lock_guard lock(_mutex); + if (!_enabled || !isResolved()) { + return; + } + for (int i = 0; i < size; i++) { + uint8_t c = buffer[i]; + bool overflow = false; + if (c != '\r' && c != '\n') { + // Replace control and non-ASCII characters with '?'. + overflow = !_udp.write(c >= 0x20 && c < 0x7f ? c : '?'); + } + if (c == '\n' || overflow) { + _udp.endPacket(); + _udp.beginPacket(_address, _port); + _udp.print(_header); + } + } +} + +void SyslogLogger::disable() +{ + MessageOutput.println("[SyslogLogger] Disable"); + std::lock_guard lock(_mutex); + if (_enabled) { + _enabled = false; + _address = INADDR_NONE; + _udp.stop(); + } +} + +void SyslogLogger::enable() +{ + // Bind random source port. + if (!_udp.begin(0)) { + MessageOutput.println("[SyslogLogger] No sockets available"); + return; + } + + std::lock_guard lock(_mutex); + _enabled = true; +} + +bool SyslogLogger::resolveAndStart() +{ + if (Configuration.get().Mdns.Enabled) { + _address = MDNS.queryHost(_syslog_hostname); // INADDR_NONE if failed + } + if (_address != INADDR_NONE) { + if (!_udp.beginPacket(_address, _port)) { + return false; + } + } else { + if (!_udp.beginPacket(_syslog_hostname.c_str(), _port)) { + return false; + } + _address = _udp.remoteIP(); // Store resolved address. + } + _udp.print(_header); + _udp.print("[SyslogLogger] Logging to "); + _udp.print(_syslog_hostname); + _udp.endPacket(); + _udp.beginPacket(_address, _port); + _udp.print(_header); + return true; +} + +void SyslogLogger::loop() +{ + std::lock_guard lock(_mutex); + if (!_enabled || !NetworkSettings.isConnected() || isResolved()) { + return; + } + if (!resolveAndStart()) { + _enabled = false; + } +} + +SyslogLogger Syslog;