Skip to content

Commit

Permalink
Feature: Add syslog logger (#1267)
Browse files Browse the repository at this point in the history
This implements RFC5424 version of the protocol.

Don't use https://github.com/arcao/Syslog since the protocol itself
is trivial and most of the libraries functionality is not needed here.
The library also doesn't support setting the PROCID field, which is set
to a random id to indicate a reboot here.

Add UI for syslog configuration to network admin view.
  • Loading branch information
ranma authored Sep 26, 2024
1 parent a7dbf0a commit 20159f3
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 2 deletions.
8 changes: 8 additions & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#define WIFI_MAX_PASSWORD_STRLEN 64
#define WIFI_MAX_HOSTNAME_STRLEN 31

#define SYSLOG_MAX_HOSTNAME_STRLEN 128

#define NTP_MAX_SERVER_STRLEN 31
#define NTP_MAX_TIMEZONE_STRLEN 50
#define NTP_MAX_TIMEZONEDESCR_STRLEN 50
Expand Down Expand Up @@ -173,6 +175,12 @@ struct CONFIG_T {
bool Enabled;
} Mdns;

struct {
bool Enabled;
char Hostname[SYSLOG_MAX_HOSTNAME_STRLEN + 1];
uint16_t Port;
} Syslog;

struct {
char Server[NTP_MAX_SERVER_STRLEN + 1];
char Timezone[NTP_MAX_TIMEZONE_STRLEN + 1];
Expand Down
34 changes: 34 additions & 0 deletions include/SyslogLogger.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <WiFiUdp.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>

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;
2 changes: 2 additions & 0 deletions include/WebApi_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ enum WebApiError {
NetworkDns1Invalid,
NetworkDns2Invalid,
NetworkApTimeoutInvalid,
NetworkSyslogHostnameLength,
NetworkSyslogPort,

NtpBase = 9000,
NtpServerLength,
Expand Down
3 changes: 3 additions & 0 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@

#define MDNS_ENABLED false

#define SYSLOG_ENABLED false
#define SYSLOG_PORT 514

#define NTP_SERVER_OLD "pool.ntp.org"
#define NTP_SERVER "opendtu.pool.ntp.org"
#define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"
Expand Down
10 changes: 10 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ bool ConfigurationClass::write()
JsonObject mdns = doc["mdns"].to<JsonObject>();
mdns["enabled"] = config.Mdns.Enabled;

JsonObject syslog = doc["syslog"].to<JsonObject>();
syslog["enabled"] = config.Syslog.Enabled;
syslog["hostname"] = config.Syslog.Hostname;
syslog["port"] = config.Syslog.Port;

JsonObject ntp = doc["ntp"].to<JsonObject>();
ntp["server"] = config.Ntp.Server;
ntp["timezone"] = config.Ntp.Timezone;
Expand Down Expand Up @@ -450,6 +455,11 @@ bool ConfigurationClass::read()
JsonObject mdns = doc["mdns"];
config.Mdns.Enabled = mdns["enabled"] | MDNS_ENABLED;

JsonObject syslog = doc["syslog"];
config.Syslog.Enabled = syslog["enabled"] | SYSLOG_ENABLED;
strlcpy(config.Syslog.Hostname, syslog["hostname"] | "", sizeof(config.Syslog.Hostname));
config.Syslog.Port = syslog["port"] | SYSLOG_PORT;

JsonObject ntp = doc["ntp"];
strlcpy(config.Ntp.Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp.Server));
strlcpy(config.Ntp.Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp.Timezone));
Expand Down
3 changes: 3 additions & 0 deletions src/MessageOutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
#include <HardwareSerial.h>
#include "MessageOutput.h"
#include "SyslogLogger.h"

MessageOutputClass MessageOutput;

Expand Down Expand Up @@ -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<message_t>(std::move(_lines.front())));
_lines.pop();
}
Expand Down
5 changes: 5 additions & 0 deletions src/NetworkSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -294,6 +297,8 @@ void NetworkSettingsClass::applyConfig()
}
MessageOutput.println("done");
setStaticIp();

Syslog.updateSettings(getHostname());
}

void NetworkSettingsClass::setHostname()
Expand Down
138 changes: 138 additions & 0 deletions src/SyslogLogger.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include <HardwareSerial.h>
#include <ESPmDNS.h>
#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 = "<14>1 - "; // RFC5424: Facility USER, severity INFO, version 1, NIL timestamp.
_header += hostname;
_header += " OpenDTU ";
_header += _proc_id;
// NIL values for message id and structured data
_header += " - - ";

