Feature: Allow setting the log level for the different modules at runtime

This commit is contained in:
Thomas Basler
2025-04-19 11:07:31 +02:00
parent d51e79c90a
commit d4c29d708b
20 changed files with 478 additions and 1 deletions

View File

@@ -8,7 +8,7 @@
#include <mutex>
#define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change
#define CONFIG_VERSION 0x00011e00 // 0.1.30 // make sure to clean all after change
#define WIFI_MAX_SSID_STRLEN 32
#define WIFI_MAX_PASSWORD_STRLEN 64
@@ -35,6 +35,9 @@
#define DEV_MAX_MAPPING_NAME_STRLEN 63
#define LOCALE_STRLEN 2
#define LOG_MODULE_COUNT 16
#define LOG_MODULE_NAME_STRLEN 32
struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower;
char Name[CHAN_MAX_NAME_STRLEN];
@@ -161,6 +164,14 @@ struct CONFIG_T {
INVERTER_CONFIG_T Inverter[INV_MAX_COUNT];
char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1];
struct {
int8_t Default;
struct {
char Name[LOG_MODULE_NAME_STRLEN + 1];
int8_t Level;
} Modules[LOG_MODULE_COUNT];
} Logging;
};
class ConfigurationClass {
@@ -187,6 +198,8 @@ public:
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
void deleteInverterById(const uint8_t id);
int8_t getIndexForLogModule(const String& moduleName) const;
private:
void loop();

17
include/Logging.h Normal file
View File

@@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <WString.h>
#include <vector>
class LoggingClass {
public:
LoggingClass();
void applyLogLevels();
const std::vector<String>& getConfigurableModules() const;
private:
std::vector<String> _configurableModules;
};
extern LoggingClass Logging;

View File

@@ -12,6 +12,7 @@
#include "WebApi_i18n.h"
#include "WebApi_inverter.h"
#include "WebApi_limit.h"
#include "WebApi_logging.h"
#include "WebApi_maintenance.h"
#include "WebApi_mqtt.h"
#include "WebApi_network.h"
@@ -57,6 +58,7 @@ private:
WebApiI18nClass _webApiI18n;
WebApiInverterClass _webApiInverter;
WebApiLimitClass _webApiLimit;
WebApiLoggingClass _webApiLogging;
WebApiMaintenanceClass _webApiMaintenance;
WebApiMqttClass _webApiMqtt;
WebApiNetworkClass _webApiNetwork;

14
include/WebApi_logging.h Normal file
View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiLoggingClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onLoggingAdminGet(AsyncWebServerRequest* request);
void onLoggingAdminPost(AsyncWebServerRequest* request);
};

View File

@@ -22,6 +22,7 @@
"MQTTSettings": "MQTT Ρυθμίσεις",
"InverterSettings": "Ρυθμίσεις Μετατροπέα",
"SecuritySettings": "Ρυθμίσεις Ασφάλειας",
"LoggingSettings": "Logging Settings",
"DTUSettings": "DTU Ρυθμίσεις",
"DeviceManager": "Διαχείριση Συσκευών",
"ConfigManagement": "Διαχείριση διαμόρφωσης",
@@ -700,6 +701,19 @@
"format_herf_valid": "Μορφή E-Star HERF (θα αποθηκευτεί μετατρεπόμενη): {serial}",
"format_herf_invalid": "Μορφή E-Star HERF: Μη έγκυρο άθροισμα ελέγχου",
"format_unknown": "Άγνωστη μορφή"
},
"loggingadmin": {
"LoggingSettings": "Logging Settings",
"LogLevel": "Log Level",
"DefaultLevel": "Default log level",
"Module": "Module",
"log_inherit": "Inherit from default",
"log_none": "None",
"log_error": "(E) Error",
"log_warn": "(W) Warning",
"log_info": "(I) Info",
"log_debug": "(D) Debug",
"log_verbose": "(V) Verbose"
}
}
}

View File

