github.com/minio/console@v1.4.1/web-app/src/screens/Console/Trace/Trace.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 { Fragment, useEffect, useState } from "react"; 18 import { DateTime } from "luxon"; 19 import { useSelector } from "react-redux"; 20 import { 21 Box, 22 breakPoints, 23 Button, 24 Checkbox, 25 DataTable, 26 FilterIcon, 27 Grid, 28 InputBox, 29 PageLayout, 30 } from "mds"; 31 import { AppState, useAppDispatch } from "../../../store"; 32 import { TraceMessage } from "./types"; 33 import { niceBytes, timeFromDate } from "../../../common/utils"; 34 import { wsProtocol } from "../../../utils/wsUtils"; 35 import { 36 setTraceStarted, 37 traceMessageReceived, 38 traceResetMessages, 39 } from "./traceSlice"; 40 import { setHelpName } from "../../../systemSlice"; 41 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 42 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 43 import HelpMenu from "../HelpMenu"; 44 import useWebSocket, { ReadyState } from "react-use-websocket"; 45 46 const Trace = () => { 47 const dispatch = useAppDispatch(); 48 49 const messages = useSelector((state: AppState) => state.trace.messages); 50 const traceStarted = useSelector( 51 (state: AppState) => state.trace.traceStarted, 52 ); 53 54 const [statusCode, setStatusCode] = useState<string>(""); 55 const [method, setMethod] = useState<string>(""); 56 const [func, setFunc] = useState<string>(""); 57 const [path, setPath] = useState<string>(""); 58 const [threshold, setThreshold] = useState<number>(0); 59 const [all, setAll] = useState<boolean>(false); 60 const [s3, setS3] = useState<boolean>(true); 61 const [internal, setInternal] = useState<boolean>(false); 62 const [storage, setStorage] = useState<boolean>(false); 63 const [os, setOS] = useState<boolean>(false); 64 const [errors, setErrors] = useState<boolean>(false); 65 66 const [toggleFilter, setToggleFilter] = useState<boolean>(false); 67 const [logActive, setLogActive] = useState(false); 68 const [wsUrl, setWsUrl] = useState<string>(""); 69 70 useEffect(() => { 71 const url = new URL(window.location.toString()); 72 const wsProt = wsProtocol(url.protocol); 73 const port = process.env.NODE_ENV === "development" ? "9090" : url.port; 74 const calls = all 75 ? "all" 76 : (() => { 77 const c = []; 78 if (s3) c.push("s3"); 79 if (internal) c.push("internal"); 80 if (storage) c.push("storage"); 81 if (os) c.push("os"); 82 return c.join(","); 83 })(); 84 85 // check if we are using base path, if not this always is `/` 86 const baseLocation = new URL(document.baseURI).pathname; 87 88 const wsUrl = new URL( 89 `${wsProt}://${url.hostname}:${port}${baseLocation}ws/trace`, 90 ); 91 wsUrl.searchParams.append("calls", calls); 92 wsUrl.searchParams.append("threshold", threshold.toString()); 93 wsUrl.searchParams.append("onlyErrors", errors ? "yes" : "no"); 94 wsUrl.searchParams.append("statusCode", statusCode); 95 wsUrl.searchParams.append("method", method); 96 wsUrl.searchParams.append("funcname", func); 97 wsUrl.searchParams.append("path", path); 98 setWsUrl(wsUrl.href); 99 }, [ 100 all, 101 s3, 102 internal, 103 storage, 104 os, 105 threshold, 106 errors, 107 statusCode, 108 method, 109 func, 110 path, 111 ]); 112 113 const { sendMessage, lastJsonMessage, readyState } = 114 useWebSocket<TraceMessage>( 115 wsUrl, 116 { 117 heartbeat: { 118 message: "ok", 119 interval: 10 * 1000, // send ok every 10 seconds 120 timeout: 365 * 24 * 60 * 60 * 1000, // disconnect after 365 days (workaround, because heartbeat gets no response) 121 }, 122 }, 123 logActive, 124 ); 125 126 useEffect(() => { 127 if (readyState === ReadyState.CONNECTING) { 128 dispatch(traceResetMessages()); 129 } else if (readyState === ReadyState.OPEN) { 130 dispatch(setTraceStarted(true)); 131 } else if (readyState === ReadyState.CLOSED) { 132 dispatch(setTraceStarted(false)); 133 } 134 }, [readyState, dispatch, sendMessage]); 135 136 useEffect(() => { 137 if (lastJsonMessage) { 138 lastJsonMessage.ptime = DateTime.fromISO(lastJsonMessage.time).toJSDate(); 139 lastJsonMessage.key = Math.random(); 140 dispatch(traceMessageReceived(lastJsonMessage)); 141 } 142 }, [lastJsonMessage, dispatch]); 143 144 useEffect(() => { 145 dispatch(setHelpName("trace")); 146 // eslint-disable-next-line react-hooks/exhaustive-deps 147 }, []); 148 149 return ( 150 <Fragment> 151 <PageHeaderWrapper label={"Trace"} actions={<HelpMenu />} /> 152 153 <PageLayout> 154 <Box withBorders> 155 <Grid container> 156 <Grid 157 item 158 xs={12} 159 sx={{ 160 display: "flex", 161 flexFlow: "column", 162 163 "& .trace-Checkbox-label": { 164 fontSize: "14px", 165 fontWeight: "normal", 166 }, 167 }} 168 > 169 <Box 170 sx={{ 171 fontSize: "16px", 172 fontWeight: 600, 173 padding: "20px 0px 20px 0", 174 }} 175 > 176 Calls to Trace 177 </Box> 178 <Box 179 className={`${traceStarted ? "inactive-state" : ""}`} 180 sx={{ 181 display: "flex", 182 alignItems: "center", 183 justifyContent: "space-between", 184 }} 185 > 186 <Box 187 sx={{ 188 display: "flex", 189 flexFlow: "row", 190 "& .trace-checked-icon": { 191 border: "1px solid red", 192 }, 193 [`@media (min-width: ${breakPoints.md}px)`]: { 194 gap: 30, 195 }, 196 }} 197 > 198 <Checkbox 199 checked={all} 200 id={"all_calls"} 201 name={"all_calls"} 202 label={"All"} 203 onChange={() => setAll(!all)} 204 value={"all"} 205 disabled={traceStarted} 206 /> 207 <Checkbox 208 checked={s3 || all} 209 id={"s3_calls"} 210 name={"s3_calls"} 211 label={"S3"} 212 onChange={() => setS3(!s3)} 213 value={"s3"} 214 disabled={all || traceStarted} 215 /> 216 <Checkbox 217 checked={internal || all} 218 id={"internal_calls"} 219 name={"internal_calls"} 220 label={"Internal"} 221 onChange={() => setInternal(!internal)} 222 value={"internal"} 223 disabled={all || traceStarted} 224 /> 225 <Checkbox 226 checked={storage || all} 227 id={"storage_calls"} 228 name={"storage_calls"} 229 label={"Storage"} 230 onChange={() => setStorage(!storage)} 231 value={"storage"} 232 disabled={all || traceStarted} 233 /> 234 <Checkbox 235 checked={os || all} 236 id={"os_calls"} 237 name={"os_calls"} 238 label={"OS"} 239 onChange={() => setOS(!os)} 240 value={"os"} 241 disabled={all || traceStarted} 242 /> 243 </Box> 244 <Box 245 sx={{ 246 display: "flex", 247 alignItems: "center", 248 justifyContent: "space-between", 249 gap: "15px", 250 }} 251 > 252 <TooltipWrapper tooltip={"More filter options"}> 253 <Button 254 id={"filter-toggle"} 255 onClick={() => setToggleFilter(!toggleFilter)} 256 label={"Filters"} 257 icon={<FilterIcon />} 258 variant={"regular"} 259 className={"filters-toggle-button"} 260 style={{ 261 width: "118px", 262 background: toggleFilter ? "rgba(8, 28, 66, 0.04)" : "", 263 }} 264 /> 265 </TooltipWrapper> 266 267 {!traceStarted && ( 268 <Button 269 id={"start-trace"} 270 label={"Start"} 271 data-test-id={"trace-start-button"} 272 variant="callAction" 273 onClick={() => setLogActive(true)} 274 style={{ 275 width: "118px", 276 }} 277 /> 278 )} 279 {traceStarted && ( 280 <Button 281 id={"stop-trace"} 282 label={"Stop Trace"} 283 data-test-id={"trace-stop-button"} 284 variant="callAction" 285 onClick={() => setLogActive(false)} 286 style={{ 287 width: "118px", 288 }} 289 /> 290 )} 291 </Box> 292 </Box> 293 </Grid> 294 {toggleFilter ? ( 295 <Box 296 useBackground 297 className={`${traceStarted ? "inactive-state" : ""}`} 298 sx={{ 299 marginTop: "25px", 300 display: "flex", 301 flexFlow: "column", 302 padding: "30px", 303 width: "100%", 304 305 "& .orient-vertical": { 306 flexFlow: "column", 307 "& label": { 308 marginBottom: "10px", 309 fontWeight: 600, 310 }, 311 "& .inputRebase": { 312 width: "90%", 313 }, 314 }, 315 316 "& .trace-Checkbox-label": { 317 fontSize: "14px", 318 fontWeight: "normal", 319 }, 320 }} 321 > 322 <Box 323 sx={{ 324 display: "flex", 325 }} 326 > 327 <InputBox 328 className="orient-vertical" 329 id="trace-status-code" 330 name="trace-status-code" 331 label="Status Code" 332 placeholder="e.g. 503" 333 value={statusCode} 334 onChange={(e) => setStatusCode(e.target.value)} 335 disabled={traceStarted} 336 /> 337 338 <InputBox 339 className="orient-vertical" 340 id="trace-function-name" 341 name="trace-function-name" 342 label="Function Name" 343 placeholder="e.g. FunctionName2055" 344 value={func} 345 onChange={(e) => setFunc(e.target.value)} 346 disabled={traceStarted} 347 /> 348 349 <InputBox 350 className="orient-vertical" 351 id="trace-method" 352 name="trace-method" 353 label="Method" 354 placeholder="e.g. Method 2056" 355 value={method} 356 onChange={(e) => setMethod(e.target.value)} 357 disabled={traceStarted} 358 /> 359 </Box> 360 <Box 361 sx={{ 362 gap: "30px", 363 display: "grid", 364 gridTemplateColumns: "2fr 1fr", 365 width: "100%", 366 marginTop: "33px", 367 }} 368 > 369 <Box 370 sx={{ 371 flex: 2, 372 width: "calc( 100% + 10px)", 373 }} 374 > 375 <InputBox 376 className="orient-vertical" 377 id="trace-path" 378 name="trace-path" 379 label="Path" 380 placeholder="e.g. my-bucket/my-prefix/*" 381 value={path} 382 onChange={(e) => setPath(e.target.value)} 383 disabled={traceStarted} 384 /> 385 </Box> 386 <Box 387 sx={{ 388 marginLeft: "15px", 389 }} 390 > 391 <InputBox 392 className="orient-vertical" 393 id="trace-fthreshold" 394 name="trace-fthreshold" 395 label="Response Threshold" 396 type="number" 397 placeholder="e.g. website.io.3249.114.12" 398 value={`${threshold}`} 399 onChange={(e) => setThreshold(parseInt(e.target.value))} 400 disabled={traceStarted} 401 /> 402 </Box> 403 </Box> 404 <Box 405 sx={{ 406 display: "flex", 407 alignItems: "center", 408 justifyContent: "flex-start", 409 marginTop: "40px", 410 }} 411 > 412 <Checkbox 413 checked={errors} 414 id={"only_errors"} 415 name={"only_errors"} 416 label={"Display only Errors"} 417 onChange={() => setErrors(!errors)} 418 value={"only_errors"} 419 disabled={traceStarted} 420 /> 421 </Box> 422 </Box> 423 ) : null} 424 425 <Grid item xs={12}> 426 <Box 427 sx={{ 428 fontSize: "16px", 429 fontWeight: 600, 430 marginBottom: "30px", 431 marginTop: "30px", 432 }} 433 > 434 Trace Results 435 </Box> 436 </Grid> 437 <Grid item xs={12}> 438 <DataTable 439 columns={[ 440 { 441 label: "Time", 442 elementKey: "ptime", 443 renderFunction: (time: Date) => { 444 const timeParse = new Date(time); 445 return timeFromDate(timeParse); 446 }, 447 width: 100, 448 }, 449 { label: "Name", elementKey: "api" }, 450 { 451 label: "Status", 452 elementKey: "", 453 renderFunction: (fullElement: TraceMessage) => 454 `${fullElement.statusCode} ${fullElement.statusMsg}`, 455 renderFullObject: true, 456 }, 457 { 458 label: "Location", 459 elementKey: "configuration_id", 460 renderFunction: (fullElement: TraceMessage) => 461 `${fullElement.host} ${fullElement.client}`, 462 renderFullObject: true, 463 }, 464 { 465 label: "Load Time", 466 elementKey: "callStats.duration", 467 width: 150, 468 }, 469 { 470 label: "Upload", 471 elementKey: "callStats.rx", 472 renderFunction: niceBytes, 473 width: 150, 474 }, 475 { 476 label: "Download", 477 elementKey: "callStats.tx", 478 renderFunction: niceBytes, 479 width: 150, 480 }, 481 ]} 482 isLoading={false} 483 records={messages} 484 entityName="Traces" 485 idField="api" 486 customEmptyMessage={ 487 traceStarted 488 ? "No Traced elements received yet" 489 : "Trace is not started yet" 490 } 491 customPaperHeight={"calc(100vh - 292px)"} 492 autoScrollToBottom 493 /> 494 </Grid> 495 </Grid> 496 </Box> 497 </PageLayout> 498 </Fragment> 499 ); 500 }; 501 502 export default Trace;