Magento 2. Monolog ou comment écrire des journaux

En étudiant les différents modules de Magento 2, vous remarquerez que la journalisation est utilisée beaucoup moins fréquemment que Magento 1. Cela est dû en grande partie au fait que la journalisation est devenue plus difficile. Ici, je voudrais me concentrer sur le côté technique du problème, à savoir comment enregistrer les données, comment écrire des journaux dans votre propre fichier et qu'est-ce que Monolog.

Table des matières


Monolog
Fonctionnalités de l'application dans Magento 2
Implémentation
Journalisation à l'aide de l'enregistreur standard
Journalisation à l'aide d'un enregistreur standard avec un canal personnalisé
Écriture dans un fichier personnalisé à l'aide de votre propre gestionnaire
Écriture dans un fichier personnalisé à l'aide de virtualType
Enregistrement rapide des données
Conclusion

Monolog


Commençons par la question la plus importante - Qu'est-ce que Monolog et d'où vient-il?

Monolog - Il s'agit d'une bibliothèque qui implémente la norme PSR-3 pour l'enregistrement des données. C'est Monolog qui est utilisé dans Magento 2 pour enregistrer les journaux.

Le PSR-3 est à son tour une norme qui décrit une approche commune pour l'enregistrement des données et des recommandations pour la mise en œuvre des enregistreurs qui fournissent une interface commune.

Points forts du PSR-3
1. L' enregistreur (objet) doit implémenter l'interface \ Psr \ Log \ LoggerInterface.
2. Nous avons les niveaux d'erreur suivants (indiqués par ordre de priorité du plus grand au moins):
URGENCE - Le système est inutilisable.
ALERTE - Des mesures doivent être prises immédiatement. Exemple: site Web entier en panne, base de données indisponible, etc.
CRITIQUE - Conditions critiques. Exemple: composant d'application non disponible, exception inattendue.
ERREUR - Erreurs d'exécution qui ne nécessitent pas d'action immédiate mais doivent généralement être surveillées.
AVERTISSEMENT - Occurrences exceptionnelles qui ne sont pas des erreurs. Exemple: utilisation d'API obsolètes.
AVIS - Événements normaux mais significatifs.
INFO - Evénements intéressants. Exemple: l'utilisateur se connecte, les journaux SQL.
DEBUG - Informations de débogage détaillées.