@@ -22,6 +22,7 @@
"MQTTSettings": "Ajustes MQTT",
"InverterSettings": "Ajustes Inversor",
"SecuritySettings": "Ajustes Seguridad",
"LoggingSettings": "Logging Settings",
"DTUSettings": "Ajustes DTU",
"DeviceManager": "Administrador Dispositivos",
"ConfigManagement": "Gestión configuración",
@@ -700,6 +701,19 @@
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
},
"loggingadmin": {
"LoggingSettings": "Logging Settings",
"LogLevel": "Log Level",
"DefaultLevel": "Default log level",
"Module": "Module",
"log_inherit": "Inherit from default",
"log_none": "None",
"log_error": "(E) Error",
"log_warn": "(W) Warning",
"log_info": "(I) Info",
"log_debug": "(D) Debug",
"log_verbose": "(V) Verbose"
}
}
}

View File

@@ -22,6 +22,7 @@
"MQTTSettings": "Impostazioni MQTT",
"InverterSettings": "Impostazioni Inverter",
"SecuritySettings": "Impostazioni di Sicurezza",
"LoggingSettings": "Logging Settings",
"DTUSettings": "Impostazioni DTU",
"DeviceManager": "Gestione Dispositivi",
"ConfigManagement": "Gestione Configurazione",
@@ -700,6 +701,19 @@
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
},
"loggingadmin": {
"LoggingSettings": "Logging Settings",
"LogLevel": "Log Level",
"DefaultLevel": "Default log level",
"Module": "Module",
"log_inherit": "Inherit from default",
"log_none": "None",
"log_error": "(E) Error",
"log_warn": "(W) Warning",
"log_info": "(I) Info",
"log_debug": "(D) Debug",
"log_verbose": "(V) Verbose"
}
}
}

View File

@@ -22,6 +22,7 @@
"MQTTSettings": "Ustawienia serwera MQTT",
"InverterSettings": "Ustawienia falownika",
"SecuritySettings": "Ustawienia zabezpieczeń",
"LoggingSettings": "Logging Settings",
"DTUSettings": "Ustawienia DTU",
"DeviceManager": "Zarządzanie urządzeniami",
"ConfigManagement": "Zarządzanie konfiguracją",
@@ -700,6 +701,19 @@
"format_herf_valid": "Format E-Star HERF (zostanie zapisany po konwersji): {serial}",
"format_herf_invalid": "Format E-Star HERF: Nieprawidłowa suma kontrolna",
"format_unknown": "Nieznany format"
},
"loggingadmin": {
"LoggingSettings": "Logging Settings",
"LogLevel": "Log Level",
"DefaultLevel": "Default log level",
"Module": "Module",
"log_inherit": "Inherit from default",
"log_none": "None",
"log_error": "(E) Error",
"log_warn": "(W) Warning",
"log_info": "(I) Info",
"log_debug": "(D) Debug",
"log_verbose": "(V) Verbose"
}
}
}

View File

@@ -8,6 +8,7 @@
#include "defaults.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <esp_log.h>
#include <nvs_flash.h>
#undef TAG
@@ -153,6 +154,15 @@ bool ConfigurationClass::write()
}
}
JsonObject logging = doc["logging"].to<JsonObject>();
logging["default"] = config.Logging.Default;
JsonArray modules = logging["modules"].to<JsonArray>();
for (uint8_t i = 0; i < LOG_MODULE_COUNT; i++) {
JsonObject module = modules.add<JsonObject>();
module["level"] = config.Logging.Modules[i].Level;
module["name"] = config.Logging.Modules[i].Name;
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
@@ -329,6 +339,15 @@ bool ConfigurationClass::read()
}
}
JsonObject logging = doc["logging"];
config.Logging.Default = logging["default"] | ESP_LOG_ERROR;
JsonArray modules = logging["modules"];
for (uint8_t i = 0; i < LOG_MODULE_COUNT; i++) {
JsonObject module = modules[i].as<JsonObject>();
strlcpy(config.Logging.Modules[i].Name, module["name"] | "", sizeof(config.Logging.Modules[i].Name));
config.Logging.Modules[i].Level = module["level"] | ESP_LOG_VERBOSE;
}
f.close();
// Check for default DTU serial
@@ -426,6 +445,12 @@ void ConfigurationClass::migrate()
}
}
if (config.Cfg.Version < 0x00011e00) {
config.Logging.Default = ESP_LOG_VERBOSE;
strlcpy(config.Logging.Modules[0].Name, "CORE", sizeof(config.Logging.Modules[0].Name));
config.Logging.Modules[0].Level = ESP_LOG_ERROR;
}
f.close();
config.Cfg.Version = CONFIG_VERSION;
@@ -487,6 +512,17 @@ void ConfigurationClass::deleteInverterById(const uint8_t id)
}
}
int8_t ConfigurationClass::getIndexForLogModule(const String& moduleName) const
{
for (uint8_t i = 0; i < LOG_MODULE_COUNT; i++) {
if (strcmp(config.Logging.Modules[i].Name, moduleName.c_str()) == 0) {
return i;
}
}
return -1;
}
void ConfigurationClass::loop()
{
std::unique_lock<std::mutex> lock(sWriterMutex);

37
src/Logging.cpp Normal file
View File

@@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2025 Thomas Basler and others
*/
#include "Logging.h"
#include "Configuration.h"
LoggingClass Logging;
LoggingClass::LoggingClass()
{
_configurableModules.reserve(3);
_configurableModules.push_back("CORE");
_configurableModules.push_back("hoymiles");
_configurableModules.push_back("mqtt");
_configurableModules.push_back("network");
_configurableModules.push_back("webapi");
}
const std::vector<String>& LoggingClass::getConfigurableModules() const
{
return _configurableModules;
}
void LoggingClass::applyLogLevels()
{
const CONFIG_T& config = Configuration.get();
esp_log_level_set("*", static_cast<esp_log_level_t>(config.Logging.Default));
for (int8_t i = 0; i < LOG_MODULE_COUNT; i++) {
if (strlen(config.Logging.Modules[i].Name) == 0 || config.Logging.Modules[i].Level < ESP_LOG_NONE || config.Logging.Modules[i].Level > ESP_LOG_VERBOSE) {
continue;
}
esp_log_level_set(config.Logging.Modules[i].Name, static_cast<esp_log_level_t>(config.Logging.Modules[i].Level));
}
}

View File

@@ -27,6 +27,7 @@ void WebApiClass::init(Scheduler& scheduler)
_webApiI18n.init(_server, scheduler);
_webApiInverter.init(_server, scheduler);
_webApiLimit.init(_server, scheduler);
_webApiLogging.init(_server, scheduler);
_webApiMaintenance.init(_server, scheduler);
_webApiMqtt.init(_server, scheduler);
_webApiNetwork.init(_server, scheduler);

105
src/WebApi_logging.cpp Normal file
View File

@@ -0,0 +1,105 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2025 Thomas Basler and others
*/
#include "WebApi_logging.h"
#include "Configuration.h"
#include "Logging.h"
#include "WebApi.h"
#include "WebApi_errors.h"
#include "helper.h"
#include <AsyncJson.h>
#include <vector>
void WebApiLoggingClass::init(AsyncWebServer& server, Scheduler& scheduler)
{
using std::placeholders::_1;
server.on("/api/logging/config", HTTP_GET, std::bind(&WebApiLoggingClass::onLoggingAdminGet, this, _1));
server.on("/api/logging/config", HTTP_POST, std::bind(&WebApiLoggingClass::onLoggingAdminPost, this, _1));
}
void WebApiLoggingClass::onLoggingAdminGet(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get();
auto& configurableModules = Logging.getConfigurableModules();
JsonObject loglevel = root["loglevel"].to<JsonObject>();
loglevel["default"] = config.Logging.Default;
JsonArray logModules = loglevel["modules"].to<JsonArray>();
for (const auto& availModule : configurableModules) {
JsonObject logModule = logModules.add<JsonObject>();
logModule["name"] = availModule;
int8_t idx = Configuration.getIndexForLogModule(availModule);
// Set to inherit if unknown
logModule["level"] = idx < 0 || idx > ESP_LOG_VERBOSE ? -1 : config.Logging.Modules[idx].Level;
}
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiLoggingClass::onLoggingAdminPost(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
auto& configurableModules = Logging.getConfigurableModules();
{
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
config.Logging.Default = std::max<int8_t>(ESP_LOG_NONE, std::min<int8_t>(ESP_LOG_VERBOSE, root["loglevel"]["default"].as<int8_t>()));
for (uint8_t i = 0; i < LOG_MODULE_COUNT; i++) {
config.Logging.Modules[i].Level = ESP_LOG_NONE;
config.Logging.Modules[i].Name[0] = '\0';
}
JsonArray logmodules = root["loglevel"]["modules"].as<JsonArray>();
uint8_t i = 0;
for (const auto& logmodule : logmodules) {
bool isValidModule = std::find(configurableModules.begin(), configurableModules.end(), logmodule["name"] | "") != configurableModules.end();
if (!isValidModule) {
continue;
}
int8_t level = std::max<int8_t>(-1, std::min<int8_t>(ESP_LOG_VERBOSE, logmodule["level"].as<int8_t>()));
if (level < ESP_LOG_NONE) {
// Skip modules set to inherit
continue;
}
config.Logging.Modules[i].Level = level;
strlcpy(config.Logging.Modules[i].Name, logmodule["name"] | "", sizeof(config.Logging.Modules[i].Name));
if (++i >= LOG_MODULE_COUNT) {
break;
}
}
}
Logging.applyLogLevels();
WebApi.writeConfig(retMsg);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@@ -8,6 +8,7 @@
#include "I18n.h"
#include "InverterSettings.h"
#include "Led_Single.h"
#include "Logging.h"
#include "MessageOutput.h"
#include "MqttHandleDtu.h"
#include "MqttHandleHass.h"
@@ -73,6 +74,10 @@ void setup()
Configuration.migrate();
}
// Set configured log levels
Logging.applyLogLevels();
esp_log_level_set(TAG, ESP_LOG_VERBOSE);
// Read languate pack
ESP_LOGI(TAG, "Reading language pack...");
I18n.init(scheduler);

View File

@@ -63,6 +63,11 @@
>{{ $t('menu.SecuritySettings') }}
</router-link>
</li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/logging"
>{{ $t('menu.LoggingSettings') }}
</router-link>
</li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/dtu">{{
$t('menu.DTUSettings')

View File

@@ -7,6 +7,7 @@
"MQTTSettings": "MQTT",
"InverterSettings": "Wechselrichter",
"SecuritySettings": "Sicherheit",
"LoggingSettings": "Protokollierung",
"DTUSettings": "DTU",
"DeviceManager": "Hardware",
"ConfigManagement": "Konfigurationsverwaltung",
@@ -684,5 +685,18 @@
"format_herf_valid": "E-Star HERF Format (wird konvertiert gespeichert): {serial}",
"format_herf_invalid": "E-Star HERF Format: Ungültige Prüfsumme",
"format_unknown": "Unbekanntes Format"
},
"loggingadmin": {
"LoggingSettings": "Protokollierungseinstellungen",
"LogLevel": "Protokollierungsstufe",
"DefaultLevel": "Standard-Protokollierungsstufe",
"Module": "Modul",
"log_inherit": "Von Standard erben",
"log_none": "Keine",
"log_error": "(E) Fehler",
"log_warn": "(W) Warnung",
"log_info": "(I) Info",
"log_debug": "(D) Debug",
"log_verbose": "(V) Ausführlich"
}
}

