Améliore le healthcheck Docker pour détecter les erreurs en temps réel

This commit is contained in:
skylanix
2025-10-07 02:22:50 +02:00
parent 22abbcb02d
commit 45347be85b
8 changed files with 120 additions and 50 deletions

1
.gitignore vendored
View File

@@ -3,4 +3,5 @@
**/.venv
__pycache__
instance
logs
.tio.tokens.json

View File

@@ -5,6 +5,7 @@ WORKDIR /app
ENV DEBIAN_FRONTEND=noninteractive
ENV LANG=fr_FR.UTF-8
ENV LC_ALL=fr_FR.UTF-8
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
apt-utils \
@@ -34,7 +35,7 @@ RUN python3 -m venv /app/venv && \
chmod +x /start.sh && \
mkdir -p /app/logs
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
HEALTHCHECK --interval=1s --timeout=10s --start-period=5s --retries=3 \
CMD pgrep python > /dev/null && ! (tail -n 1000 $(ls -t /app/logs/*.log 2>/dev/null | head -1) 2>/dev/null | grep -iE "(ERROR|CRITICAL|Exception|sqlite3\.OperationalError)")
CMD ["/start.sh"]

View File

@@ -44,10 +44,13 @@ class DiscordBot(discord.Client):
return channels
def begin(self) :
def begin(self) :
token = Configuration.query.filter_by(key='discord_token').first()
if token :
self.run(token.value)
try:
self.run(token.value)
except Exception as e:
logging.error(f'Erreur fatale lors du démarrage du bot Discord : {e}')
else :
logging.error('Aucun token Discord configuré. Le bot ne peut pas être démarré')
@@ -66,8 +69,10 @@ async def on_message(message: Message):
commande = Commande.query.filter_by(discord_enable=True, trigger=command_name).first()
if commande:
try:
await message.channel.send(commande.response, suppress_embeds=True)
await asyncio.wait_for(message.channel.send(commande.response, suppress_embeds=True), timeout=30.0)
return
except asyncio.TimeoutError:
logging.error(f'Timeout lors de l\'envoi de la commande Discord : {command_name}')
except Exception as e:
logging.error(f'Échec de l\'exécution de la commande Discord : {e}')
@@ -89,7 +94,9 @@ async def on_message(message: Message):
if (rest > 0):
msg += f'- et encore {rest} autres jeux'
try :
await message.channel.send(msg, suppress_embeds=True)
await asyncio.wait_for(message.channel.send(msg, suppress_embeds=True), timeout=30.0)
except asyncio.TimeoutError:
logging.error(f'Timeout lors de l\'envoi du message ProtonDB')
except Exception as e:
logging.error(f'Échec de l\'envoi du message ProtonDB : {e}')

View File

@@ -1,3 +1,4 @@
import asyncio
import datetime
import logging
import json
@@ -14,11 +15,18 @@ def _isEnable():
return helper.getValue('humble_bundle_enable') and helper.getIntValue('humble_bundle_channel') != 0
def _callGithub():
response = requests.get("https://raw.githubusercontent.com/shionn/HumbleBundleGamePack/refs/heads/master/data/game-bundles.json")
if response.status_code == 200:
return response.json()
logging.error(f"Échec de la connexion à la ressource Humble Bundle. Code de statut HTTP : {response.status_code}")
return None
try:
response = requests.get("https://raw.githubusercontent.com/shionn/HumbleBundleGamePack/refs/heads/master/data/game-bundles.json", timeout=30)
if response.status_code == 200:
return response.json()
logging.error(f"Échec de la connexion à la ressource Humble Bundle. Code de statut HTTP : {response.status_code}")
return None
except (requests.exceptions.SSLError, requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
logging.error(f"Erreur de connexion à la ressource Humble Bundle : {e}")
return None
except Exception as e:
logging.error(f"Erreur inattendue lors de la récupération des bundles : {e}")
return None
def _isNotAlreadyNotified(bundle):
return GameBundle.query.filter_by(url=bundle['url']).first() == None
@@ -46,9 +54,14 @@ async def checkHumbleBundleAndNotify(bot: Client):
bundle = _findFirstNotNotified(bundles)
if bundle != None :
message = _formatMessage(bundle)
await bot.get_channel(ConfigurationHelper().getIntValue('humble_bundle_channel')).send(message)
db.session.add(GameBundle(url=bundle['url'], name=bundle['name'], json = json.dumps(bundle)))
db.session.commit()
try:
await asyncio.wait_for(bot.get_channel(ConfigurationHelper().getIntValue('humble_bundle_channel')).send(message), timeout=30.0)
db.session.add(GameBundle(url=bundle['url'], name=bundle['name'], json = json.dumps(bundle)))
db.session.commit()
except asyncio.TimeoutError:
logging.error(f'Timeout lors de l\'envoi du message Humble Bundle')
except Exception as send_error:
logging.error(f'Erreur lors de l\'envoi du message Humble Bundle : {send_error}')
except Exception as e:
logging.error(f"Échec de la vérification des offres Humble Bundle : {e}")
else:

View File

@@ -8,24 +8,38 @@ from database.helpers import ConfigurationHelper
from database.models import GameAlias
from sqlalchemy import desc,func
def _call_algoliasearch(search_name:str):
config = SearchConfig(ConfigurationHelper().getValue('proton_db_api_id'),
ConfigurationHelper().getValue('proton_db_api_key'))
config.set_default_hosts()
client = SearchClientSync(config=config)
return client.search_single_index(index_name="steamdb",
search_params={
"query":search_name,
"facetFilters":[["appType:Game"]],
"hitsPerPage":50},
request_options= {'headers':{'Referer':'https://www.protondb.com/'}})
def _call_algoliasearch(search_name:str):
try:
config = SearchConfig(ConfigurationHelper().getValue('proton_db_api_id'),
ConfigurationHelper().getValue('proton_db_api_key'))
config.set_default_hosts()
client = SearchClientSync(config=config)
return client.search_single_index(index_name="steamdb",
search_params={
"query":search_name,
"facetFilters":[["appType:Game"]],
"hitsPerPage":50},
request_options= {
'headers':{'Referer':'https://www.protondb.com/'},
'timeout': 30
})
except Exception as e:
logging.error(f'Erreur lors de la recherche Algolia pour "{search_name}" : {e}')
return None
def _call_summary(id):
response = requests.get(f'http://jazzy-starlight-aeea19.netlify.app/api/v1/reports/summaries/{id}.json')
if (response.status_code == 200) :
return response.json()
logging.error(f'Échec de la récupération des données ProtonDB pour le jeu {id}. Code de statut HTTP : {response.status_code}')
return None
try:
response = requests.get(f'http://jazzy-starlight-aeea19.netlify.app/api/v1/reports/summaries/{id}.json', timeout=30)
if (response.status_code == 200) :
return response.json()
logging.error(f'Échec de la récupération des données ProtonDB pour le jeu {id}. Code de statut HTTP : {response.status_code}')
return None
except (requests.exceptions.SSLError, requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
logging.error(f'Erreur de connexion ProtonDB pour le jeu {id} : {e}')
return None
except Exception as e:
logging.error(f'Erreur inattendue lors de la récupération ProtonDB pour le jeu {id} : {e}')
return None
def _is_name_match(name:str, search_name:str) -> bool:
normalized_game_name = re.sub("[^a-z0-9]", "", name.lower())
@@ -37,10 +51,12 @@ def _apply_game_aliases(search_name:str) -> str:
search_name = re.sub(re.escape(alias.alias), alias.name, search_name, flags=re.IGNORECASE)
return search_name
def searhProtonDb(search_name:str):
def searhProtonDb(search_name:str):
results = []
search_name = _apply_game_aliases(search_name)
responses = _call_algoliasearch(search_name)
if responses is None:
return results
for hit in responses.model_dump().get('hits'):
id = hit.get('object_id')
name:str = hit.get('name')

View File

@@ -37,14 +37,16 @@ class TwitchBot() :
if _isConfigured() :
try :
helper = ConfigurationHelper()
self.twitch = await Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret'))
await self.twitch.set_user_authentication(helper.getValue('twitch_access_token'), USER_SCOPE, helper.getValue('twitch_refresh_token'))
self.chat = await Chat(self.twitch)
self.twitch = await asyncio.wait_for(Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')), timeout=30.0)
await asyncio.wait_for(self.twitch.set_user_authentication(helper.getValue('twitch_access_token'), USER_SCOPE, helper.getValue('twitch_refresh_token')), timeout=30.0)
self.chat = await asyncio.wait_for(Chat(self.twitch), timeout=30.0)
self.chat.register_event(ChatEvent.READY, _onReady)
self.chat.register_event(ChatEvent.MESSAGE, _onMessage)
# chat.register_event(ChatEvent.SUB, on_sub)
self.chat.register_command('hello', _helloCommand)
self.chat.start()
except asyncio.TimeoutError:
logging.error('Timeout lors de la connexion à Twitch. Vérifiez votre connexion réseau.')
except Exception as e:
logging.error(f'Échec de l\'authentification Twitch. Vérifiez vos identifiants et redémarrez après correction : {e}')
else:

View File

@@ -1,3 +1,4 @@
import asyncio
import logging
from twitchAPI.twitch import Twitch
@@ -36,14 +37,24 @@ async def _notifyAlert(alert : LiveAlert, stream : Stream):
async def _sendMessage(channel : int, message : str) :
logger.info(f'Envoi de notification : {message}')
await bot.get_channel(channel).send(message)
logger.info(f'Notification envoyé')
try:
await asyncio.wait_for(bot.get_channel(channel).send(message), timeout=30.0)
logger.info(f'Notification envoyée')
except asyncio.TimeoutError:
logger.error(f'Timeout lors de l\'envoi de notification live alert')
except Exception as e:
logger.error(f'Erreur lors de l\'envoi de notification live alert : {e}')
async def _retreiveStreams(twitch: Twitch, alerts : list[LiveAlert]) -> list[Stream] :
streams : list[Stream] = []
logger.info(f'Recherche de streams pour : {alerts}')
async for stream in twitch.get_streams(user_login = [alert.login for alert in alerts]):
streams.append(stream)
logger.info(f'Ces streams sont en ligne : {streams}')
try:
async for stream in asyncio.wait_for(twitch.get_streams(user_login = [alert.login for alert in alerts]), timeout=30.0):
streams.append(stream)
logger.info(f'Ces streams sont en ligne : {streams}')
except asyncio.TimeoutError:
logger.error('Timeout lors de la récupération des streams Twitch')
except Exception as e:
logger.error(f'Erreur lors de la récupération des streams Twitch : {e}')
return streams

View File

@@ -17,34 +17,53 @@ auth: UserAuthenticator
def twitchConfigurationHelp():
return render_template("twitch-aide.html", token_redirect_url = _buildUrl())
@webapp.route("/configurations/twitch/request-token")
async def twitchRequestToken():
@webapp.route("/configurations/twitch/request-token")
async def twitchRequestToken():
global auth
helper = ConfigurationHelper()
twitch = await Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret'))
auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl())
return redirect(auth.return_auth_url())
try:
helper = ConfigurationHelper()
import asyncio
twitch = await asyncio.wait_for(
Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')),
timeout=30.0
)
auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl())
return redirect(auth.return_auth_url())
except asyncio.TimeoutError:
logging.error('Timeout lors de la connexion à Twitch API pour la demande de token')
return redirect(url_for('openConfigurations'))
except TwitchAPIException as e:
logging.error(f'Erreur API Twitch lors de la demande de token : {e}')
return redirect(url_for('openConfigurations'))
except Exception as e:
logging.error(f'Erreur inattendue lors de la demande de token Twitch : {e}')
return redirect(url_for('openConfigurations'))
@webapp.route("/configurations/twitch/receive-token")
@webapp.route("/configurations/twitch/receive-token")
async def twitchReceiveToken():
global auth
state = request.args.get('state')
code = request.args.get('code')
if state != auth.state :
logging('bad returned state')
logging.error('bad returned state')
return redirect(url_for('openConfigurations'))
if code == None :
logging('no returned state')
logging.error('no returned code')
return redirect(url_for('openConfigurations'))
try:
token, refresh = await auth.authenticate(user_token=code)
import asyncio
token, refresh = await asyncio.wait_for(auth.authenticate(user_token=code), timeout=30.0)
helper = ConfigurationHelper()
helper.createOrUpdate('twitch_access_token', token)
helper.createOrUpdate('twitch_refresh_token', refresh)
db.session.commit()
except asyncio.TimeoutError:
logging.error('Timeout lors de l\'authentification Twitch')
except TwitchAPIException as e:
logging(e)
logging.error(f'Erreur API Twitch lors de l\'authentification : {e}')
except Exception as e:
logging.error(f'Erreur inattendue lors de l\'authentification Twitch : {e}')
return redirect(url_for('openConfigurations'))
# hack pas fou mais on estime qu'on sera toujours en ssl en connecté