Stockage des journaux dans une base SQLite
Stockage des journaux dans une base SQLite
Cette page s'adresse aux développeurs. Cinq DocTypes de journalisation stockent leurs lignes dans une base SQLite dédiée du site — la base de logs — distincte de la base principale (MariaDB ou PostgreSQL). Ce sont des DocTypes virtuels : l'interface, les listes et l'ORM continuent de fonctionner normalement, et le routage vers la base de logs est transparent.
DocTypes concernés
| DocType | Rôle |
|---|---|
Journal d'activité (Activity Log) | Historique des actions : connexions, créations et modifications de documents. |
Journal des Erreurs (Error Log) | Erreurs et traces d'exceptions. |
Journal d'accès (Access Log) | Journal des accès aux documents et aux méthodes. |
Historique de navigation (Route History) | Historique de navigation de chaque utilisateur. |
Vue de page web (Web Page View) | Vues des pages du site web. |
Chaque contrôleur hérite de SQLiteLogDocument (voir ci-dessous) et chaque DocType porte is_virtual: 1 dans son JSON.
La base de logs
La connexion à la base de logs est gérée par le module frappe.utils.logging. Les tables y portent le nom habituel tab<DocType> et sont créées à la demande. Les sauvegardes du site, la suppression des données personnelles et le nettoyage périodique des journaux prennent en compte cette base.
Référence de frappe.utils.logging
| Fonction | Rôle |
|---|---|
get_log_db() | Renvoie la connexion (Database) vers la base de logs. |
log_table(doctype) | Renvoie le couple (qb, table) : un query builder et une table liés au dialecte SQLite de la base de logs. La table est créée si elle n'existe pas encore. |
log_cutoff(days) | Renvoie la date limite « il y a days jours », sous une forme directement utilisable dans un filtre sur creation (SQLite ne dispose pas de NOW()). |
is_log_doctype(doctype) | Indique si les lignes du DocType sont stockées dans la base de logs. |
connection_for(doctype) | Renvoie la connexion qui contient les lignes du DocType : la base de logs pour un DocType de journalisation, la base principale sinon. |
Écrire un contrôleur de DocType de journalisation
- Faites hériter le contrôleur de
frappe.utils.sqlite_document.SQLiteLogDocumentau lieu deDocument. L'insertion, la lecture et les vues de liste écrivent et lisent alors la base de logs. - Marquez le DocType
is_virtualdans son JSON. - Implémentez
clear_old_logs(days)à l'aide deget_log_db(),log_table()etlog_cutoff():
@staticmethod
def clear_old_logs(days=30):
db = get_log_db()
qb, table = log_table("Mon Journal")
db.sql(qb.from_(table).where(table.creation < log_cutoff(days)).delete())
db.commit()
Les Paramètres des journaux (Log Settings) délèguent le nettoyage à clear_old_logs pour ces DocTypes : la stratégie de copie puis renommage de clear_log_table ne s'applique qu'aux tables de la base principale, où un DELETE massif peut être coûteux.
Requêtes : utiliser le bon dialecte
frappe.qb et QueryBuilder.run() sont liés à la base principale du site. Construire une requête avec frappe.qb pour l'exécuter sur la base de logs produirait du SQL du dialecte principal envoyé à SQLite.
- Obtenez le query builder et la table avec
log_table(doctype). - Exécutez la requête avec
get_log_db().sql(...), ou avec les méthodes de connexion :
| Méthode | Rôle |
|---|---|
Database.qb | Query builder propre au dialecte de la connexion (propriété calculée). |
Database._run_query(query, ...) | Exécute un objet Query Builder sur cette connexion, comme QueryBuilder.run(), sans être limité à frappe.local.db. |
Database._get_query(...) | Construit une requête de type reportview sur cette connexion. |
frappe.database.query.Engine accepte aussi une instance de Database : Engine(db=get_log_db()).
Les helpers de Database (get_value, set_value, get_values, count, delete, bulk_insert, etc.) utilisent le query builder de leur propre connexion : get_log_db().set_value(...) écrit bien dans la base de logs.
Ne pas joindre les deux bases
Une requête SQL ne peut pas joindre une table de la base de logs à une table de la base principale. Le rapport Connexions par type d'utilisateur (User Connections) illustre la démarche retenue : compter les connexions par utilisateur et par jour dans la base de logs, puis compléter le type de chaque utilisateur (User.user_type) depuis la base principale avant d'agréger les résultats en Python.
Pour regrouper par jour dans la base de logs, utilisez la fonction Date de frappe.query_builder.functions (DATE() de SQLite) plutôt que CalendarDate, qui suit le dialecte de la base principale.
Index
frappe.db.add_index cible la base principale. Créez les index composites sur la base de logs, après avoir créé la table :
def on_doctype_update():
log_table("Activity Log")
db = get_log_db()
db.add_index("Activity Log", ["reference_doctype", "reference_name"])
Points de vigilance
- Truncate :
frappe.db.truncate("Error Log")échoue, car aucune tabletabError Logn'existe dans la base principale.clear_error_logsexécute unDELETEsans filtre sur la connexion de la base de logs. - Mise à jour d'un champ : préférez
connection_for(doctype).set_value(...)àfrappe.db.set_value(...)lorsque le DocType peut être un DocType de journalisation. C'est le choix retenu par l'anonymisation des politiques de rétention des données, qui parcourt aussi la base de logs. db_set:SQLiteLogDocument.db_setécrit dans la base de logs et valide (commit) cette connexion. Ne validez pas la transaction de la base principale à sa place.- Commande
clear-log-table:bench --site <site> clear-log-table --doctype <DocType> --days <N>ne fait pas de sauvegarde préalable pour un DocType de journalisation (scheduled_backupn'exporte que la base principale) et nettoie directement la base de logs.
Confidentialité et rétention
La suppression des données personnelles et l'anonymisation des politiques de rétention des données couvrent aussi la base de logs, via connection_for. Les durées de rétention saisies dans les Paramètres des journaux s'appliquent aussi à ces DocTypes.