Skip to content

Commit

Permalink
Merge pull request #2440 from andrewbaldwin44/feature/2437
Browse files Browse the repository at this point in the history
Add Log Viewer to Modern UI
  • Loading branch information
cyberw authored Nov 1, 2023
2 parents 59fb560 + a6e471e commit 0427175
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 10 deletions.
20 changes: 15 additions & 5 deletions locust/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
unhandled_greenlet_exception = False


class LogReader(logging.Handler):
def __init__(self):
super().__init__()
self.logs = []

def emit(self, record):
self.logs.append(self.format(record))


def setup_logging(loglevel, logfile=None):
loglevel = loglevel.upper()

Expand All @@ -32,21 +41,22 @@ def setup_logging(loglevel, logfile=None):
"class": "logging.StreamHandler",
"formatter": "plain",
},
"log_reader": {"class": "locust.log.LogReader", "formatter": "default"},
},
"loggers": {
"locust": {
"handlers": ["console"],
"handlers": ["console", "log_reader"],
"level": loglevel,
"propagate": False,
},
"locust.stats_logger": {
"handlers": ["console_plain"],
"handlers": ["console_plain", "log_reader"],
"level": "INFO",
"propagate": False,
},
},
"root": {
"handlers": ["console"],
"handlers": ["console", "log_reader"],
"level": loglevel,
},
}
Expand All @@ -58,8 +68,8 @@ def setup_logging(loglevel, logfile=None):
"filename": logfile,
"formatter": "default",
}
LOGGING_CONFIG["loggers"]["locust"]["handlers"] = ["file"]
LOGGING_CONFIG["root"]["handlers"] = ["file"]
LOGGING_CONFIG["loggers"]["locust"]["handlers"] = ["file", "log_reader"]
LOGGING_CONFIG["root"]["handlers"] = ["file", "log_reader"]

logging.config.dictConfig(LOGGING_CONFIG)

Expand Down
16 changes: 15 additions & 1 deletion locust/test/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
import re
import textwrap
import traceback
import logging
from io import StringIO
from tempfile import NamedTemporaryFile, TemporaryDirectory

import gevent
import requests
from pyquery import PyQuery as pq

import locust
from locust import constant, LoadTestShape
from locust.argument_parser import get_parser, parse_options
Expand All @@ -21,6 +21,7 @@
from locust import stats
from locust.stats import StatsCSVFileWriter
from locust.web import WebUI
from locust.log import LogReader

from .mock_locustfile import mock_locustfile
from .testcases import LocustTestCase
Expand Down Expand Up @@ -1013,6 +1014,19 @@ def test_html_stats_report(self):
self.assertIn("Script: <span>locust.py</span>", str(d))
self.assertIn("Target Host: <span>http://localhost</span>", str(d))

def test_logs(self):
log_handler = LogReader()
log_handler.name = "log_reader"
log_handler.setLevel(logging.INFO)
logger = logging.getLogger("root")
logger.addHandler(log_handler)
log_line = "some log info"
logger.info(log_line)

response = requests.get("http://127.0.0.1:%i/logs" % self.web_port)

self.assertIn(log_line, response.json().get("logs"))


class TestWebUIAuth(LocustTestCase):
def setUp(self):
Expand Down
20 changes: 20 additions & 0 deletions locust/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ def __init__(
if not delayed_start:
self.start()

@app.errorhandler(Exception)
def handle_exception(error):
error_message = str(error)
logger.log(logging.CRITICAL, error_message)
return make_response(error_message, 500)

@app.route("/assets/<path:path>")
def send_assets(path):
webui_build_path = self.webui_build_path
Expand Down Expand Up @@ -478,6 +484,20 @@ def tasks() -> Dict[str, Dict[str, Dict[str, float]]]:
}
return task_data

@app.route("/logs")
@self.auth_required_if_enabled
def logs():
log_reader_handler = [
handler for handler in logging.getLogger("root").handlers if handler.name == "log_reader"
]

if log_reader_handler:
logs = log_reader_handler[0].logs
else:
logs = []

return jsonify({"logs": logs})

def start(self):
self.greenlet = gevent.spawn(self.start_server)
self.greenlet.link_exception(greenlet_exception_handler)
Expand Down
236 changes: 236 additions & 0 deletions locust/webui/dist/assets/index-b46361c2.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion locust/webui/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />

<title>Locust</title>
<script type="module" crossorigin src="/assets/index-908b1e34.js"></script>
<script type="module" crossorigin src="/assets/index-b46361c2.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
23 changes: 23 additions & 0 deletions locust/webui/src/components/LogViewer/LogViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Box, Typography } from '@mui/material';

import { SWARM_STATE } from 'constants/swarm';
import useInterval from 'hooks/useInterval';
import { useGetLogsQuery } from 'redux/api/swarm';
import { useSelector } from 'redux/hooks';

export default function LogViewer() {
const swarm = useSelector(({ swarm }) => swarm);
const { data, refetch: refetchLogs } = useGetLogsQuery();

useInterval(refetchLogs, 5000, { shouldRunInterval: swarm.state !== SWARM_STATE.STOPPED });

return (
<Box>
<Typography component='h2' variant='h4'>
Logs
</Typography>

<ul>{data && data.logs.map((log, index) => <li key={`log-${index}`}>{log}</li>)}</ul>
</Box>
);
}
6 changes: 6 additions & 0 deletions locust/webui/src/components/Tabs/Tabs.constants.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ExceptionsTable from 'components/ExceptionsTable/ExceptionsTable';
import FailuresTable from 'components/FailuresTable/FailuresTable';
import LogViewer from 'components/LogViewer/LogViewer';
import Reports from 'components/Reports/Reports';
import StatsTable from 'components/StatsTable/StatsTable';
import SwarmCharts from 'components/SwarmCharts/SwarmCharts';
Expand Down Expand Up @@ -38,6 +39,11 @@ export const baseTabs = [
key: 'reports',
title: 'Download Data',
},
{
component: LogViewer,
key: 'log_viewer',
title: 'Logs',
},
];

export const conditionalTabs = [
Expand Down
20 changes: 17 additions & 3 deletions locust/webui/src/redux/api/swarm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

import { IStatsResponse, ISwarmExceptionsResponse, ISwarmRatios } from 'types/ui.types';
import {
IStatsResponse,
ISwarmExceptionsResponse,
ISwarmRatios,
ILogsResponse,
} from 'types/ui.types';
import { createFormData } from 'utils/object';
import { camelCaseKeys, snakeCaseKeys } from 'utils/string';

Expand All @@ -19,6 +24,10 @@ export const api = createApi({
query: () => 'exceptions',
transformResponse: camelCaseKeys<ISwarmExceptionsResponse>,
}),
getLogs: builder.query<ILogsResponse, void>({
query: () => 'logs',
transformResponse: camelCaseKeys<ILogsResponse>,
}),

startSwarm: builder.mutation({
query: body => ({
Expand All @@ -31,5 +40,10 @@ export const api = createApi({
}),
});

export const { useGetStatsQuery, useGetTasksQuery, useGetExceptionsQuery, useStartSwarmMutation } =
api;
export const {
useGetStatsQuery,
useGetTasksQuery,
useGetExceptionsQuery,
useGetLogsQuery,
useStartSwarmMutation,
} = api;
4 changes: 4 additions & 0 deletions locust/webui/src/types/ui.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ export interface IStatsResponse {
currentResponseTimePercentile2: number | null;
userCount: number;
}

export interface ILogsResponse {
logs: string[];
}

0 comments on commit 0427175

Please sign in to comment.