Building a Real-Time System Monitoring Dashboard with Python

C. C. Python Programming
9 min readOct 2, 2024

--

System Dashboard with Plotly in Python

Let’s create a dashboard using Python that monitors your system’s performance in real-time. The dashboard will display your PC’s RAM, CPU load and disk space usage.

We will use Dash for the web interface, Plotly for interactive graphs, psutil for system monitoring.

This will work on both Windows and Linux systems. However, for macOS users or special configurations, slight modifications may be needed.

Project Setup

Step 1: Create a Project Folder

Begin by creating a folder for the project. Open a terminal (or command prompt) and create a new directory.

mkdir system_monitor_dashboard
cd system_monitor_dashboard

Step 2: Set Up a Virtual Environment

Isolating your project dependencies is important. We’ll create a virtual environment to keep our environment clean and predictable.

For Windows:

python -m venv venv
venv\Scripts\activate

For Linux/Mac:

python3 -m venv venv
source venv/bin/activate

Step 3: Install Required Libraries

We need to install the following Python packages:

  • dash: Web framework for building dashboards.
  • plotly: Used to create graphs and charts.
  • pandas: For handling data in a tabular format.
  • psutil: For monitoring system resources.

Install them by running:

pip install dash plotly pandas psutil

This will install all the libraries required to build our dashboard.

Building the Dashboard

Step 4: Writing the Code

Now we’ll create a Python script that gathers system statistics and displays them using a dynamic, real-time dashboard.

  1. Monitor RAM, CPU, and Disk
    The psutil library provides cross-platform access to system monitoring information. It works on Windows, Linux, and macOS.

Create a new Python file called app.py:

touch app.py

Open app.py and paste the following code:

import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import plotly.graph_objs as go
import psutil
import logging
from collections import deque
from datetime import datetime
import sys

# Set up basic logging to debug
logging.basicConfig(level=logging.INFO)

# Initialize the Dash app
app = dash.Dash(__name__)

# Define fixed-size lists (deque) to store the last 20 data points for RAM, CPU, Disk usage, and time
history = {
'ram': deque(maxlen=20),
'cpu': deque(maxlen=20),
'disk': deque(maxlen=20),
'time': deque(maxlen=20) # Store timestamps for x-axis
}

# Function to get system statistics (RAM, CPU, and Disk)
def get_system_stats():
try:
# Get memory stats
memory = psutil.virtual_memory()
ram = memory.percent

# Get CPU usage
cpu = psutil.cpu_percent(interval=1)

# Get Disk usage
disk = psutil.disk_usage('/').percent

# Return RAM, CPU, and Disk data
return {
'RAM Usage (%)': ram,
'CPU Usage (%)': cpu,
'Disk Usage (%)': disk
}
except Exception as e:
logging.error(f"Error fetching system stats: {e}")
return {}

# Determine whether to run in 'one' or 'multiple' mode based on command-line argument
mode = sys.argv[1] if len(sys.argv) > 1 else 'multiple'

if mode == 'one':
app.layout = html.Div([
html.H1('System Monitoring Dashboard (Combined Graph)'),

# Combined Line Chart for RAM, CPU, and Disk
dcc.Graph(id='combined-graph'),

# Interval for updating the dashboard every 5 seconds
dcc.Interval(
id='interval-component',
interval=5*1000, # 5000 milliseconds (5 seconds)
n_intervals=0
)
])

# Update callback to refresh the combined RAM, CPU, and Disk usage graph every interval
@app.callback(
Output('combined-graph', 'figure'),
[Input('interval-component', 'n_intervals')]
)
def update_combined_graph(n):
# Fetch system stats (RAM, CPU, and Disk)
data = get_system_stats()

if not data:
logging.info("No data fetched")
return {}

# Log fetched data in the terminal
logging.info(f"Fetched data: {data}")

# Append the current time, RAM, CPU, and Disk usage to history
current_time = datetime.now().strftime('%H:%M:%S') # Get the current time as a string
history['ram'].append(data['RAM Usage (%)'])
history['cpu'].append(data['CPU Usage (%)'])
history['disk'].append(data['Disk Usage (%)'])
history['time'].append(current_time)

# Create Combined Line Chart
combined_figure = {
'data': [
go.Scatter(
x=list(history['time']),
y=list(history['ram']),
mode='lines+markers',
name='RAM Usage (%)'
),
go.Scatter(
x=list(history['time']),
y=list(history['cpu']),
mode='lines+markers',
name='CPU Usage (%)'
),
go.Scatter(
x=list(history['time']),
y=list(history['disk']),
mode='lines+markers',
name='Disk Usage (%)'
)
],
'layout': go.Layout(
title='RAM, CPU, and Disk Usage Over Time',
xaxis=dict(title='Time', tickformat='%H:%M:%S'), # Format the time
yaxis=dict(title='Percentage'),
)
}

return combined_figure

else:
# Layout for multiple graphs (RAM, CPU, Disk each on its own graph)
app.layout = html.Div([
html.H1('System Monitoring Dashboard (Separate Graphs)'),

# RAM Usage Line Chart
dcc.Graph(id='ram-usage-graph'),

# CPU Usage Line Chart
dcc.Graph(id='cpu-usage-graph'),

# Disk Usage Line Chart
dcc.Graph(id='disk-usage-graph'),

# Interval for updating the dashboard every 5 seconds
dcc.Interval(
id='interval-component',
interval=5*1000, # 5000 milliseconds (5 seconds)
n_intervals=0
)
])

# Update callback to refresh the RAM, CPU, and Disk usage graphs every interval
@app.callback(
[Output('ram-usage-graph', 'figure'),
Output('cpu-usage-graph', 'figure'),
Output('disk-usage-graph', 'figure')],
[Input('interval-component', 'n_intervals')]
)
def update_separate_graphs(n):
# Fetch system stats (RAM, CPU, and Disk)
data = get_system_stats()

if not data:
logging.info("No data fetched")
return {}, {}, {}

# Log fetched data in the terminal
logging.info(f"Fetched data: {data}")

# Append the current time, RAM, CPU, and Disk usage to history
current_time = datetime.now().strftime('%H:%M:%S') # Get the current time as a string
history['ram'].append(data['RAM Usage (%)'])
history['cpu'].append(data['CPU Usage (%)'])
history['disk'].append(data['Disk Usage (%)'])
history['time'].append(current_time)

# Create RAM Usage Line Chart
ram_figure = {
'data': [go.Scatter(
x=list(history['time']),
y=list(history['ram']),
mode='lines+markers',
name='RAM Usage (%)'
)],
'layout': go.Layout(
title='RAM Usage Over Time',
xaxis=dict(title='Time', tickformat='%H:%M:%S'), # Format the time
yaxis=dict(title='Percentage'),
)
}

# Create CPU Usage Line Chart
cpu_figure = {
'data': [go.Scatter(
x=list(history['time']),
y=list(history['cpu']),
mode='lines+markers',
name='CPU Usage (%)'
)],
'layout': go.Layout(
title='CPU Usage Over Time',
xaxis=dict(title='Time', tickformat='%H:%M:%S'), # Format the time
yaxis=dict(title='Percentage'),
)
}

# Create Disk Usage Line Chart
disk_figure = {
'data': [go.Scatter(
x=list(history['time']),
y=list(history['disk']),
mode='lines+markers',
name='Disk Usage (%)'
)],
'layout': go.Layout(
title='Disk Usage Over Time',
xaxis=dict(title='Time', tickformat='%H:%M:%S'), # Format the time
yaxis=dict(title='Percentage'),
)
}

return ram_figure, cpu_figure, disk_figure

# Run the app
if __name__ == '__main__':
app.run_server(debug=True)

Step 5: Running the Dashboard

  1. Ensure your virtual environment is activated.
  2. Run the dashboard with the following command to show one chart with all three metrics:
python app.py one

3. Run the dashboard with the following command to show three charts each with one metric:

python app.py multiple

This will start the application on a local web server. Open your browser and visit:

http://127.0.0.1:8050/

You’ll see a dashboard that updates every 5 seconds, showing:

  • RAM Usage in percentage
  • CPU Usage in percentage
  • Disk Usage in percentage

Step 6: Additional Notes

  • psutil works cross-platform, so the code will run on both Windows and Linux without modification.
  • If you’re on a macOS system, all functions except disk usage should work. You might need to adjust psutil.disk_usage('/') if you have a different filesystem configuration.

