ZEP-11 : Interface de statistiques sur les tutoriels

Suivi du developpement, état d'avancement et organisation de la ZEP

a marqué ce sujet comme résolu.

La Spécification de la ZEP-11 étant quasi-terminée, j'ouvre ce topic pour le suivi du développement de cette dernière. Personnellement je ne pourrai pas démarrer le développement à proprement parlé avant 1-2 semaines, du coup je prépare un peu le terrain.

Si jamais vous avez des connaissances dans l'un ou l'autre des compétences requises pour une tâche, manifestez vous. Plus on est fou, mieux c'est.

Comment lire l'avancement ?

Je mettrai à jour l’état d'avancement ci-dessous à chaque fois que le code de la ZEP évoluera. Voici la légende qui sera utilisée.

pas commencé

début de structure

développé à 30%

développé à 70%

développé à 100%

testé et documenté

Le travail a réaliser

I. Préparation de la base de travail

Livrable : fichier access-log de travail pour les développeurs

État d'avancement Tâches Compétences requises
Installation d'une instance de ZdS sur un serveur aucune
Configuration de nginx pour servir les pages en générant un access-log complet admin-sys/nginx
Création d'un contenu de test aucune
Mise à disposition du serveur aux membres de la communauté pour recueil de données admin-sys
Mise à disposition de l'access-log de test aux développeurs de la ZEP aucune

II. Parsing et filtrage des logs

Livrable : lib de parsing des logs

État d'avancement Tâches Compétences requises
développé à 100% Analyse/Évaluation des libs de parsing de log du monde python python
développé à 100% Si aucun module satisfaisant -> développement d'une lib de parsing python
développé à 100% Rédaction de la documentation liée à la lib python

III. Stockage des logs dans la BD

  • prérequis :
    • fichier access-log de travail pour les développeurs
    • lib de parsing des logs
  • Livrables :
    • modèle physique de données
    • script de stockage des logs dans une base de données MySQL
État d'avancement Tâches Compétences requises
développé à 100% Description du modèle de données MySQL
développé à 100% Développement du script de stockage dans la base python (django ?)
développé à 100% Rédaction des tests unitaires pour le script python
développé à 100% Rédaction de la documentation du script python

IV. Développement de l'API des statistiques

  • prérequis : modèle physique de données
  • Livrables : API des statistiques
État d'avancement Tâches Compétences requises
développé à 100% Création des views Django sous le modèle CBV Django
développé à 100% Développement des fonctions de lecture, tri et agrégation des informations de la base de donnée Django
développé à 100% Développement de l'API exploitant les fonctions Django Rest Framework
développé à 100% Rédaction des tests unitaires Django Rest Framework
développé à 100% Rédaction de la documentation Django Rest Framework

V. Développement de la page d'affichage des statistiques sur le site

  • prérequis : API des statistiques
  • Livrables : template de visualisation des statistiques
État d'avancement Tâches Compétences requises
développé à 100% Création d'une vue simple qui se connecte à l'API du site pour afficher des résultat Template Django/JS
développé à 100% Ajout des critères de filtre (date, pagination, etc.) Template Django/JS
développé à 100% Ajout des critères de tri des résultats Template Django/JS

VI. Soumission du résultat

  • prérequis : page d'affichage des statistiques sur le site
  • Livrables : Pull Request
État d'avancement Tâches Compétences requises
développé à 100% Assemblage de toutes les briques python
développé à 100% QA fonctionnelle interne aucune
développé à 100% Test de charges de l'application A voir
développé à 100% Envoi de la Pull Request Aucune

J'ai avancé sur la préparation de la base de travail. J'ai configuré les access-logs nginx du coté de la préprod (login/mot de passe = clementine/orange).

Tout ce qui vous reste a faire, c'est d'y aller et de cliquer un peu partout sur les tutoriels, articles, chapitres, etc. Comme si vous consultez du contenu. N'hésitez à pas y aller depuis n'importe quel device (TV, PC, tablette, Smartphone, Nokia 3310 ^^ ).

Je mettrai à disposition des développeurs le fichiers d'access-log une fois qu'il y'aura assez d'informations.

L'update du jour concerne le parsing et filtrage des logs.

D'après mes recherches dans pypi et github, je n'ai trouvé aucune lib satisfaisante pour parser les logs web. La seule qui m'a séduite est logtools. Mais le fait qu'elle ne soit pas compatible python3 m'a tout de suite refroidi.

