feat: Add comprehensive logging

- Add rotating file logging to /var/log/gatekeeper.log
- Log application events, MQTT, settings, and errors
- Include stack traces and improved error handling
This commit is contained in:
Josh Finlay 2025-01-08 07:51:54 +10:00
parent f6ca88f012
commit da3b3ec748
2 changed files with 139 additions and 95 deletions

View File

@ -12,14 +12,42 @@ from typing import List, Optional
import logging import logging
import json import json
from mqtt_integration import HomeAssistantMQTT from mqtt_integration import HomeAssistantMQTT
import sys
# Configure logging # Configure logging
logging.basicConfig( LOG_FILE = "/var/log/gatekeeper.log"
level=logging.INFO, logger = logging.getLogger("gatekeeper")
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' logger.setLevel(logging.INFO)
# Create rotating file handler (10MB per file, keep 5 backup files)
file_handler = RotatingFileHandler(
LOG_FILE,
maxBytes=10*1024*1024, # 10MB
backupCount=5,
delay=True # Only create log file when first record is written
) )
logger = logging.getLogger(__name__) file_handler.setLevel(logging.INFO)
# Create formatter and add it to the handler
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
# Add the handler to the logger
logger.addHandler(file_handler)
# Log uncaught exceptions
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
# Call the default handler for keyboard interrupt
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
# Install exception handler
sys.excepthook = handle_exception
# Initialize FastAPI # Initialize FastAPI
app = FastAPI() app = FastAPI()
@ -27,8 +55,9 @@ ha_mqtt = HomeAssistantMQTT()
# Set up MQTT event logging # Set up MQTT event logging
async def log_mqtt_event(action: str, success: bool = True): async def log_mqtt_event(action: str, success: bool = True):
"""Log MQTT events to the database""" """Log MQTT events to the database and log file"""
async with aiosqlite.connect(DB_PATH) as db: logger.info(f"MQTT Event - {action} (Success: {success})")
async with aiosqlite.connect("gate.db") as db:
await db.execute( await db.execute(
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", "INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
(datetime.utcnow().isoformat(), action, "MQTT", success) (datetime.utcnow().isoformat(), action, "MQTT", success)
@ -402,36 +431,48 @@ app.mount("/", StaticFiles(directory="../public", html=True), name="static")
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
"""Initialize the application on startup""" """Initialize the application on startup"""
# Initialize database logger.info("Starting Gatekeeper application")
await init_db()
# Setup GPIO
setup_gpio()
# Start background tasks
app.state.status_task = asyncio.create_task(update_gate_status())
app.state.auto_close_task = asyncio.create_task(check_auto_close())
# Initialize MQTT from settings
try: try:
# Initialize database
await init_db()
logger.info("Database initialized successfully")
# Setup GPIO
setup_gpio()
logger.info("GPIO initialized successfully")
# Initialize MQTT from settings
settings = await get_settings() settings = await get_settings()
if settings["mqtt"].get("enabled"): if settings.mqtt and settings.mqtt.get("enabled"):
ha_mqtt.enable(True) logger.info("MQTT enabled in settings, initializing connection")
ha_mqtt.enable()
else:
logger.info("MQTT disabled in settings")
# Start background tasks
app.state.status_task = asyncio.create_task(update_gate_status())
app.state.auto_close_task = asyncio.create_task(check_auto_close())
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize MQTT: {e}") logger.error(f"Startup error: {str(e)}", exc_info=True)
raise
@app.on_event("shutdown") @app.on_event("shutdown")
async def shutdown_event(): async def shutdown_event():
"""Clean up on shutdown""" """Cleanup on shutdown"""
# Cancel background tasks logger.info("Shutting down Gatekeeper application")
if hasattr(app.state, "status_task"): try:
app.state.status_task.cancel() # Cancel background tasks
if hasattr(app.state, "auto_close_task"): if hasattr(app.state, "status_task"):
app.state.auto_close_task.cancel() app.state.status_task.cancel()
if hasattr(app.state, "auto_close_task"):
# Disconnect MQTT app.state.auto_close_task.cancel()
await ha_mqtt.disconnect()
# Disconnect MQTT
# Cleanup GPIO await ha_mqtt.disconnect()
GPIO.cleanup()
logger.info("Application shutdown complete") # Cleanup GPIO
GPIO.cleanup()
logger.info("Cleanup completed successfully")
except Exception as e:
logger.error(f"Shutdown error: {str(e)}", exc_info=True)
raise

View File

@ -1,11 +1,12 @@
import os import os
import json import json
import asyncio import asyncio
import logging
from gmqtt import Client as MQTTClient
from typing import Optional, Callable from typing import Optional, Callable
from gmqtt import Client as MQTTClient
import logging
logger = logging.getLogger(__name__) # Get logger
logger = logging.getLogger("gatekeeper")
class HomeAssistantMQTT: class HomeAssistantMQTT:
def __init__(self): def __init__(self):
@ -16,6 +17,8 @@ class HomeAssistantMQTT:
self.password = os.getenv("MQTT_PASSWORD", None) self.password = os.getenv("MQTT_PASSWORD", None)
self.client_id = os.getenv("MQTT_CLIENT_ID", "gatekeeper") self.client_id = os.getenv("MQTT_CLIENT_ID", "gatekeeper")
logger.info(f"Initializing MQTT client (broker: {self.broker}:{self.port}, client_id: {self.client_id})")
# Home Assistant MQTT topics # Home Assistant MQTT topics
self.node_id = "gatekeeper" self.node_id = "gatekeeper"
self.object_id = "gate" self.object_id = "gate"
@ -26,6 +29,8 @@ class HomeAssistantMQTT:
self.config_topic = f"homeassistant/cover/{self.node_id}/{self.object_id}/config" self.config_topic = f"homeassistant/cover/{self.node_id}/{self.object_id}/config"
self.availability_topic = f"homeassistant/cover/{self.node_id}/{self.object_id}/availability" self.availability_topic = f"homeassistant/cover/{self.node_id}/{self.object_id}/availability"
logger.debug(f"MQTT topics configured - State: {self.state_topic}, Command: {self.command_topic}")
self.client: Optional[MQTTClient] = None self.client: Optional[MQTTClient] = None
self.command_callback: Optional[Callable] = None self.command_callback: Optional[Callable] = None
self._connected = False self._connected = False
@ -63,53 +68,77 @@ class HomeAssistantMQTT:
while self._enabled: while self._enabled:
try: try:
if not self._connected: if not self._connected:
logger.info("Attempting to reconnect to MQTT broker...")
await self.connect() await self.connect()
await asyncio.sleep(5) # Wait before checking connection again await asyncio.sleep(5)
except asyncio.CancelledError:
break
except Exception as e: except Exception as e:
logger.error(f"Reconnection attempt failed: {e}") logger.error(f"Reconnection attempt failed: {e}")
await asyncio.sleep(5) # Wait before retrying await asyncio.sleep(5)
def on_connect(self, client, flags, rc, properties): def on_connect(self, client, flags, rc, properties):
"""Callback for when connection is established""" """Callback for when connection is established"""
logger.info("Connected to MQTT broker") logger.info(f"Connected to MQTT broker (rc: {rc})")
self._connected = True self._connected = True
asyncio.create_task(self._log_event("MQTT Connected"))
asyncio.create_task(self._post_connect()) asyncio.create_task(self._post_connect())
def on_message(self, client, topic, payload, qos, properties): def on_message(self, client, topic, payload, qos, properties):
"""Callback for when a message is received""" """Callback for when a message is received"""
if topic == self.command_topic and self.command_callback: try:
try: logger.debug(f"MQTT message received - Topic: {topic}, Payload: {payload}")
if topic == self.command_topic and self.command_callback:
command = payload.decode() command = payload.decode()
logger.info(f"MQTT command received: {command}")
asyncio.create_task(self.command_callback(command)) asyncio.create_task(self.command_callback(command))
except Exception as e: except Exception as e:
logger.error(f"Error processing command: {e}") logger.error(f"Error processing MQTT message: {e}", exc_info=True)
def on_disconnect(self, client, packet, exc=None): def on_disconnect(self, client, packet, exc=None):
"""Callback for when connection is lost""" """Callback for when connection is lost"""
logger.info("Disconnected from MQTT broker") logger.warning(f"Disconnected from MQTT broker{f': {exc}' if exc else ''}")
self._connected = False self._connected = False
# Log disconnect event
asyncio.create_task(self._log_event("MQTT Disconnected"))
# Start reconnection if enabled
if self._enabled and not self._reconnect_task: if self._enabled and not self._reconnect_task:
self._reconnect_task = asyncio.create_task(self._reconnect_loop()) self._reconnect_task = asyncio.create_task(self._reconnect_loop())
async def _post_connect(self): async def _post_connect(self):
"""Tasks to run after connection is established""" """Tasks to perform after connection is established"""
try: try:
# Send Home Assistant discovery configuration
await self.publish_discovery_config()
# Publish availability status
await self.publish(self.availability_topic, "online", retain=True)
# Subscribe to command topic # Subscribe to command topic
await self.subscribe(self.command_topic) await self.client.subscribe([(self.command_topic, 1)])
logger.info(f"Subscribed to command topic: {self.command_topic}")
# Publish discovery config
config = {
"name": "Gate",
"unique_id": f"{self.node_id}_{self.object_id}",
"device_class": "gate",
"command_topic": self.command_topic,
"state_topic": self.state_topic,
"availability_topic": self.availability_topic,
"payload_open": "OPEN",
"payload_close": "CLOSE",
"state_open": "open",
"state_closed": "closed"
}
await self.client.publish(
self.config_topic,
json.dumps(config),
qos=1,
retain=True
)
logger.info("Published Home Assistant discovery config")
# Publish initial availability
await self.client.publish(
self.availability_topic,
"online",
qos=1,
retain=True
)
logger.info("Published initial availability state")
except Exception as e: except Exception as e:
logger.error(f"Error in post-connect tasks: {e}") logger.error(f"Error in post-connect setup: {e}", exc_info=True)
async def connect(self): async def connect(self):
"""Connect to MQTT broker""" """Connect to MQTT broker"""
@ -137,7 +166,12 @@ class HomeAssistantMQTT:
"""Disconnect from MQTT broker""" """Disconnect from MQTT broker"""
if self.client and self._connected: if self.client and self._connected:
try: try:
await self.publish(self.availability_topic, "offline", retain=True) await self.client.publish(
self.availability_topic,
"offline",
qos=1,
retain=True
)
await self.client.disconnect() await self.client.disconnect()
except Exception as e: except Exception as e:
logger.error(f"Error during disconnect: {e}") logger.error(f"Error during disconnect: {e}")
@ -158,41 +192,10 @@ class HomeAssistantMQTT:
except Exception as e: except Exception as e:
logger.error(f"Failed to subscribe to topic: {e}") logger.error(f"Failed to subscribe to topic: {e}")
async def publish_discovery_config(self): async def publish_state(self, state: str):
"""Publish Home Assistant MQTT discovery configuration""" """Publish gate state"""
config = { await self.publish(self.state_topic, state)
"name": "Gate",
"unique_id": f"{self.node_id}_{self.object_id}",
"device_class": "gate",
"command_topic": self.command_topic,
"state_topic": self.state_topic,
"availability_topic": self.availability_topic,
"payload_available": "online",
"payload_not_available": "offline",
"payload_open": "OPEN",
"payload_close": "CLOSE",
"payload_stop": "STOP",
"state_open": "open",
"state_closed": "closed",
"state_opening": "opening",
"state_closing": "closing",
"device": {
"identifiers": [self.node_id],
"name": "Gate Keeper",
"model": "DLB Gate Controller",
"manufacturer": "Athena Networks",
}
}
try:
await self.publish(self.config_topic, json.dumps(config), retain=True)
except Exception as e:
logger.error(f"Failed to publish discovery config: {e}")
def set_command_callback(self, callback: Callable): def set_command_callback(self, callback: Callable):
"""Set callback for handling commands""" """Set callback for handling commands"""
self.command_callback = callback self.command_callback = callback
async def publish_state(self, state: str):
"""Publish gate state"""
await self.publish(self.state_topic, state)