Skip to content

Commit 511cc47

Browse files
authored
Add console logs api and integrate it with UI (#90)
Uses same behavior as the Trace feature using websockets. For displaying it on the UI it needed to handle colors since the log message comes with unicode colors embbeded on the message. Also a special case when an error log comes needed to be handled to show all sources of the error.
1 parent 9660650 commit 511cc47

File tree

18 files changed

+995
-156
lines changed

18 files changed

+995
-156
lines changed

portal-ui/bindata_assetfs.go

Lines changed: 116 additions & 116 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

portal-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@types/superagent": "^4.1.4",
2121
"@types/webpack-env": "^1.14.1",
2222
"@types/websocket": "^1.0.0",
23+
"ansi-to-react": "^6.0.5",
2324
"codemirror": "^5.52.2",
2425
"history": "^4.10.1",
2526
"local-storage-fallback": "^4.1.1",

portal-ui/src/screens/Console/Console.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import ConfigurationsList from "./Configurations/ConfigurationPanels/Configurati
6262
import { Button, LinearProgress } from "@material-ui/core";
6363
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
6464
import Trace from "./Trace/Trace";
65+
import Logs from "./Logs/Logs";
6566

6667
function Copyright() {
6768
return (
@@ -304,6 +305,7 @@ class Console extends React.Component<
304305
<Route exact path="/webhook/logger" component={WebhookPanel} />
305306
<Route exact path="/webhook/audit" component={WebhookPanel} />
306307
<Route exct path="/trace" component={Trace} />
308+
<Route exct path="/logs" component={Logs} />
307309
<Route exact path="/">
308310
<Redirect to="/dashboard" />
309311
</Route>
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2020 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
import React, { useEffect } from "react";
17+
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
18+
import storage from "local-storage-fallback";
19+
import { AppState } from "../../../store";
20+
import { connect } from "react-redux";
21+
import { logMessageReceived, logResetMessages } from "./actions";
22+
import { LogMessage } from "./types";
23+
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
24+
import { niceBytes } from "../../../common/utils";
25+
import Ansi from "ansi-to-react";
26+
import { isNull, isNullOrUndefined } from "util";
27+
28+
const styles = (theme: Theme) =>
29+
createStyles({
30+
logList: {
31+
background: "white",
32+
maxHeight: "400px",
33+
overflow: "auto",
34+
"& ul": {
35+
margin: "4px",
36+
padding: "0px"
37+
},
38+
"& ul li": {
39+
listStyle: "none",
40+
margin: "0px",
41+
padding: "0px",
42+
borderBottom: "1px solid #dedede"
43+
}
44+
},
45+
tab: {
46+
padding: "25px"
47+
},
48+
ansiblue: {
49+
color: "blue"
50+
},
51+
logerror: {
52+
color: "red"
53+
},
54+
logerror_tab: {
55+
color: "red",
56+
padding: "25px"
57+
},
58+
ansidefault: {
59+
color: "black"
60+
},
61+
});
62+
63+
interface ILogs {
64+
classes: any;
65+
logMessageReceived: typeof logMessageReceived;
66+
logResetMessages: typeof logResetMessages;
67+
messages: LogMessage[];
68+
}
69+
70+
const Logs = ({
71+
classes,
72+
logMessageReceived,
73+
logResetMessages,
74+
messages
75+
}: ILogs) => {
76+
useEffect(() => {
77+
logResetMessages();
78+
const url = new URL(window.location.toString());
79+
const isDev = process.env.NODE_ENV === "development";
80+
const port = isDev ? "9090" : url.port;
81+
82+
const c = new W3CWebSocket(`ws://${url.hostname}:${port}/ws/console`);
83+
84+
let interval: any | null = null;
85+
if (c !== null) {
86+
c.onopen = () => {
87+
console.log("WebSocket Client Connected");
88+
c.send("ok");
89+
interval = setInterval(() => {
90+
c.send("ok");
91+
}, 10 * 1000);
92+
};
93+
c.onmessage = (message: IMessageEvent) => {
94+
// console.log(message.data.toString())
95+
let m: LogMessage = JSON.parse(message.data.toString());
96+
m.time = new Date(m.time.toString());
97+
m.key = Math.random();
98+
logMessageReceived(m);
99+
};
100+
c.onclose = () => {
101+
clearInterval(interval);
102+
console.log("connection closed by server");
103+
};
104+
return () => {
105+
c.close(1000);
106+
clearInterval(interval);
107+
console.log("closing websockets");
108+
};
109+
}
110+
}, [logMessageReceived]);
111+
112+
const timeFromdate = (d: Date) => {
113+
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
114+
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
115+
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
116+
117+
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
118+
};
119+
120+
// replaces a character of a string with other at a given index
121+
const replaceWeirdChar = (origString: string, replaceChar: string, index: number) => {
122+
let firstPart = origString.substr(0, index);
123+
let lastPart = origString.substr(index + 1);
124+
125+
let newString = firstPart + replaceChar + lastPart;
126+
return newString;
127+
};
128+
129+
130+
const colorify = (str: string) => {
131+
// matches strings starting like: `[34mEndpoint: [0m`
132+
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g;
133+
let matches = colorRegex.exec(str)
134+
if (!isNullOrUndefined(matches)) {
135+
let start_color = matches[1];
136+
let text = matches[2];
137+
138+
if (start_color === "[34m") {
139+
return <span className={classes.ansiblue}>
140+
{text}
141+
</span>
142+
}
143+
if (start_color === "[1m") {
144+
return <span className={classes.ansidarkblue}>
145+
{text}
146+
</span>
147+
}
148+
}
149+
};
150+
151+
const renderError = (logElement: LogMessage) => {
152+
let errorElems = [];
153+
if (!isNullOrUndefined(logElement.error)) {
154+
if (logElement.api && logElement.api.name) {
155+
errorElems.push(
156+
<li key={`api-${logElement.key}`}>
157+
<span className={classes.logerror}>
158+
API: {logElement.api.name}
159+
</span>
160+
</li>
161+
)
162+
}
163+
if (logElement.time) {
164+
errorElems.push(
165+
<li key={`time-${logElement.key}`}>
166+
<span className={classes.logerror}>
167+
Time: {timeFromdate(logElement.time)}
168+
</span>
169+
</li>
170+
)
171+
}
172+
if (logElement.deploymentid) {
173+
errorElems.push(
174+
<li key={`deploytmentid-${logElement.key}`}>
175+
<span className={classes.logerror}>
176+
DeploymentID: {logElement.deploymentid}
177+
</span>
178+
</li>
179+
)
180+
}
181+
if (logElement.requestID) {
182+
errorElems.push(
183+
<li key={`requestid-${logElement.key}`}>
184+
<span className={classes.logerror}>
185+
RequestID: {logElement.requestID}
186+
</span>
187+
</li>
188+
)
189+
}
190+
if (logElement.remotehost) {
191+
errorElems.push(
192+
<li key={`remotehost-${logElement.key}`}>
193+
<span className={classes.logerror}>
194+
RemoteHost: {logElement.remotehost}
195+
</span>
196+
</li>
197+
)
198+
}
199+
if (logElement.host) {
200+
errorElems.push(
201+
<li key={`host-${logElement.key}`}>
202+
<span className={classes.logerror}>
203+
Host: {logElement.host}
204+
</span>
205+
</li>
206+
)
207+
}
208+
if (logElement.userAgent) {
209+
errorElems.push(
210+
<li key={`useragent-${logElement.key}`}>
211+
<span className={classes.logerror}>
212+
UserAgent: {logElement.userAgent}
213+
</span>
214+
</li>
215+
)
216+
}
217+
if (logElement.error.message) {
218+
errorElems.push(
219+
<li key={`message-${logElement.key}`}>
220+
<span className={classes.logerror}>
221+
Error: {logElement.error.message}
222+
</span>
223+
</li>
224+
)
225+
}
226+
if (logElement.error.source) {
227+
// for all sources add padding
228+
for (let s in logElement.error.source) {
229+
errorElems.push(
230+
<li key={`source-${logElement.key}-${s}`}>
231+
<span className={classes.logerror_tab}>
232+
{logElement.error.source[s]}
233+
</span>
234+
</li>
235+
)
236+
}
237+
}
238+
}
239+
return errorElems
240+
};
241+
242+
const renderLog = (logElement: LogMessage) => {
243+
let logMessage = logElement.ConsoleMsg;
244+
// if somehow after the color code starts with unicode 10 = Line feed
245+
// delete it
246+
const regexInit = /(\[[0-9]+m)/g;
247+
let match = regexInit.exec(logMessage);
248+
if (match) {
249+
if (logMessage.slice(match[0].length).codePointAt(1) == 10) {
250+
logMessage = replaceWeirdChar(logMessage, "", match[0].length + 1);
251+
}
252+
}
253+
254+
// Select what to add color and what not to.
255+
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g;
256+
let m = colorRegex.exec(logMessage);
257+
258+
// get substring if there was a match for to split what
259+
// is going to be colored and what not, here we add color
260+
// only to the first match.
261+
let substr = logMessage.slice(colorRegex.lastIndex);
262+
substr = substr.replace(regexInit, "");
263+
264+
// strClean used for corner case when string has unicode 32 for
265+
// space instead of normal space.
266+
let strClean = logMessage.replace(regexInit, "");
267+
// if starts with multiple spaces add padding
268+
if (strClean.startsWith(" ") || strClean.codePointAt(1) === 32) {
269+
return <li key={logElement.key}>
270+
<span className={classes.tab}>
271+
{colorify(logMessage)} {substr}
272+
</span>
273+
</li>
274+
} else if (!isNullOrUndefined(logElement.error)) {
275+
// list error message and all sources and error elems
276+
return (
277+
renderError(logElement)
278+
)
279+
} else {
280+
// for all remaining set default class
281+
return <li key={logElement.key}>
282+
<span className={classes.ansidefault}>
283+
{colorify(logMessage)} {substr}
284+
</span>
285+
</li>
286+
}
287+
};
288+
289+
return (
290+
<div>
291+
<h1>Logs</h1>
292+
<div className={classes.logList}>
293+
<ul>
294+
{messages.map(m => {
295+
return renderLog(m)
296+
})}
297+
</ul>
298+
</div>
299+
</div>
300+
);
301+
};
302+
303+
const mapState = (state: AppState) => ({
304+
messages: state.logs.messages
305+
});
306+
307+
const connector = connect(mapState, {
308+
logMessageReceived: logMessageReceived,
309+
logResetMessages: logResetMessages
310+
});
311+
312+
export default connector(withStyles(styles)(Logs));
313+
314+
315+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2020 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import { LogMessage } from "./types";
18+
19+
export const LOG_MESSAGE_RECEIVED = "LOG_MESSAGE_RECEIVED";
20+
export const LOG_RESET_MESSAGES = "LOG_RESET_MESSAGES";
21+
22+
interface LogMessageReceivedAction {
23+
type: typeof LOG_MESSAGE_RECEIVED;
24+
message: LogMessage;
25+
}
26+
27+
interface LogResetMessagesAction {
28+
type: typeof LOG_RESET_MESSAGES;
29+
}
30+
31+
export type LogActionTypes =
32+
| LogMessageReceivedAction
33+
| LogResetMessagesAction;
34+
35+
export function logMessageReceived(message: LogMessage) {
36+
return {
37+
type: LOG_MESSAGE_RECEIVED,
38+
message: message
39+
};
40+
}
41+
42+
export function logResetMessages() {
43+
return {
44+
type: LOG_RESET_MESSAGES
45+
};
46+
}

0 commit comments

Comments
 (0)