Étant donné que les traitements de nos logs sont assez spécifiques (non seulement il faut parser chaque ligne de logs, mais il faut parser chaque url pour déterminer de quel type de contenu il s'agit) la lecture des logs sera faite maison. Cependant, pour ce qui est du parsing du user-agent ainsi que de la géolocalisation de l'adresse IP, nous utiliserons respectivement les lib python-user-agents et pygeoip (cette lib étant déjà incluse dans le projet).

Pour l'instant, le modèle de données de la table des statistiques imaginé ressemble à ça :

colonne type description
zds_id int Identifiant du contenu sur ZdS
content_type string Type de contenu visité
timestamp string Timestamp de la visite
referal_path string Url source de la visite
referal_dns string DNS source de la visite
ip_adress string Adresse IP du visiteur
town string Ville du visiteur
country string Pays du visiteur
device_name string Device du visiteur (Iphone, etc.)
device_type string Type de device du visiteur (mobile, tablette, pc, etc.)
os_family string Famille du système d'exploitation du visiteur (Windows, Linux, Android, etc)
os_version string Version du système d'exploitation du visiteur
browser_family string Famille du navigateur du visiteur (IE, Firefox, etc.)
browser_version string Version du navigateur du visiteur
body_bytes int taille de la page
request_time float temps de chargement de la page

Du coup, l'agent qui parse les logs et remplit la base, en termes d'architecture ça donne quoi ? "Cron" lancé depuis le code du site ? Agent à part (à la logstash-shipper).

D'ailleurs, en parlant de logstash, c'est un truc qu'il sait vachement bien faire ça, de lire/parser des lignes de logs et remplir une base (je l'ai toujours utilisé avec Redis, je crois que c'est le fonctionnement par défaut, mais il existe peut-être d'autres connecteurs).

Parce que rien que le développement de l'agent me paraît casse-gueule. Le nombre de fois où j'ai redémarré des agents logstash / logstash-shipper dans les premières versions me foutrait un peu la trouille pour me lancer dans un développement à la mano d'un tel truc.

Faut être failsafe (une ligne de log merdique qu'on sait pas ce qu'elle fout là), faut faire fichtrement efficace (volume de données énorme à traiter, grosso merdo : si tu traites moins vite que ça arrive tu peux dire adieu à ton projet, ça tiendra pas en prod), faut pas laisser traîner de handler ouverts sur les fichiers (évidemment) et faire gaffe aux fuites mémoire en tout genre, sous peine d'écrouler le serveur qui a lancé l'agent ou, a minima, d'occuper beaucoup trop de ressources et détériorer les perfs du serveur en question.

+0 -0

Je me rends pas bien compte du volume des logs. Mais oui, ça semble être la bonne solution sachant que (contrairement à logstash ou autres) on ne fait pas de supervision ici, donc le temps-réel n'a pas franchement d'importance.

Ça se benchmarke ce genre de trucs du coup, non ? Si tu fais avaler à ta moulinette N fichiers de logs (j'imagine que c'est des logs tournants) correspondant à une journée bien active, ça donne quoi en termes de temps d'exécution, occupation mémoire, etc. ?

Sachant qu'en approche naïve (je lis une ligne, je stocke, je lis la suivante je stocke, …) ça fait beaucoup de "round-trip" avec la BDD. Gaffe à la gestion des connexions / du pool aussi, si y'a un bug là-dedans tu vas te retrouver à bloquer les utilisateurs du site, ça serait moche…

'Fin bref, y'a plein de trucs auxquels il faut tout de même faire attention. (et ledit agent, ou le broker, ou la routine, ou le cron, ou peu importe comment on l'appelle va devoir être bien supervisé au début histoire d'être sûr qu'il ne fait pas de bêtises).

NB : autre truc "de merde" : la reprise sur arrêt / plantage. Faut savoir où il s'est arrêté, et récupérer la ligne correspondant dans les logs. 'Fin y'a vraiment plein de petits trucs pénibles quand on code ce genre de trucs…

+0 -0

Merci pour ces conseils avisés Javier, pour répondre à certaines de tes interrogations.

Du coup, l'agent qui parse les logs et remplit la base, en termes d'architecture ça donne quoi ? "Cron" lancé depuis le code du site ? Agent à part (à la logstash-shipper).

Ici j'ai opté pour la solution Cron. Le mode agent à la logstash-forwarder n'est pas foncièrement nécessaire pour ce qu'on cherche à faire. Notre parseur de logs ici, ne va pas faire que du parsing, il va aussi compléter la log avec des informations qu'on retrouve dans d'autres tables de la base de donnée pour obtenir la ligne à stocker.

D'ailleurs, en parlant de logstash, c'est un truc qu'il sait vachement bien faire ça, de lire/parser des lignes de logs et remplir une base (je l'ai toujours utilisé avec Redis, je crois que c'est le fonctionnement par défaut, mais il existe peut-être d'autres connecteurs).