Breakdown of Above Code:

1. Setting Up the Logging System

The logging configuration is initialized early in the script, allowing us to capture and print important debugging information to the console.# Set up basic logging to debug

# Set up basic logging to debug
logging.basicConfig(level=logging.INFO)

This line configures the logging system to show messages at the INFO level or higher, providing useful feedback during the program's execution.

2. Initializing the Dash Application

Dash is a powerful Python framework for building interactive web applications. The app is initialized as follows:

# Initialize the Dash app
app = dash.Dash(__name__)

This creates a Dash application, which will serve as the basis for the real-time dashboard. The __name__ parameter helps Dash locate resources correctly.

3. Setting Up History with Deques

Deques (double-ended queues) are used to store the last 20 data points for RAM, CPU, Disk usage, and timestamps.

# Define fixed-size lists (deque) to store the last 20 data points for RAM, CPU, Disk usage, and time
history = {
'ram': deque(maxlen=20),
'cpu': deque(maxlen=20),
'disk': deque(maxlen=20),
'time': deque(maxlen=20) # Store timestamps for x-axis
}

The maxlen=20 ensures that only the last 20 values are kept in memory, and older values are automatically removed. This is particularly useful for real-time graphs, which only need to show a limited number of recent data points.

4. Fetching System Statistics

We use the psutil library to gather system data such as RAM, CPU, and disk usage. The get_system_stats function is responsible for this:

def get_system_stats():
try:
# Get memory stats
memory = psutil.virtual_memory()
ram = memory.percent

# Get CPU usage
cpu = psutil.cpu_percent(interval=1)

# Get Disk usage
disk = psutil.disk_usage('/').percent

# Return RAM, CPU, and Disk data
return {
'RAM Usage (%)': ram,
'CPU Usage (%)': cpu,
'Disk Usage (%)': disk
}
except Exception as e:
logging.error(f"Error fetching system stats: {e}")
return {}

RAM Usage: We call psutil.virtual_memory() to get memory information and extract the percentage of RAM in use.

CPU Usage: The psutil.cpu_percent() function returns the current CPU usage. The interval=1 argument tells the function to calculate CPU usage over a 1-second period.

Disk Usage: We use psutil.disk_usage('/') to get the disk usage percentage for the root directory (/).

This function gathers all this data and returns it as a dictionary. If an error occurs, it logs the error and returns an empty dictionary.

5. Mode Selection: Single or Multiple Graphs

The dashboard can operate in two modes: either all data is combined in a single graph, or each metric (RAM, CPU, and Disk) has its own graph. This is determined by checking the command-line arguments:

# Determine whether to run in 'one' or 'multiple' mode based on command-line argument
mode = sys.argv[1] if len(sys.argv) > 1 else 'multiple'

If the argument one is provided when the script is run, the application will combine all the data into a single graph. If no argument or multiple is provided, separate graphs will be displayed.

6. Creating the Layout and Callbacks for Combined Graph Mode

When running in the one mode, the layout contains a single graph, and the data is refreshed every 5 seconds:

if mode == 'one':
app.layout = html.Div([
html.H1('System Monitoring Dashboard (Combined Graph)'),

# Combined Line Chart for RAM, CPU, and Disk
dcc.Graph(id='combined-graph'),

# Interval for updating the dashboard every 5 seconds
dcc.Interval(
id='interval-component',
interval=5*1000, # 5000 milliseconds (5 seconds)
n_intervals=0
)
])

This layout includes a title (html.H1) and a graph component (dcc.Graph). The dcc.Interval component is used to refresh the data every 5 seconds (5000 milliseconds).

The callback function below updates the combined graph by fetching the latest system stats and adding them to the history deques:

@app.callback(
Output('combined-graph', 'figure'),
[Input('interval-component', 'n_intervals')]
)
def update_combined_graph(n):
# Fetch system stats (RAM, CPU, and Disk)
data = get_system_stats()

if not data:
logging.info("No data fetched")
return {}

# Log fetched data in the terminal
logging.info(f"Fetched data: {data}")

# Append the current time, RAM, CPU, and Disk usage to history
current_time = datetime.now().strftime('%H:%M:%S') # Get the current time as a string
history['ram'].append(data['RAM Usage (%)'])
history['cpu'].append(data['CPU Usage (%)'])
history['disk'].append(data['Disk Usage (%)'])
history['time'].append(current_time)

# Create Combined Line Chart
combined_figure = {
'data': [
go.Scatter(
x=list(history['time']),
y=list(history['ram']),
mode='lines+markers',
name='RAM Usage (%)'
),
go.Scatter(
x=list(history['time']),
y=list(history['cpu']),
mode='lines+markers',
name='CPU Usage (%)'
),
go.Scatter(
x=list(history['time']),
y=list(history['disk']),
mode='lines+markers',
name='Disk Usage (%)'
)
],
'layout': go.Layout(
title='RAM, CPU, and Disk Usage Over Time',
xaxis=dict(title='Time', tickformat='%H:%M:%S'), # Format the time
yaxis=dict(title='Percentage'),
)
}

return combined_figure

This callback fetches the data every 5 seconds, appends it to the history deque, and then creates a combined line chart to display the three metrics over time.

7. Creating the Layout and Callbacks for Multiple Graphs Mode

In multiple mode, the layout consists of three separate graphs (one for each metric):

else:
app.layout = html.Div([
html.H1('System Monitoring Dashboard (Separate Graphs)'),

# RAM Usage Line Chart
dcc.Graph(id='ram-usage-graph'),

# CPU Usage Line Chart
dcc.Graph(id='cpu-usage-graph'),

# Disk Usage Line Chart
dcc.Graph(id='disk-usage-graph'),

# Interval for updating the dashboard every 5 seconds
dcc.Interval(
id='interval-component',
interval=5*1000, # 5000 milliseconds (5 seconds)
n_intervals=0
)
])

The callback in this mode updates each graph individually:

@app.callback(
[Output('ram-usage-graph', 'figure'),
Output('cpu-usage-graph', 'figure'),
Output('disk-usage-graph', 'figure')],
[Input('interval-component', 'n_intervals')]
)
def update_separate_graphs(n):
# Fetch system stats (RAM, CPU, and Disk)
data = get_system_stats()

if not data:
logging.info("No data fetched")
return {}, {}, {}

# Append the current time, RAM, CPU, and Disk usage to history
current_time = datetime.now().strftime('%H:%M:%S')
history['ram'].append(data['RAM Usage (%)'])
history['cpu'].append(data['CPU Usage (%)'])
history['disk'].append(data['Disk Usage (%)'])
history['time'].append(current_time)

# Create RAM, CPU, and Disk Usage Line Charts
ram_figure = {
'data': [go.Scatter(
x=list(history['time']),
y=list(history['ram']),
mode='lines+markers',
name='RAM Usage (%)'
)],
'layout': go.Layout(title='RAM Usage Over Time', xaxis=dict(title='Time'), yaxis=dict(title='Percentage'))
}

cpu_figure = {
'data': [go.Scatter(
x=list(history['time']),
y=list(history['cpu']),
mode='lines+markers',
name='CPU Usage (%)'
)],
'layout': go.Layout(title='CPU Usage Over Time', xaxis=dict(title='Time'), yaxis=dict(title='Percentage'))
}

disk_figure = {
'data': [go.Scatter(
x=list(history['time']),
y=list(history['disk']),
mode='lines+markers',
name='Disk Usage (%)'
)],
'layout': go.Layout(title='Disk Usage Over Time', xaxis=dict(title='Time'), yaxis=dict(title='Percentage'))
}

return ram_figure, cpu_figure, disk_figure

Each graph displays its own data set, pulled from the history deque and updated every 5 seconds.

8. Running the App

Finally, the Dash app is started using the app.run_server() function:

if __name__ == '__main__':
app.run_server(debug=True)

Running the app with python app.py one will display a combined graph, while python app.py multiple will display separate graphs for each metric.

Thank you for reading this article. I hope you found it helpful and informative. If you have any questions, or if you would like to suggest new Python code examples or topics for future tutorials, please feel free to reach out. Your feedback and suggestions are always welcome!

Happy coding!
C. C. Python Programming

--

--

C. C. Python Programming

Python programmer focused on data transfer between systems. Major strength: Python interaction with legacy programming/management interfaces (console windows).