diff --git a/interface/src/components/SectionContent.tsx b/interface/src/components/SectionContent.tsx index 3d200b66a..fdd0ede8a 100644 --- a/interface/src/components/SectionContent.tsx +++ b/interface/src/components/SectionContent.tsx @@ -15,13 +15,14 @@ const useStyles = makeStyles((theme: Theme) => interface SectionContentProps { title: string; titleGutter?: boolean; + id?: string; } const SectionContent: React.FC = (props) => { - const { children, title, titleGutter } = props; + const { children, title, titleGutter, id } = props; const classes = useStyles(); return ( - + {title} diff --git a/interface/src/project/EMSESPDevicesController.tsx b/interface/src/project/EMSESPDevicesController.tsx index 66a52bd3a..6b958f722 100644 --- a/interface/src/project/EMSESPDevicesController.tsx +++ b/interface/src/project/EMSESPDevicesController.tsx @@ -6,6 +6,7 @@ import { RestFormLoader, SectionContent } from '../components'; + import { ENDPOINT_ROOT } from '../api'; import EMSESPDevicesForm from './EMSESPDevicesForm'; import { EMSESPDevices } from './EMSESPtypes'; diff --git a/interface/src/project/EMSESPDevicesForm.tsx b/interface/src/project/EMSESPDevicesForm.tsx index bb604151c..98630dae2 100644 --- a/interface/src/project/EMSESPDevicesForm.tsx +++ b/interface/src/project/EMSESPDevicesForm.tsx @@ -36,6 +36,7 @@ import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication'; + import { RestFormProps, FormButton, extractEventValue } from '../components'; import { diff --git a/interface/src/system/LogEventConsole.tsx b/interface/src/system/LogEventConsole.tsx index 7d250c5bc..6ad675c4e 100644 --- a/interface/src/system/LogEventConsole.tsx +++ b/interface/src/system/LogEventConsole.tsx @@ -38,10 +38,10 @@ const useStyles = makeStyles((theme: Theme) => ({ whiteSpace: 'nowrap' }, debug: { - color: '#0000ff' + color: '#00FFFF' }, info: { - color: '#00ff00' + color: '#ffff00' }, notice: { color: '#ffff00' @@ -67,7 +67,8 @@ const LogEventConsole: FC = (props) => { return classes.info; case LogLevel.NOTICE: return classes.notice; - case LogLevel.ERR: + case LogLevel.WARNING: + case LogLevel.ERROR: return classes.err; default: return classes.unknown; @@ -80,10 +81,14 @@ const LogEventConsole: FC = (props) => { return 'DEBUG'; case LogLevel.INFO: return 'INFO'; - case LogLevel.ERR: - return 'ERR'; + case LogLevel.ERROR: + return 'ERROR'; case LogLevel.NOTICE: return 'NOTICE'; + case LogLevel.WARNING: + return 'WARNING'; + case LogLevel.TRACE: + return 'TRACE'; default: return '?'; } @@ -91,17 +96,23 @@ const LogEventConsole: FC = (props) => { const paddedLevelLabel = (level: LogLevel) => { const label = levelLabel(level); - return label.padStart(7, '\xa0'); + return label.padStart(8, '\xa0'); + }; + + const paddedNameLabel = (name: string) => { + const label = '[' + name + ']'; + return label.padStart(8, '\xa0'); }; return ( - + {events.map((e) => (
- {e.time} + {e.time} {paddedLevelLabel(e.level)}{' '} + {paddedNameLabel(e.name)} {e.message}
))} diff --git a/interface/src/system/LogEventController.tsx b/interface/src/system/LogEventController.tsx index 3a2cb0fd4..9782762d4 100644 --- a/interface/src/system/LogEventController.tsx +++ b/interface/src/system/LogEventController.tsx @@ -1,23 +1,26 @@ import { Component } from 'react'; -import { FormActions, FormButton } from '../components'; + +import { createStyles, WithStyles, Theme } from '@material-ui/core'; import { - createStyles, - WithStyles, - withStyles, - Typography, - Theme, - Paper -} from '@material-ui/core'; + restController, + RestControllerProps, + RestFormLoader, + SectionContent +} from '../components'; -import { LogEvent } from './types'; -import { EVENT_SOURCE_ROOT } from '../api/Env'; -import LogEventConsole from './LogEventConsole'; import { addAccessTokenParameter } from '../authentication'; -import SaveIcon from '@material-ui/icons/Save'; +import { ENDPOINT_ROOT, EVENT_SOURCE_ROOT } from '../api'; +export const FETCH_LOG_ENDPOINT = ENDPOINT_ROOT + 'fetchLog'; +export const LOG_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'logSettings'; -const LOG_EVENT_EVENT_SOURCE_URL = EVENT_SOURCE_ROOT + 'log'; +export const LOG_EVENT_EVENT_SOURCE_URL = EVENT_SOURCE_ROOT + 'log'; + +import LogEventForm from './LogEventForm'; +import LogEventConsole from './LogEventConsole'; + +import { LogEvent, LogSettings } from './types'; interface LogEventControllerState { eventSource?: EventSource; @@ -32,7 +35,8 @@ const styles = (theme: Theme) => } }); -type LogEventControllerProps = WithStyles; +type LogEventControllerProps = RestControllerProps & + WithStyles; class LogEventController extends Component< LogEventControllerProps, @@ -49,6 +53,8 @@ class LogEventController extends Component< } componentDidMount() { + this.props.loadData(); + this.fetchLog(); this.configureEventSource(); } @@ -61,6 +67,24 @@ class LogEventController extends Component< } } + fetchLog = () => { + fetch(FETCH_LOG_ENDPOINT) + .then((response) => { + if (response.status === 200) { + return response.json(); + } else { + throw Error('Unexpected status code: ' + response.status); + } + }) + .then((json) => { + this.setState({ events: json.events }); + }) + .catch((error) => { + this.setState({ events: [] }); + throw Error('Unexpected error: ' + error); + }); + }; + configureEventSource = () => { this.eventSource = new EventSource( addAccessTokenParameter(LOG_EVENT_EVENT_SOURCE_URL) @@ -86,24 +110,16 @@ class LogEventController extends Component< }; render() { - const { classes } = this.props; return ( - - System Log - - } - variant="contained" - color="secondary" - // onClick={this.requestNetworkScan} - > - Save - - + + } + /> - + ); } } -export default withStyles(styles)(LogEventController); +export default restController(LOG_SETTINGS_ENDPOINT, LogEventController); diff --git a/interface/src/system/LogEventForm.tsx b/interface/src/system/LogEventForm.tsx new file mode 100644 index 000000000..e266788c2 --- /dev/null +++ b/interface/src/system/LogEventForm.tsx @@ -0,0 +1,87 @@ +import { Component } from 'react'; + +import { + ValidatorForm, + SelectValidator +} from 'react-material-ui-form-validator'; + +import MenuItem from '@material-ui/core/MenuItem'; + +import { + redirectingAuthorizedFetch, + withAuthenticatedContext, + AuthenticatedContextProps +} from '../authentication'; + +import { RestFormProps } from '../components'; +import { LogSettings } from './types'; + +import { ENDPOINT_ROOT } from '../api'; +export const LOG_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'logSettings'; + +type LogEventFormProps = AuthenticatedContextProps & RestFormProps; + +class LogEventForm extends Component { + changeLevel = (event: React.ChangeEvent) => { + const { data, setData } = this.props; + setData({ + ...data, + level: parseInt(event.target.value) + }); + + redirectingAuthorizedFetch(LOG_SETTINGS_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ level: event.target.value }), + headers: { + 'Content-Type': 'application/json' + } + }) + .then((response) => { + if (response.status === 200) { + return response.json(); + } + throw Error('Unexpected response code: ' + response.status); + }) + .then((json) => { + this.props.enqueueSnackbar('Log settings changed', { + variant: 'success' + }); + setData({ + ...data, + level: json.level + }); + }) + .catch((error) => { + this.props.enqueueSnackbar( + error.message || 'Problem changing log settings', + { variant: 'warning' } + ); + }); + }; + + render() { + const { data, saveData } = this.props; + return ( + + + OFF + ERROR + WARNING + NOTICE + INFO + DEBUG + TRACE + + + ); + } +} + +export default withAuthenticatedContext(LogEventForm); diff --git a/interface/src/system/OTASettingsController.tsx b/interface/src/system/OTASettingsController.tsx index 0ad07d6ce..b18f5ce5a 100644 --- a/interface/src/system/OTASettingsController.tsx +++ b/interface/src/system/OTASettingsController.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import { Component } from 'react'; import { restController, diff --git a/interface/src/system/OTASettingsForm.tsx b/interface/src/system/OTASettingsForm.tsx index c511e9db9..394eba4a8 100644 --- a/interface/src/system/OTASettingsForm.tsx +++ b/interface/src/system/OTASettingsForm.tsx @@ -11,6 +11,7 @@ import { FormButton, FormActions } from '../components'; + import { isIP, isHostname, or } from '../validators'; import { OTASettings } from './types'; diff --git a/interface/src/system/types.ts b/interface/src/system/types.ts index e406be64b..ea0a446df 100644 --- a/interface/src/system/types.ts +++ b/interface/src/system/types.ts @@ -38,14 +38,21 @@ export interface OTASettings { } export enum LogLevel { - ERR = 3, + ERROR = 3, + WARNING = 4, NOTICE = 5, INFO = 6, - DEBUG = 7 + DEBUG = 7, + TRACE = 8 } export interface LogEvent { time: string; level: LogLevel; + name: string; message: string; } + +export interface LogSettings { + level: LogLevel; +} diff --git a/mock-api/server.js b/mock-api/server.js index 3794dd923..634f07686 100644 --- a/mock-api/server.js +++ b/mock-api/server.js @@ -14,6 +14,54 @@ const server = express() const es_port = 3090 const ES_ENDPOINT_ROOT = '/es/' +// LOG +const LOG_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'logSettings' +const log_settings = { + level: 6, +} + +const FETCH_LOG_ENDPOINT = REST_ENDPOINT_ROOT + 'fetchLog' +const fetch_log = { + events: [ + { + time: '000+00:00:00.001', + level: 3, + name: 'system', + message: 'this is message 3', + }, + { + time: '000+00:00:00.002', + level: 4, + name: 'system', + message: 'this is message 4', + }, + { + time: '000+00:00:00.002', + level: 5, + name: 'system', + message: 'this is message 5', + }, + { + time: '000+00:00:00.002', + level: 6, + name: 'system', + message: 'this is message 6', + }, + { + time: '000+00:00:00.002', + level: 7, + name: 'emsesp', + message: 'this is message 7', + }, + { + time: '000+00:00:00.002', + level: 8, + name: 'mqtt', + message: 'this is message 8', + }, + ], +} + // NTP const NTP_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'ntpStatus' const NTP_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'ntpSettings' @@ -703,6 +751,21 @@ const emsesp_devicedata_3 = { data: [], } +// LOG +app.get(FETCH_LOG_ENDPOINT, (req, res) => { + res.json(fetch_log) +}) +app.get(LOG_SETTINGS_ENDPOINT, (req, res) => { + res.json(log_settings) +}) +app.post(LOG_SETTINGS_ENDPOINT, (req, res) => { + console.log('New log level is ' + req.body.level) + const data = { + level: req.body.level, + } + res.json(data) +}) + // NETWORK app.get(NETWORK_STATUS_ENDPOINT, (req, res) => { res.json(network_status) @@ -943,7 +1006,8 @@ const streamLog = (req, res) => { const data = { time: '000+00:00:00.000', - level: 4, + level: 3, + name: 'system', message: 'this is message #' + count, } diff --git a/src/system.cpp b/src/system.cpp index b35d6d9ce..5368f1cae 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -413,7 +413,6 @@ void System::loop() { send_heartbeat(); } - /* #ifndef EMSESP_STANDALONE #if defined(EMSESP_DEBUG) static uint32_t last_memcheck_ = 0; @@ -423,7 +422,6 @@ void System::loop() { } #endif #endif -*/ #endif } diff --git a/src/test/test.cpp b/src/test/test.cpp index bf531746b..c2d35469d 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -930,6 +930,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { } if (command == "api") { +#if defined(EMSESP_STANDALONE) shell.printfln(F("Testing RESTful API...")); Mqtt::ha_enabled(false); run_test("general"); @@ -942,6 +943,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { request.url("/api/boiler/syspress"); EMSESP::webAPIService.webAPIService_get(&request); +#endif } } diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 557936849..2cd541b3f 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -223,11 +223,8 @@ void WebAPIService::send_message_response(AsyncWebServerRequest * request, uint1 } /** - * Extract only the path component from the passed URI - * and normalized it. - * Ex. //one/two////three/// - * becomes - * /one/two/three + * Extract only the path component from the passed URI and normalized it. + * Ex. //one/two////three/// becomes /one/two/three */ std::string SUrlParser::path() { std::string s = "/"; // set up the beginning slash diff --git a/src/web/WebLogService.cpp b/src/web/WebLogService.cpp index bbdc35249..5d1c534a5 100644 --- a/src/web/WebLogService.cpp +++ b/src/web/WebLogService.cpp @@ -23,10 +23,22 @@ using namespace std::placeholders; namespace emsesp { WebLogService::WebLogService(AsyncWebServer * server, SecurityManager * securityManager) - : _events(EVENT_SOURCE_LOG_PATH) { + : _events(EVENT_SOURCE_LOG_PATH) + , _setLevel(LOG_SETTINGS_PATH, std::bind(&WebLogService::setLevel, this, _1, _2), 256) { // for POSTS + _events.setFilter(securityManager->filterRequest(AuthenticationPredicates::IS_ADMIN)); server->addHandler(&_events); server->on(EVENT_SOURCE_LOG_PATH, HTTP_GET, std::bind(&WebLogService::forbidden, this, _1)); + + // for bring back the whole log + server->on(FETCH_LOG_PATH, HTTP_GET, std::bind(&WebLogService::fetchLog, this, _1)); + + server->on(LOG_SETTINGS_PATH, HTTP_GET, std::bind(&WebLogService::getLevel, this, _1)); + + // for setting a level + server->addHandler(&_setLevel); + + // start event source service start(); } @@ -42,22 +54,6 @@ uuid::log::Level WebLogService::log_level() const { return uuid::log::Logger::get_log_level(this); } -void WebLogService::remove_queued_messages(uuid::log::Level level) { - unsigned long offset = 0; - - for (auto it = log_messages_.begin(); it != log_messages_.end();) { - if (it->content_->level > level) { - offset++; - it = log_messages_.erase(it); - } else { - it->id_ -= offset; - it++; - } - } - - log_message_id_ -= offset; -} - void WebLogService::log_level(uuid::log::Level level) { uuid::log::Logger::register_handler(this, level); } @@ -86,22 +82,22 @@ void WebLogService::operator<<(std::shared_ptr message) { } void WebLogService::loop() { - if (!_events.count()) { + if (!_events.count() || log_messages_.empty()) { return; } - while (!log_messages_.empty() && can_transmit()) { - transmit(log_messages_.front()); - log_messages_.pop_front(); - } -} - -bool WebLogService::can_transmit() { + // put a small delay in const uint64_t now = uuid::get_uptime_ms(); - if (now < last_transmit_ || now - last_transmit_ < 100) { - return false; + if (now < last_transmit_ || now - last_transmit_ < 50) { + return; + } + + // see if we've advanced + if (log_messages_.back().id_ > log_message_id_tail_) { + transmit(log_messages_.back()); + log_message_id_tail_ = log_messages_.back().id_; + last_transmit_ = uuid::get_uptime_ms(); } - return true; } // send to web eventsource @@ -110,6 +106,7 @@ void WebLogService::transmit(const QueuedLogMessage & message) { JsonObject logEvent = jsonDocument.to(); logEvent["time"] = uuid::log::format_timestamp_ms(message.content_->uptime_ms, 3); logEvent["level"] = message.content_->level; + logEvent["name"] = message.content_->name; logEvent["message"] = message.content_->text; size_t len = measureJson(jsonDocument); @@ -119,8 +116,57 @@ void WebLogService::transmit(const QueuedLogMessage & message) { _events.send(buffer, "message", millis()); } delete[] buffer; +} - last_transmit_ = uuid::get_uptime_ms(); +// send the current log buffer to the API +void WebLogService::fetchLog(AsyncWebServerRequest * request) { + AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE_DYN); + JsonObject root = response->getRoot(); + + JsonArray log = root.createNestedArray("events"); + + for (const auto & msg : log_messages_) { + JsonObject logEvent = log.createNestedObject(); + auto message = std::move(msg); + logEvent["time"] = uuid::log::format_timestamp_ms(message.content_->uptime_ms, 3); + logEvent["level"] = message.content_->level; + logEvent["name"] = message.content_->name; + logEvent["message"] = message.content_->text; + } + + log_message_id_tail_ = log_messages_.back().id_; + + response->setLength(); + request->send(response); +} + +// sets the level +void WebLogService::setLevel(AsyncWebServerRequest * request, JsonVariant & json) { + if (not json.is()) { + return; + } + auto && body = json.as(); + uuid::log::Level level = body["level"]; + log_level(level); + + // send the value back + AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); + JsonObject root = response->getRoot(); + root["level"] = log_level(); + response->setLength(); + request->send(response); +} + +// return the current log level +void WebLogService::getLevel(AsyncWebServerRequest * request) { + AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); + JsonObject root = response->getRoot(); + + auto level = log_level(); + root["level"] = level; + + response->setLength(); + request->send(response); } } // namespace emsesp diff --git a/src/web/WebLogService.h b/src/web/WebLogService.h index f5b1a9ea8..a888c00d6 100644 --- a/src/web/WebLogService.h +++ b/src/web/WebLogService.h @@ -27,6 +27,8 @@ #include #define EVENT_SOURCE_LOG_PATH "/es/log" +#define FETCH_LOG_PATH "/rest/fetchLog" +#define LOG_SETTINGS_PATH "/rest/logSettings" namespace emsesp { @@ -59,14 +61,18 @@ class WebLogService : public uuid::log::Handler { }; void forbidden(AsyncWebServerRequest * request); - void remove_queued_messages(uuid::log::Level level); - bool can_transmit(); void transmit(const QueuedLogMessage & message); + void fetchLog(AsyncWebServerRequest * request); + void getLevel(AsyncWebServerRequest * request); + + void setLevel(AsyncWebServerRequest * request, JsonVariant & json); + AsyncCallbackJsonWebHandler _setLevel; // for POSTs uint64_t last_transmit_ = 0; // Last transmit time size_t maximum_log_messages_ = MAX_LOG_MESSAGES; // Maximum number of log messages to buffer before they are output unsigned long log_message_id_ = 0; // The next identifier to use for queued log messages - std::list log_messages_; // Queued log messages, in the order they were received + unsigned long log_message_id_tail_ = 0; + std::list log_messages_; // Queued log messages, in the order they were received }; } // namespace emsesp