J'y ai bien pensé, mais le fait est que logstash ne supporte pas de manière officielle (en input comme en output) les bases de données relationnelles. Or ici nous voulons écrire dans une base de donnée MySQL, il y'a toujours moyen de se débrouiller pour y arriver, mais je ne suis pas sur qu'alourdir notre stack soit une bonne chose. Bien que ça pourrait être bénéfique de passer sur du Logstash, on va éviter de faire des formations à nos troupes sur de nouveaux outils.

Parce que rien que le développement de l'agent me paraît casse-gueule. Le nombre de fois où j'ai redémarré des agents logstash / logstash-shipper dans les premières versions me foutrait un peu la trouille pour me lancer dans un développement à la mano d'un tel truc.

En spécifiant, on s'est dit qu'on fera du parsing par période, donc on a pas besoin d'avoir un collecteur branché en flux tendu sur les logs. Ce que j'envisage de faire néanmoins est de mettre à disposition une commande qui prend en paramètre la période de parsing (12h, 24h, ou 48h) à la logstash et qui s'assure qu'elle ne stockera pas de doublon (pour ça, je pense à stocker un hash de la ligne de log).

Du coup les problèmes de handler, on ne les rencontrera normalement jamais.

Je rappelle que des tests de charges sont prévus à la fin du développement de cette ZEP pour valider certaines inquiétudes.

Ceci dit, la bonne nouvelle du jour, c'est que j'aurai du temps ce soir certainement pour créer la branche officielle de développement et coder un bout du collecteur.

EDIT : arf, y'a eu des messages en cours, bon bah j'ai plus le temps de lire, je poste quand même

Merci beaucoup pour vos réponses :)

J'ai déjà fait des parseurs de logs en Java. Plus simples que ce qu'on imagine ici,

Yep moi aussi et ça tenait plutôt bien la charge. Mais justement je suis tombé sur le cas que je citais en le codant : "ah tiens merde je vais lire des infos dans une BDD relationnelle, et en écrire d'autres, gaffe au pool de connexion", c'est pour ça que j'en parlais.

Mieux vaut prévenir comme on dit.

+0 -0

J'ai l'impression qu'on part sur des solutions très compliquées là (je vois des propositions à base de fenêtre et de hash de ligne de log).

De mon point de vue, on doit simplifier ce genre de processus au maximum (simplicité, fiabilité, maintenance). Ça peut se faire avec une série de contraintes de ce genre :

  1. Le parseur ne parse qu'un fichier en entier. Les fichiers font 24h et sont "correctement découpés" (pas de chevauchement)
  2. Pendant le parsing, toutes les données calculées sont stockées en RAM
  3. Ceci fait, on enregistre le tout en même temps (c'est plus performant) et de manière atomique : soit toutes les nouvelles données sont enregistrées, soit aucune (penser à dégager l'autocommit…)
  4. On peut lancer l'import d'un fichier arbitraire (au cas où un import se soit mal passé)
  5. Bonus : on garde pour chaque donnée l'indication de sa source (hash, …) pour pouvoir la supprimer et la réimporter au cas où

Normalement on s'en sort avec une seule connexion (qui prends cher par contre) pendant le parsing et quelques très grosses requêtes à la fin.

J'ai l'impression qu'on part sur des solutions très compliquées là (je vois des propositions à base de fenêtre et de hash de ligne de log).

La fenêtre et le hash sont surtout là pour prévenir les cas de doublons. si il y'a eu un problème lors du parsing et qu'il faut le relancer, on évite de stocker deux fois la même ligne en relançant le traitement sur le même fichier.

Pendant le parsing, toutes les données calculées sont stockées en RAM

Hmm, je suis moyen chaud. Il suffit qu'on ait un gros process consommateurs de RAM en prod qui tourne au même moment pour se retrouver avec un cas tordu. Je suis partisan d'une solution a base de pool de connexion, ça représente moins de risque.

Hmm, je suis moyen chaud. Il suffit qu'on ait un gros process consommateurs de RAM en prod qui tourne au même moment pour se retrouver avec un cas tordu. Je suis partisan d'une solution a base de pool de connexion, ça représente moins de risque.

