PHP Log Collection

Overview

To send your PHP logs to Datadog, log to a file and then tail that file with your Datadog Agent. This page details setup examples for the Monolog, Zend-Log, and Symfony logging libraries.

Setup

Installation

Run this command to use Composer to add Monolog as a dependency:

composer require "monolog/monolog"

Alternatively, install Monolog manually by doing the following:

  1. Download Monolog from the repository and include it in the libraries.

  2. Add the following in the application’s bootstrap to initialize the instance:

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

Zend-Log is a part of the Zend framework. Run this command to use Composer to add Zend-Log:

composer require "zendframework/zend-log"

Alternatively, install Zend-Log manually by doing the following:

  1. Download the source from the repository and include it in the libraries.
  2. Add the following the application’s bootstrap to initialize the instance:
<?php
  require __DIR__ . '/vendor/autoload.php';

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

Add the following to declare a Monolog JSON formatter as a service:

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

Configure your logger

The following configuration enables JSON formatting and writes the logs and events into the application-json.log file. In your code, add a new handler after the initialization of the Monolog instance:

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

  // load Monolog library
  use Monolog\Logger;
  use Monolog\Handler\StreamHandler;
  use Monolog\Formatter\JsonFormatter;

  // create a log channel
  $log = new Logger('channel_name');

  // create a Json formatter
  $formatter = new JsonFormatter();

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

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

  // an example
  $log->info('Adding a new user', array('username' => 'Seldaek'));

The following configuration enables the JSON formatting and writes the logs and events into the application-json.log file. In your code, add a new handler after the initialization of the Zend-Log instance.

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

  // create a logger
  $logger = new Logger();

  // create a writer
  $writer = new Stream('file://' . __DIR__ . '/application-json.log');

  // create a Json formatter
  $formatter = new JsonFormatter();
  $writer->setFormatter($formatter);

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

To configure the formatter in your Monolog configuration, declare the formatter field as follows:

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

Configure the Datadog Agent

Once log collection is enabled, do the following to set up custom log collection to tail your log files and send new logs to Datadog.

  1. Create a php.d/ folder in the conf.d/ Agent configuration directory.
  2. Create a conf.yaml file in php.d/ with the following content:
init_config:

instances:

## Log section
logs:

  - type: file
    path: "/path/to/your/php/application-json.log"
    service: "<SERVICE_NAME>"
    source: php
    sourcecategory: sourcecode

Connect your services across logs and traces

If APM is enabled for this application, the correlation between application logs and traces can be improved by following the APM PHP logging instructions to automatically add trace and span IDs in your logs.

Add more context to logs

It can be useful to add additional context to your logs and events. Monolog provides methods for setting thread-local context that is then submitted automatically with all events. For example, to log an event with contextual data:

<?php
  $logger->info('Adding a new user', array('username' => 'Seldaek'));

Monolog’s pre-processor has a feature that is a simple callback and enriches your events with metadata you can set (for example, the session ID, or the request id):

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

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

      // Add various tags
      $record['ddtags'] = array('key' => 'value');

      // Add various generic context
      $record['extra']['key'] = 'value';

      return $record;
  });

It can be useful to add additional context to your logs and events. Zend-Log provides methods to set thread-local context that is then submitted automatically with all events. For example, to log an event with contextual data:

<?php
  $logger->info('Adding a new user', array('username' => 'Seldaek'));

See Zend’s Processor documentation for more information on providing additional information to your logs.

Follow these steps to add variable context in your logs using a session processor.

  1. Implement your session processor: In the following example, the processor knows the current session and enriches the content of the log record with information such as the requestId, sessionId, and so on.

    <?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. Integrate the processor with Symfony by adding the following:

      services:
          monolog.processor.session_request:
              class: Acme\Bundle\MonologBundle\Log\SessionRequestProcessor
              arguments:  [ @session ]
              tags:
                  - { name: monolog.processor, method: processRecord }
    
  3. Stream the generated JSON file to Datadog.

Monolog framework integration

Monolog can be used with the following frameworks:

To integrate Monolog with your framework, add the following:

 <?php
  // Check if the Monolog library is well loaded
  //use Monolog\Logger;
  //use Monolog\Handler\StreamHandler;
  //use Monolog\Formatter\JsonFormatter;

  // with the monolog instance
  $monolog = ...

  ///// Log shipper configuration

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

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

Then, configure your logger for Monolog.

In your configuration directory /path/to/config/directory/, add the following to the config_dev.yml and config_prod.yml. Modify the example to configure it for your development and production environments.

# app/config/config.yml
monolog:

# Uncomment this section, if you want to use a Processor
#       Processor :
#           session_processor:
#               class: Acme\Bundle\MonologBundle\Log\SessionRequestProcessor
#            arguments:  [ @session ]
#            tags:
#               - { name: monolog.processor, method: processRecord }

    json_formatter:
        class: Monolog\Formatter\JsonFormatter

    handlers:

        # Log shipper configuration
        to_json_files:
            # log to var/logs/(environment).log
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            # includes all channels (doctrine, errors, and so on)
            channels: ~
            # use json formatter
            formatter: monolog.json_formatter
            # set the log level (for example: debug, error, or alert)
            level: debug

In your configuration directory /path/to/config/directory/, add the following to the config_dev.yml and config_prod.yml. Modify the example to configure it for your development and production environments.

monolog:
    handlers:

        # Log shipper configuration
        to_json_files:
            # log to var/logs/(environment).log
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            # use json formatter
            formatter: monolog.json_formatter
            # set the log level (for example: debug, error, or alert)
            level: debug
The function \DDTrace\current_context() has been introduced in version 0.61.0.

Add the following:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // Get the Monolog instance
        $monolog = logger()->getLogger();
        if (!$monolog instanceof \Monolog\Logger) {
            return;
        }

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

        // Inject the trace and span ID to connect the log entry with the APM trace
        $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 any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

Add the following:

<?php
  // file: bootstrap
  $app->extend('monolog', function($monolog, $app) {
      $monolog->pushHandler(...);
      // configure your logger below
      return $monolog;
  });

Add the following:

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

  return $app;

Further Reading