Skip to content

Add MQTT connectivity diagnostics and startup logging#126

Merged
DaTiC0 merged 7 commits into
mainfrom
feat/mqtt-connectivity
May 4, 2026
Merged

Add MQTT connectivity diagnostics and startup logging#126
DaTiC0 merged 7 commits into
mainfrom
feat/mqtt-connectivity

Conversation

@DaTiC0

@DaTiC0 DaTiC0 commented May 4, 2026

Copy link
Copy Markdown
Owner

This PR adds MQTT connectivity diagnostics for the HiveMQ broker setup. It logs broker initialization details and registers MQTT connect/disconnect event handlers so connection state is visible in application logs.

Summary by Sourcery

Add in-memory MQTT event monitoring, expose broker connection and TLS status in the MQTT monitor view, and log broker initialization and connection lifecycle events.

New Features:

  • Expose recent MQTT events via an in-memory log buffer accessible from the MQTT monitor route.
  • Display MQTT broker connection and TLS security status in the MQTT monitor template.

Enhancements:

  • Log MQTT broker initialization details and connection lifecycle events at application startup.
  • Simplify MQTT topic-specific handlers and centralize message logging.
  • Render MQTT monitor entries dynamically instead of using static sample rows.

Tests:

  • Add unit tests covering MQTT notification handlers, log buffering behavior, and the MQTT monitor route view model.

Co-authored-by: Copilot <copilot@github.com>
@sourcery-ai

sourcery-ai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds in-memory MQTT connectivity diagnostics and logging, wires them into the /mqtt view and template, and logs broker initialization and lifecycle events, with unit tests for the new behavior.

Sequence diagram for MQTT connection lifecycle logging

sequenceDiagram
    participant AppModule as AppModule
    participant FlaskMqtt as FlaskMqtt
    participant Broker as Broker
    participant NotificationsModule as NotificationsModule
    participant Logger as Logger

    AppModule->>Logger: info Initializing MQTT broker at host:port (TLS=flag)
    AppModule->>FlaskMqtt: init_app(app)
    AppModule->>FlaskMqtt: subscribe(+/notification)
    AppModule->>FlaskMqtt: subscribe(+/status)
    AppModule->>Logger: info MQTT initialized, lifecycle reported by callbacks

    Broker-->>FlaskMqtt: TCP connection established
    FlaskMqtt-->>NotificationsModule: on_connect(client, userdata, flags, rc)
    alt rc == 0
        NotificationsModule->>Logger: info Connected to MQTT broker, flags, rc
        NotificationsModule->>NotificationsModule: _append_mqtt_log(system, Connected, Connected)
    else rc != 0
        NotificationsModule->>Logger: error MQTT connection failed, flags, rc
        NotificationsModule->>NotificationsModule: _append_mqtt_log(system, Connection failed, Connection failed)
    end

    Broker-->>FlaskMqtt: Disconnect
    FlaskMqtt-->>NotificationsModule: on_disconnect(client, userdata, rc)
    alt rc == 0
        NotificationsModule->>Logger: info MQTT broker disconnected cleanly, rc
        NotificationsModule->>NotificationsModule: _append_mqtt_log(system, Disconnected cleanly, Clean disconnect)
    else rc != 0
        NotificationsModule->>Logger: warning MQTT broker disconnected unexpectedly, rc
        NotificationsModule->>NotificationsModule: _append_mqtt_log(system, Unexpected disconnect, Disconnected)
    end

    Broker-->>FlaskMqtt: PUBLISH topic payload
    FlaskMqtt-->>NotificationsModule: on_message(client, userdata, message)
    NotificationsModule->>NotificationsModule: _decode_payload(message.payload)
    NotificationsModule->>Logger: debug Received message topic, payload
    NotificationsModule->>NotificationsModule: _append_mqtt_log(topic, payload, Received)
Loading

Class diagram for MQTT diagnostics and logging utilities

classDiagram
    class NotificationsModule {
        +Mqtt mqtt
        +threading.Lock mqtt_log_lock
        +deque mqtt_log_entries
        +_append_mqtt_log(topic, payload, status)
        +get_mqtt_logs()
        +_decode_payload(payload)
        +is_mqtt_connected()
        +handle_connect(client, userdata, flags, rc)
        +handle_disconnect(client, userdata, rc)
        +handle_messages(client, userdata, message)
        +handle_publish(client, userdata, mid)
        +handle_subscribe(client, userdata, mid, granted_qos)
    }

    class RoutesModule {
        +mqtt_log()
    }

    class AppModule {
        +mqtt_init_block()
    }

    class FlaskMqtt {
        +connected
        +init_app(app)
        +subscribe(topic)
        +on_connect()
        +on_disconnect()
        +on_message()
        +on_publish()
        +on_subscribe()
    }

    class MqttLogTemplate {
        +broker_connected
        +tls_enabled
        +log_entries
    }

    NotificationsModule "1" --> "1" FlaskMqtt : wraps
    RoutesModule --> NotificationsModule : uses get_mqtt_logs
    RoutesModule --> NotificationsModule : uses is_mqtt_connected
    RoutesModule --> MqttLogTemplate : renders
    AppModule --> NotificationsModule : uses mqtt
    AppModule --> FlaskMqtt : init_app, subscribe
Loading

File-Level Changes

Change Details Files
Introduce thread-safe in-memory MQTT event log and richer message handling in notifications module.
  • Add a locked deque buffer for last 100 MQTT events and helper to append and snapshot entries
  • Implement UTF-8-or-hex payload decoding for MQTT messages with logging for non-UTF8 payloads
  • Extend on_message handler to log decoded payloads into the buffer with status metadata
  • Add on_connect and on_disconnect handlers that log connection lifecycle events and record them in the buffer
  • Remove unused topic-specific handlers and rely on generic message handling
notifications.py
Render MQTT monitor page from real broker state and in-memory log entries.
  • Update /mqtt route to compute broker status and annotate log entries with CSS status classes
  • Pass broker connectivity, TLS enablement, and log entries to the mqtt_log template
  • Compute positive vs non-positive statuses to drive pill styling
routes.py
Make MQTT monitor template dynamic based on broker connectivity, TLS config, and log entries.
  • Bind Live/Offline feed pill class and label to broker_connected flag
  • Bind broker status label to connection status and security label to TLS enablement
  • Iterate over provided log_entries, rendering timestamp, topic, payload, and a status pill per entry, with an empty-state row when no entries exist
templates/mqtt_log.html
Add startup-time logging around MQTT broker initialization.
  • Log MQTT broker URL, port, and TLS settings before initializing MQTT
  • Log that MQTT initialization completed and that lifecycle will be reported by MQTT callbacks
app.py
Add unit tests for MQTT notification handlers and /mqtt route view model.
  • Test that connect/disconnect handlers record appropriate status entries
  • Test that binary MQTT payloads are stored as hex and that the log buffer size is capped to 100 entries with newest-first ordering
  • Test that /mqtt route renders with expected context, including broker_connected, tls_enabled boolean, and computed status_class on log entries
tests/test_notifications.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves MQTT observability by adding detailed initialization logs in app.py and implementing connection and disconnection handlers in notifications.py. The review feedback suggests refining these handlers to correctly interpret return codes, specifically by logging connection failures as errors and distinguishing between intentional and unexpected disconnections to reduce log noise.

Comment thread notifications.py Outdated
Comment thread notifications.py Outdated

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Consider differentiating between clean and error disconnects in handle_disconnect (e.g., log at info for rc == 0 and warning otherwise) so that normal shutdowns don’t generate warning noise in the logs.
  • The logger.info('MQTT initialized; connected=%s', mqtt.connected) call immediately after mqtt.init_app(app) may be misleading because the connection state is usually established asynchronously; you might want to rely on the on_connect/on_disconnect callbacks for accurate connection status instead of the immediate post-init value.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider differentiating between clean and error disconnects in `handle_disconnect` (e.g., log at `info` for `rc == 0` and `warning` otherwise) so that normal shutdowns don’t generate warning noise in the logs.
- The `logger.info('MQTT initialized; connected=%s', mqtt.connected)` call immediately after `mqtt.init_app(app)` may be misleading because the connection state is usually established asynchronously; you might want to rely on the `on_connect`/`on_disconnect` callbacks for accurate connection status instead of the immediate post-init value.

## Individual Comments

### Comment 1
<location path="notifications.py" line_range="18-20" />
<code_context>
+    logger.info('Connected to MQTT broker; flags=%s, rc=%s', flags, rc)
+
+
+@mqtt.on_disconnect()
+def handle_disconnect(_client, _userdata, rc):
+    logger.warning('Disconnected from MQTT broker; rc=%s', rc)
+
+
</code_context>
<issue_to_address>
**suggestion:** Consider differentiating between clean and unclean disconnects in logging

All disconnects are currently logged as warnings regardless of `rc`. Since `rc == 0` typically means a clean, expected disconnect, consider logging those at `INFO` and reserving `WARNING` for non‑zero `rc` to reduce noise and highlight actual issues.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread notifications.py Outdated
@DaTiC0

DaTiC0 commented May 4, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The status strings (e.g. 'Connected', 'Connection failed', 'Clean disconnect') and the positive_statuses mapping are duplicated across notifications.py and routes.mqtt_log; consider centralizing these in a shared constant or enum so status handling and styling stay consistent as new cases are added.
  • The _decode_payload helper logs a warning on every non-UTF8 payload; if binary payloads are expected, consider downgrading this to debug or adding some form of rate limiting to avoid noisy logs in high-throughput scenarios.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The status strings (e.g. 'Connected', 'Connection failed', 'Clean disconnect') and the `positive_statuses` mapping are duplicated across `notifications.py` and `routes.mqtt_log`; consider centralizing these in a shared constant or enum so status handling and styling stay consistent as new cases are added.
- The `_decode_payload` helper logs a warning on every non-UTF8 payload; if binary payloads are expected, consider downgrading this to debug or adding some form of rate limiting to avoid noisy logs in high-throughput scenarios.

## Individual Comments

### Comment 1
<location path="routes.py" line_range="168-172" />
<code_context>
 def mqtt_log():
-    return render_template('mqtt_log.html')
+    connected = is_mqtt_connected()
+    positive_statuses = {'Received', 'Connected', 'Clean disconnect'}
+    log_entries = [
+        {
+            **entry,
+            'status_class': 'status-pill--active' if entry.get('status') in positive_statuses else '',
+        }
+        for entry in get_mqtt_logs()
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid duplicating status strings in multiple places by centralizing them as constants or deriving them from a single source.

These status labels are now defined both in the MQTT callbacks (e.g. `'Connected'`, `'Clean disconnect'`) and here for `status_class`, so changes in one place can silently desync the mapping. Please either define these statuses (or a status→class mapping) once in `notifications.py` and reuse it here, or compute `status_class` inside `_append_mqtt_log` to keep the logic centralized.

Suggested implementation:

```python
def mqtt_log():
    connected = is_mqtt_connected()
    log_entries = get_mqtt_logs()
    return render_template(
        'mqtt_log.html',
        broker_connected=connected,
        tls_enabled=current_app.config.get('MQTT_TLS_ENABLED', False),
        log_entries=log_entries,
    )

```

To fully implement the suggestion and avoid duplicated status strings:

1. In `notifications.py`, within `_append_mqtt_log` (or the equivalent function that creates MQTT log entries), compute and attach `status_class` there, e.g.:

   - Define a single shared mapping or set for statuses:
     - `POSITIVE_STATUSES = {'Received', 'Connected', 'Clean disconnect'}` **or**
     - `STATUS_CLASSES = {'Received': 'status-pill--active', 'Connected': 'status-pill--active', 'Clean disconnect': 'status-pill--active'}`.
   - When constructing each log entry, set `entry['status_class']` based on this mapping/set.

2. Ensure `get_mqtt_logs()` returns entries that include `status_class`, so `routes.py` can just pass through the list without recomputing it.

This keeps all status/visual mapping logic in one module (`notifications.py`) and prevents future drift between callbacks and the route.
</issue_to_address>

### Comment 2
<location path="routes.py" line_range="176-181" />
<code_context>
+    return render_template(
+        'mqtt_log.html',
+        broker_connected=connected,
+        tls_enabled=current_app.config.get('MQTT_TLS_ENABLED', False),
+        log_entries=log_entries,
+    )
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Normalize `MQTT_TLS_ENABLED` to a strict boolean to avoid surprising truthy/falsey behavior in the template.

As written, non-boolean config values like `'false'` or `0` will be treated as truthy by Jinja, so the UI may show "Encrypted" when TLS is effectively off. Coercing the config value to a real boolean (e.g., `bool(current_app.config.get('MQTT_TLS_ENABLED', False))` or explicit string handling) will ensure the status reflects the actual TLS setting.

```suggestion
    return render_template(
        'mqtt_log.html',
        broker_connected=connected,
        tls_enabled=bool(current_app.config.get('MQTT_TLS_ENABLED', False)),
        log_entries=log_entries,
    )
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread routes.py Outdated
Comment thread routes.py
@DaTiC0
DaTiC0 merged commit c798731 into main May 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant