Nouvelles annonces sur les technologies sans serveur et réseau ainsi que sur le RUM (Real-User Monitoring) dévoilées à la conférence Dash ! Nouvelles annonces dévoilées à la conférence Dash !

Collecte de logs avec PHP

Présentation

Rédigez vos logs PHP dans un fichier, puis utilisez l’Agent pour les transmettre à Datadog. Vous avez la possibilité de choisir parmi les bibliothèques de journalisation suivantes : Monolog, Zend-Log ou Symfonie.

Implémentation

Installation

Utilisez Composer pour ajouter Monolog en tant que dépendance :

composer require "monolog/monolog"

Vous pouvez également l’installer manuellement :

  1. Téléchargez Monolog depuis le référentiel et ajoutez-le aux bibliothèques.
  2. Lancez l’instance dans le bootstrap de l’application.

    <?php
      require __DIR__ . '/vendor/autoload.php';
    
      // load Monolog library
      use Monolog\Logger;
      use Monolog\Handler\StreamHandler;
      use Monolog\Formatter\JsonFormatter;
    ?>
    

Zend-log fait partie du framework Zend. Utilisez Composer pour ajouter Zend-Log :

composer require "zendframework/zend-log"

Vous pouvez également l’installer manuellement :

  1. Téléchargez la source depuis le référentiel et ajoutez-la aux bibliothèques.
  2. Lancez l’instance dans le bootstrap de l’application.
<?php
  require __DIR__ . '/vendor/autoload.php';

  use Zend\Log\Logger;
  use Zend\Log\Writer\Stream;
  use Zend\Log\Formatter\JsonFormatter;
?>

Déclarez un formateur JSON Monolog en tant que service :

services:
    monolog.json_formatter:
        class: Monolog\Formatter\JsonFormatter

Configuration du logger