3. Chaque niveau a sa propre méthode (débogage, info, avis, avertissement, erreur, critique, alerte, urgence / urgence) et il devrait également y avoir une méthode de journalisation, qui prend le niveau d'erreur comme premier paramètre.
4. Les méthodes acceptent une chaîne ou tout ce qui implémente __toString () (c'est-à-dire que vous devez utiliser print_r ($ message, true) manuellement pour les tableaux ou les passer dans le paramètre suivant).
5. Toutes les méthodes acceptent un tableau $ context qui complète le journal.
6. Peut, mais pas nécessairement, implémenter la substitution des données du tableau $ context dans le message. Dans ce cas, le format {nom} est recommandé, où nom -> la clé du tableau dans $ context.


Monolog est assez facile à utiliser. Regardons l'exemple suivant.

use Monolog\Logger; use Monolog\Handler\StreamHandler; use Monolog\Formatter\HtmlFormatter; //     "name" $log = new Logger('name'); //  ,      "path/to/your1.log"       "WARNING"   (notice, info  debug   ). $log->pushHandler(new StreamHandler('path/to/your1.log', Logger::WARNING)); //  ,      "path/to/your2.log"       "ALERT"   (..        alert  emergency).    ,    .         html   HtmlFormatter. $log->pushHandler(new StreamHandler('path/to/your2.log', Logger::ALERT,false) ->setFormatter(new HtmlFormatter)); //  ,       . $log->pushProcessor(function ($record) { $record['extra']['dummy'] = 'Hello world!'; return $record; }); //   $log->warning('Foo'); $log->error('Bar',['test']); $log->info('Test'); //   ,    1     INFO //your1.log // [2019-08-12 02:57:52] name.WARNING: Foo [] ['extra'=>['dummy'=>'Hello world!']] // [2019-08-12 02:57:53] name.ERROR: BAR ['test'] ['extra'=>['dummy'=>'Hello world!']] //your2.log // , .    ALERT  EMERGENCY 

Points saillants du travail de Monolog à garder à l'esprit:

  • L'enregistreur est un objet que nous utilisons pour enregistrer des journaux. L'enregistreur lui-même n'enregistre pas, mais gère les gestionnaires. N'importe quel nombre peut être créé.
  • Handler - un objet qui traite directement les données. Vous pouvez ajouter autant de gestionnaires à l'enregistreur que vous le souhaitez. Tous seront appelés tour à tour, que le gestionnaire donné ait pu traiter l'erreur ou non. La méthode isHandling détermine si ce gestionnaire sera capable de gérer l'erreur reçue.

     public function isHandling(array $record) { return $record['level'] >= $this->level; } 

    La ressemblance la plus proche avec le gestionnaire, à mon avis, est l'observateur d'événements.
  • Processeur - toute entité appelée (appelable). Peut-être quelques-uns. Ils peuvent être attribués globalement et installés pour le gestionnaire. Premièrement, des processeurs mondiaux sont lancés. La tâche principale du processeur consiste à ajouter des données supplémentaires au journal (par exemple, l'adresse IP à partir de laquelle la connexion était, la valeur des variables globales, les informations sur la branche git sur laquelle se trouve le code, etc.).
  • Formateur - Convertit la sortie du message avant l'écriture. Il ne peut y en avoir qu'un par gestionnaire. Il est nécessaire de modifier la mise en forme du corps du message, par exemple, pour convertir du texte en html ou json.
  • Canal - le nom de l'enregistreur. Il sera écrit lors de l'enregistrement du journal. Comme 1 gestionnaire peut être utilisé dans 2 enregistreurs différents (il écrira les journaux dans 1 même fichier), cela déterminera d'où vient l'erreur.
  • Niveau - niveau d'erreur. Ce paramètre pour le gestionnaire signifie le niveau d'erreur minimum qu'il gérera.
  • Bulle - popup de message. Une fois que le gestionnaire a traité le message, l'enregistreur transmet le message au gestionnaire suivant. Ce processus peut être arrêté à l'aide de la propriété bubble. Si le gestionnaire a la valeur de cette propriété false (la valeur par défaut est toujours vraie), après que ce gestionnaire a fait son travail (a pu traiter cette erreur), les autres gestionnaires ne démarrent pas.
  • Ordre de tri - ordre d'exécution. Le dernier gestionnaire ajouté est toujours lancé le tout premier. C'est cette fonctionnalité qui vous permet de mettre en œuvre un mécanisme pour désactiver complètement l'enregistreur (via bullage faux). Les gestionnaires ajoutés via le constructeur vont dans l'ordre spécifié dans le constructeur.

Étant donné que PSR-3 n'oblige pas les développeurs à implémenter des valeurs de correction automatique dans le texte, Monolog ne le fait pas par défaut. Si vous écrivez -> Emerg ('test 1111 {placeholder}', ['placeholder' => 'foo']), vous obtiendrez ce qui suit
[2019-08-12 02:57:52] main.EMERGENCY: test 1111 {placeholder} {"placeholder": "foo"} []

Pour que le remplacement fonctionne, vous devez connecter un processeur supplémentaire - \ Monolog \ Processor \ PsrLogMessageProcessor.
Il vaut la peine de dire que Monolog a un grand nombre de formateurs, processeurs, gestionnaires prêts à l'emploi. Vous pouvez les utiliser ou écrire les vôtres.

Fonctionnalités de l'application dans Magento 2


Sur le site officiel de Magento, vous pouvez trouver un exemple général de la façon d'utiliser l'enregistreur. Malheureusement, l'exemple présenté ne révèle pas tous les détails et, hélas, ne répond pas à la question «comment écrire des journaux dans votre propre fichier». Par conséquent, comprenons tout plus en détail.

À l'époque de Magento 1, tout le monde utilisait probablement tôt ou tard la méthode Mage :: log, qui était disponible partout dans le code et l'entrée de journal la plus simple ressemblait à Mage :: log ('ALARM!', Null, 'api.log'). Par conséquent, nous avions un enregistrement du formulaire suivant dans le fichier var / log / api.log

 2019-08-12T01:00:27+00:00 DEBUG (7): ALARM! 

Format par défaut:% timestamp %% priorityName% (% priority%):% message%.

Voyons comment enregistrer les données dans le cas le plus simple de Magento 2. Le plus souvent, vous utiliserez $ this -> _ logger-> info ('ALARM!'); (si un objet possède une telle propriété, par exemple, hérité).

À la suite d'un tel appel, nous obtenons l'entrée suivante dans le fichier var / log / system.log

 [2019-08-12 02:56:43] main.INFO: ALARM! [] [] 

Le format par défaut est [% datetime%]% channel%.% Level_name%:% message %% context %% extra%
Si l'objet n'a pas une telle propriété (_logger ou logger), alors nous devons d'abord ajouter la dépendance \ Psr \ Log \ LoggerInterface à votre classe et écrire l'objet résultant dans la propriété $ logger (selon la clause 4.2 du PSR-2 et l' exemple présenté sur le site Web de Magento ) .
Contrairement à Magento 1, il y a beaucoup plus de nuances ici.

1. Paramètres de journalisation.

Considérons un appel général à la méthode d'écriture

 $this->_logger->{level}($message, $context = []); //$this->_logger->log('{level}', $message, $context = []); 

1) Où {niveau} est, selon le PSR-3, 1 des méthodes réservées pour enregistrer un certain niveau d'erreur (débogage, info, avis, avertissement, erreur, critique, alerte, émergence / urgence).
2) $ message - contrairement à Magento 1, ce devrait être une chaîne. C'est-à-dire $ object-> getData () ne fonctionnera pas ici. Le tableau de données doit être passé au paramètre suivant. Les objets \ Exception sont une exception, car l'implémentation de \ Magento \ Framework \ Logger \ Monolog les traite séparément et fait automatiquement rouler -> getMessage () plus loin en tant que $ message si l'objet \ Exception a été passé en tant que message.
3) $ context est un paramètre facultatif, un tableau.

