Skip to content

Commit

Permalink
Interactive Debug Bar: Toggle-able interface providing real-time perf…
Browse files Browse the repository at this point in the history
…ormance metrics and MQTT status
  • Loading branch information
terdia committed Aug 24, 2024
1 parent 01250a5 commit de2724a
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 11 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
.env
.env
__pycache__

static/mqttui-logo-svg.svg
static/mqttui-logo.png

.DS_Store
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [1.1.0] - 2024-08-24
### Added
- Debug Bar feature for enhanced developer insights
- Real-time websocket connection/disconnect status
- MQTT connection status and last message details
- Request duration tracking
- Toggle functionality to show/hide the Debug Bar

## [1.0.0] - 2024-08-19
### Added
- Initial release of MQTT Web Interface
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ MQTT Web Interface is an open-source web application that provides a real-time v

![Application Screenshot](static/screenshot.png)

![Debug Screenshot](static/screenshot_1.png)

## Features

- Real-time visualization of MQTT topic hierarchy and message flow
- Ability to publish messages to MQTT topics
- Display of message statistics (connection count, topic count, message count)
- Interactive network graph showing topic relationships
- Dockerized for easy deployment
- Debug Bar

## Installation

Expand Down
89 changes: 80 additions & 9 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
__version__ = "1.0.0"

from flask import Flask, render_template, request, jsonify, send_from_directory
from flask_socketio import SocketIO
from flask_socketio import SocketIO, emit
import paho.mqtt.client as mqtt
from datetime import datetime
import os
from debug_bar import debug_bar, debug_bar_middleware
import logging

app = Flask(__name__, static_url_path='/static')
socketio = SocketIO(app)
socketio = SocketIO(app, async_mode='threading')

logging.basicConfig(level=logging.DEBUG)

app.before_request(debug_bar_middleware)

@app.after_request
def after_request(response):
debug_bar.record('request', 'status_code', response.status_code)
debug_bar.end_request()
return response

# MQTT setup
mqtt_client = mqtt.Client()
Expand All @@ -19,17 +31,47 @@
messages = []
topics = set()
connection_count = 0
active_websockets = 0
error_log = []

@socketio.on('connect')
def handle_connect():
global active_websockets
active_websockets += 1
debug_bar.record('performance', 'active_websockets', active_websockets)
logging.info(f"WebSocket connected. Total active: {active_websockets}")

@socketio.on('disconnect')
def handle_disconnect():
global active_websockets
active_websockets -= 1
debug_bar.record('performance', 'active_websockets', active_websockets)
logging.info(f"WebSocket disconnected. Total active: {active_websockets}")

def on_connect(client, userdata, flags, rc):
global connection_count
connection_status = 'Connected' if rc == 0 else f'Failed (rc: {rc})'
debug_bar.record('mqtt', 'connection_status', connection_status)
debug_bar.record('mqtt', 'broker', mqtt_broker)
debug_bar.record('mqtt', 'port', mqtt_port)
debug_bar.record('mqtt', 'username', mqtt_username if mqtt_username else 'Not set')
debug_bar.record('mqtt', 'password', 'Set' if mqtt_password else 'Not set')

if rc == 0:
print("Connected to MQTT broker")
connection_count += 1
client.subscribe("#") # Subscribe to all topics
logging.info(f"Connected to MQTT broker. Total connections: {connection_count}")
else:
print(f"Failed to connect to MQTT broker with code {rc}")
error_log.append(f"Failed to connect to MQTT broker with code {rc}")
error_message = f"Connection failed with code {rc}"
error_log.append(error_message)
debug_bar.record('mqtt', 'last_error', error_message)
logging.error(error_message)

def on_disconnect(client, userdata, rc):
disconnect_reason = 'Clean disconnect' if rc == 0 else f'Unexpected disconnect (rc: {rc})'
debug_bar.record('mqtt', 'last_disconnect', disconnect_reason)
error_log.append(f"Disconnected: {disconnect_reason}")
logging.warning(f"Disconnected from MQTT broker: {disconnect_reason}")

def on_message(client, userdata, msg):
message = {
Expand All @@ -42,9 +84,12 @@ def on_message(client, userdata, msg):
if len(messages) > 100:
messages.pop(0)
socketio.emit('mqtt_message', message)
debug_bar.record('mqtt', 'last_message', message)
logging.debug(f"MQTT message received: {message}")

mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
mqtt_client.on_disconnect = on_disconnect

@app.route('/')
def index():
Expand All @@ -55,6 +100,7 @@ def publish_message():
topic = request.form['topic']
message = request.form['message']
mqtt_client.publish(topic, message)
debug_bar.record('mqtt', 'last_publish', {'topic': topic, 'message': message})
return jsonify(success=True)

@app.route('/stats')
Expand All @@ -70,6 +116,30 @@ def get_stats():
def send_static(path):
return send_from_directory('static', path)

@app.route('/debug-bar')
def get_debug_bar_data():
try:
data = debug_bar.get_data()
return jsonify(data)
except Exception as e:
logging.error(f"Error fetching debug bar data: {e}")
return jsonify({"error": "Failed to fetch debug bar data"}), 500

@app.route('/toggle-debug-bar', methods=['POST'])
def toggle_debug_bar():
if debug_bar.enabled:
debug_bar.disable()
else:
debug_bar.enable()
return jsonify(enabled=debug_bar.enabled)

@app.route('/record-client-performance', methods=['POST'])
def record_client_performance():
data = request.json
debug_bar.record('performance', 'page_load_time', f"{data['pageLoadTime']}ms")
debug_bar.record('performance', 'dom_ready_time', f"{data['domReadyTime']}ms")
return jsonify(success=True)

@app.route('/version')
def get_version():
return jsonify({'version': __version__})
Expand All @@ -81,8 +151,9 @@ def get_version():
mqtt_client.connect(mqtt_broker, mqtt_port, 60)
mqtt_client.loop_start()
except Exception as e:
print(f"Failed to connect to MQTT broker: {str(e)}")
error_log.append(f"Failed to connect to MQTT broker: {str(e)}")
error_message = f"Failed to connect to MQTT broker: {str(e)}"
debug_bar.record('mqtt', 'connection_error', error_message)
error_log.append(error_message)
logging.error(error_message)

print(f"Starting MQTT Web Interface v{__version__}")
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
socketio.run(app, host='0.0.0.0', port=5000, debug=True, use_reloader=False)
70 changes: 70 additions & 0 deletions debug_bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import time
import psutil
from flask import request
from threading import Lock
import logging

class DebugBarPanel:
def __init__(self, name):
self.name = name
self.data = {}

def record(self, key, value):
self.data[key] = value

def get_data(self):
return self.data

class DebugBar:
def __init__(self):
self.panels = {}
self.enabled = False
self.start_time = None
self.lock = Lock()
try:
self.process = psutil.Process()
except Exception as e:
logging.error(f"Failed to initialize psutil Process: {e}")
self.process = None

def add_panel(self, name):
with self.lock:
if name not in self.panels:
self.panels[name] = DebugBarPanel(name)

def record(self, panel, key, value):
with self.lock:
if panel in self.panels:
self.panels[panel].record(key, value)

def start_request(self):
self.start_time = time.time()

def end_request(self):
if self.start_time:
duration = time.time() - self.start_time
self.record('request', 'duration', f"{duration:.2f}s")
self.start_time = None

def get_data(self):
with self.lock:
#self.update_performance_metrics()
return {name: panel.get_data() for name, panel in self.panels.items()}

def enable(self):
self.enabled = True

def disable(self):
self.enabled = False

debug_bar = DebugBar()

# Initialize default panels
debug_bar.add_panel('mqtt')
debug_bar.add_panel('request')
debug_bar.add_panel('performance')

def debug_bar_middleware():
debug_bar.start_request()
debug_bar.record('request', 'path', request.path)
debug_bar.record('request', 'method', request.method)
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Flask==2.0.1
Flask-SocketIO==5.1.1
paho-mqtt==1.5.1
Werkzeug==2.0.1
Werkzeug==2.0.1
psutil==5.9.0
Binary file added static/screenshot_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,91 @@ document.getElementById('topic-filter').addEventListener('change', function(e) {
document.getElementById('message-list').innerHTML = '';
});



let debugBar;
let debugBarToggle;

function initDebugBar() {
debugBar = document.createElement('div');
debugBar.id = 'debug-bar';
debugBar.style.display = 'none';
document.body.appendChild(debugBar);

debugBarToggle = document.createElement('button');
debugBarToggle.id = 'debug-bar-toggle';
debugBarToggle.innerHTML = '🐞 Debug';
debugBarToggle.onclick = toggleDebugBar;
document.body.appendChild(debugBarToggle);

const closeButton = document.createElement('button');
closeButton.id = 'debug-bar-close';
closeButton.innerHTML = '×';
closeButton.onclick = closeDebugBar;
debugBar.appendChild(closeButton);

updateDebugBar();
setInterval(updateDebugBar, 1000); // Update every second
}

function toggleDebugBar() {
fetch('/toggle-debug-bar', { method: 'POST' })
.then(response => response.json())
.then(data => {
debugBar.style.display = data.enabled ? 'block' : 'none';
debugBarToggle.classList.toggle('active', data.enabled);
});
}

function closeDebugBar() {
debugBar.style.display = 'none';
fetch('/toggle-debug-bar', { method: 'POST' });
debugBarToggle.classList.remove('active');
}

function trackClientPerformance() {
const perfData = window.performance.timing;
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
const domReadyTime = perfData.domContentLoadedEventEnd - perfData.navigationStart;

fetch('/record-client-performance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pageLoadTime,
domReadyTime,
}),
});
}

function updateDebugBar() {
fetch('/debug-bar')
.then(response => response.json())
.then(data => {
let content = '<div class="debug-content">';
for (const [panelName, panelData] of Object.entries(data)) {
content += `<div class="debug-panel"><h3>${panelName}</h3><ul>`;
for (const [key, value] of Object.entries(panelData)) {
let displayValue = value;
if (typeof value === 'object' && value !== null) {
displayValue = '<pre>' + JSON.stringify(value, null, 2) + '</pre>';
}
content += `<li><strong>${key}:</strong> ${displayValue}</li>`;
}
content += '</ul></div>';
}
content += '</div>';
debugBar.innerHTML = content;
debugBar.appendChild(document.getElementById('debug-bar-close'));
});
}
document.addEventListener('DOMContentLoaded', function() {
initChart();
initNetwork();
setInterval(updateChart, 1000);
setInterval(updateStats, 5000);
initDebugBar();
trackClientPerformance();
});
Loading

0 comments on commit de2724a

Please sign in to comment.