paper-dynasty-discord/paperdynasty.py
Cal Corum 440f017c92 Add HTTP health check endpoint for container monitoring
Implements a comprehensive health check system using aiohttp to support
container orchestration and external monitoring systems.

Features:
- /health endpoint: Basic liveness check (is process running?)
- /ready endpoint: Readiness check (is bot connected to Discord?)
- /metrics endpoint: Detailed bot metrics (guilds, users, cogs, latency)

Changes:
- Add aiohttp to requirements.txt
- Create health_server.py module with HTTP server
- Update paperdynasty.py to run health server alongside bot
- Update docker-compose.yml with HTTP-based healthcheck
- Fix deploy.sh Docker image name

Benefits:
- Auto-restart on bot hangs/deadlocks
- Foundation for external monitoring (Prometheus, Grafana, etc.)
- Detailed diagnostics for troubleshooting
- Industry-standard health check pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 14:44:53 -06:00

122 lines
3.7 KiB
Python

import discord
import datetime
import logging
from logging.handlers import RotatingFileHandler
import asyncio
import os
from discord.ext import commands
from in_game.gameplay_models import create_db_and_tables
from health_server import run_health_server
raw_log_level = os.getenv('LOG_LEVEL')
if raw_log_level == 'DEBUG':
log_level = logging.DEBUG
elif raw_log_level == 'INFO':
log_level = logging.INFO
elif raw_log_level == 'WARN':
log_level = logging.WARNING
else:
log_level = logging.ERROR
# date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}'
# logger.basicConfig(
# filename=f'logs/{date}.log',
# format='%(asctime)s - %(levelname)s - %(message)s',
# level=log_level
# )
# logger.getLogger('discord.http').setLevel(logger.INFO)
logger = logging.getLogger('discord_app')
logger.setLevel(log_level)
handler = RotatingFileHandler(
filename='logs/discord.log',
# encoding='utf-8',
maxBytes=32 * 1024 * 1024, # 32 MiB
backupCount=5, # Rotate through 5 files
)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
# dt_fmt = '%Y-%m-%d %H:%M:%S'
# formatter = logger.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{')
# handler.setFormatter(formatter)
logger.addHandler(handler)
COGS = [
'cogs.owner',
'cogs.admins',
'cogs.economy',
'cogs.players',
'cogs.gameplay',
]
intents = discord.Intents.default()
intents.members = True
intents.message_content = True
bot = commands.Bot(command_prefix='.',
intents=intents,
# help_command=None,
description='The Paper Dynasty Bot\nIf you have questions, feel free to contact Cal.',
case_insensitive=True,
owner_id=258104532423147520)
@bot.event
async def on_ready():
logger.info('Logged in as:')
logger.info(bot.user.name)
logger.info(bot.user.id)
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: discord.app_commands.AppCommandError):
"""Global error handler for all app commands (slash commands)."""
logger.error(f'App command error in {interaction.command}: {error}', exc_info=error)
# Try to respond to the user
try:
if not interaction.response.is_done():
await interaction.response.send_message(
f'❌ An error occurred: {str(error)}',
ephemeral=True
)
else:
await interaction.followup.send(
f'❌ An error occurred: {str(error)}',
ephemeral=True
)
except Exception as e:
logger.error(f'Failed to send error message to user: {e}')
async def main():
create_db_and_tables()
for c in COGS:
try:
await bot.load_extension(c)
logger.info(f'Loaded cog: {c}')
except Exception as e:
logger.error(f'Failed to load cog: {c}')
logger.error(f'{e}')
# Start health server and bot concurrently
async with bot:
# Create health server task
health_task = asyncio.create_task(run_health_server(bot))
try:
# Start bot (this blocks until bot stops)
await bot.start(os.environ.get('BOT_TOKEN', 'NONE'))
finally:
# Cleanup: cancel health server when bot stops
health_task.cancel()
try:
await health_task
except asyncio.CancelledError:
pass
asyncio.run(main())