mirror of
https://github.com/deunix-educ/Fail2banMqttActionBanishment.git
synced 2026-08-24 03:11:58 +02:00
First commit
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
"""Django settings shared by every environment (dev, production)."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import DisallowedHost
|
||||
from dotenv import load_dotenv
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# .env vit à la racine du dépôt (BASE_DIR/../.env), pas dans emitter/ — voir
|
||||
# .env.example. Chargé ici pour que `manage.py ...` lise les mêmes valeurs
|
||||
# que systemd (EnvironmentFile), sans avoir à les exporter à la main en dev.
|
||||
# N'écrase jamais une variable déjà présente dans l'environnement (override=False
|
||||
# par défaut) : sous systemd, EnvironmentFile reste la source de vérité.
|
||||
load_dotenv(BASE_DIR.parent / '.env')
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'daphne',
|
||||
# banevents AVANT django.contrib.admin : override du thème admin
|
||||
# (templates/admin/base_site.html) — avec APP_DIRS=True, la résolution
|
||||
# de template prend le premier app_dirs match dans l'ordre
|
||||
# d'INSTALLED_APPS, donc notre template ne serait jamais atteint si
|
||||
# django.contrib.admin passait en premier (piège documenté par Django
|
||||
# lui-même pour toute personnalisation de l'admin).
|
||||
'banevents',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'channels',
|
||||
'axes',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
# Doit se trouver après SessionMiddleware et avant CommonMiddleware
|
||||
# (exigé par Django) : lit la langue depuis le cookie de session, puis
|
||||
# l'en-tête Accept-Language, avant que CommonMiddleware ne traite la
|
||||
# requête. Bascule fr/en du dashboard (Phase 5) — voir set_language
|
||||
# dans config/urls.py et le sélecteur dans _sidebar_stats.html.
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
# Doit rester en dernier (exigé par django-axes).
|
||||
'axes.middleware.AxesMiddleware',
|
||||
]
|
||||
|
||||
# django-axes en premier (exigé) : verrouille après trop d'échecs de
|
||||
# connexion admin, cf. banevents/signals.py (receiver user_locked_out) qui
|
||||
# journalise l'événement pour fail2ban — voir generic/fail2ban/jail.d/
|
||||
# emitter-admin-auth.conf. AXES ne bannit rien au réseau lui-même, juste
|
||||
# l'application ; le ban réseau + la propagation aux autres noeuds
|
||||
# viennent de fail2ban comme pour toutes les autres jails.
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'axes.backends.AxesBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
# LANGUAGE_CODE/LANGUAGES dans les templates (ex.
|
||||
# <html lang="{{ LANGUAGE_CODE }}">, sélecteur de langue
|
||||
# dans _sidebar_stats.html) — Phase 5.
|
||||
'django.template.context_processors.i18n',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
ASGI_APPLICATION = 'config.asgi.application'
|
||||
|
||||
REDIS_HOST = os.environ.get('REDIS_HOST', '127.0.0.1')
|
||||
REDIS_PORT = int(os.environ.get('REDIS_PORT', '6379'))
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
'hosts': [(REDIS_HOST, REDIS_PORT)],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
TIME_ZONE = 'UTC'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# fr (défaut, langue source des templates) / en (traduction, voir
|
||||
# locale/en/LC_MESSAGES/django.po, généré par makemessages -l en et
|
||||
# compilé par compilemessages — make lint/test ne le font pas
|
||||
# automatiquement, cf. README).
|
||||
LANGUAGES = [
|
||||
('fr', 'Français'),
|
||||
('en', 'English'),
|
||||
]
|
||||
LOCALE_PATHS = [BASE_DIR / 'locale']
|
||||
|
||||
# Celery — DB Redis distincte de celle des Channels (0), pour ne pas mélanger
|
||||
# les deux usages du même serveur Redis.
|
||||
CELERY_BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/1'
|
||||
CELERY_RESULT_BACKEND = f'redis://{REDIS_HOST}:{REDIS_PORT}/1'
|
||||
CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_ACCEPT_CONTENT = ['json']
|
||||
CELERY_TIMEZONE = TIME_ZONE
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [BASE_DIR / 'banevents' / 'static']
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Client MQTT local (management command banevents/mqtt_listen) — même broker
|
||||
# local que celui utilisé par f2b-mqtt-action-banisher (generic/mosquitto/),
|
||||
# mais avec un compte dédié à l'émetteur Django.
|
||||
#MQTT_BROKER_HOST = os.environ.get('MQTT_BROKER_HOST', '127.0.0.1')
|
||||
MQTT_BROKER_HOST = os.environ.get('MQTT_BROKER_HOST', '192.168.250.1')
|
||||
MQTT_BROKER_PORT = int(os.environ.get('MQTT_BROKER_PORT', '1883'))
|
||||
MQTT_BROKER_USERNAME = os.environ.get('MQTT_BROKER_USERNAME', 'emitter')
|
||||
MQTT_BROKER_PASSWORD = os.environ.get('MQTT_BROKER_PASSWORD', 'banishing')
|
||||
MQTT_TOPIC_SUBSCRIBE = os.environ.get('MQTT_TOPIC_SUBSCRIBE', 'fail2ban/+/jail')
|
||||
|
||||
# Client MQTT vers le broker master (Phase 2.b, management command
|
||||
# banevents/master_client) — mTLS : le certificat client (signé par la CA
|
||||
# privée du master, cf. scripts/generate-node-cert.sh) sert d'identité,
|
||||
# pas de mot de passe. Par défaut, cherche les fichiers dans
|
||||
# emitter/master-tls/ (jamais commité, cf. .gitignore).
|
||||
# Pas de MQTT_MASTER_CA_CERT : le certificat SERVEUR du master est signé
|
||||
# Let's Encrypt, vérifié par le magasin de CA système par défaut. La CA
|
||||
# privée du master ne sert qu'à ce que LUI vérifie NOTRE certificat client —
|
||||
# elle n'a pas sa place ici (cf. master_client.py).
|
||||
MQTT_MASTER_HOST = os.environ.get('MQTT_MASTER_HOST', 'mqtt.linuxtarn.org')
|
||||
MQTT_MASTER_PORT = int(os.environ.get('MQTT_MASTER_PORT', '8883'))
|
||||
MQTT_MASTER_CLIENT_CERT = os.environ.get('MQTT_MASTER_CLIENT_CERT', str(BASE_DIR / 'master-tls' / 'node.crt'))
|
||||
MQTT_MASTER_CLIENT_KEY = os.environ.get('MQTT_MASTER_CLIENT_KEY', str(BASE_DIR / 'master-tls' / 'node.key'))
|
||||
# Doit être identique au nom passé à scripts/generate-node-cert.sh (le CN du
|
||||
# certificat client) : le master ACL ce noeud sur fail2ban/<ce nom>/... via
|
||||
# use_identity_as_username, donc un nom qui ne correspond pas au CN se fera
|
||||
# rejeter toute publication/abonnement par l'ACL.
|
||||
MQTT_MASTER_NODE_NAME = os.environ.get('MQTT_MASTER_NODE_NAME', 'testclient')
|
||||
|
||||
# Client MQTT côté master (Phase 3, management command
|
||||
# banevents/master_listen) — identité mTLS DISTINCTE de MQTT_MASTER_CLIENT_*
|
||||
# ci-dessus : ce process doit lire fail2ban/+/ban (tous les noeuds), un
|
||||
# droit plus large que celui de n'importe quel noeud pris individuellement,
|
||||
# donc son propre certificat (CN "master-internal" par convention, voir
|
||||
# generic/mosquitto/master-acl.conf) plutôt que de réutiliser l'identité
|
||||
# d'un noeud.
|
||||
MQTT_MASTER_LISTENER_CERT = os.environ.get('MQTT_MASTER_LISTENER_CERT', str(BASE_DIR / 'master-tls' / 'listener.crt'))
|
||||
MQTT_MASTER_LISTENER_KEY = os.environ.get('MQTT_MASTER_LISTENER_KEY', str(BASE_DIR / 'master-tls' / 'listener.key'))
|
||||
|
||||
# CA privée du master (scripts/master-ca-init.sh) — master uniquement,
|
||||
# lue par banevents/views.py::join_node pour renvoyer ca.crt au noeud qui
|
||||
# s'auto-inscrit (flux par jeton, voir ROADMAP.md). Même défaut que
|
||||
# master-ca-init.sh/generate-node-cert.sh/sign-node-csr.sh : à garder
|
||||
# cohérent si jamais CA_DIR est un jour personnalisé sur ce VPS.
|
||||
MQTT_MASTER_CA_DIR = os.environ.get('MQTT_MASTER_CA_DIR', '/etc/mosquitto/master-ca')
|
||||
|
||||
# Page publique /communaute/ (master uniquement) : désactivée par défaut
|
||||
# — quiconque peut y déposer une demande d'inscription (email, nom de
|
||||
# noeud souhaité), mais aucun jeton n'est émis/envoyé sans validation
|
||||
# manuelle dans /admin/ (voir banevents/admin.py::EnrollmentRequestAdmin).
|
||||
COMMUNITY_ENROLLMENT_ENABLED = os.environ.get('COMMUNITY_ENROLLMENT_ENABLED', 'false').lower() == 'true'
|
||||
|
||||
# Email (validation d'une demande d'inscription, cf. ci-dessus) — backend
|
||||
# sendmail (django-sendmail-backend), pas le backend SMTP standard de
|
||||
# Django : ce dernier exige un socket TCP en écoute (ex. 127.0.0.1:25),
|
||||
# absent sur un VPS dont le MTA local (exim4) n'est configuré qu'en
|
||||
# remise directe (invocation du binaire /usr/sbin/sendmail, vérifié
|
||||
# fonctionnel en conditions réelles, sans dépendre du daemon exim4.service
|
||||
# — qui peut très bien être arrêté/en échec sans que ça n'empêche l'envoi
|
||||
# via ce backend). Aucune garantie de délivrabilité vers une adresse
|
||||
# EXTERNE sans relais SMTP (smarthost) configuré par ailleurs.
|
||||
EMAIL_BACKEND = 'django_sendmail_backend.backends.EmailBackend'
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', '')
|
||||
|
||||
# ADMINS alimente mail_admins() — notification à l'admin qu'une nouvelle
|
||||
# EnrollmentRequest attend une décision dans /admin/ (banevents/views.py::
|
||||
# community_landing), en plus de l'email envoyé au demandeur une fois
|
||||
# validée. ADMIN_EMAIL vide => ADMINS reste vide => mail_admins() ne fait
|
||||
# rien (pas d'erreur), la demande reste visible dans /admin/ de toute
|
||||
# façon — cette notification est un confort, pas la source de vérité.
|
||||
_admin_email = os.environ.get('ADMIN_EMAIL', '')
|
||||
ADMINS = [('admin', _admin_email)] if _admin_email else []
|
||||
|
||||
# Chemin absolu de fail2ban-client, utilisé par master_client pour exécuter
|
||||
# un SYNC_BAN reçu (sudo fail2ban-client set master-sync banip <ip>) — doit
|
||||
# correspondre exactement au chemin autorisé dans le sudoers déployé par
|
||||
# install.sh (generic/emitter/sudoers-master-sync), sudo ne fait pas de
|
||||
# résolution de $PATH.
|
||||
FAIL2BAN_CLIENT_PATH = os.environ.get('FAIL2BAN_CLIENT_PATH', '/usr/bin/fail2ban-client')
|
||||
|
||||
# AbuseIPDB (REPORT_ABUSE, Phase 4, banevents/abuseipdb.py) — désactivé
|
||||
# par défaut : ABUSEIPDB_ENABLED=false ou ABUSEIPDB_API_KEY vide font
|
||||
# tomber publish_command.py en dry-run (rien envoyé, juste journalisé).
|
||||
# Jamais de rapport public réel sans configuration explicite dans .env.
|
||||
ABUSEIPDB_API_KEY = os.environ.get('ABUSEIPDB_API_KEY', '')
|
||||
ABUSEIPDB_ENABLED = os.environ.get('ABUSEIPDB_ENABLED', 'false').lower() == 'true'
|
||||
|
||||
# Lien vers le dashboard du master (sidebar) — vide sur le master
|
||||
# lui-même (pas de lien vers soi-même), renseigné sur les noeuds clients.
|
||||
# Un seul lien hiérarchique (client -> master), pas un maillage entre
|
||||
# noeuds (tentative de liens croisés N x N essayée puis abandonnée —
|
||||
# ne passe pas à l'échelle avec beaucoup de clients abonnés).
|
||||
MASTER_DASHBOARD_URL = os.environ.get('MASTER_DASHBOARD_URL', '').rstrip('/')
|
||||
|
||||
# Largeur de la sidebar (CSS, ex. "220px" ou "16rem") — la valeur par
|
||||
# défaut (--sidebar-w dans main.css) convient à peu de noeuds/alias courts ;
|
||||
# ajustable sans toucher au CSS si le roster (Phase 3) fait grandir le
|
||||
# bloc "Noeuds" au point que les libellés soient tronqués.
|
||||
# `or` plutôt que get(key, default) : .env.example laisse la variable
|
||||
# présente mais vide (convention de ce fichier), qui doit retomber sur le
|
||||
# défaut au même titre qu'une variable absente.
|
||||
DASHBOARD_SIDEBAR_WIDTH = os.environ.get('DASHBOARD_SIDEBAR_WIDTH') or '220px'
|
||||
|
||||
# django-axes (protection anti-brute-force sur /admin/, cf. AUTHENTICATION_
|
||||
# BACKENDS ci-dessus) — verrouille par IP seule (pas IP+utilisateur) pour
|
||||
# rester cohérent avec le modèle fail2ban : une IP qui abuse doit être
|
||||
# bloquée quel que soit le compte visé, pas juste ce compte-là.
|
||||
AXES_FAILURE_LIMIT = 5
|
||||
AXES_LOCKOUT_PARAMETERS = ['ip_address']
|
||||
AXES_COOLOFF_TIME = 1 # heures — verrouillage applicatif ; le vrai blocage
|
||||
# réseau (fail2ban, cf. plus bas) dure bien plus longtemps (24h).
|
||||
# Le dashboard est derrière nginx (generic/nginx/fail2ban-emitter.example.conf,
|
||||
# qui pose déjà X-Real-IP/X-Forwarded-For) : sans ceci, axes verrait
|
||||
# 127.0.0.1 (nginx) sur chaque requête au lieu de la vraie IP cliente.
|
||||
# 0, pas 1 : nginx est l'unique proxy et un vrai client n'envoie jamais son
|
||||
# propre X-Forwarded-For, donc $proxy_add_x_forwarded_for (gabarit nginx)
|
||||
# ne contient qu'UNE entrée (le $remote_addr observé par nginx lui-même).
|
||||
# python-ipware exige (nb d'entrées - 1) >= proxy_count : avec 1 entrée et
|
||||
# proxy_count=1, la validation échoue systématiquement et l'IP résolue
|
||||
# vaut None (vérifié en conditions réelles : ligne de log
|
||||
# "... depuis None ..." malgré 5 échecs de connexion réels). proxy_count=0
|
||||
# prend ip_list[-1], donc TOUJOURS le dernier maillon ajouté par nginx —
|
||||
# fiable même si le client falsifie son propre X-Forwarded-For en amont.
|
||||
AXES_IPWARE_PROXY_COUNT = 0
|
||||
# axes ignore X-Forwarded-For par défaut (AXES_IPWARE_META_PRECEDENCE_ORDER
|
||||
# vaut ('REMOTE_ADDR',) tant que ce n'est pas explicite — protection contre
|
||||
# le spoofing par un déploiement mal configuré) : sans ceci, chaque requête
|
||||
# est vue avec l'IP de nginx (127.0.0.1), jamais la vraie IP cliente.
|
||||
AXES_IPWARE_META_PRECEDENCE_ORDER = ('HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR')
|
||||
|
||||
# Log dédié pour fail2ban (generic/fail2ban/jail.d/emitter-admin-auth.conf)
|
||||
# — à l'intérieur du checkout (ReadWritePaths=__EMITTER_DIR__ dans
|
||||
# fail2ban-emitter-web.service couvre déjà tout l'arbre, pas besoin
|
||||
# d'étendre le sandboxing systemd pour un nouveau chemin /var/log/...).
|
||||
# Le receiver banevents/signals.py écrit une ligne stable et contrôlée par
|
||||
# nous sur le logger 'fail2ban.axes', pas les logs internes d'axes (le
|
||||
# format exact de la librairie n'est pas un contrat, il pourrait changer
|
||||
# entre versions).
|
||||
LOG_DIR = BASE_DIR / 'logs'
|
||||
# install.sh (deploy_emitter) crée déjà ce dossier en prod avec le bon
|
||||
# propriétaire avant le premier manage.py, mais un checkout local (dev,
|
||||
# CI) n'a jamais lancé install.sh : sans ce mkdir, django.setup() plante
|
||||
# dès l'import des settings (FileHandler ne crée pas son dossier parent).
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
def _skip_disallowed_host(record):
|
||||
"""Coupe l'email admin (mail_admins, cf. handler ci-dessous) pour les
|
||||
DisallowedHost — bruit constant sur un serveur à IP publique (scanners
|
||||
qui tapent l'IP brute plutôt que DASHBOARD_DOMAIN), sans intérêt
|
||||
actionnable contrairement aux autres erreurs 500. Django rejette déjà
|
||||
correctement la requête (ALLOWED_HOSTS) ; seule la notification est
|
||||
supprimée, pas la protection elle-même. Recette officielle Django, voir
|
||||
https://docs.djangoproject.com/en/stable/howto/error-reporting/#filtering-error-reports
|
||||
"""
|
||||
if record.exc_info:
|
||||
exc_type = record.exc_info[0]
|
||||
if exc_type is not None and issubclass(exc_type, DisallowedHost):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'filters': {
|
||||
# Redéfini ici (identique à DEFAULT_LOGGING de Django) : dictConfig
|
||||
# ne fusionne pas entre l'appel interne de Django et celui-ci pour
|
||||
# notre propre LOGGING, un nom de filtre référencé plus bas sans
|
||||
# être défini dans CE dict lèverait "Unable to configure filter".
|
||||
'require_debug_false': {'()': 'django.utils.log.RequireDebugFalse'},
|
||||
'skip_disallowed_host': {
|
||||
'()': 'django.utils.log.CallbackFilter',
|
||||
'callback': _skip_disallowed_host,
|
||||
},
|
||||
},
|
||||
'formatters': {
|
||||
# TIME_ZONE='UTC' ci-dessus force tout le process en UTC
|
||||
# (Settings.__init__ de Django appelle time.tzset() dès le
|
||||
# chargement des settings) — %(asctime)s (time.localtime() sous
|
||||
# le capot) produit donc des heures UTC, mais SANS le dire.
|
||||
# fail2ban (process séparé, non affecté par Django) lit l'horloge
|
||||
# système réelle du VPS (ex. Europe/Berlin, CEST = UTC+2) : un
|
||||
# timestamp UTC non marqué paraît alors "vieux de 2h", ce qui l'a
|
||||
# fait ignorer silencieusement une ligne de verrouillage fraîche
|
||||
# juste après un restart de fail2ban.service ("Ignoring all log
|
||||
# entries older than 3600s" — repéré en re-testant après la
|
||||
# généralisation du filtre app-auth). Suffixe "Z" explicite
|
||||
# (ISO 8601 UTC, reconnu par la détection de date de fail2ban) :
|
||||
# supprime toute ambiguïté, quel que soit le fuseau du VPS.
|
||||
'fail2ban': {'format': '%(asctime)s.%(msecs)03dZ %(message)s', 'datefmt': '%Y-%m-%dT%H:%M:%S'},
|
||||
},
|
||||
'handlers': {
|
||||
'fail2ban_axes': {
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': str(LOG_DIR / 'admin-auth.log'),
|
||||
'formatter': 'fail2ban',
|
||||
},
|
||||
# Redéfinit le handler par défaut de Django (même class/level que
|
||||
# DEFAULT_LOGGING) pour n'y ajouter que le filtre ci-dessus — une
|
||||
# config LOGGING partielle remplace entièrement une clé du même nom
|
||||
# plutôt que de la fusionner, il faut donc la réécrire au complet.
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'filters': ['require_debug_false', 'skip_disallowed_host'],
|
||||
'class': 'django.utils.log.AdminEmailHandler',
|
||||
},
|
||||
# Requis par le logger 'django' ci-dessous (même piège que
|
||||
# require_debug_false : un handler référencé doit être défini dans
|
||||
# CE dict, jamais hérité de l'appel dictConfig() interne de
|
||||
# Django) — conserve la visibilité des erreurs dans journalctl,
|
||||
# inchangée par rapport à avant ce correctif.
|
||||
'console': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'fail2ban.axes': {
|
||||
'handlers': ['fail2ban_axes'],
|
||||
'level': 'WARNING',
|
||||
'propagate': False,
|
||||
},
|
||||
# Redéfini explicitement (même comportement que DEFAULT_LOGGING de
|
||||
# Django : handlers=['console', 'mail_admins'], level='INFO') pour
|
||||
# forcer le rattachement à NOTRE instance de mail_admins ci-dessus.
|
||||
# Sans ça, le logger 'django' garde en mémoire la référence vers
|
||||
# l'instance créée par le tout premier dictConfig() interne de
|
||||
# Django (DEFAULT_LOGGING, jamais filtrée) : les objets logger
|
||||
# Python conservent leurs handlers par référence directe, pas par
|
||||
# nom, un dictConfig() ultérieur qui ne re-déclare pas le logger
|
||||
# ne le fait donc jamais pointer vers le nouveau handler — piège
|
||||
# vécu : le filtre ci-dessus n'avait justement aucun effet tant
|
||||
# que ce logger n'était pas explicitement redéclaré ici.
|
||||
'django': {
|
||||
'handlers': ['console', 'mail_admins'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user