Certes, mais du coup tu ne peux pas tout commiter d'un coup, donc tu es obligé de gérer tout ce dont tu parles : le problème de parsing qui a planté au milieu, etc.

PS : je ne suis pas sûr qu'on ait assez de données calculées pour que ça pose vraiment problème.

PS : je ne suis pas sûr qu'on ait assez de données calculées pour que ça pose vraiment problème.

Ce n'est pas le nombre de données calculées qui me font peur, mais la somme des données calculées de la log sur une journée. Si on stocke toutes ces données calculées en mémoire d'un coup, on court à la catastrophe le jour ou on a un pic sur un tutoriel en particulier.

D'où la solution d'ouvrir un pool au début du traitement de fichier, de traiter ligne par ligne, de flusher dans la base les données calculées de chaque ligne au fur et à mesure de leur traitement et de fermer le pool à la fin du fichier.

Ce n'est pas le nombre de données calculées qui me font peur, mais la somme des données calculées de la log sur une journée. Si on stocke toutes ces données calculées en mémoire d'un coup, on court à la catastrophe le jour ou on a un pic sur un tutoriel en particulier.

En fait, j'ai un problème avec cette assertion. Pourquoi ?

Parce que si un pic d'accès sur un tuto provoque la création de tellement de données que ça flingue le processus qui fait les calculs, ça implique forcément qu'on va essayer de stocker plusieurs centaines de Mo en base et ce uniquement pour ce jour là. Ce qui implique que le modèle de données lui-même est cassé à la base.

Donc, pour moi, soit tout tient en base et en RAM sur la journée, soit le modèle est cassé par construction.

Parce que si un pic d'accès sur un tuto provoque la création de tellement de données que ça flingue le processus qui fait les calculs, ça implique forcément qu'on va essayer de stocker plusieurs centaines de Mo en base et ce uniquement pour ce jour là. Ce qui implique que le modèle de données lui-même est cassé à la base.

Pour prendre un exemple. Imaginons qu'en une journée on ait 50 lignes de logs à traiter et que chaque ligne implique 16 données calculées. On se retrouve avec 16 x 50 = 800 données calculées en RAM (parce qu'on ne les flush pas au fur et a mesure) d'un coup. Le jour ou on a un pic, disons qu'on passe à 1000 lignes de logs, ça revient à 1000 x 16 = 16 000 données calculées à monter en RAM d'une traite.

Il y aura biensur de la place sur le disque pour accueillir toutes ses données, mais il peut ne pas y avoir assez de RAM disponible pour ça.

Merci de me prouver que j'ai raison avec ton calcul :)

Même si tes 16000 "données" font 10 Mo chacune en RAM (ce qui est démesuré), ça fait un total de… 160 Mo de RAM. Ce qui n'est absolument pas un problème.

Non, j'insiste : on traite les données au jour le jour (c'est la clé de la compréhension du truc : on traite toujours et uniquement un jour entier de données à la fois).

Si jamais on se retrouve à avoir tant de données sur une journée que ça ne rentre pas dans quelques centaines de Mo de RAM, c'est qu'on a besoin d'insérer quelques centaines de Mo à insérer en base.

Et un modèle de stats qui nécessite d'insérer "quelques centaines de Mo" sur une seule journée n'est pas viable, quoiqu'il arrive.

Donc, dans tous les cas, on a garantie par construction que tout modèle viable permet de stocker toutes les données calculées en RAM avant de les enregistrer.

Ben d'habitude j'essaie d'éviter ; mais là ça nous permet de tout commiter d'un coup et nous simplifie tellement le système si ça fonctionne que ce serait dommage de s'en passer je trouve.

Pour moi, la difficulté serait plus de faire comprendre à MySQL qu'on a une transaction de potentiellement plusieurs dizaines de Mo. Je sais que ça existe avec Oracle, mais j'ai jamais essayé avec MySQL.

J'avais en tête de poser ça sur une base de donnée MySQL avec un moteur MyISAM pour accélérer la lecture derrière. Du coup c'est pour ça qu'il va falloir benchmarker ces solutions pour voir ce qui passe le mieux.

J'utiliserai ce topic certainement pour le résultat des benchs.

Les perfs de InnoDB se sont beaucoup améliorées avec la v5.5 et la v5.6 (plus difficile à trouver dans les packages).

Peut-être qu'on pourrait commencer avec du InnoDB standard, et si ça paraît juste vérifier avec MyISAM si c'est vraiment plus rapide ?

L'intérêt derrière tout ça, c'est de conserver la notion de transaction (qui je crois est absente de chez MyISAM).

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte