Files
healthcheck-container/app.py
T
Milan PandurovandClaude Fable 5.1 62b57a7e56 Add MQTT diagnostics, broker connection probe, and switch to Debian base
The MQTT publisher now tracks connection state, publish counters, discovery
and availability timestamps, and the last error, exposed at /api/mqtt and on
the dashboard as an MQTT card and a Home Assistant section. Connection
failures, disconnects, and dropped publishes are logged; paho's
on_connect_fail callback was not registered before, so failed attempts were
silent. MQTT connect, disconnect, and unreachable events go to the timeline.

When a connection attempt fails the service runs a probe from inside the
container: system resolver, each nameserver from resolv.conf queried directly,
/etc/hosts, and a TCP connect to every address found. The result is shown as
a summary and raw JSON on the dashboard and can be rerun via
POST /api/mqtt/probe.

The image base moves from Alpine to Debian slim. musl queries all
nameservers in parallel and accepts the first reply, so a fast public
NXDOMAIN beats a slower local server that knows the name. glibc asks the
nameservers in order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 12:08:48 +02:00

103 lines
2.8 KiB
Python

import logging
import time
from flask import Flask, jsonify, render_template, request
from config import Config
from monitors import State, build_monitors
from storage import Storage
cfg = Config()
logging.basicConfig(level=cfg.log_level, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
log = logging.getLogger("app")
storage = Storage(cfg.db_path)
state = State()
publisher = None
app = Flask(__name__)
WIFI_KEYS = ("wifi_interface", "wifi_interfaces", "wifi_status", "wifi_error",
"wifi_link", "wifi_link_checked_at", "wifi_link_error",
"wifi_scan", "wifi_scan_error")
@app.route('/alive', methods=['GET'])
def get_healthcheck():
response = {
"alive": True
}
return jsonify(response)
@app.route('/')
def index():
return render_template("index.html")
@app.route('/api/status')
def api_status():
snapshot = state.snapshot()
snapshot.pop("wifi_scan", None)
snapshot["now"] = time.time()
snapshot["config"] = cfg.public()
return jsonify(snapshot)
@app.route('/api/wifi')
def api_wifi():
snapshot = state.snapshot()
result = {key: snapshot.get(key) for key in WIFI_KEYS}
result["now"] = time.time()
result["scan_interval"] = cfg.wifi_scan_interval
return jsonify(result)
@app.route('/api/mqtt')
def api_mqtt():
result = publisher.diagnostics() if publisher else {"enabled": False}
result["now"] = time.time()
return jsonify(result)
@app.route('/api/mqtt/probe', methods=['POST'])
def api_mqtt_probe():
if not publisher:
return jsonify({"error": "MQTT is not configured"}), 404
return jsonify(publisher.run_probe())
@app.route('/api/events')
def api_events():
limit = min(max(request.args.get("limit", 200, type=int), 1), 1000)
return jsonify(storage.events(limit))
@app.route('/api/samples')
def api_samples():
prefix = request.args.get("prefix", "")
span = min(max(request.args.get("range", 3600, type=int), 60), cfg.sample_retention_days * 86400)
bucket = max(10, span // 400)
since = time.time() - span
return jsonify({"since": since, "bucket": bucket, "series": storage.samples_by_prefix(prefix, since, bucket)})
def start_background():
global publisher
storage.add_event("service_started", "Health check service started")
storage.prune_samples(cfg.sample_retention_days)
if cfg.mqtt_host:
from hamqtt import HomeAssistantPublisher
publisher = HomeAssistantPublisher(cfg, storage)
state.on_change(publisher.publish_state)
publisher.start()
for monitor in build_monitors(cfg, state, storage):
monitor.start()
log.info("started monitor %s every %ss", monitor.name, monitor.interval)
if __name__ == "__main__":
start_background()
app.run(host="0.0.0.0", port=cfg.port)