add max messages and make web log dynamic - #71

This commit is contained in:
proddy
2021-07-27 21:44:12 +02:00
parent dc8c322b42
commit e809ed3743
7 changed files with 223 additions and 172 deletions

View File

@@ -6,6 +6,8 @@ import { useWindowSize } from '../components';
interface LogEventConsoleProps { interface LogEventConsoleProps {
events: LogEvent[]; events: LogEvent[];
compact: boolean;
level: number;
} }
interface Offsets { interface Offsets {
@@ -63,7 +65,9 @@ const useStyles = makeStyles((theme: Theme) => ({
const LogEventConsole: FC<LogEventConsoleProps> = (props) => { const LogEventConsole: FC<LogEventConsoleProps> = (props) => {
useWindowSize(); useWindowSize();
const classes = useStyles({ topOffset, leftOffset }); const classes = useStyles({ topOffset, leftOffset });
const { events } = props; const { events, compact, level } = props;
const filter_events = events.filter((e) => e.l <= level);
const styleLevel = (level: LogLevel) => { const styleLevel = (level: LogLevel) => {
switch (level) { switch (level) {
@@ -103,29 +107,34 @@ const LogEventConsole: FC<LogEventConsoleProps> = (props) => {
} }
}; };
const paddedLevelLabel = (level: LogLevel) => { const paddedLevelLabel = (level: LogLevel, compact: boolean) => {
const label = levelLabel(level); const label = levelLabel(level);
return label.padStart(8, '\xa0'); return compact ? ' ' + label[0] : label.padStart(8, '\xa0');
}; };
const paddedNameLabel = (name: string) => { const paddedNameLabel = (name: string, compact: boolean) => {
const label = '[' + name + ']'; const label = '[' + name + ']';
return label.padEnd(12, '\xa0'); return compact ? label : label.padEnd(12, '\xa0');
}; };
const paddedIDLabel = (id: number) => { const paddedIDLabel = (id: number, compact: boolean) => {
const label = id + ':'; const label = id + ':';
return label.padEnd(7, '\xa0'); return compact ? label : label.padEnd(7, '\xa0');
}; };
return ( return (
<Box id="log-window" className={classes.console}> <Box id="log-window" className={classes.console}>
{events.map((e) => ( {filter_events.map((e) => (
<div className={classes.entry} key={e.i}> <div className={classes.entry} key={e.i}>
<span>{e.t}</span> <span>{e.t}</span>
<span className={styleLevel(e.l)}>{paddedLevelLabel(e.l)} </span> {compact && <span>{paddedLevelLabel(e.l, compact)} </span>}
<span>{paddedIDLabel(e.i)} </span> {!compact && (
<span>{paddedNameLabel(e.n)} </span> <span className={styleLevel(e.l)}>
{paddedLevelLabel(e.l, compact)}{' '}
</span>
)}
<span>{paddedIDLabel(e.i, compact)} </span>
<span>{paddedNameLabel(e.n, compact)} </span>
<span>{e.m}</span> <span>{e.m}</span>
</div> </div>
))} ))}

View File

@@ -3,19 +3,27 @@ import { Component } from 'react';
import { import {
restController, restController,
RestControllerProps, RestControllerProps,
RestFormLoader, SectionContent,
SectionContent BlockFormControlLabel
} from '../components'; } from '../components';
import { addAccessTokenParameter } from '../authentication'; import {
ValidatorForm,
SelectValidator
} from 'react-material-ui-form-validator';
import { Grid, Slider, FormLabel, Checkbox, MenuItem } from '@material-ui/core';
import {
addAccessTokenParameter,
redirectingAuthorizedFetch
} from '../authentication';
import { ENDPOINT_ROOT, EVENT_SOURCE_ROOT } from '../api'; import { ENDPOINT_ROOT, EVENT_SOURCE_ROOT } from '../api';
export const FETCH_LOG_ENDPOINT = ENDPOINT_ROOT + 'fetchLog'; export const FETCH_LOG_ENDPOINT = ENDPOINT_ROOT + 'fetchLog';
export const LOG_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'logSettings'; export const LOG_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'logSettings';
export 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 LogEventConsole from './LogEventConsole';
import { LogEvent, LogSettings } from './types'; import { LogEvent, LogSettings } from './types';
@@ -26,6 +34,9 @@ const decoder = new Decoder();
interface LogEventControllerState { interface LogEventControllerState {
eventSource?: EventSource; eventSource?: EventSource;
events: LogEvent[]; events: LogEvent[];
compact: boolean;
level: number;
max_messages: number;
} }
type LogEventControllerProps = RestControllerProps<LogSettings>; type LogEventControllerProps = RestControllerProps<LogSettings>;
@@ -40,12 +51,15 @@ class LogEventController extends Component<
constructor(props: LogEventControllerProps) { constructor(props: LogEventControllerProps) {
super(props); super(props);
this.state = { this.state = {
events: [] events: [],
compact: false,
level: 6,
max_messages: 25
}; };
} }
componentDidMount() { componentDidMount() {
this.props.loadData(); this.fetchValues();
this.fetchLog(); this.fetchLog();
this.configureEventSource(); this.configureEventSource();
} }
@@ -59,6 +73,15 @@ class LogEventController extends Component<
} }
} }
changeCompact = (
event: React.ChangeEvent<HTMLInputElement>,
checked: boolean
) => {
this.setState({
compact: checked
});
};
fetchLog = () => { fetchLog = () => {
fetch(FETCH_LOG_ENDPOINT) fetch(FETCH_LOG_ENDPOINT)
.then((response) => { .then((response) => {
@@ -78,6 +101,25 @@ class LogEventController extends Component<
}); });
}; };
fetchValues = () => {
redirectingAuthorizedFetch(LOG_SETTINGS_ENDPOINT)
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error('Unexpected status code: ' + response.status);
})
.then((json) => {
this.setState({ level: json.level, max_messages: json.max_messages });
})
.catch((error) => {
const errorMessage = error.message || 'Unknown error';
this.props.enqueueSnackbar('Problem fetching: ' + errorMessage, {
variant: 'error'
});
});
};
configureEventSource = () => { configureEventSource = () => {
this.eventSource = new EventSource( this.eventSource = new EventSource(
addAccessTokenParameter(LOG_EVENT_EVENT_SOURCE_URL) addAccessTokenParameter(LOG_EVENT_EVENT_SOURCE_URL)
@@ -102,14 +144,114 @@ class LogEventController extends Component<
} }
}; };
changeMaxMessages = (
event: React.ChangeEvent<{}>,
value: number | number[]
) => {
this.setState({
max_messages: value as number
});
this.send_data(this.state.level, value as number);
};
changeLevel = (event: React.ChangeEvent<HTMLSelectElement>) => {
this.setState({
level: parseInt(event.target.value)
});
this.send_data(parseInt(event.target.value), this.state.max_messages);
};
send_data = (level: number, max_messages: number) => {
redirectingAuthorizedFetch(LOG_SETTINGS_ENDPOINT, {
method: 'POST',
body: JSON.stringify({
level: level,
max_messages: max_messages
}),
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.status !== 200) {
throw Error('Unexpected response code: ' + response.status);
}
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || 'Problem applying log settings',
{ variant: 'warning' }
);
});
};
render() { render() {
const { saveData } = this.props;
return ( return (
<SectionContent title="System Log" id="log-window"> <SectionContent title="System Log" id="log-window">
<RestFormLoader <ValidatorForm onSubmit={saveData}>
{...this.props} <Grid
render={(formProps) => <LogEventForm {...formProps} />} container
spacing={3}
direction="row"
justify="flex-start"
alignItems="center"
>
<Grid item xs={2}>
<SelectValidator
name="level"
label="Log Level"
value={this.state.level}
fullWidth
variant="outlined"
onChange={this.changeLevel}
margin="normal"
>
<MenuItem value={3}>ERROR</MenuItem>
<MenuItem value={4}>WARNING</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
<MenuItem value={8}>ALL</MenuItem>
</SelectValidator>
</Grid>
<Grid item xs={2}>
<FormLabel>Buffer size</FormLabel>
<Slider
value={this.state.max_messages}
valueLabelDisplay="auto"
name="max_messages"
marks={[
{ value: 25, label: '25' },
{ value: 50, label: '50' },
{ value: 75, label: '75' }
]}
step={25}
min={25}
max={75}
onChange={this.changeMaxMessages}
/>
</Grid>
<Grid item xs={4}>
<BlockFormControlLabel
control={
<Checkbox
checked={this.state.compact}
onChange={this.changeCompact}
value="compact"
/>
}
label="Compact Layout"
/>
</Grid>
</Grid>
</ValidatorForm>
<LogEventConsole
level={this.state.level}
compact={this.state.compact}
events={this.state.events}
/> />
<LogEventConsole events={this.state.events} />
</SectionContent> </SectionContent>
); );
} }

View File

@@ -1,107 +0,0 @@
import { Component } from 'react';
import {
ValidatorForm,
SelectValidator
} from 'react-material-ui-form-validator';
import { Typography, Grid } from '@material-ui/core';
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<LogSettings>;
class LogEventForm extends Component<LogEventFormProps> {
changeLevel = (event: React.ChangeEvent<HTMLSelectElement>) => {
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 (
<ValidatorForm onSubmit={saveData}>
<Grid
container
direction="row"
justify="flex-start"
alignItems="center"
>
<Grid item xs={2}>
<SelectValidator
name="level"
label="Filter on Log Level"
value={data.level}
fullWidth
variant="outlined"
onChange={this.changeLevel}
margin="normal"
>
<MenuItem value={-1}>OFF</MenuItem>
<MenuItem value={3}>ERROR</MenuItem>
<MenuItem value={4}>WARNING</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
<MenuItem value={8}>ALL</MenuItem>
</SelectValidator>
</Grid>
<Grid item md>
<Typography color="primary" variant="body2">
<i>
&nbsp;(the last {data.max_messages} messages are retained and
all new log events are shown in real time below)
</i>
</Typography>
</Grid>
</Grid>
</ValidatorForm>
);
}
}
export default withAuthenticatedContext(LogEventForm);

View File

@@ -18,7 +18,7 @@ const ES_ENDPOINT_ROOT = '/es/'
const LOG_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'logSettings' const LOG_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'logSettings'
const log_settings = { const log_settings = {
level: 6, level: 6,
max_messages: 30, max_messages: 50,
} }
const FETCH_LOG_ENDPOINT = REST_ENDPOINT_ROOT + 'fetchLog' const FETCH_LOG_ENDPOINT = REST_ENDPOINT_ROOT + 'fetchLog'
@@ -770,14 +770,22 @@ app.get(FETCH_LOG_ENDPOINT, (req, res) => {
res.end(null, 'binary') res.end(null, 'binary')
}) })
app.get(LOG_SETTINGS_ENDPOINT, (req, res) => { app.get(LOG_SETTINGS_ENDPOINT, (req, res) => {
console.log(
'Fetching log settings ' +
log_settings.level +
',' +
log_settings.max_messages,
)
res.json(log_settings) res.json(log_settings)
}) })
app.post(LOG_SETTINGS_ENDPOINT, (req, res) => { app.post(LOG_SETTINGS_ENDPOINT, (req, res) => {
console.log('New log level is ' + req.body.level) console.log(
const data = { 'Setting new level=' +
level: req.body.level, req.body.level +
} ' max_messages=' +
res.json(data) req.body.max_messages,
)
res.sendStatus(200)
}) })
// NETWORK // NETWORK

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.2.0b0" #define EMSESP_APP_VERSION "3.2.0b1"