2. La propriété $ this -> _ logger n'est pas disponible dans toutes les classes.

Présent dans: Block, Helper, Model, Collection, etc.
Non disponible dans: ResourceModel, Controller, Comand, Setup, etc.

En savoir plus sur ResourceModel et Collection.
ResourceModel a la propriété _logger, mais elle n'est pas remplie dans le constructeur. Il est rempli uniquement à l'aide de la méthode getLogger privée dans \ Magento \ Framework \ Model \ ResourceModel \ AbstractResource. La méthode n'est appelée qu'en cas d'erreur lors de l'écriture dans la base de données (dans le bloc catch) à l'intérieur de la méthode commit (). Jusque-là, le modèle de ressource n'aura pas d'enregistreur.

 public function commit() { $this->getConnection()->commit(); if ($this->getConnection()->getTransactionLevel() === 0) { $callbacks = CallbackPool::get(spl_object_hash($this->getConnection())); try { foreach ($callbacks as $callback) { call_user_func($callback); } } catch (\Exception $e) { $this->getLogger()->critical($e); } } return $this; } … private function getLogger() { if (null === $this->_logger) { $this->_logger = ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); } return $this->_logger; } 

La collection a un enregistreur dès le début. Il est attribué dans le constructeur \ Magento \ Framework \ Data \ Collection \ AbstractDb et hérité plus tard.

Il est impossible de ne pas le dire, mais dans les contrôleurs, il existe un moyen d'obtenir l'enregistreur à l'aide de l'ObjectManager (via la propriété $ this -> _ objectManager). Mais ce n'est bien sûr pas la manière la plus correcte.

3. L'enregistreur par défaut et la liste des gestionnaires.

Dans le fichier di.xml global (app / etc / di.xml), vous pouvez trouver que \ Psr \ Log \ LoggerInterface est implémenté par la classe \ Magento \ Framework \ Logger \ Monolog, qui à son tour hérite de \ Monolog \ Logger. Le nom de l'enregistreur est principal. Plusieurs gestionnaires y sont également définis.

 <preference for="Psr\Log\LoggerInterface" type="Magento\Framework\Logger\Monolog" /> ... <type name="Magento\Framework\Logger\Monolog"> <arguments> <argument name="name" xsi:type="string">main</argument> <argument name="handlers" xsi:type="array"> <item name="system" xsi:type="object">Magento\Framework\Logger\Handler\System</item> <item name="debug" xsi:type="object">Magento\Framework\Logger\Handler\Debug</item> <item name="syslog" xsi:type="object">Magento\Framework\Logger\Handler\Syslog</item> </argument> </arguments> </type> ... 

Certaines classes diffèrent de celles répertoriées ci-dessus (car elles sont redéfinies dans le module Magento \ Developer):

1) Magento \ Framework \ Logger \ Handler \ System ( écoute INFO)
2) Magento \ Developer \ Model \ Logger \ Handler \ Debug ( écoute sur DEBUG )
3) Magento \ Developer \ Model \ Logger \ Handler \ Syslog ( écoute DEBUG )

Dans les classes spécifiées (Debug et Syslog) , la possibilité de désactiver la journalisation (dev / debug / debug_logging et dev / syslog / syslog_logging, respectivement) est ajoutée.

Notez qu'il n'y a aucun gestionnaire d'exceptions dans la liste des gestionnaires qui écrit dans exception.log. Il est appelé dans le gestionnaire de système.

Magento \ Framework \ Logger \ Handler \ System
 ... public function write(array $record) { if (isset($record['context']['exception'])) { $this->exceptionHandler->handle($record); return; } $record['formatted'] = $this->getFormatter()->format($record); parent::write($record); } ... 


Magento 2 à 2.2 a eu un problème avec l'enregistreur incapable de passer à un autre gestionnaire après le premier trouvé. Ce problème était dû au fait que Monolog s'attendait à ce que tous les gestionnaires y arrivent dans un tableau avec des clés numériques et avec des clés alphabétiques (['system' =>, 'debug' =>, ...]). Les développeurs de Magento ont ensuite corrigé la situation - ils convertissent le hachage en un tableau régulier avec des clés numériques avant de le transmettre à Monolog. Monolog a désormais également modifié l'algorithme d'énumération du gestionnaire et utilise la méthode next ().
4. Présentation de votre gestionnaire dans la liste des gestionnaires existants.

Nous arrivons à la chose la plus intéressante, qui gâche un peu l'impression d'implémentation dans Magento 2. Vous ne pouvez pas ajouter un gestionnaire personnalisé à la liste des gestionnaires existants en utilisant di.xml sans ... "gestes supplémentaires". Cela est dû au principe des configurations de fusion.

Il existe plusieurs étendues de configuration :

