├── .gitignore
├── README.md
├── client
├── App.jsx
├── actions
│ ├── actionTypes.js
│ └── actions.js
├── components
│ ├── BrokerDisplay.jsx
│ ├── ConnectCluster.jsx
│ ├── ConsumerDisplay.jsx
│ ├── DisconnectCluster.jsx
│ ├── NetworkDisplay.jsx
│ ├── PortAlert.jsx
│ ├── ProducerDisplay.jsx
│ ├── Sidebar.jsx
│ ├── UnderConstruction.jsx
│ ├── charts
│ │ ├── BarChart.jsx
│ │ ├── LineChart.jsx
│ │ ├── MultipleLineChart.jsx
│ │ └── ScoreCard.jsx
│ └── timeFunction.js
├── images
│ └── monokl_white.svg
├── index.js
├── reducers
│ ├── index.js
│ └── mainReducer.js
├── store.js
└── styles.css
├── icon
└── icon.png
├── index.html
├── main.js
├── package.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | build
4 | dist
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | An open source monitoring tool for Apache Kafka
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Table of Contents
19 |
20 | * [Features](#features)
21 | * [Demo](#demo)
22 | * [Installation](#installation)
23 | * [Engineering Team](#monokl-engineering-team)
24 |
27 | ## Features
28 | * ### User-friendly GUI
29 | * ### Insights into cluster brokers, producers, consumers, and network metrics
30 | * ### Graphical displays of key performance metrics
31 |
32 |
33 | ## Demo
34 |
35 | Enter the Prometheus port where your Kafka instance is running
36 |
37 |
38 |
39 |
40 | Upon successful submission, critical information about your brokers, topics, and consumer groups becomes immediately available
41 |
42 |
43 |
44 |
45 |
46 |
47 | ## Installation
48 | - To download our desktop application, please visit the releases and downlaod the appropriate file for your OS. (*Note: If you are using the desktop application, you may need to configure your privacy or security settings to allow the application to open without being verified.)
49 | - You can also clone the repository directly from GitHub and run these commands in your terminal:
50 | 1. npm install
51 | 2. npm run watch
52 | 3. npm start (in a separate terminal)
53 | - Monokl requires a Kafka Cluster configured with JMX and Prometheus to capture advanced metrics.
54 | - Enter the port number for Prometheus to verify connection
55 | - Select through various Displays for up to date chart information
56 |
57 |
58 |
59 |
60 |
61 | ## Monokl Engineering Team
62 | [Wesley Mungal](https://github.com/lagnum22)
63 | | [Savitri Beaver](https://github.com/savybeav)
64 | | [Tyler Holt](https://github.com/tylerprestonholt)
65 |
66 | We welcome contributions, so please feel free to fork, clone, and help monokl grow! Remember to leave a [](https://github.com/oslabs-beta/monokl/stargazers) if you'd like to support our work!
67 |
71 |
72 |
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Sidebar from './components/Sidebar.jsx';
3 |
4 | function App() {
5 | return (
6 | <>
7 |
8 | >
9 | );
10 | }
11 |
12 | export default App;
13 |
--------------------------------------------------------------------------------
/client/actions/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_PORT = "ADD_PORT";
2 | export const REMOVE_PORT = "REMOVE_PORT";
3 | export const ADD_CONNECTION_TIME = 'ADD_CONNECTION_TIME';
4 |
5 |
--------------------------------------------------------------------------------
/client/actions/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from "./actionTypes.js";
2 |
3 | //action creators below
4 | export const addPortAction = (userPort) => {
5 | return {
6 | type: types.ADD_PORT,
7 | payload: userPort,
8 | };
9 | };
10 |
11 | export const removePortAction = () => {
12 | return {
13 | type: types.REMOVE_PORT,
14 | payload: "",
15 | };
16 | };
17 |
18 | export const addConnectionTimeAction = (timestamp) => {
19 | return {
20 | type: types.ADD_CONNECTION_TIME,
21 | payload: timestamp,
22 | };
23 | };
24 |
25 |
--------------------------------------------------------------------------------
/client/components/BrokerDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import { connect } from "react-redux";
4 | //Material UI - Core
5 | import {Paper, Grid } from "@material-ui/core"
6 | //Chart Components
7 | import LineChart from "./charts/LineChart.jsx";
8 | import ScoreCard from "./charts/ScoreCard.jsx";
9 | //Time Function
10 | import { timeFunction } from "./timeFunction.js";
11 |
12 | const useStyles = makeStyles((theme) => ({
13 | root: {
14 | display: "flex",
15 | flexDirection: 'column',
16 | flexGrow: "1",
17 | alignItems: "center",
18 | },
19 | title: {
20 | color: "rgba(0, 0, 0, 0.54)",
21 | fontSize: "42px",
22 | fontWeight: "bold",
23 | },
24 | paper: {
25 | padding: theme.spacing(2),
26 | textAlign: "center",
27 | color: theme.palette.text.secondary,
28 | minHeight: "180px"
29 | },
30 | }));
31 |
32 | const mapStateToProps = (state) => {
33 | return {
34 | port: state.mainReducer.port,
35 | connectionTime: state.mainReducer.connectionTime
36 | };
37 | };
38 |
39 | function BrokerDisplay(props) {
40 | const classes = useStyles();
41 | // component state properties for each charts x and y axis
42 | const [underReplicatedPartitions, setURP] = useState([]);
43 | const [activeControllerCount, setActiveController] = useState([]);
44 | const [offlinePartitions, setOfflinePartitions] = useState([]);
45 | const [xArrayLeader, setXArrayLeader] = useState([]);
46 | const [yArrayLeader, setYArrayLeader] = useState([]);
47 | const [xArrayTTFetch, setXArrayTTFetch] = useState([]);
48 | const [yArrayTTFetch, setYArrayTTFetch] = useState([]);
49 | const [xArrayPurg, setXArrayPurg] = useState([]);
50 | const [yArrayPurg, setYArrayPurg] = useState([]);
51 | const [xArrayIn, setXArrayIn] = useState([]);
52 | const [yArrayIn, setYArrayIn] = useState([]);
53 | const [xArrayOut, setXArrayOut] = useState([]);
54 | const [yArrayOut, setYArrayOut] = useState([]);
55 |
56 | //calculate the interval
57 | let interval = timeFunction(props.connectionTime);
58 |
59 | useEffect(() => {
60 | //1. Under Replicated Partitions Count (Score Card)
61 | let underReplicated = fetch(
62 | `http://localhost:${props.port}/api/v1/query?query=kafka_server_replicamanager_underreplicatedpartitions`
63 | ).then((respose) => respose.json());
64 |
65 | //2. Active Controller Count (Score Card)
66 | let activeController = fetch(
67 | `http://localhost:${props.port}/api/v1/query?query=kafka_controller_kafkacontroller_activecontrollercount`
68 | ).then((respose) => respose.json());
69 |
70 | //3. Offline Partitions Count (Score Card)
71 | let offlinePartitions = fetch(
72 | `http://localhost:${props.port}/api/v1/query?query=kafka_controller_kafkacontroller_offlinepartitionscount`
73 | ).then((respose) => respose.json());
74 |
75 | //4. Leader Election Rate and Time Ms
76 | let leaderElectionRateMs = fetch(
77 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_controller_controllerstats_leaderelectionrateandtimems_count&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
78 | ).then((respose) => respose.json());
79 |
80 | //5. Fetch Consumer Total Time
81 | let fetchConsumerTime = fetch(
82 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_network_requestmetrics_totaltimems{request="FetchConsumer"}&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
83 | ).then((respose) => respose.json());
84 |
85 | //6. Purgatory Size
86 | let purgatorySizeFetch = fetch(
87 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_delayedoperationpurgatory_purgatorysize{delayedOperation="Fetch"}&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
88 | ).then((respose) => respose.json());
89 |
90 | //7. Bytes In Total (Range)
91 | let bytesIn = fetch(
92 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_brokertopicmetrics_bytesin_total&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
93 | ).then((respose) => respose.json());
94 | //8. Bytes Out Total (Range)
95 | let bytesOut = fetch(
96 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_brokertopicmetrics_bytesout_total&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
97 | ).then((respose) => respose.json());
98 |
99 | Promise.all([underReplicated, activeController, offlinePartitions, leaderElectionRateMs, fetchConsumerTime, purgatorySizeFetch, bytesIn, bytesOut])
100 | .then((allData) => {
101 | //1. Under Replicated Partitions Chart
102 | setURP(allData[0].data.result[0].value[1])
103 |
104 | //2. Active Controller Count Chart
105 | setActiveController(allData[1].data.result[0].value[1])
106 |
107 | //3. Offline Partitions Count Chart
108 | setOfflinePartitions(allData[2].data.result[0].value[1])
109 |
110 | //4. Leader Election Rate Ms Chart
111 | setXArrayLeader(allData[3].data.result[0].values.map((data) => {
112 | let date = new Date(data[0] * 1000);
113 | return date.toLocaleTimeString('en-GB');
114 | }))
115 | setYArrayLeader(allData[3].data.result[0].values.map((data) => Number(data[1])))
116 |
117 | //5. Fetch Consumer Time Chart
118 | setXArrayTTFetch(allData[4].data.result[0].values.map((data) => {
119 | let date = new Date(data[0] * 1000);
120 | return date.toLocaleTimeString('en-GB');
121 | }));
122 | setYArrayTTFetch(allData[4].data.result[0].values.map((data) => Number(data[1])));
123 |
124 | //6. Purgatory Size Chart
125 | setXArrayPurg(allData[5].data.result[0].values.map((data) => {
126 | let date = new Date(data[0] * 1000);
127 | return date.toLocaleTimeString('en-GB');
128 | }));
129 | setYArrayPurg(allData[5].data.result[0].values.map((data) => Number(data[1])));
130 |
131 | //7. Bytes In Chart
132 | setXArrayIn(allData[6].data.result[0].values.map((data) => {
133 | let date = new Date(data[0] * 1000);
134 | return date.toLocaleTimeString('en-GB');
135 | }));
136 | setYArrayIn(allData[6].data.result[0].values.map((data) => Number(data[1])));
137 |
138 | //8. Bytes Out Chart
139 | setXArrayOut(allData[7].data.result[0].values.map((data) => {
140 | let date = new Date(data[0] * 1000);
141 | return date.toLocaleTimeString('en-GB');
142 | }));
143 | setYArrayOut(allData[7].data.result[0].values.map((data) => Number(data[1])));
144 | })
145 | .catch(err => console.log(err))
146 | }, [])
147 |
148 | return (
149 | <>
150 |
151 |
Broker Metrics
152 |
153 | {/* 1. Under Replicated Partitions */}
154 |
155 |
156 |
160 |
161 |
162 | {/* 2. Active Controller Count */}
163 |
164 |
165 |
169 |
170 |
171 | {/* 3. Offline Partition Count */}
172 |
173 |
174 |
178 |
179 |
180 | {/* 4. Leader Election Rate and Time Ms */}
181 |
182 |
183 |
189 |
190 |
191 | {/* 5. Fetch Consumer Time - 50th quantile*/}
192 |
193 |
194 |
200 |
201 |
202 | {/* 6. Purgatory Size */}
203 |
204 |
205 |
211 |
212 |
213 | {/* 7. Bytes In */}
214 |
215 |
216 |
222 |
223 |
224 | {/* 8. Bytes Out */}
225 |
226 |
227 |
233 |
234 |
235 |
236 |
237 | >
238 | );
239 | }
240 |
241 | export default connect(mapStateToProps, null)(BrokerDisplay);
242 |
--------------------------------------------------------------------------------
/client/components/ConnectCluster.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { addPortAction, addConnectionTimeAction } from "../actions/actions";
3 | import { connect } from "react-redux";
4 | //Material UI - Core
5 | import {TextField,Button } from "@material-ui/core"
6 | //Material UI- Styles
7 | import { makeStyles } from "@material-ui/core/styles";
8 | //Application Components
9 | import PortAlert from './PortAlert.jsx';
10 |
11 | const useStyles = makeStyles((theme) => ({
12 | root: {
13 | display: "flex",
14 | flexDirection: "column",
15 | justifyContent: "center",
16 | alignItems: "center",
17 | marginTop: "185px",
18 | "& .MuiTextField-root": {
19 | margin: theme.spacing(1),
20 | width: "35ch",
21 | },
22 | },
23 | connect: {
24 | "& > *": {
25 | margin: theme.spacing(1),
26 | color: "#537791",
27 | },
28 | },
29 | }));
30 |
31 | const mapStateToProps = (state) => {
32 | return {
33 | port: state.mainReducer.port,
34 | };
35 | };
36 |
37 | const mapDispatchToProps = (dispatch) => {
38 | return {
39 | addPortAction: (userInput) => {
40 | dispatch(addPortAction(userInput));
41 | },
42 | addConnectionTimeAction: (timestamp) => {
43 | dispatch(addConnectionTimeAction(timestamp));
44 | }
45 | };
46 | };
47 |
48 | const verifyPort = async (port) => {
49 | let valid = false;
50 | const url = `http://localhost:${port}/api/v1/query?query=up`;
51 | await fetch(url)
52 | .then(res => res.json())
53 | .then(data => {
54 | if (data.status === 'success') valid = true;
55 | })
56 | .catch(err => console.log(err));
57 | return valid;
58 | }
59 |
60 | function ConnectCluster(props) {
61 | const classes = useStyles();
62 | const [attempts, addAttempt] = useState(0);
63 |
64 | return (
65 | <>
66 |
94 | >
95 | );
96 | }
97 |
98 | export default connect(mapStateToProps, mapDispatchToProps)(ConnectCluster);
99 |
--------------------------------------------------------------------------------
/client/components/ConsumerDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import { connect } from "react-redux";
4 | //Material UI - Core
5 | import {Paper, Grid } from "@material-ui/core"
6 | //App Chart Components
7 | import MultipleLineChart from "./charts/MultipleLineChart.jsx";
8 | import { timeFunction } from "./timeFunction.js";
9 |
10 |
11 | const useStyles = makeStyles((theme) => ({
12 | root: {
13 | display: "flex",
14 | flexDirection: 'column',
15 | flexGrow: "1",
16 | alignItems: "center",
17 | },
18 | title: {
19 | color: "rgba(0, 0, 0, 0.54)",
20 | fontSize: "42px",
21 | fontWeight: "bold",
22 | },
23 | paper: {
24 | padding: theme.spacing(2),
25 | textAlign: "center",
26 | color: theme.palette.text.secondary,
27 | },
28 | }));
29 |
30 | const mapStateToProps = (state) => {
31 | return {
32 | port: state.mainReducer.port,
33 | connectionTime: state.mainReducer.connectionTime
34 | };
35 | };
36 |
37 | function ConsumerDisplay(props) {
38 | const classes = useStyles();
39 | // component state properties for charts x and y axes
40 | const [xArray50, setXArray] = useState([]);
41 | const [yArray50, setYArray50] = useState([]);
42 | const [yArray75, setXArray75] = useState([]);
43 | const [yArray95, setXArray95] = useState([]);
44 | const [yArray99, setXArray99] = useState([]);
45 |
46 | //calculate the interval
47 | let interval = timeFunction(props.connectionTime);
48 |
49 | useEffect(() => {
50 | fetch(
51 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_network_requestmetrics_totaltimems{request="FetchConsumer"}&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
52 | )
53 | .then((respose) => respose.json())
54 | .then((res) => {
55 | // 1. Network Request Metrics Time
56 | setXArray(res.data.result[0].values.map((data) => {
57 | let date = new Date(data[0] * 1000);
58 | return date.toLocaleTimeString('en-GB');
59 | }))
60 | setYArray50(res.data.result[0].values.map((data) => Number(data[1])))
61 | setXArray75(res.data.result[1].values.map((data) => Number(data[1])))
62 | setXArray95(res.data.result[2].values.map((data) => Number(data[1])))
63 | setXArray99(res.data.result[4].values.map((data) => Number(data[1])))
64 | })
65 | .catch(err => console.log(err));
66 | }, []);
67 |
68 | return (
69 |
70 |
Consumer Metrics
71 |
72 | {/*1. Network Request Metrics Time */}
73 |
74 |
75 |
89 |
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | export default connect(mapStateToProps, null)(ConsumerDisplay);
97 |
--------------------------------------------------------------------------------
/client/components/DisconnectCluster.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { removePortAction } from "../actions/actions";
3 | import { connect } from "react-redux";
4 | //Material UI - Core
5 | import {Button, Paper} from "@material-ui/core"
6 | //Material UI - Styles
7 | import { makeStyles } from "@material-ui/core/styles";
8 |
9 | const useStyles = makeStyles((theme) => ({
10 | root: {
11 | display: "flex",
12 | flexDirection: "column",
13 | justifyContent: "center",
14 | flexGrow: "1",
15 | marginTop: "140px",
16 | alignItems: "center",
17 | },
18 | paper: {
19 | padding: theme.spacing(3),
20 | marginBottom: '8px',
21 | textAlign: "center",
22 | color: theme.palette.text.secondary,
23 | },
24 |
25 | connect: {
26 | "& > *": {
27 | margin: theme.spacing(1),
28 | color: "#537791",
29 | },
30 | },
31 | }));
32 |
33 | const mapStateToProps = (state) => {
34 | return {
35 | port: state.mainReducer.port,
36 | connectionTime: state.mainReducer.connectionTime
37 | };
38 | };
39 |
40 | const mapDispatchToProps = (dispatch) => {
41 | return {
42 | removePortAction: () => {
43 | dispatch(removePortAction());
44 | },
45 | };
46 | };
47 |
48 | function DisconnectCluster(props) {
49 | const classes = useStyles();
50 |
51 | return (
52 |
53 |
54 | Successfully connected to Prometheus on port: {props.port}
55 |
56 |
{
61 | e.preventDefault();
62 | props.removePortAction();
63 | }}
64 | >
65 | Disconnect
66 |
67 |
68 | );
69 | }
70 |
71 | export default connect(mapStateToProps, mapDispatchToProps)(DisconnectCluster);
72 |
--------------------------------------------------------------------------------
/client/components/NetworkDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { connect } from "react-redux";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | //Material UI - Core
5 | import {Paper, Grid } from "@material-ui/core"
6 | //Application Chart Components
7 | import LineChart from "./charts/LineChart.jsx";
8 | //Time Function
9 | import { timeFunction } from "./timeFunction.js";
10 |
11 |
12 |
13 | const useStyles = makeStyles((theme) => ({
14 | root: {
15 | display: "flex",
16 | flexDirection: 'column',
17 | flexGrow: "1",
18 | alignItems: "center",
19 | },
20 | title: {
21 | color: "rgba(0, 0, 0, 0.54)",
22 | fontSize: "42px",
23 | fontWeight: "bold",
24 | },
25 | paper: {
26 | padding: theme.spacing(2),
27 | textAlign: "center",
28 | color: theme.palette.text.secondary,
29 | },
30 | }));
31 |
32 | const mapStateToProps = (state) => {
33 | return {
34 | port: state.mainReducer.port,
35 | connectionTime: state.mainReducer.connectionTime
36 | };
37 | };
38 |
39 | const mapDispatchToProps = (dispatch) => {
40 | return {
41 | fetchNetworkMetrics: () => {
42 | dispatch(fetchNetworkMetrics());
43 | },
44 | };
45 | };
46 |
47 | function NetworkDisplay(props) {
48 | const classes = useStyles();
49 | // component state properties chart x and y axis
50 | const [xArrayIdle, setXArray] = useState([]);
51 | const [yArrayIdle, setYArray] = useState([]);
52 |
53 | //calculate the interval
54 | let interval = timeFunction(props.connectionTime);
55 |
56 | useEffect(() => {
57 | fetch(
58 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_network_socketserver_networkprocessoravgidlepercent&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
59 | )
60 | .then((response) => response.json())
61 | .then((res)=>{
62 | //1. Network Processor Avg Idle Percentage
63 | setXArray(res.data.result[0].values.map((data) => {
64 | let date = new Date(data[0] * 1000);
65 | return date.toLocaleTimeString('en-GB');
66 | }))
67 | setYArray(res.data.result[0].values.map((data) => Number(data[1])))
68 | console.log('this is x and y array: ', setXArray, setYArray)
69 | })
70 | .catch(err => console.log(err))
71 | }, []);
72 |
73 | return (
74 |
75 |
Network Metrics
76 |
77 | {/* 1. network Processor Avg Idle Percentage */}
78 |
79 |
80 |
86 |
87 |
88 |
89 |
90 | );
91 | }
92 |
93 | export default connect(mapStateToProps, mapDispatchToProps)(NetworkDisplay);
94 |
--------------------------------------------------------------------------------
/client/components/PortAlert.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | //Material UI - Material
3 | import {Alert,Stack } from "@mui/material"
4 |
5 |
6 | export default function PortAlert() {
7 | return (
8 |
9 |
10 | Invalid port number — please try again.
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/client/components/ProducerDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { connect } from "react-redux";
3 | //Material UI - Core
4 | import {Paper, Grid } from "@material-ui/core"
5 | //Material UI - Styles
6 | import { makeStyles } from "@material-ui/core/styles";
7 | //Application Chart Components
8 | import LineChart from "./charts/LineChart.jsx";
9 | import MultipleLineChart from "./charts/MultipleLineChart.jsx";
10 | import ScoreCard from "./charts/ScoreCard.jsx";
11 | //Time Function
12 | import { timeFunction } from "./timeFunction.js";
13 |
14 | const useStyles = makeStyles((theme) => ({
15 | root: {
16 | display: "flex",
17 | flexGrow: "1",
18 | flexDirection: "column",
19 | alignItems: "center",
20 | },
21 | title: {
22 | color: "rgba(0, 0, 0, 0.54)",
23 | fontSize: "42px",
24 | fontWeight: "bold",
25 | },
26 | paper: {
27 | padding: theme.spacing(2),
28 | textAlign: "center",
29 | color: theme.palette.text.secondary,
30 | },
31 | }));
32 |
33 | //use a mapstatetoprops to pass data to both line charts
34 | const mapStateToProps = (state) => {
35 | return {
36 | port: state.mainReducer.port,
37 | connectionTime: state.mainReducer.connectionTime
38 | };
39 | };
40 |
41 | function ProducerDisplay(props) {
42 | const classes = useStyles();
43 | // component state properties for each charts x and y axis
44 | const [xArray50, setXArray] = useState([]);
45 | const [yArray50, setYArray50] = useState([]);
46 | const [yArray75, setYArray75] = useState([]);
47 | const [yArray95, setYArray95] = useState([]);
48 | const [yArray99, setYArray99] = useState([]);
49 | const [xArrayTotalProducer, setXArrayTotalProducer] = useState([]);
50 | const [yArrayTotalProducer, setYArrayTotalProducer] = useState([]);
51 | const [xFailedProducerRequest, setXArrayFailedProducerRequest] = useState([]);
52 | const [yFailedProducerRequest, setYArrayFailedProducerRequest] = useState([]);
53 |
54 | //calculate the interval
55 | let interval = timeFunction(props.connectionTime);
56 |
57 | useEffect(() => {
58 | //1. Total Time for Producer Requests
59 | let totalTimeMs = fetch(
60 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_network_requestmetrics_totaltimems{request="Produce"}&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
61 | ).then((respose) => respose.json());
62 |
63 | //2. Total Producer Requests= (Aggregate)
64 | let producerReqsTotal = fetch(
65 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_brokertopicmetrics_totalproducerequests_total&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
66 | ).then((respose) => respose.json());
67 |
68 | //3. Failed Producer Requests (Aggregate)
69 | let failedProducerReqs = fetch(
70 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_brokertopicmetrics_failedproducerequests_total&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s`
71 | ).then((respose) => respose.json());
72 |
73 | Promise.all([totalTimeMs, producerReqsTotal, failedProducerReqs])
74 | .then((allData) => {
75 | //1. Total Time in Ms for Producer Requests
76 | setXArray(allData[0].data.result[0].values.map((data) => {
77 | let date = new Date(data[0] * 1000);
78 | return date.toLocaleTimeString('en-GB');
79 | }))
80 | setYArray50(allData[0].data.result[0].values.map((data) => Number(data[1])))
81 | setYArray75(allData[0].data.result[1].values.map((data) => Number(data[1])))
82 | setYArray95(allData[0].data.result[2].values.map((data) => Number(data[1])))
83 | setYArray99(allData[0].data.result[4].values.map((data) => Number(data[1])))
84 | //2. Total Producer Requests Chart
85 | setXArrayTotalProducer(allData[1].data.result[0].values.map((data) => {
86 | let date = new Date(data[0] * 1000);
87 | return date.toLocaleTimeString('en-GB');
88 | }))
89 | setYArrayTotalProducer(allData[1].data.result[0].values.map((data)=> Number(data[1])))
90 | //3. Failed Producer Requests Chart
91 | setXArrayFailedProducerRequest(allData[2].data.result[0].values.map((data) => {
92 | let date = new Date(data[0] * 1000);
93 | return date.toLocaleTimeString('en-GB');
94 | }))
95 | setYArrayFailedProducerRequest(allData[2].data.result[0].values.map((data) => Number(data[1])))
96 | })
97 | .catch(err => console.log(err));
98 | }, []);
99 |
100 | return (
101 |
102 |
Producer Metrics
103 |
104 | {/* 1. Total Time for Producer Requests */}
105 |
106 |
107 |
121 |
122 |
123 | {/* Total Producer Requests */}
124 |
125 |
126 |
132 |
133 |
134 | {/* Failed Producer Requests */}
135 |
136 |
137 |
143 |
144 |
145 |
146 |
147 | );
148 | }
149 |
150 | export default connect(mapStateToProps, null)(ProducerDisplay);
151 |
--------------------------------------------------------------------------------
/client/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | //React Router
4 | import { HashRouter, Switch, Route, Redirect, Link } from "react-router-dom";
5 | //Material UI
6 | import clsx from "clsx";
7 | //Material UI Components - Core
8 | import { Drawer, AppBar, Toolbar, List, CssBaseline, Typography, Divider, IconButton, ListItem, ListItemIcon, ListItemText } from "@material-ui/core"
9 | //Material UI Icons
10 | import MenuIcon from "@material-ui/icons/Menu";
11 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
12 | import ChevronRightIcon from "@material-ui/icons/ChevronRight";
13 | import ConnectIcon from "@material-ui/icons/SettingsEthernetOutlined";
14 | import AlertIcon from "@material-ui/icons/ErrorOutlineOutlined";
15 | import BrokerIcon from '@mui/icons-material/OpenWithOutlined';
16 | import ProducerIcon from '@mui/icons-material/StoreOutlined';
17 | import ConsumerIcon from '@mui/icons-material/ShoppingBagOutlined';
18 | import NetworkIcon from "@material-ui/icons/NetworkCheckOutlined";
19 | import AlertSettingsIcon from "@material-ui/icons/SettingsApplicationsOutlined";
20 | //Material UI - Styles
21 | import { makeStyles, useTheme } from "@material-ui/core/styles";
22 | //Application Components
23 | import ConnectCluster from "./ConnectCluster.jsx";
24 | import DisconnectCluster from "./DisconnectCluster.jsx";
25 | import BrokerDisplay from "./BrokerDisplay.jsx";
26 | import ProducerDisplay from "./ProducerDisplay.jsx";
27 | import ConsumerDisplay from "./ConsumerDisplay.jsx";
28 | import NetworkDisplay from "./NetworkDisplay.jsx";
29 | import UnderConstruction from "./UnderConstruction.jsx";
30 | //Logo
31 | import Logo from "../images/monokl_white.svg";
32 |
33 | const drawerWidth = 240;
34 |
35 | const useStyles = makeStyles((theme) => ({
36 | root: {
37 | display: "flex",
38 | minHeight: "100vh",
39 | },
40 | appBar: {
41 | backgroundColor: "#537791",
42 | zIndex: theme.zIndex.drawer + 1,
43 | transition: theme.transitions.create(["width", "margin"], {
44 | easing: theme.transitions.easing.sharp,
45 | duration: theme.transitions.duration.leavingScreen,
46 | }),
47 | },
48 | appBarShift: {
49 | marginLeft: drawerWidth,
50 | width: `calc(100% - ${drawerWidth}px)`,
51 | transition: theme.transitions.create(["width", "margin"], {
52 | easing: theme.transitions.easing.sharp,
53 | duration: theme.transitions.duration.enteringScreen,
54 | }),
55 | },
56 | menuButton: {
57 | marginRight: 36,
58 | },
59 | hide: {
60 | display: "none",
61 | },
62 | drawer: {
63 | width: drawerWidth,
64 | flexShrink: 0,
65 | whiteSpace: "nowrap",
66 | },
67 | drawerOpen: {
68 | width: drawerWidth,
69 | transition: theme.transitions.create("width", {
70 | easing: theme.transitions.easing.sharp,
71 | duration: theme.transitions.duration.enteringScreen,
72 | }),
73 | },
74 | drawerClose: {
75 | transition: theme.transitions.create("width", {
76 | easing: theme.transitions.easing.sharp,
77 | duration: theme.transitions.duration.leavingScreen,
78 | }),
79 | overflowX: "hidden",
80 | width: theme.spacing(7) + 1,
81 | [theme.breakpoints.up("sm")]: {
82 | width: theme.spacing(9) + 1,
83 | },
84 | },
85 | toolbar: {
86 | backgroundColor: "#537791",
87 | display: "flex",
88 | alignItems: "center",
89 | justifyContent: "flex-end",
90 | padding: theme.spacing(0, 1),
91 | // necessary for content to be below app bar
92 | ...theme.mixins.toolbar,
93 | },
94 | content: {
95 | flexGrow: "1",
96 | padding: theme.spacing(3),
97 | marginTop: "50px",
98 | minWidth: "380px",
99 | },
100 | logo: {
101 | backgroundSize: "20px 30px",
102 | marginTop: "8px"
103 | }
104 | }));
105 |
106 | const mapStateToProps = (state) => ({
107 | port: state.mainReducer.port,
108 | });
109 |
110 | function Sidebar(props) {
111 | const classes = useStyles();
112 | const theme = useTheme();
113 | const [open, setOpen] = React.useState(false);
114 |
115 | const handleDrawerOpen = () => {
116 | setOpen(true);
117 | };
118 |
119 | const handleDrawerClose = () => {
120 | setOpen(false);
121 | };
122 |
123 | return (
124 |
125 |
126 |
132 |
133 |
142 |
143 |
144 |
145 | {/* */}
146 |
147 |
148 |
149 |
162 |
163 |
164 | {theme.direction === "rtl" ? (
165 |
166 | ) : (
167 |
168 | )}
169 |
170 |
171 |
172 |
173 |
174 | { }
175 |
176 |
177 | {(() => {
178 | if (props.port) {
179 | return (
180 | <>
181 |
187 | { }
188 |
189 |
190 |
196 | { }
197 |
198 |
199 |
205 | { }
206 |
207 |
208 |
214 | { }
215 |
216 |
217 |
223 | { }
224 |
225 |
226 | >
227 | );
228 | }
229 | })()}
230 |
231 |
232 | {(() => {
233 | if (props.port) {
234 | return (
235 | <>
236 |
237 |
243 | { }
244 |
245 |
246 |
247 | >
248 | );
249 | }
250 | })()}
251 |
252 |
253 |
254 |
258 | }
261 | />
262 | }
265 | />
266 | }
269 | />
270 | }
273 | />
274 | }
277 | />
278 | }
281 | />
282 |
283 |
284 |
285 |
286 | );
287 | }
288 |
289 | export default connect(mapStateToProps, null)(Sidebar);
290 |
--------------------------------------------------------------------------------
/client/components/UnderConstruction.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 |
4 | const useStyles = makeStyles((theme) => ({
5 | root: {
6 | display: "flex",
7 | flexDirection: "column",
8 | justifyContent: "center",
9 | alignItems: "center",
10 | color: "rgba(0, 0, 0, 0.54)",
11 | fontSize: "42px",
12 | fontWeight: "bold",
13 | marginTop: "185px",
14 | "& .MuiTextField-root": {
15 | margin: theme.spacing(1),
16 | width: "35ch",
17 | },
18 | }
19 | }));
20 |
21 |
22 | export default function UnderConstruction() {
23 | const classes = useStyles();
24 |
25 | return (
26 |
29 | );
30 | };
31 |
32 |
33 |
--------------------------------------------------------------------------------
/client/components/charts/BarChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Bar } from "react-chartjs-2";
3 |
4 | const data = {
5 | labels: ["10:00", "11:00", "12:00", "13:00", "14:00", "15:00"],
6 | datasets: [
7 | {
8 | label: "# of Connections",
9 | data: [825, 400, 865, 740, 600, 890, 540],
10 | backgroundColor: [
11 | "rgba(255, 99, 132, 0.2)",
12 | "rgba(54, 162, 235, 0.2)",
13 | "rgba(255, 206, 86, 0.2)",
14 | "rgba(75, 192, 192, 0.2)",
15 | "rgba(153, 102, 255, 0.2)",
16 | "rgba(255, 159, 64, 0.2)",
17 | ],
18 | borderColor: [
19 | "black",
20 | "black",
21 | "black",
22 | "black",
23 | "black",
24 | "black",
25 | ],
26 | borderWidth: 1,
27 | },
28 | ]
29 | }
30 |
31 | const options = {
32 | maintainAspectRatio: false
33 | }
34 |
35 | const BarChart = (props) => {
36 | return (
37 | <>
38 | {props.metricName}
39 |
40 |
44 |
45 | >
46 | );
47 | }
48 |
49 | export default BarChart;
--------------------------------------------------------------------------------
/client/components/charts/LineChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Line } from "react-chartjs-2";
3 |
4 | const LineChart = (props) => {
5 | const data = {
6 | labels: props.x,
7 | datasets: [
8 | {
9 | label: props.label,
10 | data: props.y,
11 | fill: false,
12 | backgroundColor: "#018790",
13 | borderColor: "rgba(75, 192, 192, 0.2)",
14 | },
15 | ],
16 | };
17 |
18 | const options = {
19 | scales: {
20 | yAxes: [
21 | {
22 | ticks: {
23 | beginAtZero: true,
24 | },
25 | },
26 | ],
27 | },
28 | };
29 |
30 | return (
31 | <>
32 | {props.metricName}
33 |
34 |
35 |
36 | >
37 | );
38 | };
39 |
40 | export default LineChart;
41 |
--------------------------------------------------------------------------------
/client/components/charts/MultipleLineChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Line } from "react-chartjs-2";
3 |
4 | const MultipleLineChart = (props) => {
5 | const data = {
6 | labels: props.xtime,
7 | datasets: [
8 | {
9 | label: props.label[0],
10 | data: props.y50,
11 | fill: false,
12 | backgroundColor: "#018790",
13 | borderColor: "rgba(75, 192, 192, 0.2)",
14 | },
15 | {
16 | label: props.label[1],
17 | data: props.y75,
18 | fill: false,
19 | backgroundColor: "#db4c96",
20 | borderColor: "rgba(219, 76, 150, 0.2)",
21 | },
22 | {
23 | label: props.label[2],
24 | data: props.y95,
25 | fill: false,
26 | backgroundColor: "#4c61db",
27 | borderColor: "rgba(76, 97, 219, 0.2)",
28 | },
29 | {
30 | label: props.label[3],
31 | data: props.y99,
32 | fill: false,
33 | backgroundColor: "#f79a08",
34 | borderColor: "rgba(247, 154, 8, 0.2)",
35 | },
36 | ],
37 | };
38 |
39 | const options = {
40 | scales: {
41 | yAxes: [
42 | {
43 | ticks: {
44 | beginAtZero: true,
45 | },
46 | },
47 | ],
48 | },
49 | };
50 |
51 | return (
52 | <>
53 | {props.metricName}
54 |
55 |
56 |
57 | >
58 | );
59 | };
60 |
61 | export default MultipleLineChart;
62 |
--------------------------------------------------------------------------------
/client/components/charts/ScoreCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ScoreCard = (props) => (
4 | <>
5 | {props.metricName}
6 |
7 | {props.data}
8 |
9 | >
10 | );
11 |
12 | export default ScoreCard;
--------------------------------------------------------------------------------
/client/components/timeFunction.js:
--------------------------------------------------------------------------------
1 | export function timeFunction(connectionTIME) {
2 | let interval = 60;
3 | let timeDifference = new Date() - new Date(connectionTIME);
4 |
5 | //Less than 1 minute(60,000 ms)
6 | if (timeDifference < 60000) {
7 | //every 1 second
8 | return (interval = 1);
9 | }
10 | //More than 1 min, Less than 5 minutes
11 | if (timeDifference > 60000 && timeDifference < 300000) {
12 | //every 1 minute
13 | return (interval = 60);
14 | }
15 | //more than 1 hour but less than 2 hours
16 | if (timeDifference > 3600000 && timeDifference < 7200000) {
17 | //every 5 minutes= 300 seconds
18 | return (interval = 300);
19 | }
20 | //More than 2 hours
21 | if (timeDifference > 14400000) {
22 | //every 30 minutes = 1800 seconds
23 | return (interval = 1800);
24 | }
25 | return interval;
26 | }
27 |
--------------------------------------------------------------------------------
/client/images/monokl_white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import store from './store.js';
5 | import App from './App.jsx';
6 | import './styles.css';
7 |
8 | render(
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | );
14 |
--------------------------------------------------------------------------------
/client/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import mainReducers from './mainReducer.js';
3 |
4 | export default combineReducers({ mainReducer: mainReducers });
5 |
6 |
--------------------------------------------------------------------------------
/client/reducers/mainReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from "../actions/actionTypes.js";
2 |
3 | const initialState = {
4 | connectionTime: '',
5 | port: "",
6 | data: [],
7 | };
8 |
9 | const mainReducer = (state = initialState, action) => {
10 | switch (action.type) {
11 | case types.ADD_PORT:
12 | return {
13 | ...state,
14 | port: action.payload,
15 | };
16 | case types.REMOVE_PORT:
17 | return {
18 | ...state,
19 | port: action.payload,
20 | };
21 | case types.ADD_CONNECTION_TIME:
22 | return {
23 | ...state,
24 | connectionTime: action.payload,
25 | };
26 | default:
27 | return state;
28 | }
29 | };
30 |
31 | export default mainReducer;
32 |
33 |
--------------------------------------------------------------------------------
/client/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from "redux";
2 | import thunk from "redux-thunk";
3 | import reducers from "./reducers/index.js";
4 |
5 | const store = createStore(reducers, applyMiddleware(thunk));
6 |
7 | export default store;
8 |
9 |
--------------------------------------------------------------------------------
/client/styles.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/monokl/dc3c6b3c5f51a3e12d203e76674e1ca85e887f47/client/styles.css
--------------------------------------------------------------------------------
/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/monokl/dc3c6b3c5f51a3e12d203e76674e1ca85e887f47/icon/icon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Monokl
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require("electron");
2 | const path = require("path");
3 |
4 | function createWindow() {
5 | const win = new BrowserWindow({
6 | width: 1200,
7 | height: 800,
8 | backgroundColor: "white",
9 | webPreferences: {
10 | nodeIntegration: false,
11 | worldSafeExecuteJavaScript: true,
12 | contextIsolation: true,
13 | },
14 | });
15 | win.loadFile("index.html");
16 | }
17 | //requried for developer environment, comment this require fuction out before packaging in Electron
18 | require("electron-reload")(__dirname, {
19 | electron: path.join(__dirname, "node_modules", ".bin", "electron"),
20 | });
21 |
22 | app.whenReady().then(createWindow);
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monokl",
3 | "version": "1.0.0",
4 | "description": "Monitoring and visualization tool for Apache Kafka",
5 | "main": "main.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "electron .",
9 | "watch": "webpack --config webpack.config.js --watch",
10 | "pack": "electron-builder -mwl"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "@emotion/react": "^11.4.1",
17 | "@emotion/styled": "^11.3.0",
18 | "@material-ui/core": "^4.12.3",
19 | "@material-ui/icons": "^4.11.2",
20 | "@mui/icons-material": "^5.0.0",
21 | "@mui/material": "^5.0.0",
22 | "chart.js": "^3.5.1",
23 | "clsx": "^1.1.1",
24 | "react": "^17.0.2",
25 | "react-chartjs-2": "^3.0.4",
26 | "react-dom": "^17.0.2",
27 | "react-redux": "^7.2.5",
28 | "react-router-dom": "^5.3.0",
29 | "redux": "^4.1.1",
30 | "redux-thunk": "^2.3.0",
31 | "save": "^2.4.0"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.15.0",
35 | "@babel/plugin-transform-runtime": "^7.15.0",
36 | "@babel/preset-env": "^7.15.0",
37 | "@babel/preset-react": "^7.14.5",
38 | "@svgr/webpack": "^5.5.0",
39 | "babel-loader": "^8.2.2",
40 | "css-loader": "^6.2.0",
41 | "electron": "^15.0.0",
42 | "electron-builder": "^22.11.7",
43 | "electron-reload": "^2.0.0-alpha.1",
44 | "sass": "^1.38.2",
45 | "sass-loader": "^12.1.0",
46 | "style-loader": "^3.2.1",
47 | "webpack": "^5.51.1",
48 | "webpack-cli": "^4.8.0"
49 | },
50 | "build": {
51 | "appId": "com.oslabs.monokl",
52 | "productName": "monokl",
53 | "copyright": "wes tyler savitri",
54 | "directories": {
55 | "app": ".",
56 | "output": "dist",
57 | "buildResources": "icon"
58 | },
59 | "files": [
60 | "package.json",
61 | "**/*",
62 | "node_modules",
63 | "!README.md",
64 | "!.gitignore"
65 | ],
66 | "dmg": {
67 | "background": null,
68 | "backgroundColor": "#ffffff",
69 | "window": {
70 | "width": "400",
71 | "height": "300"
72 | },
73 | "contents": [
74 | {
75 | "x": 100,
76 | "y": 100
77 | },
78 | {
79 | "x": 300,
80 | "y": 100,
81 | "type": "link",
82 | "path": "/Applications"
83 | }
84 | ]
85 | },
86 | "mac": {
87 | "target": "dmg",
88 | "category": "public.app-category.utilities"
89 | },
90 | "win": {
91 | "target": "nsis"
92 | },
93 | "linux": {
94 | "target": "AppImage",
95 | "category": "Utility"
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | mode: "development",
5 | entry: "./client/index.js",
6 | devtool: "inline-source-map",
7 | target: "electron-renderer",
8 | module: {
9 | rules: [
10 | {
11 | test: /\.jsx?/,
12 | exclude: /node_modules/,
13 | use: {
14 | loader: "babel-loader",
15 | options: {
16 | presets: ["@babel/preset-react", "@babel/preset-env"],
17 | plugins: ["@babel/plugin-transform-runtime"]
18 | }, //end of options
19 | },
20 | },
21 | {
22 | test: [/\.s[ac]ss$/i, /\.css$/i],
23 | use: [
24 | // Creates `style` nodes from JS strings
25 | "style-loader",
26 | // Translates CSS into CommonJS
27 | "css-loader",
28 | // Compiles Sass to CSS
29 | "sass-loader",
30 | ],
31 | },
32 | {
33 | test: /\.svg$/,
34 | use: ['@svgr/webpack'],
35 | },
36 | ],
37 | },
38 | resolve: {
39 | extensions: [".js"],
40 | },
41 | output: {
42 | filename: "app.js",
43 | path: path.resolve(__dirname, "build"),
44 | },
45 | };
46 |
47 | // presets: [
48 | // [
49 | // "@babel/preset-env",
50 | // {
51 | // targets: {
52 | // esmodules: true,
53 | // },
54 | // },
55 | // ],
56 | // "@babel/preset-react",
57 | // ], //end of presets
58 |
--------------------------------------------------------------------------------