Utilisez la configuration ci-dessous pour activer le format JSON et enregistrer les logs et les événements dans le fichier application-json.log. Juste après avoir lancé l’instance Monolog, modifiez votre code afin d’ajouter un nouveau gestionnaire :

 <?php
  require __DIR__ . '/vendor/autoload.php';

  // Charger la bibliothèque Monolog
  use Monolog\Logger;
  use Monolog\Handler\StreamHandler;
  use Monolog\Formatter\JsonFormatter;

  // Créer un canal pour l'enregistrement des logs
  $log = new Logger('channel_name');

  // Créer un formateur JSON
  $formatter = new JsonFormatter();

  // Créer un gestionnaire
  $stream = new StreamHandler(__DIR__.'/application-json.log', Logger::DEBUG);
  $stream->setFormatter($formatter);

  // Connexion
  $log->pushHandler($stream);

  // Un exemple
  $log->info('Ajout d'un nouvel utilisateur', array('username' => 'Seldaek'));
?>

Utilisez la configuration ci-dessous pour activer le format JSON et enregistrer les logs et les événements dans le fichier application-json.log. Juste après avoir lancé l’instance Zend-log, modifiez votre code afin d’ajouter un nouveau gestionnaire.

<?php
  use Zend\Log\Logger;
  use Zend\Log\Writer\Stream;
  use Zend\Log\Formatter\JsonFormatter;


  // Créer un logger
  $logger = new Logger();

  // Créer un service d'écriture
  $writer = new Stream('file://' . __DIR__ . '/application-json.log');

  // Créer un formateur JSON
  $formatter = new JsonFormatter();
  $writer->setFormatter($formatter);

  // Connexion
  $logger->addWriter($writer);
  Zend\Log\Logger::registerErrorHandler($logger);
?>

Transférez ensuite vos fichiers de log à Datadog.

Configurez le formateur dans votre configuration Monolog en déclarant le champ formatter comme suit :

 monolog:
    handlers:
        main:
            type:   stream
            path:   "%kernel.logs_dir%/%kernel.environment%.log"
            level:  debug
            formatter: monolog.json_formatter

Configuration de l’Agent

Créez un fichier php.d/conf.yaml dans votre dossier conf.d/ avec le contenu suivant :

init_config:

instances:

## Section Log
logs:

    ## - type (obligatoire) : type de fichier de la source d'entrée de log (tcp/udp/file).
    ##   port / path (obligatoire) : définit le type tcp ou udp du port. Choisit le chemin si le type est défini sur file.
    ##   service (obligatoire) : nom du service propriétaire du log.
    ##   source (obligatoire) : attribut qui définit l'intégration qui envoie les logs.
    ##   sourcecategory (facultatif) : attribut à valeur multiple. Il peut être utilisé pour préciser l'attribut source.
    ##   tags (facultatif) : ajoute des tags à chaque log recueilli.

  - type: file
    path: /chemin/vers/votre/php/application-json.log
    service: php
    source: php
    sourcecategory: sourcecode

Ajout de contexte

L’ajout de données de contexte à vos logs et événements est particulièrement utile. Monolog rend cette opération simple en proposant différents moyens de définir des données de contexte propres à chaque thread, qui sont ensuite automatiquement envoyées avec tous les événements. À tout moment, il vous est possible de loguer un événement accompagné de données de contexte :

<?php
  $logger->info('Ajout d'un nouvel utilisateur', array('username' => 'Seldaek'));
?>

Monolog intègre un préprocesseur. Il s’agit d’un rappel simple qui enrichit vos événements en ajoutant les métadonnées de votre choix (ID de la session, ID de la requête, etc.) :

 <?php
  $log->pushProcessor(function ($record) {

      // Enregistrer l'utilisateur actuel
      $user = Acme::getCurrentUser();
      $record['context']['user'] = array(
          'name' => $user->getName(),
          'username' => $user->getUsername(),
          'email' => $user->getEmail(),
      );

      // Ajouter différents tags
      $record['ddtags'] = array('key' => 'value');

      // Ajouter des données de contexte générales
      $record['extra']['key'] = 'value';

      return $record;
  });
?>

La majorité des informations utiles proviennent des données de contexte supplémentaire que vous pouvez ajouter à vos logs et événements. Zend-Log rend cette opération simple en proposant différents moyens de définir des données de contexte propres à chaque thread, qui sont ensuite automatiquement envoyées avec chaque événement. À tout moment, il vous est possible de loguer un événement accompagné de données de contexte :

<?php
  $logger->info('Ajout d'un nouvel utilisateur', array('username' => 'Seldaek'));
?>

Plus utile encore, la bibliothèque intègre également un processeur. Les processeurs vous permettent d’ajouter des informations supplémentaires à vos logs de façon automatisée. Ils sont appelés par le logger avant que l’événement ne soit transmis au service d’écriture ; ils reçoivent alors le tableau des événements, puis renvoient un tableau des événements une fois l’opération terminée.

Exemples de cas d’utilisation :

  • Ajout d’informations pour le suivi des exceptions
  • Injection de substitutions dans le message
  • Injection d’un identifiant de requête (afin de pouvoir inspecter ultérieurement les logs associés à un identifiant spécifique)

Vous pouvez utiliser le code suivant si vous le souhaitez :

<?php
  $logger->addProcessor(new Zend\Log\Processor\Backtrace());
  $logger->addProcessor(new Zend\Log\Processor\PsrPlaceholder());
  $logger->addProcessor(new Zend\Log\Processor\ReferenceId());
  $logger->addProcessor(new Zend\Log\Processor\RequestId());
?>

Si vous souhaitez rédiger votre propre code, consultez la documentation relative à Zend (en anglais).

Ajoutez un processeur de session pour inclure des données de contexte variables dans vos logs :

  1. Implémentez votre processeur de session : Voici un exemple de processeur. Ce dernier connaît la session actuelle et enrichit l’entrée de log en y ajoutant des informations utiles telles que les attributs requestId, sessionId, etc.

    <?php
      namespace Acme\Bundle\MonologBundle\Log;
    
      use Symfony\Component\HttpFoundation\Session\Session;
    
      class SessionRequestProcessor {
        private $session;
        private $sessionId;
        private $requestId;
        private $_server;
        private $_get;
        private $_post;
    
        public function __construct(Session $session) {
          $this->session = $session;
        }
    
        public function processRecord(array $record) {
          if (null === $this->requestId) {
            if ('cli' === php_sapi_name()) {
              $this->sessionId = getmypid();
            } else {
              try {
                $this->session->start();
                $this->sessionId = $this->session->getId();
              } catch (\RuntimeException $e) {
                $this->sessionId = '????????';
              }
            }
            $this->requestId = substr(uniqid(), -8);
            $this->_server = array(
              'http.url' => (@$_SERVER['HTTP_HOST']).'/'.(@$_SERVER['REQUEST_URI']),
              'http.method' => @$_SERVER['REQUEST_METHOD'],
              'http.useragent' => @$_SERVER['HTTP_USER_AGENT'],
              'http.referer' => @$_SERVER['HTTP_REFERER'],
              'http.x_forwarded_for' => @$_SERVER['HTTP_X_FORWARDED_FOR']
            );
            $this->_post = $this->clean($_POST);
            $this->_get = $this->clean($_GET);
          }
          $record['http.request_id'] = $this->requestId;
          $record['http.session_id'] = $this->sessionId;
          $record['http.url'] = $this->_server['http.url'];
          $record['http.method'] = $this->_server['http.method'];
          $record['http.useragent'] = $this->_server['http.useragent'];
          $record['http.referer'] = $this->_server['http.referer'];
          $record['http.x_forwarded_for'] = $this->_server['http.x_forwarded_for'];
    
          return $record;
        }
    
        protected function clean($array) {
          $toReturn = array();
          foreach(array_keys($array) as $key) {
            if (false !== strpos($key, 'password')) {
              // Do not add
            } else if (false !== strpos($key, 'csrf_token')) {
              // Do not add
            } else {
              $toReturn[$key] = $array[$key];
            }
          }
    
          return $toReturn;
        }
      }
    ?>
    
  2. Connectez le processeur à Symfony :

    services:
      monolog.processor.session_request:
          class: Acme\Bundle\MonologBundle\Log\SessionRequestProcessor
          arguments:  [ @session ]
          tags:
              - { name: monolog.processor, method: processRecord }
  3. Transférez le fichier JSON généré à Datadog.

Intégration de Monolog à un framework

Monolog est intégré aux frameworks suivants :

Intégrez Monolog à votre framework, puis configurez votre logger :

 <?php
  // Vérifier que la bibliothèque Monolog est bien chargée
  //use Monolog\Logger;
  //use Monolog\Handler\StreamHandler;
  //use Monolog\Formatter\JsonFormatter;

  // Avec l'instance Monolog suivante
  $monolog = ...

  ///// Configuration du log shipper

  $formatter = new JsonFormatter();
  $stream = new StreamHandler(__DIR__.'/application-json.log', Logger::DEBUG);
  $stream->setFormatter($formatter);

  $monolog->pushHandler($stream);
  return $r;
?>

Symfony (v2+, v3+)

Dans votre répertoire de configuration /chemin/vers/répertoire/configuration/, modifiez les fichiers config_dev.yml et config_prod.yml afin de configurer la gestion des logs en fonction de vos besoins sur vos environnements de développement et de production.

# app/config/config.yml
monolog:

# Retirez la mise en commentaire de cette section si vous souhaitez utiliser un processeur
#       Processor :
#           session_processor:
#               class: Acme\Bundle\MonologBundle\Log\SessionRequestProcessor
#            arguments:  [ @session ]
#            tags:
#               - { name: monolog.processor, method: processRecord }


    json_formatter:
        class: Monolog\Formatter\JsonFormatter

    handlers:

        # Configuration du log shipper
        to_json_files:
            # Enregistrer les logs dans var/logs/(environment).log
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            # Inclure tous les canaux (doctrine, erreurs, etc.)
            channels: ~
            # Utiliser le formateur JSON
            formatter: monolog.json_formatter
            # Loguer tous les événements (des données de debugging aux erreurs fatales)
            level: debug

PPI

Dans votre répertoire de configuration /chemin/vers/répertoire/configuration/, modifiez les fichiers config_dev.yml et config_prod.yml afin de configurer la gestion des logs en fonction de vos besoins sur vos environnements de développement et de production.

monolog:
    handlers:

        # Configuration du log shipper
        to_json_files:
            # Enregistrer les logs dans var/logs/(environment).log
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            # Utiliser le formateur JSON
            formatter: monolog.json_formatter
            # Loguer tous les événements (des données de debugging aux erreurs fatales)
            level: debug

Laravel

<?php
  //file: bootstrap/app.php
  $app->configureMonologUsing(function($monolog) {
      $monolog->pushHandler(...);

    // Configurer votre logger ci-dessous
  });

  return $app;
?>

Silex

<?php
  // file: bootstrap
  $app->extend('monolog', function($monolog, $app) {
      $monolog->pushHandler(...);
      // Configurer votre logger ci-dessous
      return $monolog;
  });
?>

Lumen

<?php
  //file: bootstrap/app.php
  $app->configureMonologUsing(function($monolog) {
      $monolog->pushHandler(...);
      // Configurer votre logger ci-dessous
  });

  return $app;
?>

CakePHP

Commencez par ajouter la dépendance suivante au fichier composer.json, puis exécutez composer update.

{
    "require": {
        "cakephp/monolog": "*"
    }
}

Créez ensuite un fichier de configuration de journalisation (p. ex., app/Config/log.php) et ajoutez-y votre app/Config/bootstrap.php :

<?php
  include 'log.php';
?>

Voici un exemple de configuration basique permettant de reproduire les fonctionnalités de Cake avec Monolog :

CakePlugin::load('Monolog');

Enfin, enregistrez les logs dans un fichier :

CakeLog::config('debug', array(
  'engine' => 'Monolog.Monolog',
  'channel' => 'app',
  'handlers' => array(
    'Stream' => array(
      LOGS . 'application-json.log',
      'formatters' => array(
        'Json' => array("")
      )
    )
  )
));

Pour aller plus loin