github.com/minio/console@v1.4.1/web-app/src/screens/Console/Logs/ErrorLogs/ErrorLogs.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 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 React, { Fragment, useEffect, useState } from "react"; 18 import { Box, Button, Grid, PageLayout, Select, Table, TableBody } from "mds"; 19 import { useSelector } from "react-redux"; 20 import { ErrorResponseHandler } from "../../../../common/types"; 21 import { AppState, useAppDispatch } from "../../../../store"; 22 import { wsProtocol } from "../../../../utils/wsUtils"; 23 import { 24 logMessageReceived, 25 logResetMessages, 26 setLogsStarted, 27 } from "../logsSlice"; 28 import { setHelpName } from "../../../../systemSlice"; 29 import SearchBox from "../../Common/SearchBox"; 30 import api from "../../../../../src/common/api"; 31 import LogLine from "./LogLine"; 32 import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper"; 33 import HelpMenu from "../../HelpMenu"; 34 35 var socket: any = null; 36 37 const ErrorLogs = () => { 38 const dispatch = useAppDispatch(); 39 40 const messages = useSelector((state: AppState) => state.logs.logMessages); 41 const logsStarted = useSelector((state: AppState) => state.logs.logsStarted); 42 43 const [filter, setFilter] = useState<string>(""); 44 const [nodes, setNodes] = useState<string[]>([""]); 45 const [selectedNode, setSelectedNode] = useState<string>("all"); 46 const [selectedUserAgent, setSelectedUserAgent] = 47 useState<string>("Select user agent"); 48 const [userAgents, setUserAgents] = useState<string[]>(["All User Agents"]); 49 const [logType, setLogType] = useState<string>("all"); 50 const [loadingNodes, setLoadingNodes] = useState<boolean>(false); 51 52 const startLogs = () => { 53 dispatch(logResetMessages()); 54 const url = new URL(window.location.toString()); 55 const isDev = process.env.NODE_ENV === "development"; 56 const port = isDev ? "9090" : url.port; 57 58 const wsProt = wsProtocol(url.protocol); 59 // check if we are using base path, if not this always is `/` 60 const baseLocation = new URL(document.baseURI); 61 const baseUrl = baseLocation.pathname; 62 63 socket = new WebSocket( 64 `${wsProt}://${ 65 url.hostname 66 }:${port}${baseUrl}ws/console/?logType=${logType}&node=${ 67 selectedNode === "Select node" ? "" : selectedNode 68 }`, 69 ); 70 let interval: any | null = null; 71 if (socket !== null) { 72 socket.onopen = () => { 73 console.log("WebSocket Client Connected"); 74 dispatch(setLogsStarted(true)); 75 socket.send("ok"); 76 interval = setInterval(() => { 77 socket.send("ok"); 78 }, 10 * 1000); 79 }; 80 socket.onmessage = (message: MessageEvent) => { 81 // console.log(message.data.toString()) 82 // FORMAT: 00:35:17 UTC 01/01/2021 83 84 let m: any = JSON.parse(message.data.toString()); 85 let isValidEntry = true; 86 if ( 87 m.level === "" && 88 m.errKind === "" && 89 //@ts-ignore 90 m.time === "00:00:00 UTC 01/01/0001" && 91 m.ConsoleMsg === "" && 92 m.node === "" 93 ) { 94 isValidEntry = false; 95 } 96 97 m.key = Math.random(); 98 if (userAgents.indexOf(m.userAgent) < 0 && m.userAgent !== undefined) { 99 userAgents.push(m.userAgent); 100 setUserAgents(userAgents); 101 } 102 if (isValidEntry) { 103 dispatch(logMessageReceived(m)); 104 } 105 }; 106 socket.onclose = () => { 107 clearInterval(interval); 108 console.log("connection closed by server"); 109 dispatch(setLogsStarted(false)); 110 }; 111 return () => { 112 socket.close(1000); 113 clearInterval(interval); 114 console.log("closing websockets"); 115 dispatch(setLogsStarted(false)); 116 }; 117 } 118 }; 119 120 const stopLogs = () => { 121 if (socket !== null && socket !== undefined) { 122 socket.close(1000); 123 dispatch(setLogsStarted(false)); 124 } 125 }; 126 127 const filtLow = filter.toLowerCase(); 128 let filteredMessages = messages.filter((m) => { 129 if ( 130 m.userAgent === selectedUserAgent || 131 selectedUserAgent === "All User Agents" || 132 selectedUserAgent === "Select user agent" 133 ) { 134 if (filter !== "") { 135 if (m.ConsoleMsg.toLowerCase().indexOf(filtLow) >= 0) { 136 return true; 137 } else if ( 138 m.error && 139 m.error.source && 140 m.error.source.filter((x) => { 141 return x.toLowerCase().indexOf(filtLow) >= 0; 142 }).length > 0 143 ) { 144 return true; 145 } else if ( 146 m.error && 147 m.error.message.toLowerCase().indexOf(filtLow) >= 0 148 ) { 149 return true; 150 } else if (m.api && m.api.name.toLowerCase().indexOf(filtLow) >= 0) { 151 return true; 152 } 153 return false; 154 } 155 return true; 156 } else return false; 157 }); 158 159 useEffect(() => { 160 setLoadingNodes(true); 161 api 162 .invoke("GET", `/api/v1/nodes`) 163 .then((res: string[]) => { 164 setNodes(res); 165 setLoadingNodes(false); 166 }) 167 .catch((err: ErrorResponseHandler) => { 168 setLoadingNodes(false); 169 }); 170 }, []); 171 172 useEffect(() => { 173 dispatch(setHelpName("error_logs")); 174 // eslint-disable-next-line react-hooks/exhaustive-deps 175 }, []); 176 177 return ( 178 <Fragment> 179 <PageHeaderWrapper label="Logs" actions={<HelpMenu />} /> 180 181 <PageLayout> 182 <Grid container sx={{ gap: 15 }}> 183 <Grid item xs={3}> 184 {!loadingNodes ? ( 185 <Select 186 id="node-selector" 187 name="node" 188 data-test-id="node-selector" 189 value={selectedNode} 190 onChange={(value) => { 191 setSelectedNode(value as string); 192 }} 193 disabled={loadingNodes || logsStarted} 194 options={[ 195 { label: "All Nodes", value: "all" }, 196 ...nodes.map((aNode) => ({ label: aNode, value: aNode })), 197 ]} 198 /> 199 ) : ( 200 <h3> Loading nodes</h3> 201 )} 202 </Grid> 203 204 <Grid item xs={3}> 205 <Select 206 id="logType" 207 name="logType" 208 data-test-id="log-type" 209 value={logType} 210 onChange={(value) => { 211 setLogType(value as string); 212 }} 213 disabled={loadingNodes || logsStarted} 214 options={[ 215 { value: "all", label: "All Log Types" }, 216 { 217 value: "minio", 218 label: "MinIO", 219 }, 220 { value: "application", label: "Application" }, 221 ]} 222 /> 223 </Grid> 224 <Grid item xs={3}> 225 {userAgents.length > 1 && ( 226 <Select 227 id="userAgent" 228 name="userAgent" 229 data-test-id="user-agent" 230 value={selectedUserAgent} 231 onChange={(value) => { 232 setSelectedUserAgent(value as string); 233 }} 234 disabled={userAgents.length < 1 || logsStarted} 235 options={userAgents.map((anAgent) => ({ 236 label: anAgent, 237 value: anAgent, 238 }))} 239 /> 240 )} 241 </Grid> 242 <Grid 243 item 244 xs={2} 245 sx={{ display: "flex", justifyContent: "flex-end" }} 246 > 247 {!logsStarted && ( 248 <Button 249 id={"start-logs"} 250 type="submit" 251 variant="callAction" 252 disabled={false} 253 onClick={startLogs} 254 label={"Start Logs"} 255 /> 256 )} 257 {logsStarted && ( 258 <Button 259 id={"stop-logs"} 260 type="button" 261 variant="callAction" 262 onClick={stopLogs} 263 label={"Stop Logs"} 264 /> 265 )} 266 </Grid> 267 <Grid 268 item 269 xs={12} 270 sx={{ 271 display: "flex" as const, 272 justifyContent: "space-between" as const, 273 alignItems: "center", 274 marginBottom: "1rem", 275 "& button": { 276 flexGrow: 0, 277 marginLeft: 8, 278 marginBottom: 0, 279 }, 280 }} 281 > 282 <SearchBox 283 placeholder="Filter" 284 onChange={(e) => { 285 setFilter(e); 286 }} 287 value={filter} 288 /> 289 </Grid> 290 <Grid item xs={12}> 291 <Box 292 id="logs-container" 293 data-test-id="logs-list-container" 294 sx={{ 295 minHeight: 400, 296 height: "calc(100vh - 200px)", 297 overflow: "auto", 298 fontSize: 13, 299 borderRadius: 4, 300 }} 301 > 302 <Box withBorders customBorderPadding={"0px"} useBackground> 303 <Table aria-label="collapsible table"> 304 <TableBody> 305 {filteredMessages.map((m) => { 306 return <LogLine log={m} />; 307 })} 308 </TableBody> 309 </Table> 310 {filteredMessages.length === 0 && ( 311 <Box sx={{ padding: 20, textAlign: "center" }}> 312 No logs to display 313 </Box> 314 )} 315 </Box> 316 </Box> 317 </Grid> 318 </Grid> 319 </PageLayout> 320 </Fragment> 321 ); 322 }; 323 324 export default ErrorLogs;