1) Initiale (app / etc / di.xml)
2) Global ({moduleDir} /etc/di.xml)
3) Spécifique à la zone ({moduleDir} / etc / {area} /di.xml, c'est-à-dire Frontend / adminhtml / crontab / webapi_soap / webapi_rest, etc.)

À l'intérieur du niveau 1, les configurations sont fusionnées, mais le niveau suivant les redéfinit lors de la fusion (si elles y sont également déclarées). Cela rend impossible d'ajouter des gestionnaires dans leurs modules à la liste existante, car il est déclaré dans la portée initiale.

Peut-être qu'à l'avenir, nous verrons une implémentation dans laquelle l'ajout de gestionnaires sera déplacé de la portée initiale vers un autre module, transféré ainsi à la portée globale.

Implémentation


Examinons les principales façons d'enregistrer les journaux, qui peuvent nous être utiles dans la mise en œuvre des tâches.

1. Journalisation à l'aide de l'enregistreur standard


Cette méthode nous permet d'écrire facilement des journaux dans l'un des journaux standard (debug.log, system.log ou exception.log).

 class RandomClass { private $logger; public function __construct(\Psr\Log\LoggerInterface $logger) { $this->logger = $logger; } public function foo() { $this->logger->info('Something went wrong'); //[...some date...] main.INFO: Something went wrong [] [] } } 

Tout devient encore plus simple s'il existe déjà une dépendance héritée de l'enregistreur dans notre classe.

 $this->_logger->info('Something went wrong'); //    ->debug,   ,      debug.log ... 

2. Journalisation à l'aide d'un enregistreur standard avec un canal personnalisé


Cette méthode diffère de la précédente en ce sens qu'un clone de l'enregistreur est créé et qu'un autre canal (nom) lui est attribué. Ce qui simplifiera la recherche dans le fichier journal.

 class RandomClass { private $logger; public function __construct(\Psr\Log\LoggerInterface $logger) { $this->logger = $logger->withName('api'); //    } public function foo() { $this->logger->info('Something went wrong'); //[...some date...] api.INFO: Something went wrong [] [] } } 


Pour rechercher les journaux nécessaires, il suffit maintenant d'utiliser la recherche par «api» (l'enregistreur par défaut dans Magento 2 est appelé principal) dans les fichiers system.log, debug.log, exception.log existants. Peut utiliser
 grep -rn 'api' var/log/system.log 


3. Écrire dans un fichier personnalisé à l'aide de votre propre gestionnaire


Créons un gestionnaire simple qui enregistre toutes les erreurs de niveau critique et supérieur dans un fichier distinct var / log / critical.log. Ajoutez la possibilité de bloquer tous les autres gestionnaires pour un niveau d'erreur donné et plus. Cela évitera la duplication des données dans les fichiers debug.log et system.log.

 <?php namespace Oxis\Log\Logger\Handler; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Logger\Handler\Base; use Monolog\Logger; class Test extends Base { protected $fileName = 'var/log/critical.log'; protected $loggerType = Logger::CRITICAL; public function __construct(DriverInterface $filesystem) { parent::__construct($filesystem,null,null); $this->bubble = false; //      setBubble() } } 

Dans Magento 2 2.2+ dans le constructeur \ Magento \ Framework \ Logger \ Handler \ Base, la façon de traiter le chemin d'accès au fichier journal a changé
 // BP . $this->fileName // BP. DIRECTORY_SEPARATOR . $this->fileName 

Par conséquent, dans les anciens gestionnaires, vous pouvez toujours trouver / au début de $ fileName.


Par exemple, une petite explication mérite d'être donnée. Étant donné que Base ne vous permet pas de définir la propriété bubble via les paramètres du constructeur, nous devons soit répéter une partie du code du constructeur Base pour transmettre correctement le paramètre d'entrée au parent de la classe Base (qui, soit dit en passant, possède un paramètre d'entrée pour définir cette propriété), soit utiliser une telle approche. J'ai choisi la deuxième option.

 use Oxis\Log\Logger\Handler\Test; use Psr\Log\LoggerInterface; class RandomClass { private $logger; public function __construct( LoggerInterface $logger, Test $handler ) { $logger->pushHandler($handler); //  setHandlers([$handler]),        . $this->logger = $logger; } public function foo() { $this->logger->critical('Something went wrong'); //      critical.log //[...some date...] main.CRITICAL: Something went wrong [] [] } } 

Cette méthode d'ajout d'un gestionnaire n'est pas idéale, mais vous permet de vous éloigner de la portée de configuration du problème, ce qui nous obligera à dupliquer tous les enregistreurs dans notre di.xml. Si l'objectif est de remplacer tous les enregistreurs par les vôtres, il est préférable d'utiliser l'approche virtualType, que nous examinerons plus loin.

4. Écriture dans un fichier personnalisé à l'aide de virtualType


Cette approche nous permet de forcer la classe dont nous avons besoin pour écrire des journaux dans le fichier journal spécifié pour cela en utilisant di.xml. Vous pouvez trouver une approche similaire dans les modules Magento \ Paiement et Magento \ Expédition. J'attire votre attention sur le fait que cette approche fonctionne à partir de Magento 2 2.2 et supérieur.
Dans Magento 2 2.2+, un nouveau paramètre a été ajouté au constructeur \ Magento \ Framework \ Logger \ Handler \ Base, qui vous permet de créer un gestionnaire virtuel et de spécifier via di.xml le chemin relatif vers le fichier pour écrire le journal. Auparavant, il était nécessaire de spécifier le chemin d'accès complet via $ filePath, ou de créer un nouveau gestionnaire et d'écrire le chemin d'accès relatif à la propriété du fichier protégé $ fileName.

Dans le di.xml de notre module, ajoutez ce qui suit
  <!--      ,     --> <virtualType name="ApiHandler" type="Magento\Framework\Logger\Handler\Base"> <arguments> <argument name="fileName" xsi:type="string">var/log/api.log</argument> </arguments> </virtualType> <!--  ,     --> <virtualType name="ApiLogger" type="Magento\Framework\Logger\Monolog"> <arguments> <argument name="name" xsi:type="string">api</argument> <argument name="handlers" xsi:type="array"> <item name="default" xsi:type="object">ApiHandler</item> </argument> </arguments> </virtualType> <!--       --> <type name="Oxis\Log\Model\A"> <arguments> <argument name="logger" xsi:type="object">ApiLogger</argument> </arguments> </type> 

Ajoutez une classe d'enregistreur à Oxis \ Log \ Model \ A.
 namespace Oxis\Log\Model; class A { private $logger; public function __construct(\Psr\Log\LoggerInterface $logger) { $this->logger = $logger; } public function foo() { $this->logger->info('Something went wrong'); } } 

Maintenant, absolument tous les journaux qui seront écrits dans notre classe seront traités par notre version de l'enregistreur, qui écrira les journaux en utilisant notre gestionnaire dans le fichier var / log / api.log.

4.1. Si la classe reçoit l'enregistreur via l'objet $ context, et non via son constructeur.
Cela inclut \ Magento \ Catalog \ Model \ Product, dont les dépendances n'ont pas \ Psr \ Log \ LoggerInterface, mais il existe \ Magento \ Framework \ Model \ Context par lequel l'enregistreur est défini sur la propriété de classe. Dans ce cas, nous devons compliquer un peu l'option ci-dessus et remplacer l'enregistreur situé dans l'objet $ context. Et pour que cela n'affecte pas l'ensemble de Magento, nous remplacerons $ context uniquement pour notre classe par virtualType.
 <virtualType name="ApiHandler" type="Magento\Framework\Logger\Handler\Base"> <arguments> <argument name="fileName" xsi:type="string">var/log/api.log</argument> </arguments> </virtualType> <virtualType name="ApiLogger" type="Magento\Framework\Logger\Monolog"> <arguments> <argument name="name" xsi:type="string">api</argument> <argument name="handlers" xsi:type="array"> <item name="default" xsi:type="object">ApiHandler</item> </argument> </arguments> </virtualType> <!--       logger--> <virtualType name="ApiLogContainingContext" type="Magento\Framework\Model\Context"> <arguments> <argument name="logger" xsi:type="object">ApiLogger</argument> </arguments> </virtualType> <!--    --> <type name="Oxis\Log\Model\A"> <arguments> <argument name="context" xsi:type="object">ApiLogContainingContext</argument> </arguments> </type> 


5. Enregistrement rapide des données


Il y a des moments où nous devons ajouter rapidement la journalisation. Le plus souvent, cela peut être nécessaire sur le serveur de production ou pour des tests rapides.
 ... $log = new \Monolog\Logger('custom', [new \Monolog\Handler\StreamHandler(BP.'/var/log/custom.log')]); $log->error('test'); ... 

Avantages de cette approche: écrit une date, il y a un contexte (tableau), ajoute automatiquement \ n à la fin

Dans l'exemple ci-dessus, \ Monolog \ Logger est spécifiquement appliqué, et non \ Magento \ Framework \ Logger \ Monolog qui l'étend. Le fait est qu'avec cette utilisation il n'y a pas de différence, mais écrire moins (et s'en souvenir plus facilement).

\ Monolog \ Handler \ StreamHandler, à son tour, est utilisé à la place de \ Magento \ Framework \ Logger \ Handler \ Base car l'utilisation de Base comme extrait de code n'est pas très pratique en raison de dépendances supplémentaires sur les classes tierces.

Une autre approche dont on ne peut pas parler est le bon vieux file_put_contents.

 ... file_put_contents(BP.'/var/log/custom.log', 'test',FILE_APPEND); ... 

Avantages de cette approche: écrivez relativement rapidement et n'avez pas besoin de vous souvenir des cours.

Dans les deux cas, le rôle principal est joué par le BP constant. Elle pointe toujours vers le dossier avec le magenta (1 niveau plus haut que le pub), ce qui est pratique et nous aide toujours à écrire les journaux au bon endroit.

Conclusion


J'espère que les informations ci-dessus vous ont été ou vous seront utiles.

Source: https://habr.com/ru/post/fr450636/


All Articles