View File

@@ -7,6 +7,7 @@
"MQTTSettings": "MQTT Settings",
"InverterSettings": "Inverter Settings",
"SecuritySettings": "Security Settings",
"LoggingSettings": "Logging Settings",
"DTUSettings": "DTU Settings",
"DeviceManager": "Device-Manager",
"ConfigManagement": "Config Management",
@@ -685,5 +686,18 @@
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
},
"loggingadmin": {
"LoggingSettings": "Logging Settings",
"LogLevel": "Log Level",
"DefaultLevel": "Default log level",
"Module": "Module",
"log_inherit": "Inherit from default",
"log_none": "None",
"log_error": "(E) Error",
"log_warn": "(W) Warning",
"log_info": "(I) Info",
"log_debug": "(D) Debug",
"log_verbose": "(V) Verbose"
}
}

View File

@@ -7,6 +7,7 @@
"MQTTSettings": "MQTT",
"InverterSettings": "Onduleurs",
"SecuritySettings": "Sécurité",
"LoggingSettings": "Logging Settings",
"DTUSettings": "DTU",
"DeviceManager": "Périphériques",
"ConfigManagement": "Gestion de la configuration",
@@ -666,5 +667,18 @@
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
},
"loggingadmin": {
"LoggingSettings": "Logging Settings",
"LogLevel": "Log Level",
"DefaultLevel": "Default log level",
"Module": "Module",
"log_inherit": "Inherit from default",
"log_none": "None",
"log_error": "(E) Error",
"log_warn": "(W) Warning",
"log_info": "(I) Info",
"log_debug": "(D) Debug",
"log_verbose": "(V) Verbose"
}
}

View File

@@ -9,6 +9,7 @@ import HomeView from '@/views/HomeView.vue';
import InverterAdminView from '@/views/InverterAdminView.vue';
import LoginView from '@/views/LoginView.vue';
import MaintenanceRebootView from '@/views/MaintenanceRebootView.vue';
import LoggingAdminView from '@/views/LoggingAdminView.vue';
import MqttAdminView from '@/views/MqttAdminView.vue';
import MqttInfoView from '@/views/MqttInfoView.vue';
import NetworkAdminView from '@/views/NetworkAdminView.vue';
@@ -121,6 +122,11 @@ const router = createRouter({
name: 'Security',
component: SecurityAdminView,
},
{
path: '/settings/logging',
name: 'Logging',
component: LoggingAdminView,
},
{
path: '/maintenance/reboot',
name: 'Device Reboot',

View File

@@ -0,0 +1,13 @@
export interface LogModule {
name: string;
level: number;
}
export interface LogLevel {
default: number;
modules: Array<LogModule>;
}
export interface LoggingConfig {
loglevel: LogLevel;
}

View File

@@ -0,0 +1,125 @@
<template>
<BasePage :title="$t('loggingadmin.LoggingSettings')" :isLoading="dataLoading">
<BootstrapAlert v-model="alert.show" dismissible :variant="alert.type">
{{ alert.message }}
</BootstrapAlert>
<form @submit="saveLogConfig">
<CardElement :text="$t('loggingadmin.LogLevel')" textVariant="text-bg-primary">
<div class="row mb-3">
<label for="inputDefaultLevel" class="col-sm-2 col-form-label">{{
$t('loggingadmin.DefaultLevel')
}}</label>
<div class="col-sm-10">
<select class="form-select" id="inputDefaultLevel" v-model="loggingList.loglevel.default">
<option
v-for="level in logLevelList.filter((property) => property.key >= 0)"
:value="level.key"
:key="level.key"
>
{{ $t('loggingadmin.' + level.value) }}
</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">{{ $t('loggingadmin.Module') }}</th>
<th>{{ $t('loggingadmin.LogLevel') }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="module in loggingList.loglevel.modules"
v-bind:key="module.name"
:data-id="module.name"
class="align-middle"
>
<td>{{ module.name }}</td>
<td>
<select class="form-select" v-model="module.level">
<option v-for="level in logLevelList" :value="level.key" :key="level.key">
{{ $t('loggingadmin.' + level.value) }}
</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
</CardElement>
<FormFooter @reload="getLogConfig" />
</form>
</BasePage>
</template>
<script lang="ts">
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from '@/components/BootstrapAlert.vue';
import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import type { AlertResponse } from '@/types/AlertResponse';
import type { LoggingConfig } from '@/types/LoggingConfig';
import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
BasePage,
BootstrapAlert,
CardElement,
FormFooter,
},
data() {
return {
dataLoading: true,
alert: {} as AlertResponse,
loggingList: {} as LoggingConfig,
logLevelList: [
{ key: -1, value: 'log_inherit' },
{ key: 0, value: 'log_none' },
{ key: 1, value: 'log_error' },
{ key: 2, value: 'log_warn' },
{ key: 3, value: 'log_info' },
{ key: 4, value: 'log_debug' },
{ key: 5, value: 'log_verbose' },
],
};
},
created() {
this.getLogConfig();
},
methods: {
getLogConfig() {
this.dataLoading = true;
fetch('/api/logging/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.loggingList = data;
this.dataLoading = false;
});
},
saveLogConfig(e: Event) {
e.preventDefault();
const formData = new FormData();
formData.append('data', JSON.stringify(this.loggingList));
fetch('/api/logging/config', {
method: 'POST',
headers: authHeader(),
body: formData,
})
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.alert = data;
this.alert.message = this.$t('apiresponse.' + data.code, data.param);
this.alert.show = true;
});
},
},
});
</script>