View File

@@ -23,21 +23,21 @@ using namespace std::placeholders;
namespace emsesp { namespace emsesp {
WebLogService::WebLogService(AsyncWebServer * server, SecurityManager * securityManager) 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 , setValues_(LOG_SETTINGS_PATH, std::bind(&WebLogService::setValues, this, _1, _2), 256) { // for POSTS
_events.setFilter(securityManager->filterRequest(AuthenticationPredicates::IS_ADMIN)); events_.setFilter(securityManager->filterRequest(AuthenticationPredicates::IS_ADMIN));
server->addHandler(&_events); server->addHandler(&events_);
server->on(EVENT_SOURCE_LOG_PATH, HTTP_GET, std::bind(&WebLogService::forbidden, this, _1)); server->on(EVENT_SOURCE_LOG_PATH, HTTP_GET, std::bind(&WebLogService::forbidden, this, _1));
// for bring back the whole log // for bring back the whole log
server->on(FETCH_LOG_PATH, HTTP_GET, std::bind(&WebLogService::fetchLog, this, _1)); server->on(FETCH_LOG_PATH, HTTP_GET, std::bind(&WebLogService::fetchLog, this, _1));
// get when page is loaded // get when page is loaded
server->on(LOG_SETTINGS_PATH, HTTP_GET, std::bind(&WebLogService::getLevel, this, _1)); server->on(LOG_SETTINGS_PATH, HTTP_GET, std::bind(&WebLogService::getValues, this, _1));
// for setting a level // for setting a level
server->addHandler(&_setLevel); server->addHandler(&setValues_);
} }
void WebLogService::forbidden(AsyncWebServerRequest * request) { void WebLogService::forbidden(AsyncWebServerRequest * request) {
@@ -90,7 +90,7 @@ void WebLogService::operator<<(std::shared_ptr<uuid::log::Message> message) {
} }
void WebLogService::loop() { void WebLogService::loop() {
if (!_events.count() || log_messages_.empty()) { if (!events_.count() || log_messages_.empty()) {
return; return;
} }
@@ -144,12 +144,12 @@ void WebLogService::transmit(const QueuedLogMessage & message) {
char * buffer = new char[len + 1]; char * buffer = new char[len + 1];
if (buffer) { if (buffer) {
serializeJson(jsonDocument, buffer, len + 1); serializeJson(jsonDocument, buffer, len + 1);
_events.send(buffer, "message", millis()); events_.send(buffer, "message", millis());
} }
delete[] buffer; delete[] buffer;
} }
// send the current log buffer to the API // send the complete log buffer to the API, filtering on log level
void WebLogService::fetchLog(AsyncWebServerRequest * request) { void WebLogService::fetchLog(AsyncWebServerRequest * request) {
MsgpackAsyncJsonResponse * response = new MsgpackAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN); // 8kb buffer MsgpackAsyncJsonResponse * response = new MsgpackAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN); // 8kb buffer
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
@@ -157,15 +157,17 @@ void WebLogService::fetchLog(AsyncWebServerRequest * request) {
JsonArray log = root.createNestedArray("events"); JsonArray log = root.createNestedArray("events");
for (const auto & msg : log_messages_) { for (const auto & msg : log_messages_) {
JsonObject logEvent = log.createNestedObject(); if (msg.content_->level <= log_level()) {
auto message = std::move(msg); JsonObject logEvent = log.createNestedObject();
char time_string[25]; auto message = std::move(msg);
char time_string[25];
logEvent["t"] = messagetime(time_string, message.content_->uptime_ms); logEvent["t"] = messagetime(time_string, message.content_->uptime_ms);
logEvent["l"] = message.content_->level; logEvent["l"] = message.content_->level;
logEvent["i"] = message.id_; logEvent["i"] = message.id_;
logEvent["n"] = message.content_->name; logEvent["n"] = message.content_->name;
logEvent["m"] = message.content_->text; logEvent["m"] = message.content_->text;
}
} }
log_message_id_tail_ = log_messages_.back().id_; log_message_id_tail_ = log_messages_.back().id_;
@@ -174,28 +176,25 @@ void WebLogService::fetchLog(AsyncWebServerRequest * request) {
request->send(response); request->send(response);
} }
// sets the level after a POST // sets the values like level after a POST
void WebLogService::setLevel(AsyncWebServerRequest * request, JsonVariant & json) { void WebLogService::setValues(AsyncWebServerRequest * request, JsonVariant & json) {
if (not json.is<JsonObject>()) { if (not json.is<JsonObject>()) {
return; return;
} }
auto && body = json.as<JsonObject>();
auto && body = json.as<JsonObject>();
uuid::log::Level level = body["level"]; uuid::log::Level level = body["level"];
log_level(level); log_level(level);
if (level == uuid::log::Level::OFF) {
log_messages_.clear();
}
// send the value back uint8_t max_messages = body["max_messages"];
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); maximum_log_messages(max_messages);
JsonObject root = response->getRoot();
root["level"] = log_level(); request->send(200); // OK
response->setLength();
request->send(response);
} }
// return the current log level after a GET // return the current value settings after a GET
void WebLogService::getLevel(AsyncWebServerRequest * request) { void WebLogService::getValues(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
root["level"] = log_level(); root["level"] = log_level();

View File

@@ -34,7 +34,7 @@ namespace emsesp {
class WebLogService : public uuid::log::Handler { class WebLogService : public uuid::log::Handler {
public: public:
static constexpr size_t MAX_LOG_MESSAGES = 30; static constexpr size_t MAX_LOG_MESSAGES = 50;
static constexpr size_t REFRESH_SYNC = 200; static constexpr size_t REFRESH_SYNC = 200;
WebLogService(AsyncWebServer * server, SecurityManager * securityManager); WebLogService(AsyncWebServer * server, SecurityManager * securityManager);
@@ -49,7 +49,7 @@ class WebLogService : public uuid::log::Handler {
virtual void operator<<(std::shared_ptr<uuid::log::Message> message); virtual void operator<<(std::shared_ptr<uuid::log::Message> message);
private: private:
AsyncEventSource _events; AsyncEventSource events_;
class QueuedLogMessage { class QueuedLogMessage {
public: public:
@@ -64,12 +64,12 @@ class WebLogService : public uuid::log::Handler {
void forbidden(AsyncWebServerRequest * request); void forbidden(AsyncWebServerRequest * request);
void transmit(const QueuedLogMessage & message); void transmit(const QueuedLogMessage & message);
void fetchLog(AsyncWebServerRequest * request); void fetchLog(AsyncWebServerRequest * request);
void getLevel(AsyncWebServerRequest * request); void getValues(AsyncWebServerRequest * request);
char * messagetime(char * out, const uint64_t t); char * messagetime(char * out, const uint64_t t);
void setLevel(AsyncWebServerRequest * request, JsonVariant & json); void setValues(AsyncWebServerRequest * request, JsonVariant & json);
AsyncCallbackJsonWebHandler _setLevel; // for POSTs AsyncCallbackJsonWebHandler setValues_; // for POSTs
uint64_t last_transmit_ = 0; // Last transmit time 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 size_t maximum_log_messages_ = MAX_LOG_MESSAGES; // Maximum number of log messages to buffer before they are output