// Enable logger.
enable();
}

void SyslogLogger::write(const uint8_t *buffer, size_t size)
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(_mutex);
if (!_enabled || !NetworkSettings.isConnected() || isResolved()) {
return;
}
if (!resolveAndStart()) {
_enabled = false;
}
}

SyslogLogger Syslog;
23 changes: 23 additions & 0 deletions src/WebApi_network.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
root["password"] = config.WiFi.Password;
root["aptimeout"] = config.WiFi.ApTimeout;
root["mdnsenabled"] = config.Mdns.Enabled;
root["syslogenabled"] = config.Syslog.Enabled;
root["sysloghostname"] = config.Syslog.Hostname;
root["syslogport"] = config.Syslog.Port;

WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
Expand Down Expand Up @@ -163,6 +166,23 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["syslogenabled"].as<bool>()) {
if (root["sysloghostname"].as<String>().length() == 0 || root["sysloghostname"].as<String>().length() > SYSLOG_MAX_HOSTNAME_STRLEN) {
retMsg["message"] = "Syslog Server must between 1 and " STR(SYSLOG_MAX_HOSTNAME_STRLEN) " characters long!";
retMsg["code"] = WebApiError::NetworkSyslogHostnameLength;
retMsg["param"]["max"] = SYSLOG_MAX_HOSTNAME_STRLEN;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}

if (root["syslogport"].as<uint>() == 0 || root["syslogport"].as<uint>() > 65535) {
retMsg["message"] = "Port must be a number between 1 and 65535!";
retMsg["code"] = WebApiError::NetworkSyslogPort;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}

}

CONFIG_T& config = Configuration.get();
config.WiFi.Ip[0] = ipaddress[0];
Expand Down Expand Up @@ -195,6 +215,9 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
}
config.WiFi.ApTimeout = root["aptimeout"].as<uint>();
config.Mdns.Enabled = root["mdnsenabled"].as<bool>();
config.Syslog.Enabled = root["syslogenabled"].as<bool>();
strlcpy(config.Syslog.Hostname, root["sysloghostname"].as<String>().c_str(), sizeof(config.Syslog.Hostname));
config.Syslog.Port = root["syslogport"].as<uint>();

WebApi.writeConfig(retMsg);

Expand Down
5 changes: 4 additions & 1 deletion webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,10 @@
"ApTimeoutHint": "Zeit die der AccessPoint offen gehalten wird. Ein Wert von 0 bedeutet unendlich.",
"Minutes": "Minuten",
"EnableMdns": "mDNS aktivieren",
"MdnsSettings": "mDNS-Einstellungen"
"MdnsSettings": "mDNS-Einstellungen",
"EnableSyslog": "Syslog aktivieren",
"SyslogSettings": "Syslog-Einstellungen",
"Port": "Port:"
},
"mqttadmin": {
"MqttSettings": "MQTT-Einstellungen",
Expand Down
5 changes: 4 additions & 1 deletion webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,10 @@
"ApTimeoutHint": "Time which the AccessPoint is kept open. A value of 0 means infinite.",
"Minutes": "minutes",
"EnableMdns": "Enable mDNS",
"MdnsSettings": "mDNS Settings"
"MdnsSettings": "mDNS Settings",
"EnableSyslog": "Enable Syslog",
"SyslogSettings": "Syslog Settings",
"Port": "Port:"
},
"mqttadmin": {
"MqttSettings": "MQTT Settings",
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/types/NetworkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export interface NetworkConfig {
dns2: string;
aptimeout: number;
mdnsenabled: boolean;
syslogenabled: boolean;
sysloghostname: string;
syslogport: number;
}
23 changes: 23 additions & 0 deletions webapp/src/views/NetworkAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,29 @@
/>
</CardElement>

<CardElement :text="$t('networkadmin.SyslogSettings')" textVariant="text-bg-primary" add-space>
<InputElement
:label="$t('networkadmin.EnableSyslog')"
v-model="networkConfigList.syslogenabled"
type="checkbox"
/>

<InputElement
:label="$t('networkadmin.Hostname', { num: 1 })"
v-model="networkConfigList.sysloghostname"
type="text"
maxlength="128"
/>

<InputElement
:label="$t('networkadmin.Port')"
v-model="networkConfigList.syslogport"
type="number"
min="1"
max="65535"
/>
</CardElement>

<CardElement :text="$t('networkadmin.AdminAp')" textVariant="text-bg-primary" add-space>
<InputElement
:label="$t('networkadmin.ApTimeout')"
Expand Down

0 comments on commit 20159f3

Please sign in to comment.