Collecte de logs avec PHP

Présentation

Pour envoyer vos logs PHP à Datadog, activez la journalisation au sein d’un fichier et suivez ce fichier avec l’Agent Datadog. Les exemples de configuration ci-dessous utilisent les bibliothèques de journalisation Monolog, Zend-Log et Symfony.

Implémentation

Installation

Exécutez la commande suivante pour utiliser Composer afin d’ajouter Monolog en tant que dépendance :

composer require "monolog/monolog"

Sinon, suivez les instructions ci-dessous pour installer manuellement Monolog :

  1. Téléchargez Monolog depuis le référentiel et ajoutez-le aux bibliothèques.

  2. Ajoutez ce qui suit dans le bootstrap de l’application afin d’initialiser l’instance :

    <?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. Exécutez la commande suivante pour utiliser Composer afin d’ajouter Zend-Log :

composer require "zendframework/zend-log"

Sinon, suivez les instructions ci-dessous pour installer manuellement Zend-Log :

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

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

Ajoutez ce qui suit pour déclarer un formateur JSON Monolog en tant que service :

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

Configurer votre 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. Après avoir lancé l’instance Monolog, ajoutez dans votre code 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. Après avoir lancé l’instance Zend-Log, ajoutez dans votre code 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);

Pour configurer le formateur dans votre configuration Monolog, déclarez le champ formatter comme suit :

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

Configurer l’Agent Datadog

Une fois la collecte de logs activée, procédez comme suit pour configurer la collecte de logs personnalisée afin de suivre vos fichiers de log et envoyer les nouveaux logs à Datadog.

  1. Créez un dossier php.d/ dans le répertoire de configuration de l’Agent conf.d/.
  2. Créez un fichier conf.yaml dans votre dossier php.d/ avec le contenu suivant :
init_config:

instances:

## Log section
logs:

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

Associer vos services à l’ensemble des logs et traces

Si la solution APM est activée pour cette application, vous pouvez améliorer la corrélation entre vos logs et vos traces d’application en suivant les instructions de journalisation PHP pour APM. Cela vous permet d’ajouter automatiquement des identifiants de trace et de span à vos logs.

Enrichir le contexte des logs

Il peut être utile d’enrichir le contexte de vos logs et événements. Monolog propose 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. Par exemple, pour loguer un événement accompagné de données de contexte, utilisez ce qui suit :

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

Le préprocesseur de Monolog comporte une fonctionnalité de rappel simple qui enrichit vos événements en ajoutant les métadonnées de votre choix (par exemple, l’ID de session ou l’ID de requête) :

 <?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;
  });

Il peut être utile d’enrichir le contexte de vos logs et événements. Zend-Log propose 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. Par exemple, pour loguer un événement accompagné de données de contexte, utilisez ce qui suit :

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

Consultez la documentation Zend sur le processeur (en anglais) pour en savoir plus sur l’ajout d’informations supplémentaires dans vos logs.

Suivez les étapes ci-dessous pour ajouter un contexte variable à vos logs à l’aide d’un processeur de session.

  1. Implémentez votre processeur de session : Dans l’exemple ci-dessous, le processeur a accès aux informations de la session actuelle et enrichit l’entrée de log en y ajoutant des données 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. Ajoutez ce qui suit pour intégrer le processeur avec Symfony :

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

Intégration de Monolog à un framework

Monolog peut être utilisé avec les frameworks suivants :

Ajoutez ce qui suit pour intégrer Monolog à votre framework :

 <?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;

Configurez ensuite votre logger pour Monolog.

Dans votre répertoire de configuration /chemin/vers/répertoire/configuration/, ajoutez ce qui suit aux fichiers config_dev.yml et config_prod.yml. Modifiez l’exemple afin d’adapter la configuration à vos environnements de développement et de production.

# app/config/config.yml
monolog:

# Supprimer la mise en commentaire de cette section si vous avez besoin d'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"
            # Inclut tous les canaux (doctrine, erreurs, etc.)
            channels: ~
            # Utiliser le formatteur JSON
            formatter: monolog.json_formatter
            # Définir le niveau de journalisation (par exemple, debug, error ou alert)
            level: debug

Dans votre répertoire de configuration /chemin/vers/répertoire/configuration/, ajoutez ce qui suit aux fichiers config_dev.yml et config_prod.yml. Modifiez l’exemple afin d’adapter la configuration à 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
            # Définir le niveau de journalisation (par exemple, debug, error ou alert)
            level: debug
La fonction \DDTrace\current_context() a été ajoutée avec la version 0.61.0.

Ajoutez ce qui suit :

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Enregistrer des services d'application.
     *
     * @return void
     */
    public function register()
    {
        // Récupérer l'instance Monolog
        $monolog = logger()->getLogger();
        if (!$monolog instanceof \Monolog\Logger) {
            return;
        }

        // Facultatif : utiliser le format JSON
        $useJson = false;
        foreach ($monolog->getHandlers() as $handler) {
            if (method_exists($handler, 'setFormatter')) {
                $handler->setFormatter(new \Monolog\Formatter\JsonFormatter());
                $useJson = true;
            }
        }

        // Injecter l'ID de trace et de span pour associer l'entrée de log à la trace APM
        $monolog->pushProcessor(function ($record) use ($useJson) {
            $context = \DDTrace\current_context();
            if ($useJson === true) {
                $record['dd'] = [
                    'trace_id' => $context['trace_id'],
                    'span_id'  => $context['span_id'],
                ];
            } else {
                $record['message'] .= sprintf(
                    ' [dd.trace_id=%d dd.span_id=%d]',
                    $context['trace_id'],
                    $context['span_id']
                );
            }
            return $record;
        });
    }

    /**
     * Bootstrap des services d'application.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

Ajoutez ce qui suit :

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

Ajoutez ce qui suit :

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

  return $app;

Pour aller plus loin